Skip to main content

vtcode_core/ui/
tui.rs

1//! TUI protocol types and session interface.
2//!
3//! When the `tui` feature is enabled, this module re-exports the full app-layer
4//! protocol from `vtcode-ui`.  When the feature is disabled (headless build),
5//! it re-exports the shared data types from `vtcode-commons` and provides
6//! lightweight no-op stubs for `InlineHandle`, `InlineSession`, and
7//! `InlineEvent`.
8
9// ── Shared data types (always available from vtcode-commons) ────────────────
10
11pub use vtcode_commons::ui_protocol::{
12    InlineHeaderContext, InlineHeaderHighlight, InlineHeaderStatusBadge, InlineHeaderStatusTone,
13    InlineLinkRange, InlineLinkTarget, InlineListItem, InlineListSearchConfig, InlineListSelection,
14    InlineMessageKind, InlineSegment, InlineTextStyle, InlineTheme, LayoutModeOverride,
15    PlanContent, PlanPhase, PlanStep, ReasoningDisplayMode, RewindAction, SecurePromptConfig,
16    SessionSurface, SlashCommandItem, UiMode, WizardModalMode, WizardStep, convert_style,
17    theme_from_color_fields,
18};
19
20pub use vtcode_commons::ui_protocol::KeyboardProtocolSettings;
21
22// ── Full TUI re-exports (feature = "tui") ───────────────────────────────────
23
24#[cfg(feature = "tui")]
25pub use vtcode_ui::tui::app::*;
26
27// ── Headless stubs (feature = "tui" disabled) ───────────────────────────────
28
29#[cfg(not(feature = "tui"))]
30mod headless {
31    use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
32
33    use super::{
34        InlineListItem, InlineListSearchConfig, InlineListSelection, InlineMessageKind,
35        InlineSegment, SecurePromptConfig,
36    };
37
38    use crate::ui::theme::ThemeStyles;
39
40    /// Headless `InlineEvent` — all variants present so match arms compile.
41    #[derive(Clone, Debug, PartialEq, Eq)]
42    pub enum InlineEvent {
43        Submit(String),
44        QueueSubmit(String),
45        Steer(String),
46        ProcessLatestQueued,
47        EditQueue,
48        Cancel,
49        Exit,
50        Interrupt,
51        Pause,
52        Resume,
53        BackgroundOperation,
54        ScrollLineUp,
55        ScrollLineDown,
56        ScrollPageUp,
57        ScrollPageDown,
58        OpenFileInEditor(String),
59        OpenUrl(String),
60        LaunchEditor,
61        ForceCancelPtySession,
62        RequestInlinePromptSuggestion(String),
63        HistoryPrevious,
64        HistoryNext,
65    }
66
67    /// Minimal command surface used by tests and headless sinks.
68    #[derive(Clone, Debug)]
69    pub enum InlineCommand {
70        AppendLine {
71            kind: InlineMessageKind,
72            segments: Vec<InlineSegment>,
73        },
74        AppendPastedMessage {
75            kind: InlineMessageKind,
76            text: String,
77            line_count: usize,
78        },
79        Inline {
80            kind: InlineMessageKind,
81            segment: InlineSegment,
82        },
83        ReplaceLast {
84            count: usize,
85            kind: InlineMessageKind,
86            lines: Vec<Vec<InlineSegment>>,
87        },
88        ForceRedraw,
89        Shutdown,
90        ClearScreen,
91        CloseModal,
92        SetReasoningStage(Option<String>),
93    }
94
95    /// No-op handle for headless builds. All methods silently discard.
96    #[derive(Clone, Debug)]
97    pub struct InlineHandle {
98        sender: Option<UnboundedSender<InlineCommand>>,
99    }
100
101    impl InlineHandle {
102        pub fn new_for_tests(sender: UnboundedSender<InlineCommand>) -> Self {
103            Self {
104                sender: Some(sender),
105            }
106        }
107
108        fn send_command(&self, command: InlineCommand) {
109            if let Some(sender) = &self.sender {
110                let _ = sender.send(command);
111            }
112        }
113
114        pub fn append_line(&self, kind: InlineMessageKind, segments: Vec<InlineSegment>) {
115            self.send_command(InlineCommand::AppendLine { kind, segments });
116        }
117        pub fn append_pasted_message(
118            &self,
119            kind: InlineMessageKind,
120            text: String,
121            line_count: usize,
122        ) {
123            self.send_command(InlineCommand::AppendPastedMessage {
124                kind,
125                text,
126                line_count,
127            });
128        }
129        pub fn inline(&self, kind: InlineMessageKind, segment: InlineSegment) {
130            self.send_command(InlineCommand::Inline { kind, segment });
131        }
132        pub fn replace_last(
133            &self,
134            count: usize,
135            kind: InlineMessageKind,
136            lines: Vec<Vec<InlineSegment>>,
137        ) {
138            self.send_command(InlineCommand::ReplaceLast { count, kind, lines });
139        }
140        pub fn force_redraw(&self) {
141            self.send_command(InlineCommand::ForceRedraw);
142        }
143        pub fn shutdown(&self) {
144            self.send_command(InlineCommand::Shutdown);
145        }
146        pub fn clear_screen(&self) {
147            self.send_command(InlineCommand::ClearScreen);
148        }
149        pub fn show_modal(
150            &self,
151            _title: String,
152            _lines: Vec<String>,
153            _secure_prompt: Option<SecurePromptConfig>,
154        ) {
155        }
156        pub fn show_list_modal(
157            &self,
158            _title: String,
159            _lines: Vec<String>,
160            _items: Vec<InlineListItem>,
161            _selected: Option<InlineListSelection>,
162            _search: Option<InlineListSearchConfig>,
163        ) {
164        }
165        pub fn close_modal(&self) {
166            self.send_command(InlineCommand::CloseModal);
167        }
168        pub fn set_reasoning_stage(&self, stage: Option<String>) {
169            self.send_command(InlineCommand::SetReasoningStage(stage));
170        }
171    }
172
173    /// Headless session — events never arrive.
174    pub struct InlineSession {
175        pub handle: InlineHandle,
176        pub events: UnboundedReceiver<InlineEvent>,
177    }
178
179    impl InlineSession {
180        pub async fn next_event(&mut self) -> Option<InlineEvent> {
181            self.events.recv().await
182        }
183
184        pub fn clone_inline_handle(&self) -> InlineHandle {
185            self.handle.clone()
186        }
187    }
188
189    /// Headless appearance config with sensible defaults.
190    #[derive(Debug, Clone, Default)]
191    pub struct SessionAppearanceConfig {
192        pub theme: String,
193        pub ui_mode: super::UiMode,
194        pub show_sidebar: bool,
195        pub min_content_width: u16,
196        pub min_navigation_width: u16,
197        pub navigation_width_percent: u8,
198        pub transcript_bottom_padding: u16,
199        pub dim_completed_todos: bool,
200        pub message_block_spacing: u8,
201        pub layout_mode: super::LayoutModeOverride,
202        pub reasoning_display_mode: super::ReasoningDisplayMode,
203        pub reasoning_visible_default: bool,
204        pub vim_mode: bool,
205        pub screen_reader_mode: bool,
206        pub reduce_motion_mode: bool,
207        pub reduce_motion_keep_progress_animation: bool,
208        pub customization: (),
209    }
210
211    /// Build an [`InlineTheme`](super::InlineTheme) from core theme styles.
212    pub fn theme_from_styles(styles: &ThemeStyles) -> super::InlineTheme {
213        super::theme_from_color_fields(
214            styles.foreground,
215            styles.background,
216            styles.primary,
217            styles.secondary,
218            styles.tool,
219            styles.tool_detail,
220            styles.pty_output,
221        )
222    }
223}
224
225#[cfg(not(feature = "tui"))]
226pub use headless::*;