Line data Source code
1 : /*! 2 : * \file esys/repo/tui/appmodel.cpp 3 : * \brief 4 : * 5 : * \cond 6 : * __legal_b__ 7 : * 8 : * Copyright (c) 2026 Michel Gillet 9 : * Distributed under the MIT License. 10 : * (See accompanying file LICENSE.txt or 11 : * copy at https://opensource.org/licenses/MIT) 12 : * 13 : * __legal_e__ 14 : * \endcond 15 : * 16 : */ 17 : 18 : #include "esys/repo/esysrepo_prec.h" 19 : #include "esys/repo/tui/appmodel.h" 20 : #include "esys/repo/progress/repoassignedevent.h" 21 : #include "esys/repo/progress/repodoneevent.h" 22 : #include "esys/repo/progress/repofailedevent.h" 23 : #include "esys/repo/progress/repoprogressevent.h" 24 : #include "esys/repo/progress/schedulerstateevent.h" 25 : 26 : #include <algorithm> 27 : #include <variant> 28 : 29 : namespace esys::repo::tui 30 : { 31 : 32 1 : const char *phase_to_string(progress::RepoPhase phase) 33 : { 34 1 : switch (phase) 35 : { 36 : case progress::RepoPhase::PENDING: return "Pending"; 37 1 : case progress::RepoPhase::RECEIVING: return "Receiving"; 38 0 : case progress::RepoPhase::RESOLVING: return "Resolving"; 39 0 : case progress::RepoPhase::CHECKOUT: return "Checkout"; 40 0 : case progress::RepoPhase::DONE: return "Done"; 41 0 : case progress::RepoPhase::FAILED: return "Failed"; 42 0 : case progress::RepoPhase::NOT_SET: 43 0 : default: return ""; 44 : } 45 : } 46 : 47 0 : const char *queue_reason_to_string(QueueReason reason) 48 : { 49 0 : switch (reason) 50 : { 51 : case QueueReason::EMPTY: return "empty"; 52 0 : case QueueReason::PAUSED: return "paused"; 53 0 : case QueueReason::BACKPRESSURE: return "backpressure"; 54 0 : case QueueReason::WAITING_FOR_WORKER: return "waiting for worker"; 55 0 : case QueueReason::RUNNING: return "running"; 56 : } 57 0 : return ""; 58 : } 59 : 60 0 : const char *queue_filter_to_string(QueueFilter filter) 61 : { 62 0 : switch (filter) 63 : { 64 : case QueueFilter::ALL: return "all"; 65 0 : case QueueFilter::PENDING: return "pending"; 66 0 : case QueueFilter::RUNNING: return "running"; 67 0 : case QueueFilter::FAILED: return "failed"; 68 0 : case QueueFilter::DONE: return "done"; 69 : } 70 0 : return ""; 71 : } 72 : 73 1 : AppModel::AppModel() = default; 74 : 75 1 : AppModel::~AppModel() = default; 76 : 77 2 : void AppModel::reset(std::size_t repo_count, std::size_t worker_count) 78 : { 79 2 : m_state.get_repos().assign(repo_count, RepoState{}); 80 2 : m_state.get_workers().clear(); 81 2 : m_state.get_workers().reserve(worker_count); 82 5 : for (std::size_t i = 0; i < worker_count; ++i) 83 3 : m_state.get_workers().emplace_back(static_cast<int>(i)); 84 : 85 2 : m_state.set_selected_worker(0); 86 2 : m_state.set_selected_repo(0); 87 2 : m_state.set_scheduler_paused(false); 88 2 : m_state.set_scheduler_backpressure(false); 89 2 : m_state.set_scheduler_pending_queue(0); 90 2 : m_state.set_scheduler_active_workers(0); 91 2 : m_state.set_queue_filter(QueueFilter::ALL); 92 2 : recompute_counters(); 93 2 : } 94 : 95 0 : void AppModel::reset(const std::vector<std::string> &repo_names, std::size_t worker_count) 96 : { 97 0 : reset(repo_names.size(), worker_count); 98 0 : for (std::size_t i = 0; i < repo_names.size(); ++i) 99 : { 100 0 : m_state.get_repos()[i].set_name(repo_names[i]); 101 0 : m_state.get_repos()[i].set_path(repo_names[i]); 102 : } 103 0 : } 104 : 105 9 : void AppModel::apply(const progress::SyncEvent &event) 106 : { 107 18 : std::visit([this](const auto &e) { apply_impl(e); }, event); 108 9 : recompute_counters(); 109 9 : } 110 : 111 1 : std::size_t AppModel::drain_and_apply(EventQueue &queue) 112 : { 113 1 : std::vector<progress::SyncEvent> events; 114 1 : queue.drain(events); 115 3 : for (const auto &event : events) apply(event); 116 1 : sync_log_buffer(); 117 1 : return events.size(); 118 1 : } 119 : 120 1 : void AppModel::sync_log_buffer() 121 : { 122 1 : if (m_log_buffer == nullptr) return; 123 0 : m_state.set_log_lines(m_log_buffer->snapshot()); 124 0 : if (m_state.get_log_follow_tail()) 125 : { 126 0 : const int height = m_state.get_log_pane_height(); 127 0 : const int n = static_cast<int>(m_state.get_log_lines().size()); 128 0 : const int max_off = (std::max)(0, n - height); 129 0 : m_state.set_log_scroll_offset(max_off); 130 : } 131 : else 132 0 : clamp_log_scroll(); 133 : } 134 : 135 0 : void AppModel::set_log_buffer(std::shared_ptr<LogBuffer> log_buffer) 136 : { 137 0 : m_log_buffer = std::move(log_buffer); 138 0 : } 139 : 140 0 : std::shared_ptr<LogBuffer> AppModel::get_log_buffer() const 141 : { 142 0 : return m_log_buffer; 143 : } 144 : 145 0 : void AppModel::set_phase_text(std::string phase_text) 146 : { 147 0 : m_state.set_phase_text(std::move(phase_text)); 148 0 : } 149 : 150 0 : void AppModel::clamp_log_scroll() 151 : { 152 0 : const int height = m_state.get_log_pane_height(); 153 0 : const int n = static_cast<int>(m_state.get_log_lines().size()); 154 0 : const int max_off = (std::max)(0, n - height); 155 0 : int off = m_state.get_log_scroll_offset(); 156 0 : if (off < 0) off = 0; 157 0 : if (off > max_off) off = max_off; 158 0 : m_state.set_log_scroll_offset(off); 159 0 : if (off >= max_off) m_state.set_log_follow_tail(true); 160 0 : } 161 : 162 0 : void AppModel::scroll_log_page_up() 163 : { 164 0 : sync_log_buffer(); 165 0 : const int height = (std::max)(1, m_state.get_log_pane_height()); 166 0 : m_state.set_log_follow_tail(false); 167 0 : m_state.set_log_scroll_offset(m_state.get_log_scroll_offset() - height); 168 0 : clamp_log_scroll(); 169 0 : } 170 : 171 0 : void AppModel::scroll_log_page_down() 172 : { 173 0 : sync_log_buffer(); 174 0 : const int height = (std::max)(1, m_state.get_log_pane_height()); 175 0 : m_state.set_log_scroll_offset(m_state.get_log_scroll_offset() + height); 176 0 : clamp_log_scroll(); 177 0 : } 178 : 179 39 : AppState &AppModel::get_state() 180 : { 181 39 : return m_state; 182 : } 183 : 184 0 : const AppState &AppModel::get_state() const 185 : { 186 0 : return m_state; 187 : } 188 : 189 2 : void AppModel::apply_impl(const progress::RepoAssignedEvent &event) 190 : { 191 2 : auto &repo = m_state.get_repos().at(event.get_repo_index()); 192 2 : auto &worker = m_state.get_workers().at(static_cast<std::size_t>(event.get_worker_id())); 193 : 194 2 : clear_worker_assignment(repo); 195 : 196 2 : if (worker.get_repo_index().has_value()) 197 : { 198 0 : const std::size_t prev_repo = *worker.get_repo_index(); 199 0 : if (prev_repo != event.get_repo_index() && prev_repo < m_state.get_repos().size()) 200 : { 201 0 : auto &prev = m_state.get_repos()[prev_repo]; 202 0 : if (prev.get_assigned_worker_id() == event.get_worker_id()) 203 0 : prev.set_assigned_worker_id(std::nullopt); 204 : } 205 : } 206 : 207 2 : repo.set_status(RepoStatus::RUNNING); 208 2 : repo.set_assigned_worker_id(event.get_worker_id()); 209 2 : repo.set_phase_text("Pending"); 210 2 : repo.set_error_text(""); 211 : 212 2 : worker.set_busy(true); 213 2 : worker.set_repo_index(event.get_repo_index()); 214 2 : worker.set_errored(false); 215 2 : worker.set_last_event("assigned"); 216 2 : } 217 : 218 1 : void AppModel::apply_impl(const progress::RepoProgressEvent &event) 219 : { 220 1 : auto &repo = m_state.get_repos().at(event.get_repo_index()); 221 1 : repo.set_phase_text(phase_to_string(event.get_phase())); 222 1 : repo.set_progress(event.get_counters()); 223 1 : repo.set_detail_text(event.get_detail()); 224 1 : repo.set_status(RepoStatus::RUNNING); 225 : 226 1 : if (repo.get_assigned_worker_id().has_value()) 227 : { 228 1 : const int wid = *repo.get_assigned_worker_id(); 229 1 : if (wid >= 0 && static_cast<std::size_t>(wid) < m_state.get_workers().size()) 230 2 : m_state.get_workers()[static_cast<std::size_t>(wid)].set_last_event("progress"); 231 : } 232 1 : } 233 : 234 2 : void AppModel::apply_impl(const progress::RepoDoneEvent &event) 235 : { 236 2 : auto &repo = m_state.get_repos().at(event.get_repo_index()); 237 : // Fast repos may never emit progress; still show Rx/Idx/Chk at 100% when Done. 238 2 : auto counters = repo.get_progress(); 239 2 : counters.mark_complete(); 240 2 : repo.set_progress(counters); 241 2 : clear_worker_assignment(repo); 242 2 : repo.set_status(RepoStatus::DONE); 243 2 : repo.set_phase_text("Done"); 244 2 : repo.set_detail_text("completed successfully"); 245 2 : repo.set_error_text(""); 246 2 : } 247 : 248 1 : void AppModel::apply_impl(const progress::RepoFailedEvent &event) 249 : { 250 1 : auto &repo = m_state.get_repos().at(event.get_repo_index()); 251 1 : if (repo.get_assigned_worker_id().has_value()) 252 : { 253 1 : const int wid = *repo.get_assigned_worker_id(); 254 1 : if (wid >= 0 && static_cast<std::size_t>(wid) < m_state.get_workers().size()) 255 : { 256 1 : auto &worker = m_state.get_workers()[static_cast<std::size_t>(wid)]; 257 1 : worker.set_errored(true); 258 2 : worker.set_last_event("failed"); 259 : } 260 : } 261 1 : clear_worker_assignment(repo); 262 1 : repo.set_status(RepoStatus::FAILED); 263 1 : repo.set_phase_text("Failed"); 264 1 : repo.set_error_text(event.get_error()); 265 1 : } 266 : 267 3 : void AppModel::apply_impl(const progress::SchedulerStateEvent &event) 268 : { 269 3 : m_state.set_scheduler_paused(event.get_paused()); 270 3 : m_state.set_scheduler_backpressure(event.get_backpressure()); 271 3 : m_state.set_scheduler_pending_queue(event.get_pending_queue()); 272 3 : m_state.set_scheduler_active_workers(event.get_active_workers()); 273 3 : } 274 : 275 5 : void AppModel::clear_worker_assignment(RepoState &repo) 276 : { 277 5 : if (!repo.get_assigned_worker_id().has_value()) return; 278 : 279 2 : const int wid = *repo.get_assigned_worker_id(); 280 2 : if (wid >= 0 && static_cast<std::size_t>(wid) < m_state.get_workers().size()) 281 : { 282 2 : auto &worker = m_state.get_workers()[static_cast<std::size_t>(wid)]; 283 2 : worker.set_busy(false); 284 2 : worker.set_repo_index(std::nullopt); 285 : // Failure belongs on the repo row; do not keep marking idle slots Failed. 286 2 : worker.set_errored(false); 287 : } 288 2 : repo.set_assigned_worker_id(std::nullopt); 289 : } 290 : 291 11 : void AppModel::recompute_counters() 292 : { 293 11 : int done = 0; 294 11 : int running = 0; 295 11 : int pending = 0; 296 11 : int failed = 0; 297 : 298 37 : for (const auto &repo : m_state.get_repos()) 299 : { 300 26 : switch (repo.get_status()) 301 : { 302 2 : case RepoStatus::DONE: ++done; break; 303 3 : case RepoStatus::RUNNING: ++running; break; 304 5 : case RepoStatus::FAILED: ++failed; break; 305 16 : case RepoStatus::PENDING: 306 16 : case RepoStatus::SKIPPED: 307 16 : default: ++pending; break; 308 : } 309 : } 310 : 311 11 : m_state.set_done_count(done); 312 11 : m_state.set_running_count(running); 313 11 : m_state.set_pending_count(pending); 314 11 : m_state.set_failed_count(failed); 315 11 : recompute_queue_reason(); 316 11 : } 317 : 318 11 : void AppModel::recompute_queue_reason() 319 : { 320 : // Priority matches ADR-0018: paused / backpressure / empty / waiting / running. 321 11 : if (m_state.get_scheduler_paused()) 322 : { 323 1 : m_state.set_queue_reason(QueueReason::PAUSED); 324 1 : return; 325 : } 326 10 : if (m_state.get_scheduler_backpressure()) 327 : { 328 1 : m_state.set_queue_reason(QueueReason::BACKPRESSURE); 329 1 : return; 330 : } 331 9 : if (m_state.get_pending_count() == 0 && m_state.get_running_count() == 0) 332 : { 333 1 : m_state.set_queue_reason(QueueReason::EMPTY); 334 1 : return; 335 : } 336 : 337 8 : bool any_idle_worker = false; 338 8 : bool any_busy_worker = false; 339 20 : for (const auto &worker : m_state.get_workers()) 340 : { 341 12 : if (worker.get_busy()) any_busy_worker = true; 342 9 : else any_idle_worker = true; 343 : } 344 : 345 8 : if (m_state.get_pending_count() > 0 && any_busy_worker && !any_idle_worker) 346 : { 347 1 : m_state.set_queue_reason(QueueReason::WAITING_FOR_WORKER); 348 1 : return; 349 : } 350 : 351 7 : m_state.set_queue_reason(QueueReason::RUNNING); 352 : } 353 : 354 0 : void AppModel::set_layout_mode(LayoutMode layout_mode) 355 : { 356 0 : m_state.set_layout_mode(layout_mode); 357 0 : } 358 : 359 0 : void AppModel::cycle_layout_mode() 360 : { 361 : // Sequence: AUTO -> COMPACT -> BALANCED -> DETAILED -> QUEUE_FIRST -> AUTO 362 0 : switch (m_state.get_layout_mode()) 363 : { 364 0 : case LayoutMode::AUTO: m_state.set_layout_mode(LayoutMode::COMPACT); break; 365 0 : case LayoutMode::COMPACT: m_state.set_layout_mode(LayoutMode::BALANCED); break; 366 0 : case LayoutMode::BALANCED: m_state.set_layout_mode(LayoutMode::DETAILED); break; 367 0 : case LayoutMode::DETAILED: m_state.set_layout_mode(LayoutMode::QUEUE_FIRST); break; 368 0 : case LayoutMode::QUEUE_FIRST: 369 0 : default: m_state.set_layout_mode(LayoutMode::AUTO); break; 370 : } 371 0 : } 372 : 373 0 : void AppModel::set_selected_repo(int repo_idx) 374 : { 375 0 : const int n = static_cast<int>(m_state.get_repos().size()); 376 0 : if (n <= 0) 377 : { 378 0 : m_state.set_selected_repo(0); 379 0 : return; 380 : } 381 : 382 0 : int idx = repo_idx; 383 0 : if (idx < 0) idx = 0; 384 0 : if (idx >= n) idx = n - 1; 385 0 : m_state.set_selected_repo(idx); 386 : } 387 : 388 0 : void AppModel::set_selected_worker(int worker_idx) 389 : { 390 0 : const int n = static_cast<int>(m_state.get_workers().size()); 391 0 : if (n <= 0) 392 : { 393 0 : m_state.set_selected_worker(0); 394 0 : return; 395 : } 396 : 397 0 : int idx = worker_idx; 398 0 : if (idx < 0) idx = 0; 399 0 : if (idx >= n) idx = n - 1; 400 0 : m_state.set_selected_worker(idx); 401 : 402 : // Keep selected_repo aligned with selected worker's current assignment, when possible. 403 0 : const auto &workers = m_state.get_workers(); 404 0 : if (idx < static_cast<int>(workers.size()) && workers[idx].get_repo_index().has_value()) 405 0 : set_selected_repo(static_cast<int>(*workers[idx].get_repo_index())); 406 : } 407 : 408 0 : void AppModel::set_terminal_size(int terminal_cols, int terminal_rows) 409 : { 410 0 : m_state.set_terminal_cols(terminal_cols); 411 0 : m_state.set_terminal_rows(terminal_rows); 412 0 : } 413 : 414 0 : void AppModel::set_queue_filter(QueueFilter queue_filter) 415 : { 416 0 : m_state.set_queue_filter(queue_filter); 417 0 : } 418 : 419 0 : void AppModel::cycle_queue_filter() 420 : { 421 0 : switch (m_state.get_queue_filter()) 422 : { 423 0 : case QueueFilter::ALL: m_state.set_queue_filter(QueueFilter::PENDING); break; 424 0 : case QueueFilter::PENDING: m_state.set_queue_filter(QueueFilter::RUNNING); break; 425 0 : case QueueFilter::RUNNING: m_state.set_queue_filter(QueueFilter::FAILED); break; 426 0 : case QueueFilter::FAILED: m_state.set_queue_filter(QueueFilter::DONE); break; 427 0 : case QueueFilter::DONE: 428 0 : default: m_state.set_queue_filter(QueueFilter::ALL); break; 429 : } 430 0 : } 431 : 432 : } // namespace esys::repo::tui