Skip to main content

rustyline/tty/
mod.rs

1//! This module implements and describes common TTY methods & traits
2
3/// Unsupported Terminals that don't support RAW mode
4const UNSUPPORTED_TERM: [&str; 3] = ["dumb", "cons25", "emacs"];
5
6use crate::config::Config;
7use crate::highlight::Highlighter;
8use crate::keys::KeyEvent;
9use crate::layout::{GraphemeClusterMode, Layout, Position, Unit};
10use crate::line_buffer::LineBuffer;
11use crate::{Cmd, Prompt, Result};
12
13/// Terminal state
14pub trait RawMode: Sized {
15    /// Disable RAW mode for the terminal.
16    fn disable_raw_mode(&self) -> Result<()>;
17}
18
19/// Input event
20pub enum Event {
21    KeyPress(KeyEvent),
22    ExternalPrint(String),
23    #[cfg(target_os = "macos")]
24    Timeout(bool),
25}
26
27/// Translate bytes read from stdin to keys.
28pub trait RawReader {
29    type Buffer;
30    /// Blocking wait for either a key press or an external print
31    fn wait_for_input(&mut self, single_esc_abort: bool) -> Result<Event>; // TODO replace calls to `next_key` by `wait_for_input` where relevant
32    /// Blocking read of key pressed.
33    fn next_key(&mut self, single_esc_abort: bool) -> Result<KeyEvent>;
34    /// For CTRL-V support
35    #[cfg(unix)]
36    fn next_char(&mut self) -> Result<char>;
37    /// Bracketed paste
38    fn read_pasted_text(&mut self) -> Result<String>;
39    /// Check if `key` is bound to a peculiar command
40    fn find_binding(&self, key: &KeyEvent) -> Option<Cmd>;
41    /// Backup type ahead
42    fn unbuffer(self) -> Option<Buffer>;
43}
44
45/// Display prompt, line and cursor in terminal output
46pub trait Renderer {
47    type Reader: RawReader;
48
49    fn move_cursor(&mut self, old: Position, new: Position) -> Result<()>;
50
51    /// Display `prompt`, line and cursor in terminal output
52    fn refresh_line<P: Prompt + ?Sized>(
53        &mut self,
54        prompt: &P,
55        line: &LineBuffer,
56        hint: Option<&str>,
57        old_layout: Option<&Layout>, // used to clear old rows
58        new_layout: &Layout,
59        highlighter: Option<&dyn Highlighter>,
60    ) -> Result<()>;
61
62    /// Compute layout for rendering prompt + line + some info (either hint,
63    /// validation msg, ...). on the screen. Depending on screen width, line
64    /// wrapping may be applied.
65    fn compute_layout(
66        &self,
67        prompt_size: Position,
68        default_prompt: bool,
69        line: &LineBuffer,
70        info: Option<&str>,
71    ) -> Layout {
72        // calculate the desired position of the cursor
73        let pos = line.pos();
74        let cursor = self.calculate_position(&line[..pos], prompt_size);
75        // calculate the position of the end of the input line
76        let mut end = if pos == line.len() {
77            cursor
78        } else {
79            self.calculate_position(&line[pos..], cursor)
80        };
81        if let Some(info) = info {
82            end = self.calculate_position(info, end);
83        }
84
85        let new_layout = Layout {
86            grapheme_cluster_mode: self.grapheme_cluster_mode(),
87            prompt_size,
88            default_prompt,
89            cursor,
90            end,
91            has_info: info.is_some(),
92        };
93        debug_assert!(new_layout.prompt_size <= new_layout.cursor);
94        debug_assert!(new_layout.cursor <= new_layout.end);
95        new_layout
96    }
97
98    /// Calculate the number of columns and rows used to display `s` on a
99    /// `cols` width terminal starting at `orig`.
100    fn calculate_position(&self, s: &str, orig: Position) -> Position;
101
102    fn write_and_flush(&mut self, buf: &str) -> Result<()>;
103
104    /// Beep, used for completion when there is nothing to complete or when all
105    /// the choices were already shown.
106    fn beep(&mut self) -> Result<()>;
107
108    /// Clear the screen. Used to handle ctrl+l
109    fn clear_screen(&mut self) -> Result<()>;
110    /// Clear rows used by prompt and edited line
111    fn clear_rows(&mut self, layout: &Layout) -> Result<()>;
112    /// Clear from cursor to the end of line
113    fn clear_to_eol(&mut self) -> Result<()>;
114
115    /// Update the number of columns/rows in the current terminal.
116    fn update_size(&mut self);
117    /// Get the number of columns in the current terminal.
118    fn get_columns(&self) -> Unit;
119    /// Get the number of rows in the current terminal.
120    fn get_rows(&self) -> Unit;
121    /// Check if output supports colors.
122    fn colors_enabled(&self) -> bool;
123    /// Tell how grapheme clusters are rendered.
124    fn grapheme_cluster_mode(&self) -> GraphemeClusterMode;
125
126    /// Make sure prompt is at the leftmost edge of the screen
127    fn move_cursor_at_leftmost(&mut self, rdr: &mut Self::Reader) -> Result<()>;
128    /// Begin synchronized update on unix platform
129    fn begin_synchronized_update(&mut self) -> Result<()> {
130        Ok(())
131    }
132    /// End synchronized update on unix platform
133    fn end_synchronized_update(&mut self) -> Result<()> {
134        Ok(())
135    }
136}
137
138// ignore ANSI escape sequence
139fn width(gcm: GraphemeClusterMode, s: &str, esc_seq: &mut u8) -> Unit {
140    if *esc_seq == 1 {
141        if s == "[" {
142            // CSI
143            *esc_seq = 2;
144        } else {
145            // two-character sequence
146            *esc_seq = 0;
147        }
148        0
149    } else if *esc_seq == 2 {
150        if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') {
151            /*} else if s == "m" {
152            // last
153             *esc_seq = 0;*/
154        } else {
155            // not supported
156            *esc_seq = 0;
157        }
158        0
159    } else if s == "\x1b" {
160        *esc_seq = 1;
161        0
162    } else if s == "\n" {
163        0
164    } else {
165        gcm.width(s)
166    }
167}
168
169/// External printer
170pub trait ExternalPrinter {
171    /// Print message to stdout
172    fn print(&mut self, msg: String) -> Result<()>;
173}
174
175/// Terminal contract
176pub trait Term {
177    type Buffer;
178    type KeyMap;
179    type Reader: RawReader<Buffer = Self::Buffer>; // rl_instream
180    type Writer: Renderer<Reader = Self::Reader>; // rl_outstream
181    type Mode: RawMode;
182    type ExternalPrinter: ExternalPrinter;
183    type CursorGuard;
184
185    fn new(config: &Config) -> Result<Self>
186    where
187        Self: Sized;
188    /// Check if current terminal can provide a rich line-editing user
189    /// interface.
190    fn is_unsupported(&self) -> bool;
191    /// check if input stream is connected to a terminal.
192    fn is_input_tty(&self) -> bool;
193    /// check if output stream is connected to a terminal.
194    fn is_output_tty(&self) -> bool;
195    /// Enable RAW mode for the terminal.
196    fn enable_raw_mode(&mut self, config: &Config) -> Result<(Self::Mode, Self::KeyMap)>;
197    /// Create a RAW reader
198    fn create_reader(
199        &self,
200        buffer: Option<Self::Buffer>,
201        config: &Config,
202        key_map: Self::KeyMap,
203    ) -> Result<Self::Reader>;
204    /// Create a writer
205    fn create_writer(&self, config: &Config) -> Self::Writer;
206    fn writeln(&self) -> Result<()>;
207    /// Create an external printer
208    fn create_external_printer(&mut self) -> Result<Self::ExternalPrinter>;
209    /// Change cursor visibility
210    fn set_cursor_visibility(&mut self, visible: bool) -> Result<Option<Self::CursorGuard>>;
211}
212
213/// Check TERM environment variable to see if current term is in our
214/// unsupported list
215fn is_unsupported_term() -> bool {
216    match std::env::var("TERM") {
217        Ok(term) => {
218            for iter in &UNSUPPORTED_TERM {
219                if (*iter).eq_ignore_ascii_case(&term) {
220                    return true;
221                }
222            }
223            false
224        }
225        Err(_) => false,
226    }
227}
228
229// If on Windows platform import Windows TTY module
230// and re-export into mod.rs scope
231#[cfg(all(windows, not(target_arch = "wasm32")))]
232mod windows;
233#[cfg(all(windows, not(target_arch = "wasm32"), not(test)))]
234pub use self::windows::*;
235
236// If on Unix platform import Unix TTY module
237// and re-export into mod.rs scope
238#[cfg(all(unix, not(target_arch = "wasm32")))]
239mod unix;
240#[cfg(all(unix, not(target_arch = "wasm32"), not(test)))]
241pub use self::unix::*;
242
243#[cfg(any(test, target_arch = "wasm32"))]
244mod test;
245#[cfg(any(test, target_arch = "wasm32"))]
246pub use self::test::*;
247
248#[cfg(test)]
249mod test_ {
250    #[test]
251    fn test_unsupported_term() {
252        std::env::set_var("TERM", "xterm");
253        assert!(!super::is_unsupported_term());
254
255        std::env::set_var("TERM", "dumb");
256        assert!(super::is_unsupported_term());
257    }
258}