1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use crate::{Input, InputRequest, StateChanged};
use ratatui::crossterm::event::{
    Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers,
};
use ratatui::crossterm::{
    cursor::MoveTo,
    queue,
    style::{Attribute as CAttribute, Print, SetAttribute},
};
use std::io::{Result, Write};

/// Converts crossterm event into input requests.
pub fn to_input_request(evt: &CrosstermEvent) -> Option<InputRequest> {
    use InputRequest::*;
    use KeyCode::*;
    match evt {
        CrosstermEvent::Key(KeyEvent {
            code,
            modifiers,
            kind,
            state: _,
        }) if *kind == KeyEventKind::Press => match (*code, *modifiers) {
            (Backspace, KeyModifiers::NONE) | (Char('h'), KeyModifiers::CONTROL) => {
                Some(DeletePrevChar)
            }
            (Delete, KeyModifiers::NONE) => Some(DeleteNextChar),
            (Tab, KeyModifiers::NONE) => None,
            (Left, KeyModifiers::NONE) | (Char('b'), KeyModifiers::CONTROL) => {
                Some(GoToPrevChar)
            }
            (Left, KeyModifiers::CONTROL) | (Char('b'), KeyModifiers::META) => {
                Some(GoToPrevWord)
            }
            (Right, KeyModifiers::NONE) | (Char('f'), KeyModifiers::CONTROL) => {
                Some(GoToNextChar)
            }
            (Right, KeyModifiers::CONTROL) | (Char('f'), KeyModifiers::META) => {
                Some(GoToNextWord)
            }
            (Char('u'), KeyModifiers::CONTROL) => Some(DeleteLine),

            (Char('w'), KeyModifiers::CONTROL)
            | (Char('d'), KeyModifiers::META)
            | (Backspace, KeyModifiers::META) => Some(DeletePrevWord),

            (Delete, KeyModifiers::CONTROL) => Some(DeleteNextWord),
            (Char('k'), KeyModifiers::CONTROL) => Some(DeleteTillEnd),
            (Char('a'), KeyModifiers::CONTROL) | (Home, KeyModifiers::NONE) => {
                Some(GoToStart)
            }
            (Char('e'), KeyModifiers::CONTROL) | (End, KeyModifiers::NONE) => {
                Some(GoToEnd)
            }
            (Char(c), KeyModifiers::NONE) => Some(InsertChar(c)),
            (Char(c), KeyModifiers::SHIFT) => Some(InsertChar(c)),
            (_, _) => None,
        },
        _ => None,
    }
}

/// Renders the input UI at the given position with the given width.
pub fn write<W: Write>(
    stdout: &mut W,
    value: &str,
    cursor: usize,
    (x, y): (u16, u16),
    width: u16,
) -> Result<()> {
    queue!(stdout, MoveTo(x, y), SetAttribute(CAttribute::NoReverse))?;

    let val_width = width.max(1) as usize - 1;
    let len = value.chars().count();
    let start = (len.max(val_width) - val_width).min(cursor);
    let mut chars = value.chars().skip(start);
    let mut i = start;

    // Chars before cursor
    while i < cursor {
        i += 1;
        let c = chars.next().unwrap_or(' ');
        queue!(stdout, Print(c))?;
    }

    // Cursor
    i += 1;
    let c = chars.next().unwrap_or(' ');
    queue!(
        stdout,
        SetAttribute(CAttribute::Reverse),
        Print(c),
        SetAttribute(CAttribute::NoReverse)
    )?;

    // Chars after the cursor
    while i <= start + val_width {
        i += 1;
        let c = chars.next().unwrap_or(' ');
        queue!(stdout, Print(c))?;
    }

    Ok(())
}

/// Import this trait to implement `Input::handle_event()` for crossterm.
pub trait EventHandler {
    /// Handle crossterm event.
    fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged>;
}

impl EventHandler for Input {
    /// Handle crossterm event.
    fn handle_event(&mut self, evt: &CrosstermEvent) -> Option<StateChanged> {
        to_input_request(evt).and_then(|req| self.handle(req))
    }
}

#[cfg(test)]
mod tests {
    use ratatui::crossterm::event::{KeyEventKind, KeyEventState};

    use super::*;

    #[test]
    fn handle_tab() {
        let evt = CrosstermEvent::Key(KeyEvent {
            code: KeyCode::Tab,
            modifiers: KeyModifiers::NONE,
            kind: KeyEventKind::Press,
            state: KeyEventState::NONE,
        });

        let req = to_input_request(&evt);

        assert!(req.is_none());
    }
}