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}