Skip to main content

yuru_tui/
api.rs

1use crossterm::style::Color;
2use yuru_core::Candidate;
3
4const DEFAULT_SELECTED_ROW_BG: Color = Color::Rgb {
5    r: 52,
6    g: 58,
7    b: 70,
8};
9
10#[derive(Clone, Debug)]
11/// Configuration for an interactive TUI session.
12pub struct TuiOptions {
13    /// Query shown when the interface opens.
14    pub initial_query: String,
15    /// Prompt text displayed before the query.
16    pub prompt: String,
17    /// Optional header line.
18    pub header: Option<String>,
19    /// Optional footer line.
20    pub footer: Option<String>,
21    /// Accepted keys that return through the outcome.
22    pub expect_keys: Vec<String>,
23    /// Custom key bindings.
24    pub bindings: Vec<KeyBinding>,
25    /// Optional maximum interface height in terminal rows.
26    pub height: Option<usize>,
27    /// Vertical layout mode.
28    pub layout: TuiLayout,
29    /// Optional preview command.
30    pub preview: Option<PreviewCommand>,
31    /// Optional shell used for preview commands.
32    pub preview_shell: Option<String>,
33    /// Optional image preview protocol.
34    pub preview_image_protocol: Option<ImagePreviewProtocol>,
35    /// Display colors for selected UI elements.
36    pub style: TuiStyle,
37    /// Whether the selected row uses a full-width background.
38    pub highlight_line: bool,
39    /// Whether selection wraps at list boundaries.
40    pub cycle: bool,
41    /// Whether multiple candidates can be marked.
42    pub multi: bool,
43    /// Optional cap for marked candidates.
44    pub multi_limit: Option<usize>,
45    /// Whether text input is disabled.
46    pub no_input: bool,
47    /// Marker shown next to the selected row.
48    pub pointer: String,
49    /// Marker shown next to marked rows.
50    pub marker: String,
51    /// Text used when display values are truncated.
52    pub ellipsis: String,
53    /// Whether candidate display text may contain allowlisted ANSI SGR styles.
54    pub ansi: bool,
55    /// Whether case sensitivity follows the live query (fzf smart case).
56    ///
57    /// When true an uppercase character anywhere in the query makes the search
58    /// case-sensitive and removing it makes it case-insensitive again. When false the
59    /// `case_sensitive` flag of the search config is an explicit override that stays
60    /// fixed while the user types.
61    pub smart_case: bool,
62}
63
64impl Default for TuiOptions {
65    fn default() -> Self {
66        Self {
67            initial_query: String::new(),
68            prompt: "> ".to_string(),
69            header: None,
70            footer: None,
71            expect_keys: Vec::new(),
72            bindings: Vec::new(),
73            height: None,
74            layout: TuiLayout::default(),
75            preview: None,
76            preview_shell: None,
77            preview_image_protocol: None,
78            style: TuiStyle::default(),
79            highlight_line: true,
80            cycle: false,
81            multi: false,
82            multi_limit: None,
83            no_input: false,
84            pointer: ">".to_string(),
85            marker: "*".to_string(),
86            ellipsis: "..".to_string(),
87            ansi: false,
88            smart_case: false,
89        }
90    }
91}
92
93#[derive(Clone, Debug, Eq, PartialEq)]
94/// Preview source for the selected candidate.
95pub enum PreviewCommand {
96    /// Run a shell command to produce preview text.
97    Shell(String),
98    /// Use the built-in file previewer.
99    Builtin {
100        /// File extensions treated as text by the built-in previewer.
101        text_extensions: Vec<String>,
102    },
103}
104
105impl PreviewCommand {
106    pub(crate) fn cache_key(&self) -> String {
107        match self {
108            Self::Shell(command) => format!("shell:{command}"),
109            Self::Builtin { text_extensions } => {
110                format!("builtin:{}", text_extensions.join(","))
111            }
112        }
113    }
114}
115
116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117/// Terminal graphics protocol used for image previews.
118pub enum ImagePreviewProtocol {
119    /// Auto-detect an image protocol from terminal environment hints.
120    Auto,
121    /// Render images with Unicode half-block characters.
122    Halfblocks,
123    /// Render images with Sixel graphics.
124    Sixel,
125    /// Render images with Kitty graphics.
126    Kitty,
127    /// Render images with iTerm2 inline images.
128    Iterm2,
129}
130
131#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
132/// Vertical arrangement for prompt, results, and preview.
133pub enum TuiLayout {
134    /// Prompt at the bottom with results above it.
135    #[default]
136    Default,
137    /// Prompt at the top with results below it.
138    Reverse,
139    /// Prompt at the bottom with a top-down result list.
140    ReverseList,
141}
142
143impl TuiLayout {
144    pub(crate) fn prompt_at_bottom(self) -> bool {
145        matches!(self, Self::Default | Self::ReverseList)
146    }
147
148    pub(crate) fn list_bottom_up(self) -> bool {
149        matches!(self, Self::Default)
150    }
151}
152
153#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154/// RGB color used by the TUI style options.
155pub struct TuiRgb {
156    /// Red channel.
157    pub r: u8,
158    /// Green channel.
159    pub g: u8,
160    /// Blue channel.
161    pub b: u8,
162}
163
164impl From<TuiRgb> for Color {
165    fn from(color: TuiRgb) -> Self {
166        Self::Rgb {
167            r: color.r,
168            g: color.g,
169            b: color.b,
170        }
171    }
172}
173
174#[derive(Clone, Debug, Default, Eq, PartialEq)]
175/// Optional colors for TUI rendering.
176pub struct TuiStyle {
177    /// Color for the selection pointer.
178    pub pointer: Option<TuiRgb>,
179    /// Color for matched text.
180    pub highlight: Option<TuiRgb>,
181    /// Color for matched text on the selected row.
182    pub highlight_selected: Option<TuiRgb>,
183    /// Foreground color for the selected row.
184    pub selected_fg: Option<TuiRgb>,
185    /// Background color for the selected row.
186    pub selected_bg: Option<TuiRgb>,
187}
188
189impl TuiStyle {
190    pub(crate) fn pointer_color(&self) -> Option<Color> {
191        self.pointer.map(Color::from)
192    }
193
194    pub(crate) fn highlight_color(&self, selected: bool) -> Color {
195        if selected {
196            self.highlight_selected
197                .or(self.highlight)
198                .map(Color::from)
199                .unwrap_or(Color::Yellow)
200        } else {
201            self.highlight.map(Color::from).unwrap_or(Color::Yellow)
202        }
203    }
204
205    pub(crate) fn selected_bg_color(&self) -> Color {
206        self.selected_bg
207            .map(Color::from)
208            .unwrap_or(DEFAULT_SELECTED_ROW_BG)
209    }
210
211    pub(crate) fn selected_fg_color(&self) -> Option<Color> {
212        self.selected_fg.map(Color::from)
213    }
214}
215
216#[derive(Clone, Debug, Eq, PartialEq)]
217/// A key binding and the action it triggers.
218pub struct KeyBinding {
219    /// Key name in the TUI binding syntax.
220    pub key: String,
221    /// Action triggered by the key.
222    pub action: BindingAction,
223}
224
225#[derive(Clone, Copy, Debug, Eq, PartialEq)]
226/// Action assigned to a key binding.
227pub enum BindingAction {
228    /// Accept the current selection.
229    Accept,
230    /// Abort the interface.
231    Abort,
232    /// Clear the query text.
233    ClearQuery,
234    /// Move the selected row up.
235    MoveSelectionUp,
236    /// Move the selected row down.
237    MoveSelectionDown,
238    /// Move to the first row.
239    MoveSelectionFirst,
240    /// Move to the last row.
241    MoveSelectionLast,
242    /// Move one page up.
243    PageUp,
244    /// Move one page down.
245    PageDown,
246    /// Toggle the selected row mark.
247    ToggleMark,
248    /// Toggle the selected row mark and move down.
249    ToggleMarkAndDown,
250    /// Toggle the selected row mark and move up.
251    ToggleMarkAndUp,
252    /// Move the query cursor to the start.
253    MoveCursorStart,
254    /// Move the query cursor to the end.
255    MoveCursorEnd,
256    /// Move the query cursor left.
257    MoveCursorLeft,
258    /// Move the query cursor right.
259    MoveCursorRight,
260    /// Move the query cursor to the start of the previous word.
261    MoveCursorWordLeft,
262    /// Move the query cursor to the end of the next word.
263    MoveCursorWordRight,
264    /// Delete the character before the cursor.
265    Backspace,
266    /// Delete the character at the cursor.
267    Delete,
268    /// Delete from cursor to end of line.
269    DeleteToEnd,
270    /// Delete word before cursor.
271    DeleteWord,
272    /// Scroll preview up.
273    PreviewUp,
274    /// Scroll preview down.
275    PreviewDown,
276    /// Scroll preview one page up.
277    PreviewPageUp,
278    /// Scroll preview one page down.
279    PreviewPageDown,
280    /// Scroll preview to the top.
281    PreviewTop,
282    /// Scroll preview to the bottom.
283    PreviewBottom,
284}
285
286#[derive(Clone, Debug, Eq, PartialEq)]
287/// Result returned by an interactive TUI session.
288pub enum TuiOutcome {
289    /// The user accepted one or more candidates.
290    Accepted {
291        /// Accepted candidate ids.
292        ids: Vec<usize>,
293        /// Final query text.
294        query: String,
295        /// Matched expected key, when acceptance used one.
296        expect: Option<String>,
297    },
298    /// Acceptance was requested with no selected candidate.
299    NoSelection,
300    /// The user aborted the interface.
301    Aborted,
302}
303
304#[derive(Clone, Debug, Eq, PartialEq)]
305/// Message sent to a streaming TUI session.
306pub enum CandidateStreamMessage {
307    /// Append one candidate.
308    Candidate(Candidate),
309    /// Mark the stream as finished.
310    Finished,
311    /// Stop the session with an error.
312    Error(String),
313}