Skip to main content

stakpak_tui/
event_loop.rs

1//! Event Loop Module
2//!
3//! Contains the main TUI event loop and related helper functions.
4
5use crate::app::{AppState, AppStateOptions, InputEvent, OutputEvent};
6use crate::services::detect_term::is_unsupported_terminal;
7use crate::services::handlers::tool::{
8    clear_streaming_tool_results, handle_tool_result, update_session_tool_calls_queue,
9};
10use crate::services::message::Message;
11use crate::view::view;
12use crossterm::event::{
13    DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
14};
15use crossterm::{execute, terminal::EnterAlternateScreen};
16use ratatui::{Terminal, backend::CrosstermBackend};
17use stakpak_shared::models::integrations::openai::{AgentModel, ToolCallResultStatus};
18use std::io;
19use std::sync::Arc;
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::time::Duration;
22use tokio::sync::mpsc::{Receiver, Sender};
23use tokio::time::interval;
24
25use crate::app::ToolCallStatus;
26use crate::terminal::TerminalGuard;
27
28// Rulebook config struct (re-defined here to avoid circular dependency)
29#[derive(Clone, Debug)]
30pub struct RulebookConfig {
31    pub include: Option<Vec<String>>,
32    pub exclude: Option<Vec<String>>,
33    pub include_tags: Option<Vec<String>>,
34    pub exclude_tags: Option<Vec<String>>,
35}
36
37#[allow(clippy::too_many_arguments)]
38pub async fn run_tui(
39    mut input_rx: Receiver<InputEvent>,
40    output_tx: Sender<OutputEvent>,
41    cancel_tx: Option<tokio::sync::broadcast::Sender<()>>,
42    shutdown_tx: tokio::sync::broadcast::Sender<()>,
43    latest_version: Option<String>,
44    redact_secrets: bool,
45    privacy_mode: bool,
46    is_git_repo: bool,
47    auto_approve_tools: Option<&Vec<String>>,
48    allowed_tools: Option<&Vec<String>>,
49    current_profile_name: String,
50    rulebook_config: Option<RulebookConfig>,
51    agent_model: AgentModel,
52    editor_command: Option<String>,
53    auth_display_info: (Option<String>, Option<String>, Option<String>),
54) -> io::Result<()> {
55    let _guard = TerminalGuard;
56
57    crossterm::terminal::enable_raw_mode()?;
58
59    // Detect terminal support for mouse capture
60    #[cfg(unix)]
61    let terminal_info = crate::services::detect_term::detect_terminal();
62    #[cfg(unix)]
63    let enable_mouse_capture = is_unsupported_terminal(&terminal_info.emulator);
64
65    execute!(
66        std::io::stdout(),
67        EnterAlternateScreen,
68        EnableBracketedPaste
69    )?;
70
71    #[cfg(unix)]
72    if enable_mouse_capture {
73        execute!(std::io::stdout(), EnableMouseCapture)?;
74    } else {
75        execute!(std::io::stdout(), DisableMouseCapture)?;
76    }
77
78    let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
79
80    let term_size = terminal.size()?;
81
82    // Create internal channel for event handling (needed for error reporting during initialization)
83    let (internal_tx, mut internal_rx) = tokio::sync::mpsc::channel::<InputEvent>(100);
84
85    // Get board_agent_id from environment variable
86    let board_agent_id = std::env::var("AGENT_BOARD_AGENT_ID").ok();
87
88    let mut state = AppState::new(AppStateOptions {
89        latest_version,
90        redact_secrets,
91        privacy_mode,
92        is_git_repo,
93        auto_approve_tools,
94        allowed_tools,
95        input_tx: Some(internal_tx.clone()),
96        agent_model,
97        editor_command,
98        auth_display_info,
99        board_agent_id,
100    });
101
102    // Set mouse_capture_enabled based on terminal detection (matches the execute logic above)
103    #[cfg(unix)]
104    {
105        state.mouse_capture_enabled = enable_mouse_capture;
106    }
107    #[cfg(not(unix))]
108    {
109        state.mouse_capture_enabled = false;
110    }
111
112    // Set the current profile name and rulebook config
113    state.current_profile_name = current_profile_name;
114    state.rulebook_config = rulebook_config;
115
116    // Add welcome messages after state is created
117    let welcome_msg =
118        crate::services::helper_block::welcome_messages(state.latest_version.clone(), &state);
119    state.messages.extend(welcome_msg);
120
121    // Trigger initial board tasks refresh if agent ID is configured
122    if state.board_agent_id.is_some() {
123        let _ = internal_tx.try_send(InputEvent::RefreshBoardTasks);
124    }
125
126    let internal_tx_thread = internal_tx.clone();
127    // Create atomic pause flag for input thread
128    let input_paused = Arc::new(AtomicBool::new(false));
129    let input_paused_thread = input_paused.clone();
130
131    // Spawn input handling thread
132    // This thread reads from crossterm and converts to internal events
133    // It must be pausable when we yield terminal control to external programs (like nano/vim)
134    std::thread::spawn(move || {
135        loop {
136            // Check if we should pause input reading
137            if input_paused_thread.load(Ordering::Relaxed) {
138                std::thread::sleep(Duration::from_millis(50));
139                continue;
140            }
141
142            // Use poll with timeout instead of blocking read to allow checking pause flag
143            if let Ok(true) = crossterm::event::poll(Duration::from_millis(50))
144                && let Ok(event) = crossterm::event::read()
145                && let Some(event) = crate::event::map_crossterm_event_to_input_event(event)
146                && internal_tx_thread.blocking_send(event).is_err()
147            {
148                break;
149            }
150        }
151    });
152
153    let shell_event_tx = internal_tx.clone();
154
155    let mut spinner_interval = interval(Duration::from_millis(100));
156
157    // Main async update/view loop
158    terminal.draw(|f| view(f, &mut state))?;
159    let mut should_quit = false;
160
161    // Scroll batching: count consecutive scroll events to process in one frame
162    // These are reset at the start of each scroll batch
163    #[allow(unused_assignments)]
164    let mut pending_scroll_up: i32 = 0;
165    #[allow(unused_assignments)]
166    let mut pending_scroll_down: i32 = 0;
167
168    loop {
169        // Check if double Ctrl+C timer expired
170        if state.ctrl_c_pressed_once
171            && let Some(timer) = state.ctrl_c_timer
172            && std::time::Instant::now() > timer
173        {
174            state.ctrl_c_pressed_once = false;
175            state.ctrl_c_timer = None;
176        }
177        tokio::select! {
178               event = input_rx.recv() => {
179                let Some(event) = event else {
180                    should_quit = true;
181                    continue;
182                };
183                   if matches!(event, InputEvent::ShellOutput(_) | InputEvent::ShellError(_) |
184                   InputEvent::ShellWaitingForInput | InputEvent::ShellCompleted(_) | InputEvent::ShellClear) {
185            // These are shell events, forward them to the shell channel
186            let _ = shell_event_tx.send(event).await;
187            continue;
188        }
189                   if let InputEvent::EmergencyClearTerminal = event {
190                    emergency_clear_and_redraw(&mut terminal, &mut state)?;
191                    continue;
192                   }
193                   if let InputEvent::RunToolCall(tool_call) = &event {
194                       crate::services::update::update(&mut state, InputEvent::ShowConfirmationDialog(tool_call.clone()), 10, 40, &internal_tx, &output_tx, cancel_tx.clone(), &shell_event_tx, term_size);
195                       state.poll_file_search_results();
196                       terminal.draw(|f| view(f, &mut state))?;
197                       continue;
198                   }
199                   if let InputEvent::ToolResult(ref tool_call_result) = event {
200                       clear_streaming_tool_results(&mut state);
201
202                       // For run_command, also remove any message that matches the tool call ID
203                       // (handles case where streaming message uses tool_call_id directly)
204                       // The tool call ID is a String, but message IDs are Uuid
205                       if let Ok(tool_call_uuid) = uuid::Uuid::parse_str(&tool_call_result.call.id) {
206                           state.messages.retain(|m| m.id != tool_call_uuid);
207                       }
208
209                       state.session_tool_calls_queue.insert(tool_call_result.call.id.clone(), ToolCallStatus::Executed);
210                       update_session_tool_calls_queue(&mut state, tool_call_result);
211                       let tool_name = crate::utils::strip_tool_name(&tool_call_result.call.function.name);
212
213                       if tool_call_result.status == ToolCallResultStatus::Cancelled && tool_name == "run_command" {
214                           state.latest_tool_call = Some(tool_call_result.call.clone());
215                       }
216                       // Determine the state for run_command tools
217                       let is_cancelled = tool_call_result.status == ToolCallResultStatus::Cancelled;
218                       let is_error = tool_call_result.status == ToolCallResultStatus::Error;
219
220                       if (is_cancelled || is_error) && tool_name != "run_command" {
221                           // For non-run_command tools with cancelled/error, use old renderer
222                           state.messages.push(Message::render_result_border_block(tool_call_result.clone()));
223                           state.messages.push(Message::render_full_content_message(tool_call_result.clone()));
224                       } else {
225                           match tool_name {
226                               "str_replace" | "create" => {
227                                   // TUI: Show diff result block with yellow border (is_collapsed: None)
228                                   state.messages.push(Message::render_result_border_block(tool_call_result.clone()));
229                                   // Full screen popup: Show diff-only view without border (is_collapsed: Some(true))
230                                   state.messages.push(Message::render_collapsed_message(tool_call_result.call.clone()));
231                               }
232                               "run_command_task" => {
233                                   // TUI: bordered result block (is_collapsed: None)
234                                   state.messages.push(Message::render_result_border_block(tool_call_result.clone()));
235                                   // Full screen popup: full content without border (is_collapsed: Some(true))
236                                   state.messages.push(Message::render_full_content_message(tool_call_result.clone()));
237                               }
238                                "run_command" => {
239                                    // Use unified run command block with appropriate state
240                                    let command = crate::services::handlers::shell::extract_command_from_tool_call(&tool_call_result.call)
241                                        .unwrap_or_else(|_| "command".to_string());
242                                    let run_state = if is_error {
243                                        crate::services::bash_block::RunCommandState::Error
244                                    } else if is_cancelled {
245                                        // Cancelled could be user rejection or actual cancellation
246                                        // Use Cancelled for now (user pressed ESC during execution)
247                                        crate::services::bash_block::RunCommandState::Cancelled
248                                    } else {
249                                        crate::services::bash_block::RunCommandState::Completed
250                                    };
251
252                                    let run_cmd_msg = Message::render_run_command_block(
253                                        command,
254                                        Some(tool_call_result.result.clone()),
255                                        run_state,
256                                        None,
257                                    );
258                                    let popup_msg = Message::render_full_content_message(tool_call_result.clone());
259
260                                    // If shell is visible/running, insert cancelled block BEFORE the shell message
261                                    // so the order is: cancelled command -> shell box
262                                    if is_cancelled && state.shell_popup_visible {
263                                        if let Some(shell_msg_id) = state.interactive_shell_message_id {
264                                            // Find the position of the shell message
265                                            if let Some(pos) = state.messages.iter().position(|m| m.id == shell_msg_id) {
266                                                // Insert cancelled block and popup before shell message
267                                                state.messages.insert(pos, popup_msg);
268                                                state.messages.insert(pos, run_cmd_msg);
269                                            } else {
270                                                // Shell message not found, just push normally
271                                                state.messages.push(run_cmd_msg);
272                                                state.messages.push(popup_msg);
273                                            }
274                                        } else {
275                                            // No shell message ID, just push normally
276                                            state.messages.push(run_cmd_msg);
277                                            state.messages.push(popup_msg);
278                                        }
279                                    } else {
280                                        // Normal case: just push to the end
281                                        state.messages.push(run_cmd_msg);
282                                        state.messages.push(popup_msg);
283                                    }
284                                }
285                                "read" | "view" | "read_file" => {
286                                    // View file tool - show compact view with file icon and line count
287                                    // Extract file path and optional grep/glob from tool call arguments
288                                    let (file_path, grep, glob) = crate::services::handlers::tool::extract_view_params_from_tool_call(&tool_call_result.call);
289                                    let file_path = file_path.unwrap_or_else(|| "file".to_string());
290                                    let total_lines = tool_call_result.result.lines().count();
291                                    state.messages.push(Message::render_view_file_block(file_path.clone(), total_lines, grep.clone(), glob.clone()));
292                                    // Full screen popup: same compact view without borders
293                                    state.messages.push(Message::render_view_file_block_popup(file_path, total_lines, grep, glob));
294                                }
295                               _ => {
296                                   // TUI: collapsed command message - last 3 lines (is_collapsed: None)
297                                   state.messages.push(Message::render_collapsed_command_message(tool_call_result.clone()));
298                                   // Full screen popup: full content (is_collapsed: Some(true))
299                                   state.messages.push(Message::render_full_content_message(tool_call_result.clone()));
300                               }
301                           }
302
303                           // Handle file changes for the Changeset (only for non-cancelled/error)
304                           if !is_cancelled && !is_error {
305                               handle_tool_result(&mut state, tool_call_result.clone());
306                           }
307                       }
308                       // Invalidate cache and scroll to bottom to show the result
309                       crate::services::message::invalidate_message_lines_cache(&mut state);
310                       state.stay_at_bottom = true;
311
312                       // Refresh board tasks after tool execution (agent may have updated tasks)
313                       // Always trigger refresh - the handler will extract agent_id from messages if needed
314                       let _ = internal_tx.try_send(InputEvent::RefreshBoardTasks);
315                   }
316                   if let InputEvent::ToggleMouseCapture = event {
317                       #[cfg(unix)]
318                       toggle_mouse_capture_with_redraw(&mut terminal, &mut state)?;
319                       continue;
320                   }
321
322                   if let InputEvent::Quit = event {
323                       should_quit = true;
324                   }
325                   else {
326                       // Calculate main area width accounting for side panel
327                       let main_area_width = if state.show_side_panel {
328                           term_size.width.saturating_sub(32 + 1) // side panel width + margin
329                       } else {
330                           term_size.width
331                       };
332                       let term_rect = ratatui::layout::Rect::new(0, 0, main_area_width, term_size.height);
333                       let input_height = 3;
334                       let margin_height = 2;
335                       let dropdown_showing = state.show_helper_dropdown
336                           && ((!state.filtered_helpers.is_empty() && state.input().starts_with('/'))
337                               || !state.filtered_files.is_empty());
338                       let dropdown_height = if dropdown_showing {
339                           state.filtered_helpers.len() as u16
340                       } else {
341                           0
342                       };
343                       let hint_height = if dropdown_showing { 0 } else { margin_height };
344                       let outer_chunks = ratatui::layout::Layout::default()
345                           .direction(ratatui::layout::Direction::Vertical)
346                           .constraints([
347                               ratatui::layout::Constraint::Min(1), // messages
348                               ratatui::layout::Constraint::Length(1), // loading indicator
349                               ratatui::layout::Constraint::Length(input_height as u16),
350                               ratatui::layout::Constraint::Length(dropdown_height),
351                               ratatui::layout::Constraint::Length(hint_height),
352                           ])
353                           .split(term_rect);
354                       // Subtract 2 for padding (matches view.rs padded_message_area)
355                       let message_area_width = outer_chunks[0].width.saturating_sub(2) as usize;
356                       let message_area_height = outer_chunks[0].height as usize;
357                        crate::services::update::update(&mut state, event, message_area_height, message_area_width, &internal_tx, &output_tx, cancel_tx.clone(), &shell_event_tx, term_size);
358                        state.poll_file_search_results();
359                       // Handle pending editor open request
360                       if let Some(file_path) = state.pending_editor_open.take() {
361                           // Disable mouse capture before opening editor to prevent weird input
362                           let was_mouse_capture_enabled = state.mouse_capture_enabled;
363                           if was_mouse_capture_enabled {
364                               let _ = execute!(std::io::stdout(), DisableMouseCapture);
365                               state.mouse_capture_enabled = false;
366                           }
367
368                           match crate::services::editor::open_in_editor(
369                               &mut terminal,
370                               &state.editor_command,
371                               &file_path,
372                               None,
373                           ) {
374                               Ok(()) => {
375                                   // Editor closed successfully
376                               }
377                               Err(error) => {
378                                   // Show error message
379                                   state.messages.push(Message::info(
380                                       format!("Failed to open editor: {}", error),
381                                       Some(ratatui::style::Style::default().fg(ratatui::style::Color::Red)),
382                                   ));
383                               }
384                           }
385
386                           // Restore mouse capture if it was enabled before
387                           if was_mouse_capture_enabled {
388                               let _ = execute!(std::io::stdout(), EnableMouseCapture);
389                               state.mouse_capture_enabled = true;
390                           }
391                       }
392                   }
393               }
394               event = internal_rx.recv() => {
395
396                let Some(event) = event else {
397                    should_quit = true;
398                    continue;
399                };
400
401                if let InputEvent::ToggleMouseCapture = event {
402                    #[cfg(unix)]
403                    toggle_mouse_capture_with_redraw(&mut terminal, &mut state)?;
404                    continue;
405                }
406                if let InputEvent::Quit = event {
407                    should_quit = true;
408                }
409                   else {
410                       let term_size = terminal.size()?;
411                       // Calculate main area width accounting for side panel
412                       let main_area_width = if state.show_side_panel {
413                           term_size.width.saturating_sub(32 + 1) // side panel width + margin
414                       } else {
415                           term_size.width
416                       };
417                       let term_rect = ratatui::layout::Rect::new(0, 0, main_area_width, term_size.height);
418                       let input_height = 3;
419                       let margin_height = 2;
420                       let dropdown_showing = state.show_helper_dropdown
421                           && ((!state.filtered_helpers.is_empty() && state.input().starts_with('/'))
422                               || !state.filtered_files.is_empty());
423                       let dropdown_height = if dropdown_showing {
424                           state.filtered_helpers.len() as u16
425                       } else {
426                           0
427                       };
428                       let hint_height = if dropdown_showing { 0 } else { margin_height };
429                       let outer_chunks = ratatui::layout::Layout::default()
430                           .direction(ratatui::layout::Direction::Vertical)
431                           .constraints([
432                               ratatui::layout::Constraint::Min(1), // messages
433                               ratatui::layout::Constraint::Length(1), // loading indicator
434                               ratatui::layout::Constraint::Length(input_height as u16),
435                               ratatui::layout::Constraint::Length(dropdown_height),
436                               ratatui::layout::Constraint::Length(hint_height),
437                           ])
438                           .split(term_rect);
439                       // Subtract 2 for padding (matches view.rs padded_message_area)
440                       let message_area_width = outer_chunks[0].width.saturating_sub(2) as usize;
441                       let message_area_height = outer_chunks[0].height as usize;
442                    if let InputEvent::EmergencyClearTerminal = event {
443                    emergency_clear_and_redraw(&mut terminal, &mut state)?;
444                    continue;
445                   }
446
447                   // Batch scroll events: if this is a scroll event, drain any pending scroll events
448                   // and combine them into a single scroll operation for better performance
449                   if matches!(event, InputEvent::ScrollUp | InputEvent::ScrollDown) {
450                       pending_scroll_up = 0;
451                       pending_scroll_down = 0;
452
453                       // Count the initial event
454                       match event {
455                           InputEvent::ScrollUp => pending_scroll_up += 1,
456                           InputEvent::ScrollDown => pending_scroll_down += 1,
457                           _ => {}
458                       }
459
460                       // Drain any additional scroll events from the channel (non-blocking)
461                       let mut other_event: Option<InputEvent> = None;
462                       while let Ok(next_event) = internal_rx.try_recv() {
463                           match next_event {
464                               InputEvent::ScrollUp => pending_scroll_up += 1,
465                               InputEvent::ScrollDown => pending_scroll_down += 1,
466                               // Non-scroll event - save it for later
467                               other => {
468                                   other_event = Some(other);
469                                   break;
470                               }
471                           }
472                       }
473
474                       // Process net scroll (combine up and down into single direction)
475                       let net_scroll = pending_scroll_down - pending_scroll_up;
476                       if net_scroll > 0 {
477                           // More downs than ups - scroll down by accumulated amount
478                           for _ in 0..net_scroll {
479                               crate::services::update::update(&mut state, InputEvent::ScrollDown, message_area_height, message_area_width, &internal_tx, &output_tx, cancel_tx.clone(), &shell_event_tx, term_size);
480                           }
481                       } else if net_scroll < 0 {
482                           // More ups than downs - scroll up by accumulated amount
483                           for _ in 0..(-net_scroll) {
484                               crate::services::update::update(&mut state, InputEvent::ScrollUp, message_area_height, message_area_width, &internal_tx, &output_tx, cancel_tx.clone(), &shell_event_tx, term_size);
485                           }
486                       }
487
488                       // If we encountered a non-scroll event, process it too
489                       if let Some(other) = other_event {
490                           crate::services::update::update(&mut state, other, message_area_height, message_area_width, &internal_tx, &output_tx, cancel_tx.clone(), &shell_event_tx, term_size);
491                       }
492                   } else {
493                       crate::services::update::update(&mut state, event, message_area_height, message_area_width, &internal_tx, &output_tx, cancel_tx.clone(), &shell_event_tx, term_size);
494                   }
495                   state.poll_file_search_results();
496
497                        // Handle pending editor open request
498                         if let Some(file_path) = state.pending_editor_open.take() {
499                             // Pause input thread to avoid stealing input from editor
500                             input_paused.store(true, Ordering::Relaxed);
501                             // Small delay to ensure input thread cycle completes
502                             std::thread::sleep(Duration::from_millis(10));
503
504                             // Disable mouse capture before opening editor to prevent weird input
505                             let was_mouse_capture_enabled = state.mouse_capture_enabled;
506                             if was_mouse_capture_enabled {
507                                 let _ = execute!(std::io::stdout(), DisableMouseCapture);
508                                 state.mouse_capture_enabled = false;
509                             }
510
511                             match crate::services::editor::open_in_editor(
512                                 &mut terminal,
513                                 &state.editor_command,
514                                 &file_path,
515                                 None,
516                             ) {
517                                 Ok(()) => {
518                                     // Editor closed successfully
519                                 }
520                                 Err(error) => {
521                                     // Show error message
522                                     state.messages.push(Message::info(
523                                         format!("Failed to open editor: {}", error),
524                                         Some(ratatui::style::Style::default().fg(ratatui::style::Color::Red)),
525                                     ));
526                                 }
527                             }
528
529                             // Restore mouse capture if it was enabled before
530                             if was_mouse_capture_enabled {
531                                 let _ = execute!(std::io::stdout(), EnableMouseCapture);
532                                 state.mouse_capture_enabled = true;
533                             }
534
535                             // Resume input thread
536                             input_paused.store(false, Ordering::Relaxed);
537                         }
538
539                        state.update_session_empty_status();
540                    }
541                }
542               _ = spinner_interval.tick() => {
543                   // Also check double Ctrl+C timer expiry on every tick
544                   if state.ctrl_c_pressed_once
545                       && let Some(timer) = state.ctrl_c_timer
546                           && std::time::Instant::now() > timer {
547                               state.ctrl_c_pressed_once = false;
548                               state.ctrl_c_timer = None;
549                           }
550                   state.spinner_frame = state.spinner_frame.wrapping_add(1);
551                   // Update shell cursor blink (toggles every ~5 ticks = 500ms)
552                   crate::services::shell_popup::update_cursor_blink(&mut state);
553                   state.poll_file_search_results();
554                   terminal.draw(|f| view(f, &mut state))?;
555               }
556           }
557        if should_quit {
558            break;
559        }
560        state.poll_file_search_results();
561        state.update_session_empty_status();
562        terminal.draw(|f| view(f, &mut state))?;
563    }
564
565    let _ = shutdown_tx.send(());
566    crossterm::terminal::disable_raw_mode()?;
567    execute!(
568        std::io::stdout(),
569        crossterm::terminal::LeaveAlternateScreen,
570        DisableBracketedPaste,
571        DisableMouseCapture
572    )?;
573    Ok(())
574}
575
576pub fn emergency_clear_and_redraw<B: ratatui::backend::Backend>(
577    terminal: &mut Terminal<B>,
578    state: &mut AppState,
579) -> io::Result<()> {
580    use crossterm::{
581        cursor::MoveTo,
582        execute,
583        terminal::{Clear, ClearType},
584    };
585
586    // Nuclear option - clear everything including scrollback
587    execute!(
588        std::io::stdout(),
589        Clear(ClearType::All),
590        Clear(ClearType::Purge),
591        MoveTo(0, 0)
592    )?;
593
594    // Force a complete redraw of the TUI
595    terminal.clear()?;
596    terminal.draw(|f| view(f, state))?;
597
598    Ok(())
599}
600
601fn toggle_mouse_capture_with_redraw<B: ratatui::backend::Backend>(
602    terminal: &mut Terminal<B>,
603    state: &mut AppState,
604) -> io::Result<()> {
605    crate::toggle_mouse_capture(state)?;
606    emergency_clear_and_redraw(terminal, state)?;
607    Ok(())
608}