Skip to main content

line_editor/
editor.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::config::{ColumnWidth, Config, DEFAULT_COLUMN_COUNT, OperatingMode};
6use crate::control::{self, *};
7use crate::history::{History, HistoryDir};
8use crate::io::UnbufferedStdout;
9use crate::state::State;
10use crate::types::{CompletionHandler, HintHandler, ReadlineError};
11use bstr::{BStr, BString, ByteSlice};
12use std::io::{IsTerminal, Read, Write};
13
14/// Interactive line editor instance.
15pub struct Editor {
16    /// Configuration for this editor instance.
17    pub config: Config,
18    /// History manager.
19    pub(crate) history: History,
20    pub(crate) completion_handler: Option<Box<dyn CompletionHandler>>,
21    pub(crate) hint_handler: Option<Box<dyn HintHandler>>,
22    pub(crate) last_cols: std::cell::Cell<usize>,
23}
24
25impl Default for Editor {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl Editor {
32    /// Creates a new editor with default configuration.
33    pub fn new() -> Self {
34        Self::with_config(Config::default())
35    }
36
37    /// Creates a new editor with the specified configuration.
38    pub fn with_config(config: Config) -> Self {
39        let max_history_len = config.max_history_len;
40        Self {
41            config,
42            history: History::new(max_history_len),
43            completion_handler: None,
44            hint_handler: None,
45            last_cols: std::cell::Cell::new(DEFAULT_COLUMN_COUNT),
46        }
47    }
48
49    /// Reads a line from stdin/stdout using this editor.
50    pub fn readline(&mut self, prompt: impl AsRef<BStr>) -> Result<BString, ReadlineError> {
51        let stdin = std::io::stdin();
52        let mode = self.config.resolve_operating_mode(|| stdin.is_terminal());
53        self.readline_from(stdin.lock(), UnbufferedStdout, mode, prompt)
54    }
55
56    /// Reads a line from the provided reader and writer streams using the specified operating mode.
57    pub fn readline_from<R: Read, W: Write>(
58        &mut self,
59        mut reader: R,
60        mut writer: W,
61        mode: OperatingMode,
62        prompt: impl AsRef<BStr>,
63    ) -> Result<BString, ReadlineError> {
64        let prompt_bstr = prompt.as_ref();
65        match mode {
66            OperatingMode::Interactive => {
67                let res = self.readline_stream(&mut reader, &mut writer, prompt_bstr);
68                let _ = writer.write_all(b"\n");
69                let _ = writer.flush();
70                res
71            }
72            fallback_mode => {
73                self.readline_fallback_mode(reader, writer, fallback_mode, prompt_bstr)
74            }
75        }
76    }
77
78    /// Reads a line directly from raw reader and writer streams in interactive mode.
79    pub fn readline_stream<R: Read, W: Write>(
80        &mut self,
81        reader: &mut R,
82        writer: &mut W,
83        prompt: &BStr,
84    ) -> Result<BString, ReadlineError> {
85        let column_count = self.get_columns(reader, writer);
86        let mut state = State {
87            reader,
88            writer,
89            buffer: BString::default(),
90            prompt,
91            prompt_length: prompt.len(),
92            cursor_position: 0,
93            previous_cursor_position: 0,
94            column_count,
95            max_rows: 0,
96            history_index: 0,
97            draft_line: None,
98            editor: self,
99            render_buf: Vec::with_capacity(512),
100        };
101
102        if state.write_all(prompt.as_bytes()).is_err() {
103            return Err(ReadlineError::Eof);
104        }
105
106        loop {
107            let Some(mut c) = control::read_byte(state.reader)? else {
108                if state.buffer.is_empty() {
109                    return Err(ReadlineError::Eof);
110                } else {
111                    return Ok(state.buffer);
112                }
113            };
114
115            let has_completion = state.editor.completion_handler.is_some();
116            if c == KEY_TAB && has_completion {
117                c = state.complete_line();
118                if c == 0 {
119                    continue;
120                }
121            }
122
123            match c {
124                KEY_ENTER | KEY_CARRIAGE_RETURN => {
125                    if state.editor.config.multiline_mode {
126                        state.edit_move_end();
127                    }
128                    if state.editor.hint_handler.is_some() {
129                        let handler = state.editor.hint_handler.take();
130                        state.refresh_line();
131                        state.editor.hint_handler = handler;
132                    }
133                    return Ok(state.buffer);
134                }
135                KEY_CTRL_C => {
136                    return Err(ReadlineError::Interrupted);
137                }
138                KEY_BACKSPACE | KEY_CTRL_H => {
139                    state.edit_backspace();
140                }
141                KEY_CTRL_D => {
142                    if !state.buffer.is_empty() {
143                        state.delete();
144                    } else {
145                        return Err(ReadlineError::Eof);
146                    }
147                }
148                KEY_CTRL_T => {
149                    if state.cursor_position > 0 && state.cursor_position < state.buffer.len() {
150                        state.buffer.swap(state.cursor_position - 1, state.cursor_position);
151                        if state.cursor_position != state.buffer.len() - 1 {
152                            state.cursor_position += 1;
153                        }
154                        state.refresh_line();
155                    }
156                }
157                KEY_CTRL_B => {
158                    state.edit_move_left();
159                }
160                KEY_CTRL_F => {
161                    state.edit_move_right();
162                }
163                KEY_CTRL_P => {
164                    state.history_next(HistoryDir::Prev);
165                }
166                KEY_CTRL_N => {
167                    state.history_next(HistoryDir::Next);
168                }
169                KEY_ESC => {
170                    if let Some((b1, b2)) = control::read_bytes_2(state.reader)? {
171                        state.handle_escape_sequence(b1, b2);
172                    }
173                }
174                KEY_CTRL_U => {
175                    state.buffer.clear();
176                    state.cursor_position = 0;
177                    state.refresh_line();
178                }
179                KEY_CTRL_K => {
180                    state.buffer.truncate(state.cursor_position);
181                    state.refresh_line();
182                }
183                KEY_CTRL_A => {
184                    state.edit_move_home();
185                }
186                KEY_CTRL_E => {
187                    state.edit_move_end();
188                }
189                KEY_CTRL_L => {
190                    let _ = control::write_clear_screen(state.writer);
191                    state.refresh_line();
192                }
193                KEY_CTRL_W => {
194                    state.edit_delete_prev_word();
195                }
196                _ => {
197                    state.edit_insert(c);
198                }
199            }
200        }
201    }
202
203    pub(crate) fn readline_fallback_mode<R: Read, W: Write>(
204        &mut self,
205        mut reader: R,
206        mut writer: W,
207        mode: OperatingMode,
208        prompt: &BStr,
209    ) -> Result<BString, ReadlineError> {
210        match mode {
211            OperatingMode::PromptOnly | OperatingMode::UartEcho => {
212                let _ = writer.write_all(prompt.as_bytes());
213                let _ = writer.flush();
214            }
215            OperatingMode::NonTty => {}
216            OperatingMode::Interactive => unreachable!(),
217        }
218
219        let mut buf = BString::default();
220        let mut hit_newline = false;
221        let is_nontty = mode == OperatingMode::NonTty;
222
223        while is_nontty || buf.len() < self.config.max_line_len - 1 {
224            let Some(mut ch) = control::read_byte(&mut reader)? else {
225                break;
226            };
227
228            if mode == OperatingMode::UartEcho {
229                if ch == KEY_CARRIAGE_RETURN {
230                    continue;
231                }
232                if ch == KEY_DELETE {
233                    ch = KEY_BACKSPACE;
234                }
235                if ch == KEY_BACKSPACE {
236                    if !buf.is_empty() {
237                        let _ = control::write_erase_previous_char_uart(&mut writer);
238                        let _ = writer.flush();
239                        buf.pop();
240                    }
241                    continue;
242                } else {
243                    let _ = writer.write_all(&[ch]);
244                    let _ = writer.flush();
245                }
246            }
247
248            if ch == KEY_ENTER {
249                hit_newline = true;
250                break;
251            }
252            buf.push(ch);
253        }
254
255        if buf.is_empty() && !hit_newline {
256            return Err(ReadlineError::Eof);
257        }
258
259        while buf.ends_with(b"\n") || buf.ends_with(b"\r") {
260            buf.pop();
261        }
262
263        Ok(buf)
264    }
265
266    /// Registers a completion handler for tab autocompletion.
267    pub fn set_completion_handler<H: CompletionHandler + 'static>(&mut self, handler: H) {
268        self.completion_handler = Some(Box::new(handler));
269    }
270
271    /// Removes any registered completion handler.
272    pub fn clear_completion_handler(&mut self) {
273        self.completion_handler = None;
274    }
275
276    /// Registers a hint handler for inline completions/hints.
277    pub fn set_hint_handler<H: HintHandler + 'static>(&mut self, handler: H) {
278        self.hint_handler = Some(Box::new(handler));
279    }
280
281    /// Removes any registered hint handler.
282    pub fn clear_hint_handler(&mut self) {
283        self.hint_handler = None;
284    }
285
286    /// Adds a line to the editor's history buffer.
287    ///
288    /// Returns `true` if the entry was added (deduplicating adjacent identical entries).
289    pub fn add_history(&mut self, line: impl Into<BString>) -> bool {
290        self.history.add(line)
291    }
292
293    /// Returns a shared reference to the editor's history manager.
294    pub fn history(&self) -> &History {
295        &self.history
296    }
297
298    /// Returns a mutable reference to the editor's history manager.
299    pub fn history_mut(&mut self) -> &mut History {
300        &mut self.history
301    }
302
303    /// Clears the terminal screen using stdout.
304    pub fn clear_screen(&self) -> Result<(), std::io::Error> {
305        let mut stdout = UnbufferedStdout;
306        self.clear_screen_writer(&mut stdout)
307    }
308
309    /// Clears the terminal screen using the provided writer.
310    pub fn clear_screen_writer<W: Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
311        control::write_clear_screen(writer)?;
312        writer.flush()
313    }
314
315    /// Determines the number of columns in the terminal window.
316    pub(crate) fn get_columns<R: Read, W: Write>(&self, reader: &mut R, writer: &mut W) -> usize {
317        match self.config.column_width {
318            ColumnWidth::Fixed(cols) => cols,
319            ColumnWidth::AnsiCursor => self.get_columns_ansi(reader, writer),
320            ColumnWidth::Auto => {
321                let mut ws: libc::winsize = unsafe { std::mem::zeroed() };
322                if unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) } == 0
323                    && ws.ws_col > 0
324                {
325                    ws.ws_col as usize
326                } else {
327                    self.get_columns_ansi(reader, writer)
328                }
329            }
330        }
331    }
332
333    fn get_columns_ansi<R: Read, W: Write>(&self, reader: &mut R, writer: &mut W) -> usize {
334        let start_pos = get_cursor_position(reader, writer);
335        if start_pos.is_none() {
336            return self.last_cols.get();
337        }
338        let (_start_row, start_col) = start_pos.unwrap();
339
340        if control::write_query_column_width(writer).is_err() {
341            return self.last_cols.get();
342        }
343
344        let max_pos = get_cursor_position(reader, writer);
345        let cols = match max_pos {
346            None => self.last_cols.get(),
347            Some((_, col)) => {
348                if col <= 1 {
349                    self.last_cols.get()
350                } else {
351                    self.last_cols.set(col);
352                    col
353                }
354            }
355        };
356
357        if cols > start_col {
358            let seq = format!("\x1b[{}D", cols - start_col);
359            let _ = writer.write_all(seq.as_bytes());
360            let _ = writer.flush();
361        }
362        cols
363    }
364}
365
366fn get_cursor_position<R: Read, W: Write>(
367    reader: &mut R,
368    writer: &mut W,
369) -> Option<(usize, usize)> {
370    if control::write_query_cursor_position(writer).is_err() {
371        return None;
372    }
373
374    let mut buf = [0u8; 32];
375    let mut i = 0;
376    while i < buf.len() - 1 {
377        let Some(byte) = control::read_byte(reader).ok()? else {
378            return None;
379        };
380        buf[i] = byte;
381        if buf[i] == b'R' {
382            i += 1;
383            break;
384        }
385        i += 1;
386    }
387
388    if buf[0] != b'\x1b' || buf[1] != b'[' {
389        return None;
390    }
391
392    let s = std::str::from_utf8(&buf[2..i - 1]).ok()?;
393    let mut parts = s.split(';');
394    let row: usize = parts.next()?.parse().ok()?;
395    let col: usize = parts.next()?.parse().ok()?;
396    Some((row, col))
397}