vtcode_tui/core_tui/runner/
mod.rs1use std::io;
2use std::time::Duration;
3
4use anyhow::{Context, Result};
5use ratatui::crossterm::{
6 cursor::{MoveToColumn, RestorePosition, SavePosition},
7 execute,
8 terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, SetTitle},
9};
10use ratatui::{Terminal, backend::CrosstermBackend};
11use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
12use tokio_util::sync::CancellationToken;
13
14use crate::config::types::UiSurfacePreference;
15use crate::ui::tui::log::{clear_tui_log_sender, register_tui_log_sender, set_log_theme_name};
16
17use super::{
18 session::{Session, config::AppearanceConfig},
19 types::{
20 FocusChangeCallback, InlineCommand, InlineEvent, InlineEventCallback, InlineTheme,
21 SlashCommandItem,
22 },
23};
24
25mod drive;
26mod events;
27mod signal;
28mod surface;
29mod terminal_io;
30mod terminal_modes;
31
32use drive::{DriveRuntimeOptions, drive_terminal};
33use events::{EventListener, spawn_event_loop};
34use signal::SignalCleanupGuard;
35use surface::TerminalSurface;
36use terminal_io::{drain_terminal_events, finalize_terminal, prepare_terminal};
37use terminal_modes::{enable_terminal_modes, restore_terminal_modes};
38
39const ALTERNATE_SCREEN_ERROR: &str = "failed to enter alternate inline screen";
40
41pub struct TuiOptions {
42 pub theme: InlineTheme,
43 pub placeholder: Option<String>,
44 pub surface_preference: UiSurfacePreference,
45 pub inline_rows: u16,
46 pub show_logs: bool,
47 pub log_theme: Option<String>,
48 pub event_callback: Option<InlineEventCallback>,
49 pub focus_callback: Option<FocusChangeCallback>,
50 pub active_pty_sessions: Option<std::sync::Arc<std::sync::atomic::AtomicUsize>>,
51 pub keyboard_protocol: crate::config::KeyboardProtocolConfig,
52 pub workspace_root: Option<std::path::PathBuf>,
53 pub slash_commands: Vec<SlashCommandItem>,
54 pub appearance: Option<AppearanceConfig>,
55 pub app_name: String,
56}
57
58pub async fn run_tui(
59 mut commands: UnboundedReceiver<InlineCommand>,
60 events: UnboundedSender<InlineEvent>,
61 options: TuiOptions,
62) -> Result<()> {
63 let _panic_guard = crate::ui::tui::panic_hook::TuiPanicGuard::new();
66
67 let _signal_guard = SignalCleanupGuard::new()?;
68
69 let surface = TerminalSurface::detect(options.surface_preference, options.inline_rows)?;
70 set_log_theme_name(options.log_theme.clone());
71 let mut session = Session::new_with_logs(
72 options.theme,
73 options.placeholder,
74 surface.rows(),
75 options.show_logs,
76 options.appearance.clone(),
77 options.slash_commands,
78 options.app_name.clone(),
79 );
80 session.show_logs = options.show_logs;
81 session.active_pty_sessions = options.active_pty_sessions;
82 session.set_workspace_root(options.workspace_root.clone());
83 if options.show_logs {
84 let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel();
85 session.set_log_receiver(log_rx);
86 register_tui_log_sender(log_tx);
87 } else {
88 clear_tui_log_sender();
89 }
90
91 let keyboard_flags = crate::config::keyboard_protocol_to_flags(&options.keyboard_protocol);
92 let mut stderr = io::stderr();
93 let cursor_position_saved = match execute!(stderr, SavePosition) {
94 Ok(_) => true,
95 Err(error) => {
96 tracing::debug!(%error, "failed to save cursor position for inline session");
97 false
98 }
99 };
100 let mode_state = enable_terminal_modes(&mut stderr, keyboard_flags)?;
101 if surface.use_alternate() {
102 execute!(stderr, EnterAlternateScreen).context(ALTERNATE_SCREEN_ERROR)?;
103 }
104
105 let initial_title = options
107 .workspace_root
108 .as_ref()
109 .and_then(|path| {
110 path.file_name()
111 .or_else(|| path.parent()?.file_name())
112 .map(|name| format!("> {} ({})", options.app_name, name.to_string_lossy()))
113 })
114 .unwrap_or_else(|| format!("> {}", options.app_name));
115
116 if let Err(error) = execute!(stderr, SetTitle(&initial_title)) {
117 tracing::debug!(%error, "failed to set initial terminal title");
118 }
119
120 let backend = CrosstermBackend::new(stderr);
121 let mut terminal = Terminal::new(backend).context("failed to initialize inline terminal")?;
122 prepare_terminal(&mut terminal)?;
123
124 let (mut input_listener, event_channels) = EventListener::new();
126 let cancellation_token = CancellationToken::new();
127 let event_loop_token = cancellation_token.clone();
128 let event_channels_for_loop = event_channels.clone();
129 let rx_paused = event_channels.rx_paused.clone();
130 let last_input_elapsed_ms = event_channels.last_input_elapsed_ms.clone();
131 let session_start = event_channels.session_start;
132
133 drain_terminal_events();
136
137 let event_loop_handle = tokio::spawn(async move {
140 spawn_event_loop(
141 event_channels_for_loop.tx.clone(),
142 event_loop_token,
143 rx_paused,
144 last_input_elapsed_ms,
145 session_start,
146 )
147 .await;
148 });
149
150 let drive_result = drive_terminal(
151 &mut terminal,
152 &mut session,
153 &mut commands,
154 &events,
155 &mut input_listener,
156 event_channels,
157 DriveRuntimeOptions {
158 event_callback: options.event_callback,
159 focus_callback: options.focus_callback,
160 use_alternate_screen: surface.use_alternate(),
161 keyboard_flags,
162 },
163 )
164 .await;
165
166 cancellation_token.cancel();
168 let _ = tokio::time::timeout(Duration::from_millis(100), event_loop_handle).await;
169
170 drain_terminal_events();
172
173 let _ = execute!(io::stderr(), MoveToColumn(0), Clear(ClearType::CurrentLine));
175
176 let finalize_result = finalize_terminal(&mut terminal);
177 let leave_alternate_result = if surface.use_alternate() {
178 Some(execute!(terminal.backend_mut(), LeaveAlternateScreen))
179 } else {
180 None
181 };
182
183 if let Some(result) = leave_alternate_result
184 && let Err(error) = result
185 {
186 tracing::warn!(%error, "failed to leave alternate screen");
187 }
188
189 let restore_modes_result = restore_terminal_modes(&mode_state);
191 if let Err(error) = restore_modes_result {
192 tracing::warn!(%error, "failed to restore terminal modes");
193 }
194
195 session.clear_terminal_title();
197
198 if cursor_position_saved && let Err(error) = execute!(io::stderr(), RestorePosition) {
199 tracing::debug!(%error, "failed to restore cursor position for inline session");
200 }
201
202 drive_result?;
203 finalize_result?;
204
205 clear_tui_log_sender();
206
207 Ok(())
208}