thedes_tui/
input.rs

1use std::iter;
2
3pub use style::Style;
4use thedes_tui_core::{
5    App,
6    event::{Event, Key, KeyEvent},
7    geometry::Coord,
8    mutation::Set,
9    screen::{self, FlushError},
10};
11use thiserror::Error;
12use unicode_segmentation::UnicodeSegmentation;
13
14use crate::{
15    cancellability::{Cancellation, NonCancellable},
16    text,
17};
18
19mod style;
20
21pub type KeyBindingMap = crate::key_bindings::KeyBindingMap<Command>;
22
23pub fn default_key_bindings() -> KeyBindingMap {
24    KeyBindingMap::new()
25        .with(Key::Enter, Command::Confirm)
26        .with(Key::Esc, Command::ConfirmCancel)
27        .with(Key::Up, Command::ItemAbove)
28        .with(Key::Down, Command::ItemBelow)
29        .with(Key::Left, Command::MoveLeft)
30        .with(Key::Right, Command::MoveRight)
31        .with(Key::Backspace, Command::DeleteBehind)
32        .with(Key::Delete, Command::DeleteAhead)
33}
34
35#[derive(Debug, Error)]
36pub enum Error {
37    #[error("Invalid zero maximum input length")]
38    InvalidZeroMax,
39    #[error("Failed to render text")]
40    RenderText(
41        #[from]
42        #[source]
43        text::Error,
44    ),
45    #[error("Failed to flush tiles to canvas")]
46    CanvasFlush(
47        #[from]
48        #[source]
49        FlushError,
50    ),
51    #[error("Input was cancelled")]
52    Cancelled,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq)]
56pub enum Command {
57    Confirm,
58    ConfirmCancel,
59    ConfirmOk,
60    ItemAbove,
61    ItemBelow,
62    SelectCancel,
63    SelectOk,
64    MoveLeft,
65    MoveRight,
66    Move(Coord),
67    Insert(char),
68    DeleteAhead,
69    DeleteBehind,
70}
71
72#[derive(Debug, Clone)]
73pub struct Config<'a, F> {
74    pub filter: F,
75    pub max: Coord,
76    pub title: &'a str,
77}
78
79#[derive(Debug, Clone)]
80pub struct Input<F = fn(char) -> bool, C = NonCancellable> {
81    filter: F,
82    title: String,
83    max: Coord,
84    buffer: Vec<char>,
85    cancellation: C,
86    cursor: Coord,
87    style: Style,
88    key_bindings: KeyBindingMap,
89}
90
91impl<F> Input<F>
92where
93    F: FnMut(char) -> bool,
94{
95    pub fn new(config: Config<'_, F>) -> Result<Self, Error> {
96        Self::from_cancellation(config, NonCancellable)
97    }
98}
99
100impl<F, C> Input<F, C>
101where
102    F: FnMut(char) -> bool,
103    C: Cancellation<String>,
104{
105    pub fn from_cancellation(
106        config: Config<'_, F>,
107        cancellation: C,
108    ) -> Result<Self, Error> {
109        if config.max == 0 {
110            Err(Error::InvalidZeroMax)?
111        }
112        Ok(Self {
113            filter: config.filter,
114            title: config.title.to_owned(),
115            max: config.max,
116            cancellation,
117            key_bindings: default_key_bindings(),
118            style: Style::default(),
119            buffer: Vec::with_capacity(usize::from(config.max)),
120            cursor: 0,
121        })
122    }
123
124    pub fn with_title(mut self, title: &str) -> Self {
125        self.set_title(title);
126        self
127    }
128
129    pub fn set_title(&mut self, title: &str) -> &mut Self {
130        self.title = title.to_owned();
131        self
132    }
133
134    pub fn with_cancellation(mut self, cancellation: C) -> Self {
135        self.set_cancellation(cancellation);
136        self
137    }
138
139    pub fn set_cancellation(&mut self, cancellation: C) -> &mut Self {
140        self.cancellation = cancellation;
141        self
142    }
143
144    pub fn with_max(mut self, max: Coord) -> Result<Self, Error> {
145        self.set_max(max)?;
146        Ok(self)
147    }
148
149    pub fn set_max(&mut self, max: Coord) -> Result<&mut Self, Error> {
150        if max == 0 {
151            Err(Error::InvalidZeroMax)?
152        }
153        self.max = max;
154        self.buffer.truncate(usize::from(max));
155        Ok(self)
156    }
157
158    pub fn with_style(mut self, style: Style) -> Self {
159        self.set_style(style);
160        self
161    }
162
163    pub fn set_style(&mut self, style: Style) -> &mut Self {
164        self.style = style;
165        self
166    }
167
168    pub fn with_buffer<I>(mut self, chars: I) -> Result<Self, usize>
169    where
170        I: IntoIterator<Item = char>,
171    {
172        self.set_buffer(chars)?;
173        Ok(self)
174    }
175
176    pub fn set_buffer<I>(&mut self, chars: I) -> Result<&mut Self, usize>
177    where
178        I: IntoIterator<Item = char>,
179    {
180        self.clear_buffer();
181        self.insert_chars(chars)?;
182        Ok(self)
183    }
184
185    pub fn style(&self) -> &Style {
186        &self.style
187    }
188
189    pub fn key_bindings(&self) -> &KeyBindingMap {
190        &self.key_bindings
191    }
192
193    pub fn cancellation(&self) -> &C {
194        &self.cancellation
195    }
196
197    pub fn is_cancellable(&self) -> bool {
198        self.cancellation().is_cancellable()
199    }
200
201    pub fn is_cancelling(&self) -> bool {
202        self.cancellation().is_cancelling()
203    }
204
205    pub fn set_cancelling(&mut self, is_it: bool) {
206        self.cancellation.set_cancelling(is_it);
207    }
208
209    pub fn finish_buffer(&self) -> String {
210        self.buffer.iter().copied().collect()
211    }
212
213    pub fn output(&self) -> C::Output {
214        self.cancellation().make_output(self.finish_buffer())
215    }
216
217    pub fn insert_char(&mut self, char: char) {
218        if (self.filter)(char) {
219            if self.len() < self.max() {
220                self.buffer.insert(usize::from(self.cursor), char);
221                self.cursor += 1;
222            }
223        }
224    }
225
226    pub fn delete_behind(&mut self) {
227        if self.len() > 0 {
228            self.cursor -= 1;
229            self.buffer.remove(usize::from(self.cursor()));
230        }
231    }
232
233    pub fn delete_ahead(&mut self) {
234        if self.len() > 0 && self.cursor() < self.len() {
235            self.buffer.remove(usize::from(self.cursor()));
236            self.cursor = self.cursor().min(self.len().saturating_sub(1));
237        }
238    }
239
240    pub fn insert_chars<I>(&mut self, chars: I) -> Result<(), usize>
241    where
242        I: IntoIterator<Item = char>,
243    {
244        let mut chars = chars.into_iter();
245        while self.buffer.len() < usize::from(self.max()) {
246            let Some(char) = chars.next() else { break };
247            self.insert_char(char);
248        }
249        let chars_left = chars.count();
250        if chars_left == 0 { Ok(()) } else { Err(chars_left) }
251    }
252
253    pub fn move_left(&mut self) {
254        let _ = self.set_cursor(self.cursor().saturating_sub(1));
255    }
256
257    pub fn move_right(&mut self) {
258        let _ = self.set_cursor(self.cursor().saturating_add(1));
259    }
260
261    pub fn title(&self) -> &str {
262        &self.title
263    }
264
265    pub fn max(&self) -> Coord {
266        self.max
267    }
268
269    pub fn len(&self) -> Coord {
270        self.buffer.len() as Coord
271    }
272
273    pub fn cursor(&self) -> Coord {
274        self.cursor
275    }
276
277    pub fn set_cursor(&mut self, position: Coord) -> Result<(), Coord> {
278        self.cursor = position.min(self.len());
279        if position == self.cursor {
280            Ok(())
281        } else {
282            Err(position - self.cursor)
283        }
284    }
285
286    pub fn clear_buffer(&mut self) {
287        let _ = self.set_cursor(0);
288        self.buffer.clear();
289    }
290
291    pub fn run_command(&mut self, cmd: Command) -> Result<bool, Error> {
292        match cmd {
293            Command::Confirm => {
294                return Ok(false);
295            },
296            Command::ConfirmCancel => {
297                if self.is_cancellable() {
298                    self.set_cancelling(true);
299                    return Ok(false);
300                }
301            },
302            Command::ConfirmOk => {
303                self.set_cancelling(false);
304                return Ok(false);
305            },
306            Command::ItemAbove | Command::SelectOk => {
307                self.set_cancelling(false);
308            },
309            Command::ItemBelow | Command::SelectCancel => {
310                self.set_cancelling(true);
311            },
312            Command::Insert(ch) => self.insert_char(ch),
313            Command::DeleteBehind => self.delete_behind(),
314            Command::DeleteAhead => self.delete_ahead(),
315            Command::MoveLeft => self.move_left(),
316            Command::MoveRight => self.move_right(),
317            Command::Move(i) => {
318                let _ = self.set_cursor(i);
319            },
320        }
321        Ok(true)
322    }
323
324    pub async fn run(&mut self, app: &mut App) -> Result<(), Error> {
325        while self.handle_input(app)? {
326            self.render(app)?;
327            tokio::select! {
328                _ = app.tick_session.tick() => (),
329                _ = app.cancel_token.cancelled() => Err(Error::Cancelled)?,
330            }
331        }
332        Ok(())
333    }
334
335    fn handle_input(&mut self, app: &mut App) -> Result<bool, Error> {
336        let Ok(mut events) = app.events.read_until_now() else {
337            Err(Error::Cancelled)?
338        };
339        let mut should_continue = true;
340        while let Some(event) = events.next().filter(|_| should_continue) {
341            let Event::Key(key) = event else { continue };
342            if let KeyEvent {
343                main_key: Key::Char(ch),
344                alt: false,
345                ctrl: false,
346                shift: false,
347            } = key
348            {
349                self.insert_char(ch);
350            } else {
351                let Some(&command) = self.key_bindings.command_for(key) else {
352                    continue;
353                };
354                should_continue = self.run_command(command)?;
355            }
356        }
357        Ok(should_continue)
358    }
359
360    fn render(&mut self, app: &mut App) -> Result<(), Error> {
361        app.canvas
362            .queue([screen::Command::ClearScreen(self.style().background())]);
363
364        let mut height = self.style().top_margin();
365        self.render_title(app, &mut height)?;
366
367        self.render_field(app, &mut height)?;
368        self.render_cursor(app, &mut height)?;
369        self.render_ok(app, &mut height)?;
370        self.render_cancel(app, &mut height)?;
371
372        app.canvas.flush()?;
373
374        Ok(())
375    }
376
377    fn render_title(
378        &mut self,
379        app: &mut App,
380        height: &mut Coord,
381    ) -> Result<(), Error> {
382        *height = text::styled(
383            app,
384            self.title(),
385            &text::Style::new_with_colors(Set(self.style().title_colors()))
386                .with_align(1, 2)
387                .with_top_margin(*height)
388                .with_left_margin(self.style().left_margin())
389                .with_right_margin(self.style().right_margin()),
390        )?;
391        *height += self.style().title_field_padding();
392        Ok(())
393    }
394
395    fn render_field(
396        &mut self,
397        app: &mut App,
398        height: &mut Coord,
399    ) -> Result<(), Error> {
400        let padding_len = usize::from(self.max() - self.len());
401        let field_chars: String = self
402            .buffer
403            .iter()
404            .copied()
405            .chain(iter::repeat(' ').take(padding_len))
406            .collect();
407        text::styled(
408            app,
409            &field_chars,
410            &text::Style::new_with_colors(Set(self.style().field_colors()))
411                .with_align(1, 2)
412                .with_top_margin(*height)
413                .with_left_margin(self.style().left_margin())
414                .with_right_margin(self.style().right_margin()),
415        )?;
416        *height += 1;
417        Ok(())
418    }
419
420    fn render_cursor(
421        &mut self,
422        app: &mut App,
423        height: &mut Coord,
424    ) -> Result<(), Error> {
425        let prefix_len = usize::from(self.cursor() + 1);
426        let suffix_len = usize::from(self.max() - self.cursor());
427        let cursor_chars: String = iter::repeat(' ')
428            .take(prefix_len)
429            .chain(iter::once(self.style().cursor()))
430            .chain(iter::repeat(' ').take(suffix_len))
431            .collect();
432        text::styled(
433            app,
434            &cursor_chars,
435            &text::Style::new_with_colors(Set(self.style().cursor_colors()))
436                .with_align(1, 2)
437                .with_top_margin(*height)
438                .with_left_margin(self.style().left_margin())
439                .with_right_margin(self.style().right_margin()),
440        )?;
441        *height += 1;
442        *height += self.style().field_ok_padding();
443        Ok(())
444    }
445
446    fn render_ok(
447        &mut self,
448        app: &mut App,
449        height: &mut Coord,
450    ) -> Result<(), Error> {
451        let graphemes = self.style().ok_label().graphemes(true).count();
452        let right_padding = if graphemes % 2 == 0 { " " } else { "" };
453        let rendered = format!("{}{}", self.style().ok_label(), right_padding);
454        self.render_item(app, height, rendered, false)?;
455        *height += 1;
456        Ok(())
457    }
458
459    fn render_cancel(
460        &mut self,
461        app: &mut App,
462        height: &mut Coord,
463    ) -> Result<(), Error> {
464        if self.is_cancellable() {
465            *height += self.style().ok_cancel_padding();
466            let graphemes = self.style().cancel_label().graphemes(true).count();
467            let right_padding = if graphemes % 2 == 0 { " " } else { "" };
468            let rendered =
469                format!("{}{}", self.style().cancel_label(), right_padding);
470            self.render_item(app, height, rendered, true)?;
471        }
472        Ok(())
473    }
474
475    fn render_item(
476        &mut self,
477        app: &mut App,
478        height: &mut Coord,
479        item: String,
480        requires_cancelling: bool,
481    ) -> Result<(), Error> {
482        let is_selected = requires_cancelling == self.is_cancelling();
483        let (colors, rendered) = if is_selected {
484            let rendered = format!(
485                "{}{}{}",
486                self.style().selected_left(),
487                item,
488                self.style().selected_right(),
489            );
490            (self.style().selected_colors(), rendered)
491        } else {
492            (self.style().unselected_colors(), item)
493        };
494        text::styled(
495            app,
496            &rendered,
497            &text::Style::new_with_colors(Set(colors))
498                .with_align(1, 2)
499                .with_top_margin(*height)
500                .with_bottom_margin(app.canvas.size().y - *height - 2),
501        )?;
502        *height += 1;
503        Ok(())
504    }
505}