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}