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