repline/
repline.rs

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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
//! Prompts the user, reads the lines. Not much more to it than that.
//!
//! This module is in charge of parsing keyboard input and interpreting it for the line editor.

use crate::{editor::Editor, error::*, iter::*, raw::raw};
use std::{
    collections::VecDeque,
    io::{stdout, Bytes, Read, Result, Write},
};

/// Prompts the user, reads the lines. Not much more to it than that.
#[derive(Debug)]
pub struct Repline<'a, R: Read> {
    input: Chars<Flatten<Result<u8>, Bytes<R>>>,

    history: VecDeque<String>, // previous lines
    hindex: usize,             // current index into the history buffer

    ed: Editor<'a>, // the current line buffer
}

impl<'a> Repline<'a, std::io::Stdin> {
    pub fn new(color: &'a str, begin: &'a str, again: &'a str) -> Self {
        Self::with_input(std::io::stdin(), color, begin, again)
    }
}

impl<'a, R: Read> Repline<'a, R> {
    /// Constructs a [Repline] with the given [Reader](Read), color, begin, and again prompts.
    pub fn with_input(input: R, color: &'a str, begin: &'a str, again: &'a str) -> Self {
        Self {
            input: Chars(Flatten(input.bytes())),
            history: Default::default(),
            hindex: 0,
            ed: Editor::new(color, begin, again),
        }
    }
    /// Set the terminal prompt color
    pub fn set_color(&mut self, color: &'a str) {
        self.ed.color = color
    }
    /// Append line to history and clear it
    pub fn accept(&mut self) {
        self.history_append(self.ed.to_string());
        self.ed.clear();
        self.hindex = self.history.len();
    }
    /// Clear the line
    pub fn deny(&mut self) {
        self.ed.clear()
    }
    /// Reads in a line, and returns it for validation
    pub fn read(&mut self) -> ReplResult<String> {
        const INDENT: &str = "    ";
        let mut stdout = stdout().lock();
        let stdout = &mut stdout;
        let _make_raw = raw();
        // self.ed.begin_frame(stdout)?;
        // self.ed.redraw_frame(stdout)?;
        self.ed.print_head(stdout)?;
        loop {
            stdout.flush()?;
            match self.input.next().ok_or(Error::EndOfInput)?? {
                // Ctrl+C: End of Text. Immediately exits.
                '\x03' => {
                    drop(_make_raw);
                    writeln!(stdout)?;
                    return Err(Error::CtrlC(self.ed.to_string()));
                }
                // Ctrl+D: End of Transmission. Ends the current line.
                '\x04' => {
                    drop(_make_raw);
                    writeln!(stdout)?;
                    return Err(Error::CtrlD(self.ed.to_string()));
                }
                // Tab: extend line by 4 spaces
                '\t' => {
                    self.ed.extend(INDENT.chars(), stdout)?;
                }
                // ignore newlines, process line feeds. Not sure how cross-platform this is.
                '\n' => {}
                '\r' => {
                    self.ed.push('\n', stdout)?;
                    return Ok(self.ed.to_string());
                }
                // Ctrl+Backspace in my terminal
                '\x17' => {
                    self.ed.erase_word(stdout)?;
                }
                // Escape sequence
                '\x1b' => self.escape(stdout)?,
                // backspace
                '\x08' | '\x7f' => {
                    let ed = &mut self.ed;
                    if ed.ends_with(INDENT.chars()) {
                        for _ in 0..INDENT.len() {
                            ed.pop(stdout)?;
                        }
                    } else {
                        ed.pop(stdout)?;
                    }
                }
                c if c.is_ascii_control() => {
                    if cfg!(debug_assertions) {
                        self.ed.extend(c.escape_debug(), stdout)?;
                    }
                }
                c => {
                    self.ed.push(c, stdout)?;
                }
            }
        }
    }
    /// Prints a message without moving the cursor
    pub fn print_inline(&mut self, value: impl std::fmt::Display) -> ReplResult<()> {
        let mut stdout = stdout().lock();
        self.print_err(&mut stdout, value)
    }
    /// Prints a message (ideally an error) without moving the cursor
    fn print_err<W: Write>(&mut self, w: &mut W, value: impl std::fmt::Display) -> ReplResult<()> {
        self.ed.print_err(w, value)
    }
    /// Handle ANSI Escape
    fn escape<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
        match self.input.next().ok_or(Error::EndOfInput)?? {
            '[' => self.csi(w)?,
            'O' => todo!("Process alternate character mode"),
            other => self.ed.extend(other.escape_debug(), w)?,
        }
        Ok(())
    }
    /// Handle ANSI Control Sequence Introducer
    fn csi<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
        match self.input.next().ok_or(Error::EndOfInput)?? {
            'A' => {
                self.hindex = self.hindex.saturating_sub(1);
                self.restore_history(w)?
            }
            'B' => {
                self.hindex = self.hindex.saturating_add(1).min(self.history.len());
                self.restore_history(w)?
            }
            'C' => self.ed.cursor_forward(1, w)?,
            'D' => self.ed.cursor_back(1, w)?,
            'H' => self.ed.home(w)?,
            'F' => self.ed.end(w)?,
            '3' => {
                if let '~' = self.input.next().ok_or(Error::EndOfInput)?? {
                    let _ = self.ed.delete(w);
                }
            }
            other => {
                if cfg!(debug_assertions) {
                    self.ed.extend(other.escape_debug(), w)?;
                }
            }
        }
        Ok(())
    }
    /// Restores the currently selected history
    fn restore_history<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
        let Self { history, hindex, ed, .. } = self;
        ed.undraw(w)?;
        ed.clear();
        ed.print_head(w)?;
        if let Some(history) = history.get(*hindex) {
            ed.extend(history.chars(), w)?
        }
        Ok(())
    }

    /// Append line to history
    fn history_append(&mut self, mut buf: String) {
        while buf.ends_with(char::is_whitespace) {
            buf.pop();
        }
        if !self.history.contains(&buf) {
            self.history.push_back(buf)
        }
        while self.history.len() > 20 {
            self.history.pop_front();
        }
    }
}