1use std::{
2 io,
3 ops::{Deref, DerefMut},
4};
5
6use super::Widget;
7use crate::{
8 backend::{Backend, ClearType, MoveDirection, Size},
9 error,
10 events::{EventIterator, KeyCode, KeyModifiers},
11 layout::Layout,
12 style::Stylize,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Validation {
20 Finish,
22 Continue,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum OnEsc {
32 Terminate,
35 SkipQuestion,
38 Ignore,
40}
41
42pub trait Prompt: Widget {
47 type ValidateErr: Widget;
52
53 type Output;
55
56 fn validate(&mut self) -> Result<Validation, Self::ValidateErr> {
61 Ok(Validation::Finish)
62 }
63 fn finish(self) -> Self::Output;
66}
67
68#[derive(Debug)]
76pub struct Input<P, B: Backend> {
77 prompt: P,
78 on_esc: OnEsc,
79 backend: TerminalState<B>,
80 base_row: u16,
81 size: Size,
82 render_overflow: bool,
83}
84
85impl<P, B: Backend> Input<P, B> {
86 #[allow(clippy::new_ret_no_self)]
87 pub fn new(prompt: P, backend: &mut B) -> Input<P, &mut B> {
89 Input {
93 prompt,
94 on_esc: OnEsc::Ignore,
95 backend: TerminalState::new(backend, false),
96 base_row: 0,
97 size: Size::default(),
98 render_overflow: false,
99 }
100 }
101
102 pub fn hide_cursor(mut self) -> Self {
104 self.backend.hide_cursor = true;
105 self
106 }
107
108 pub fn on_esc(mut self, on_esc: OnEsc) -> Self {
116 self.on_esc = on_esc;
117 self
118 }
119}
120
121impl<P: Prompt, B: Backend> Input<P, B> {
122 fn layout(&self) -> Layout {
123 Layout::new(0, self.size).with_offset(0, self.base_row)
124 }
125
126 fn update_size(&mut self) -> io::Result<()> {
127 self.size = self.backend.size()?;
128 if self.size.area() == 0 {
129 Err(io::Error::new(
130 io::ErrorKind::Other,
131 format!(
132 "Invalid terminal {:?}. Both width and height must be larger than 0",
133 self.size
134 ),
135 ))
136 } else {
137 Ok(())
138 }
139 }
140
141 fn init(&mut self) -> io::Result<()> {
142 self.backend.init()?;
143 self.base_row = self.backend.get_cursor_pos()?.1;
144 self.render()
145 }
146
147 fn adjust_scrollback(&mut self, height: u16) -> io::Result<u16> {
148 let th = self.size.height;
149
150 let mut base_row = self.base_row;
151
152 if self.base_row > th.saturating_sub(height) {
153 let dist = self.base_row - th.saturating_sub(height);
154 base_row -= dist;
155 self.backend.scroll(-(dist as i16))?;
156 self.backend.move_cursor(MoveDirection::Up(dist))?;
157 }
158
159 Ok(base_row)
160 }
161
162 fn flush(&mut self) -> io::Result<()> {
163 if !self.backend.hide_cursor {
164 let (x, y) = self.prompt.cursor_pos(self.layout());
165
166 if self.render_overflow && y >= self.size.height - 1 {
167 if !self.backend.cursor_hidden {
171 self.backend.cursor_hidden = true;
172 self.backend.hide_cursor()?;
173 }
174 } else if self.backend.cursor_hidden {
175 self.backend.cursor_hidden = false;
177 self.backend.show_cursor()?;
178 }
179
180 self.backend.move_cursor_to(x, y)?;
181 }
182 self.backend.flush()
183 }
184
185 fn render_cutoff_msg(&mut self) -> io::Result<()> {
186 let cross = crate::symbols::current().cross;
187 self.backend.set_fg(crate::style::Color::DarkGrey)?;
188 write!(
189 self.backend,
190 "{0} the window height is too small, the prompt has been cut-off {0}",
191 cross
192 )?;
193 self.backend.set_fg(crate::style::Color::Reset)
194 }
195
196 fn render(&mut self) -> io::Result<()> {
197 self.update_size()?;
198 let height = self.prompt.height(&mut self.layout());
199 self.base_row = self.adjust_scrollback(height)?;
200 self.clear()?;
201
202 self.prompt.render(&mut self.layout(), &mut *self.backend)?;
203 self.render_overflow = height > self.size.height;
204
205 if self.render_overflow {
206 self.backend.move_cursor_to(0, self.size.height - 1)?;
207 self.render_cutoff_msg()?;
208 }
209
210 self.flush()
211 }
212
213 fn clear(&mut self) -> io::Result<()> {
214 self.backend.move_cursor_to(0, self.base_row)?;
215 self.backend.clear(ClearType::FromCursorDown)
216 }
217
218 fn goto_last_line(&mut self, height: u16) -> io::Result<()> {
219 self.base_row = self.adjust_scrollback(height + 1)?;
220 self.backend.move_cursor_to(0, self.base_row + height)
221 }
222
223 fn print_error(&mut self, mut e: P::ValidateErr) -> io::Result<()> {
224 self.update_size()?;
225 let height = self.prompt.height(&mut self.layout());
226 self.base_row = self.adjust_scrollback(height + 1)?;
227 self.clear()?;
228 self.prompt.render(&mut self.layout(), &mut *self.backend)?;
229
230 self.goto_last_line(height)?;
231
232 let mut layout = Layout::new(2, self.size).with_offset(0, self.base_row + height);
233 let err_height = e.height(&mut layout.clone());
234 self.base_row = self.adjust_scrollback(height + err_height)?;
235
236 if self.render_overflow {
237 self.backend
238 .move_cursor_to(0, self.size.height - err_height - 1)?;
239 self.backend.clear(ClearType::FromCursorDown)?;
240 self.render_cutoff_msg()?;
241 self.backend
242 .move_cursor_to(0, self.size.height - err_height)?;
243 }
244
245 self.backend
246 .write_styled(&crate::symbols::current().cross.red())?;
247 self.backend.write_all(b" ")?;
248
249 e.render(&mut layout, &mut *self.backend)?;
250
251 self.flush()
252 }
253
254 fn exit(&mut self) -> io::Result<()> {
255 self.update_size()?;
256 let height = self.prompt.height(&mut self.layout());
257 self.goto_last_line(height)?;
258 self.backend.reset()
259 }
260
261 pub fn run<E>(mut self, events: &mut E) -> error::Result<Option<P::Output>>
265 where
266 E: EventIterator,
267 {
268 self.init()?;
269
270 loop {
271 let e = events.next_event()?;
272
273 let key_handled = match e.code {
274 KeyCode::Char('c') if e.modifiers.contains(KeyModifiers::CONTROL) => {
275 self.exit()?;
276 return Err(error::ErrorKind::Interrupted);
277 }
278 KeyCode::Null => {
279 self.exit()?;
280 return Err(error::ErrorKind::Eof);
281 }
282 KeyCode::Esc if self.on_esc == OnEsc::Terminate => {
283 self.exit()?;
284 return Err(error::ErrorKind::Aborted);
285 }
286 KeyCode::Esc if self.on_esc == OnEsc::SkipQuestion => {
287 self.clear()?;
288 self.backend.reset()?;
289
290 return Ok(None);
291 }
292 KeyCode::Enter => match self.prompt.validate() {
293 Ok(Validation::Finish) => {
294 self.clear()?;
295 self.backend.reset()?;
296
297 return Ok(Some(self.prompt.finish()));
298 }
299 Ok(Validation::Continue) => true,
300 Err(e) => {
301 self.print_error(e)?;
302
303 continue;
304 }
305 },
306 _ => self.prompt.handle_key(e),
307 };
308
309 if key_handled {
310 self.render()?;
311 }
312 }
313 }
314}
315
316#[derive(Debug)]
317struct TerminalState<B: Backend> {
318 backend: B,
319 hide_cursor: bool,
320 cursor_hidden: bool,
321 enabled: bool,
322}
323
324impl<B: Backend> TerminalState<B> {
325 fn new(backend: B, hide_cursor: bool) -> Self {
326 Self {
327 backend,
328 enabled: false,
329 hide_cursor,
330 cursor_hidden: false,
331 }
332 }
333
334 fn init(&mut self) -> io::Result<()> {
335 self.enabled = true;
336 if self.hide_cursor && !self.cursor_hidden {
337 self.backend.hide_cursor()?;
338 self.cursor_hidden = true;
339 }
340 self.backend.enable_raw_mode()
341 }
342
343 fn reset(&mut self) -> io::Result<()> {
344 self.enabled = false;
345 if self.cursor_hidden {
346 self.backend.show_cursor()?;
347 self.cursor_hidden = false;
348 }
349 self.backend.disable_raw_mode()
350 }
351}
352
353impl<B: Backend> Drop for TerminalState<B> {
354 fn drop(&mut self) {
355 if self.enabled {
356 let _ = self.reset();
357 }
358 }
359}
360
361impl<B: Backend> Deref for TerminalState<B> {
362 type Target = B;
363
364 fn deref(&self) -> &Self::Target {
365 &self.backend
366 }
367}
368
369impl<B: Backend> DerefMut for TerminalState<B> {
370 fn deref_mut(&mut self) -> &mut Self::Target {
371 &mut self.backend
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::{backend::TestBackend, events::TestEvents};
379
380 #[derive(Debug, Default, Clone, Copy)]
381 struct TestPrompt {
382 height: u16,
383 }
384
385 impl Widget for TestPrompt {
386 fn render<B: Backend>(&mut self, layout: &mut Layout, backend: &mut B) -> io::Result<()> {
387 for i in 0..self.height(layout) {
388 backend.write_all(format!("Line {}", i).as_bytes())?;
390 backend.move_cursor(MoveDirection::NextLine(1))?;
391 }
392 Ok(())
393 }
394
395 fn height(&mut self, layout: &mut Layout) -> u16 {
396 layout.offset_y += self.height;
397 self.height
398 }
399
400 fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) {
401 layout.offset_cursor((0, self.height))
402 }
403
404 fn handle_key(&mut self, key: crate::events::KeyEvent) -> bool {
405 todo!("{:?}", key)
406 }
407 }
408
409 impl Prompt for TestPrompt {
410 type ValidateErr = &'static str;
411
412 type Output = ();
413
414 fn finish(self) -> Self::Output {}
415 }
416
417 #[test]
418 fn test_hide_cursor() {
419 let mut backend = TestBackend::new((100, 20).into());
420 let mut backend = Input::new(TestPrompt::default(), &mut backend)
421 .hide_cursor()
422 .backend;
423
424 backend.init().unwrap();
425
426 crate::assert_backend_snapshot!(*backend);
427 }
428
429 #[test]
430 fn test_adjust_scrollback() {
431 let prompt = TestPrompt::default();
432 let size = (100, 20).into();
433
434 let mut backend = TestBackend::new(size);
435 backend.move_cursor_to(0, 14).unwrap();
436
437 assert_eq!(
438 Input {
439 prompt,
440 on_esc: OnEsc::Ignore,
441 backend: TerminalState::new(&mut backend, false),
442 base_row: 14,
443 size,
444 render_overflow: false,
445 }
446 .adjust_scrollback(3)
447 .unwrap(),
448 14
449 );
450
451 crate::assert_backend_snapshot!(backend);
452
453 assert_eq!(
454 Input {
455 prompt,
456 on_esc: OnEsc::Ignore,
457 backend: TerminalState::new(&mut backend, false),
458 base_row: 14,
459 size,
460 render_overflow: false,
461 }
462 .adjust_scrollback(6)
463 .unwrap(),
464 14
465 );
466 crate::assert_backend_snapshot!(backend);
467
468 assert_eq!(
469 Input {
470 prompt,
471 on_esc: OnEsc::Ignore,
472 backend: TerminalState::new(&mut backend, false),
473 base_row: 14,
474 size,
475 render_overflow: false,
476 }
477 .adjust_scrollback(10)
478 .unwrap(),
479 10
480 );
481 crate::assert_backend_snapshot!(backend);
482 }
483
484 #[test]
485 fn test_render() {
486 let prompt = TestPrompt { height: 5 };
487 let size = (100, 20).into();
488 let mut backend = TestBackend::new(size);
489 backend.move_cursor_to(0, 5).unwrap();
490
491 assert!(Input {
492 prompt,
493 on_esc: OnEsc::Ignore,
494 backend: TerminalState::new(&mut backend, false),
495 size,
496 base_row: 5,
497 render_overflow: false,
498 }
499 .render()
500 .is_ok());
501
502 crate::assert_backend_snapshot!(backend);
503 }
504
505 #[test]
506 fn test_goto_last_line() {
507 let size = (100, 20).into();
508 let mut backend = TestBackend::new(size);
509 backend.move_cursor_to(0, 15).unwrap();
510
511 let mut input = Input {
512 prompt: TestPrompt::default(),
513 on_esc: OnEsc::Ignore,
514 backend: TerminalState::new(&mut backend, false),
515 size,
516 base_row: 15,
517 render_overflow: false,
518 };
519
520 assert!(input.goto_last_line(9).is_ok());
521 assert_eq!(input.base_row, 10);
522 drop(input);
523
524 crate::assert_backend_snapshot!(backend);
525 }
526
527 #[test]
528 fn test_print_error() {
529 let error = "error text";
530 let size = (100, 20).into();
531 let mut backend = TestBackend::new(size);
532
533 assert!(Input {
534 prompt: TestPrompt { height: 5 },
535 on_esc: OnEsc::Ignore,
536 backend: TerminalState::new(&mut backend, true),
537 base_row: 0,
538 size,
539 render_overflow: false,
540 }
541 .print_error(error)
542 .is_ok());
543
544 crate::assert_backend_snapshot!(backend);
545 }
546
547 #[test]
548 fn test_zero_size() {
549 let mut backend = TestBackend::new((20, 0).into());
550 let err = Input::new(TestPrompt::default(), &mut backend)
551 .run(&mut TestEvents::new([]))
552 .expect_err("zero size should error");
553
554 let err = match err {
555 crate::ErrorKind::IoError(err) => err,
556 err => panic!("expected io error, got {:?}", err),
557 };
558
559 assert_eq!(err.kind(), io::ErrorKind::Other);
560 assert_eq!(
561 format!("{}", err),
562 "Invalid terminal Size { width: 20, height: 0 }. Both width and height must be larger than 0"
563 );
564
565 let mut backend = TestBackend::new((0, 20).into());
566 let err = Input::new(TestPrompt::default(), &mut backend)
567 .run(&mut TestEvents::new([]))
568 .expect_err("zero size should error");
569
570 let err = match err {
571 crate::ErrorKind::IoError(err) => err,
572 err => panic!("expected io error, got {:?}", err),
573 };
574
575 assert_eq!(err.kind(), io::ErrorKind::Other);
576 assert_eq!(
577 format!("{}", err),
578 "Invalid terminal Size { width: 0, height: 20 }. Both width and height must be larger than 0"
579 );
580 }
581}