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