linenoise_rs/
lib.rs

1//! linenoise -- Guerilla line editing library against the idea that
2//! a line editing lib needs to be 20,000 lines of C code.
3//!
4//! Does a number of crazy assumptions that happen to be true in
5//! 99.9999% of the UNIX computers around these days.
6//!
7//! Copyright (c) 2025 parazyd <parazyd at dyne dot org>
8//! Copyright (c) 2010-2023, Salvatore Sanfilippo <antirez at gmail dot com>
9//! Copyright (c) 2010-2013, Pieter Noordhuis <pcnoordhuis at gmail dot com>
10//!
11//! All rights reserved.
12//!
13//! Redistribution and use in source and binary forms, with or without
14//! modification, are permitted provided that the following conditions are
15//! met:
16//!
17//! *  Redistributions of source code must retain the above copyright
18//!    notice, this list of conditions and the following disclaimer.
19//!
20//! *  Redistributions in binary form must reproduce the above copyright
21//!    notice, this list of conditions and the following disclaimer in the
22//!    documentation and/or other materials provided with the distribution.
23//!
24//! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25//! "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26//! LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27//! A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28//! HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29//! SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30//! LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31//! DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32//! THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33//! (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34//! OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35//!
36//! References:
37//! - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
38//! - http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html
39
40#![allow(clippy::manual_div_ceil)]
41#![allow(clippy::manual_range_contains)]
42
43use std::cmp::min;
44use std::collections::VecDeque;
45use std::fs::File;
46use std::io::{self, BufRead, BufReader, Write};
47use std::os::unix::io::RawFd;
48use std::sync::Mutex;
49use std::{env, mem};
50
51use libc::{c_void, tcgetattr, tcsetattr, termios};
52
53// Constants
54const LINENOISE_DEFAULT_HISTORY_MAX_LEN: usize = 100;
55const LINENOISE_MAX_LINE: usize = 4096;
56
57// Key codes
58#[repr(u8)]
59#[derive(Clone, Copy, PartialEq, Eq, Debug)]
60enum Key {
61    CtrlA = 1,
62    CtrlB = 2,
63    CtrlC = 3,
64    CtrlD = 4,
65    CtrlE = 5,
66    CtrlF = 6,
67    CtrlH = 8,
68    Tab = 9,
69    CtrlK = 11,
70    CtrlL = 12,
71    Enter = 13,
72    CtrlN = 14,
73    CtrlP = 16,
74    CtrlT = 20,
75    CtrlU = 21,
76    CtrlW = 23,
77    Esc = 27,
78    Backspace = 127,
79}
80
81// Callback types
82pub type CompletionCallback = fn(&str, &mut Vec<String>);
83pub type HintsCallback = fn(&str) -> Option<(String, i32, bool)>;
84
85lazy_static::lazy_static! {
86    static ref G: Mutex<GlobalState> = Mutex::new(GlobalState::new());
87}
88
89struct GlobalState {
90    /// Multi-line mode. Default is single line.
91    multi_line: bool,
92    /// Show "***" instead of input. For passwords.
93    mask_mode: bool,
94    /// Input history.
95    history: History,
96    /// Callback for showing input completion.
97    completion_callback: Option<CompletionCallback>,
98    /// Callback for showing input hints.
99    hints_callback: Option<HintsCallback>,
100    /// For `atexit()` to check if restore is needed.
101    raw_mode: bool,
102    /// In order to restore at exit.
103    orig_termios: Option<termios>,
104}
105
106impl GlobalState {
107    fn new() -> Self {
108        GlobalState {
109            multi_line: false,
110            mask_mode: false,
111            history: History::new(),
112            completion_callback: None,
113            hints_callback: None,
114            raw_mode: false,
115            orig_termios: None,
116        }
117    }
118}
119
120// History management
121#[derive(Clone)]
122struct History {
123    max_len: usize,
124    entries: VecDeque<String>,
125}
126
127impl History {
128    fn new() -> Self {
129        History {
130            max_len: LINENOISE_DEFAULT_HISTORY_MAX_LEN,
131            entries: VecDeque::new(),
132        }
133    }
134
135    fn add(&mut self, line: &str) -> bool {
136        if self.max_len == 0 || line.is_empty() {
137            return false;
138        }
139
140        // Don't add duplicates
141        if self.entries.back().is_some_and(|last| last == line) {
142            return false;
143        }
144
145        // Trim to max length
146        if self.entries.len() >= self.max_len {
147            self.entries.pop_front();
148        }
149
150        self.entries.push_back(line.to_string());
151        true
152    }
153
154    fn get(&self, index: usize) -> Option<&str> {
155        self.entries
156            .get(self.entries.len().wrapping_sub(index))
157            .map(|s| s.as_str())
158    }
159}
160
161// Terminal handling
162struct Terminal {
163    ifd: RawFd,
164    ofd: RawFd,
165    cols: usize,
166}
167
168/// RAII guard that restores terminal to original mode when dropped
169struct RawModeGuard {
170    ifd: RawFd,
171    orig_termios: termios,
172}
173
174impl Drop for RawModeGuard {
175    fn drop(&mut self) {
176        unsafe {
177            tcsetattr(self.ifd, libc::TCSAFLUSH, &self.orig_termios);
178        }
179        // Also update global state
180        if let Ok(mut state) = G.lock() {
181            state.raw_mode = false;
182            state.orig_termios = None;
183        }
184    }
185}
186
187impl Terminal {
188    fn new(ifd: RawFd, ofd: RawFd) -> Self {
189        let cols = Self::get_columns(ifd, ofd);
190        Terminal { ifd, ofd, cols }
191    }
192
193    /// Raw mode: 1960 magic shit.
194    fn enable_raw_mode(&self) -> io::Result<RawModeGuard> {
195        if !self.is_tty() {
196            return Err(io::Error::other("Not a TTY"));
197        }
198
199        let mut orig = unsafe { mem::zeroed::<termios>() };
200        if unsafe { tcgetattr(self.ifd, &mut orig) } == -1 {
201            return Err(io::Error::last_os_error());
202        }
203
204        // Modify the original mode
205        let mut raw = orig;
206        // input modes: no break, no CR to NL, no parity check, no strip char,
207        // no start/stop output control
208        raw.c_iflag &= !(libc::BRKINT | libc::ICRNL | libc::INPCK | libc::ISTRIP | libc::IXON);
209        // output modes - disable post processing
210        raw.c_oflag &= !libc::OPOST;
211        // control modes - set 8 bit chars
212        raw.c_cflag |= libc::CS8;
213        // local modes - echoing off, canonical off, no extended functions,
214        // no signal chars (^Z,^C)
215        raw.c_lflag &= !(libc::ECHO | libc::ICANON | libc::IEXTEN | libc::ISIG);
216        // control chars - set return condition: min number of bytes and timer.
217        // We want read to return every single byte, without timeout
218        raw.c_cc[libc::VMIN] = 1; // 1 byte
219        raw.c_cc[libc::VTIME] = 0; // no timer
220
221        if unsafe { tcsetattr(self.ifd, libc::TCSAFLUSH, &raw) } < 0 {
222            return Err(io::Error::last_os_error());
223        }
224
225        let mut state = G.lock().unwrap();
226        state.orig_termios = Some(orig);
227        state.raw_mode = true;
228        drop(state);
229
230        Ok(RawModeGuard {
231            ifd: self.ifd,
232            orig_termios: orig,
233        })
234    }
235
236    fn is_tty(&self) -> bool {
237        unsafe { libc::isatty(self.ifd) != 0 }
238    }
239
240    fn write(&self, s: &str) -> io::Result<()> {
241        self.write_bytes(s.as_bytes())
242    }
243
244    fn write_bytes(&self, buf: &[u8]) -> io::Result<()> {
245        let mut written = 0;
246        while written < buf.len() {
247            match unsafe {
248                libc::write(
249                    self.ofd,
250                    buf[written..].as_ptr() as *const c_void,
251                    buf.len() - written,
252                )
253            } {
254                -1 => {
255                    let err = io::Error::last_os_error();
256                    if err.kind() != io::ErrorKind::Interrupted {
257                        return Err(err);
258                    }
259                }
260                0 => break,
261                n => written += n as usize,
262            }
263        }
264        if self.ofd == libc::STDOUT_FILENO {
265            io::stdout().flush()?;
266        }
267        Ok(())
268    }
269
270    fn read_byte(&self) -> io::Result<Option<u8>> {
271        let mut c = 0u8;
272        loop {
273            let n = unsafe { libc::read(self.ifd, &mut c as *mut u8 as *mut c_void, 1) };
274            if n == -1 {
275                let err = io::Error::last_os_error();
276                if err.kind() == io::ErrorKind::Interrupted {
277                    continue;
278                }
279                return Err(err);
280            }
281            return Ok(if n == 0 { None } else { Some(c) });
282        }
283    }
284
285    fn read_byte_nonblocking(&self) -> io::Result<Option<u8>> {
286        use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK};
287
288        unsafe {
289            // Get current flags
290            let flags = fcntl(self.ifd, F_GETFL, 0);
291            if flags == -1 {
292                return Err(io::Error::last_os_error());
293            }
294
295            // Set non-blocking
296            if fcntl(self.ifd, F_SETFL, flags | O_NONBLOCK) == -1 {
297                return Err(io::Error::last_os_error());
298            }
299
300            // Try to read
301            let result = self.read_byte();
302
303            // Restore blocking mode - always attempt this
304            let restore_result = fcntl(self.ifd, F_SETFL, flags);
305
306            // Check restore result after we have our read result
307            if restore_result == -1 {
308                return Err(io::Error::last_os_error());
309            }
310
311            match result {
312                Ok(b) => Ok(b),
313                Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None),
314                Err(e) => Err(e),
315            }
316        }
317    }
318
319    /// Use the ESC [6n escape sequence to query the horizontal cursor position
320    /// and return it.
321    fn get_cursor_position(&self) -> io::Result<(usize, usize)> {
322        self.write_bytes(b"\x1b[6n")?;
323
324        let mut buf = [0u8; 32];
325        let mut i = 0;
326
327        while i < buf.len() - 1 {
328            buf[i] = self.read_byte()?.ok_or_else(|| {
329                io::Error::new(io::ErrorKind::UnexpectedEof, "EOF reading cursor")
330            })?;
331            i += 1;
332            if buf[i - 1] == b'R' {
333                break;
334            }
335        }
336
337        // Parse response
338        let response =
339            std::str::from_utf8(&buf[2..i - 1]).map_err(|_| io::Error::other("Invalid UTF-8"))?;
340
341        let (rows, cols) = response
342            .split_once(';')
343            .ok_or_else(|| io::Error::other("Invalid format"))?;
344
345        Ok((
346            rows.parse().map_err(|_| io::Error::other("Invalid row"))?,
347            cols.parse().map_err(|_| io::Error::other("Invalid col"))?,
348        ))
349    }
350
351    /// Try to get the number of columns in the current terminal, or assume 80
352    /// if it fails.
353    fn get_columns(ifd: RawFd, ofd: RawFd) -> usize {
354        // First try with ioctl
355        unsafe {
356            let mut ws: libc::winsize = mem::zeroed();
357            if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col != 0 {
358                return ws.ws_col as usize;
359            }
360        }
361
362        // ioctl() failed. Try to query the terminal itself.
363        // This is the fallback method from the original linenoise.
364
365        // We need to create a temporary terminal to use its methods
366        let temp_terminal = Terminal { ifd, ofd, cols: 80 };
367
368        // Get the initial position so we can restore it later
369        let orig_pos = match temp_terminal.get_cursor_position() {
370            Ok(pos) => pos,
371            Err(_) => return 80,
372        };
373
374        // Go to right margin and get position
375        if temp_terminal.write_bytes(b"\x1b[999C").is_err() {
376            return 80;
377        }
378
379        let cols = match temp_terminal.get_cursor_position() {
380            Ok(pos) => pos.1,
381            Err(_) => 80,
382        };
383
384        // Restore position
385        if orig_pos != (0, 0) {
386            let _ = temp_terminal.write(&format!("\x1b[{};{}H", orig_pos.0, orig_pos.1));
387        }
388
389        cols
390    }
391
392    fn clear_screen(&self) -> io::Result<()> {
393        self.write("\x1b[H\x1b[2J")
394    }
395
396    fn beep(&self) {
397        let _ = self.write("\x07");
398    }
399}
400
401// Line buffer for editing
402struct LineBuffer {
403    chars: Vec<char>,
404    pos: usize,
405}
406
407impl LineBuffer {
408    fn new() -> Self {
409        LineBuffer {
410            chars: Vec::with_capacity(LINENOISE_MAX_LINE),
411            pos: 0,
412        }
413    }
414
415    fn insert(&mut self, c: char) -> bool {
416        if self.chars.len() >= LINENOISE_MAX_LINE - 1 {
417            return false;
418        }
419        self.chars.insert(self.pos, c);
420        self.pos += 1;
421        true
422    }
423
424    fn delete(&mut self) -> bool {
425        if self.pos < self.chars.len() {
426            self.chars.remove(self.pos);
427            true
428        } else {
429            false
430        }
431    }
432
433    fn backspace(&mut self) -> bool {
434        if self.pos > 0 {
435            self.pos -= 1;
436            self.chars.remove(self.pos);
437            true
438        } else {
439            false
440        }
441    }
442
443    fn move_left(&mut self) -> bool {
444        if self.pos > 0 {
445            self.pos -= 1;
446            true
447        } else {
448            false
449        }
450    }
451
452    fn move_right(&mut self) -> bool {
453        if self.pos < self.chars.len() {
454            self.pos += 1;
455            true
456        } else {
457            false
458        }
459    }
460
461    fn move_home(&mut self) {
462        self.pos = 0;
463    }
464
465    fn move_end(&mut self) {
466        self.pos = self.chars.len();
467    }
468
469    fn delete_to_end(&mut self) {
470        self.chars.truncate(self.pos);
471    }
472
473    fn delete_word(&mut self) {
474        let start = self.pos;
475
476        // Skip spaces
477        while self.pos > 0 && self.chars[self.pos - 1] == ' ' {
478            self.pos -= 1;
479        }
480
481        // Skip word
482        while self.pos > 0 && self.chars[self.pos - 1] != ' ' {
483            self.pos -= 1;
484        }
485
486        self.chars.drain(self.pos..start);
487    }
488
489    fn clear(&mut self) {
490        self.chars.clear();
491        self.pos = 0;
492    }
493
494    fn set(&mut self, s: &str) {
495        self.chars = s.chars().take(LINENOISE_MAX_LINE - 1).collect();
496        self.pos = self.chars.len();
497    }
498
499    fn as_string(&self) -> String {
500        self.chars.iter().collect()
501    }
502}
503
504struct Editor {
505    terminal: Terminal,
506    buffer: LineBuffer,
507    prompt: String,
508    history_index: usize,
509    saved_line: Option<String>,
510    completion_state: Option<CompletionState>,
511    old_rows: usize,          // For multiline mode
512    cursor_row_offset: usize, // For multiline mode
513}
514
515struct CompletionState {
516    original_line: String,
517    current_index: usize,
518}
519
520// Helper macro for common key processing pattern
521macro_rules! key_action {
522    ($self:expr, $action:expr) => {{
523        $action;
524        $self.refresh_line()?;
525        Err(io::Error::new(
526            io::ErrorKind::WouldBlock,
527            "More input needed",
528        ))
529    }};
530}
531
532impl Editor {
533    fn new(terminal: Terminal, prompt: &str) -> Self {
534        Editor {
535            terminal,
536            buffer: LineBuffer::new(),
537            prompt: prompt.to_string(),
538            history_index: 0,
539            saved_line: None,
540            completion_state: None,
541            old_rows: 0,
542            cursor_row_offset: 0,
543        }
544    }
545
546    fn refresh_line(&mut self) -> io::Result<()> {
547        // Update terminal columns in case of resize
548        self.terminal.cols = Terminal::get_columns(self.terminal.ifd, self.terminal.ofd);
549
550        let state = G.lock().unwrap();
551
552        if state.multi_line {
553            self.refresh_multiline(&state)
554        } else {
555            self.refresh_singleline(&state)
556        }
557    }
558
559    fn refresh_singleline(&mut self, state: &GlobalState) -> io::Result<()> {
560        let mut output = String::new();
561
562        // Move to start of line
563        output.push('\r');
564
565        // Write prompt
566        output.push_str(&self.prompt);
567
568        // Write buffer content
569        let content = if state.mask_mode {
570            "*".repeat(self.buffer.chars.len())
571        } else {
572            self.buffer.as_string()
573        };
574
575        // Handle line that's too long
576        let prompt_len = self.prompt.chars().count();
577        let available_cols = self.terminal.cols.saturating_sub(prompt_len);
578
579        let cursor_screen_pos = if content.chars().count() > available_cols {
580            // Show a window around the cursor
581            let window_start = self.buffer.pos.saturating_sub(available_cols / 2);
582            let window_end = min(window_start + available_cols, content.chars().count());
583            let actual_window_start = window_end.saturating_sub(available_cols);
584
585            let window: String = content
586                .chars()
587                .skip(actual_window_start)
588                .take(available_cols)
589                .collect();
590            output.push_str(&window);
591
592            // Calculate cursor position within the window
593            prompt_len + self.buffer.pos.saturating_sub(actual_window_start)
594        } else {
595            output.push_str(&content);
596
597            // Add hints if available (but not during completion)
598            if self.completion_state.is_none() {
599                if let Some(ref callback) = state.hints_callback {
600                    if let Some((hint, color, bold)) = callback(&self.buffer.as_string()) {
601                        let remaining = available_cols.saturating_sub(content.chars().count());
602                        if remaining > 0 {
603                            if bold {
604                                output.push_str("\x1b[1m");
605                            }
606                            if color >= 0 {
607                                output.push_str(&format!("\x1b[{color}m"));
608                            }
609                            let hint_truncated: String = hint.chars().take(remaining).collect();
610                            output.push_str(&hint_truncated);
611                            output.push_str("\x1b[0m");
612                        }
613                    }
614                }
615            }
616
617            // When not windowing, cursor position is trivial
618            prompt_len + self.buffer.pos
619        };
620
621        // Clear to end of line
622        output.push_str("\x1b[0K");
623
624        // Position cursor
625        output.push_str(&format!("\r\x1b[{cursor_screen_pos}C"));
626
627        self.terminal.write(&output)
628    }
629
630    fn refresh_multiline(&mut self, state: &GlobalState) -> io::Result<()> {
631        let mut output = String::new();
632        let plen = self.prompt.chars().count();
633        let cols = self.terminal.cols;
634
635        // Calculate dimensions
636        let content_len = plen + self.buffer.chars.len();
637        let cursor_pos = plen + self.buffer.pos;
638
639        // Calculate how many rows we need
640        let content_rows = if content_len == 0 {
641            1
642        } else {
643            (content_len + cols - 1) / cols
644        };
645
646        // Do we need an extra row for cursor at end of line?
647        let phantom_line =
648            self.buffer.pos == self.buffer.chars.len() && cursor_pos > 0 && cursor_pos % cols == 0;
649
650        let total_rows = if phantom_line {
651            content_rows + 1
652        } else {
653            content_rows
654        };
655
656        // Calculate where cursor should be
657        let cursor_row = if cursor_pos == 0 {
658            0
659        } else if phantom_line {
660            content_rows // 0-indexed, so this is the phantom line
661        } else {
662            (cursor_pos - 1) / cols
663        };
664
665        let cursor_col = if phantom_line {
666            0
667        } else if cursor_pos == 0 {
668            plen
669        } else {
670            (cursor_pos - 1) % cols + 1
671        };
672
673        // Move cursor to start of edit area
674        output.push('\r');
675
676        // Then move up by our tracked offset
677        if self.cursor_row_offset > 0 {
678            output.push_str(&format!("\x1b[{}A", self.cursor_row_offset));
679        }
680
681        // Now clear everything
682        let rows_to_clear = self.old_rows.max(total_rows);
683        for i in 0..rows_to_clear {
684            if i > 0 {
685                output.push_str("\r\n"); // New line
686            }
687            output.push_str("\x1b[2K"); // Clear entire line
688        }
689
690        // Go back to start
691        if rows_to_clear > 1 {
692            output.push_str(&format!("\x1b[{}A", rows_to_clear - 1));
693        }
694        output.push('\r');
695
696        // Write content
697        output.push_str(&self.prompt);
698        if state.mask_mode {
699            output.push_str(&"*".repeat(self.buffer.chars.len()));
700        } else {
701            output.push_str(&self.buffer.as_string());
702        }
703
704        // Add hints if appropriate
705        if content_rows == 1 && !phantom_line && self.completion_state.is_none() {
706            if let Some(ref cb) = state.hints_callback {
707                if let Some((hint, color, bold)) = cb(&self.buffer.as_string()) {
708                    let last_line_len = content_len % cols;
709                    let space = if last_line_len == 0 {
710                        0
711                    } else {
712                        cols - last_line_len
713                    };
714
715                    if space > 0 {
716                        let hint_str: String = hint.chars().take(space).collect();
717                        if !hint_str.is_empty() {
718                            if bold {
719                                output.push_str("\x1b[1m");
720                            }
721                            if color >= 0 {
722                                output.push_str(&format!("\x1b[{color}m"));
723                            }
724                            output.push_str(&hint_str);
725                            output.push_str("\x1b[0m");
726                        }
727                    }
728                }
729            }
730        }
731
732        // Add phantom line if needed
733        if phantom_line {
734            output.push_str("\r\n");
735        }
736
737        // Now position cursor
738        // We're currently at end of content
739        let current_row = total_rows - 1;
740
741        // Move to cursor row
742        if cursor_row < current_row {
743            output.push_str(&format!("\x1b[{}A", current_row - cursor_row));
744        } else if cursor_row > current_row {
745            output.push_str(&format!("\x1b[{}B", cursor_row - current_row));
746        }
747
748        // Move to cursor column
749        output.push_str(&format!("\r\x1b[{}C", cursor_col));
750
751        // Update state
752        self.old_rows = total_rows;
753        self.cursor_row_offset = cursor_row;
754
755        self.terminal.write(&output)
756    }
757
758    fn handle_completion(&mut self) -> io::Result<bool> {
759        // Get the completion callback
760        let callback = {
761            let state = G.lock().unwrap();
762            state.completion_callback
763        };
764
765        let Some(cb) = callback else {
766            return Ok(false);
767        };
768
769        // Determine which line to use for completion
770        let line_for_completion = if let Some(ref comp_state) = self.completion_state {
771            // Use the original line saved when completion started
772            comp_state.original_line.clone()
773        } else {
774            // First tab - use current buffer
775            self.buffer.as_string()
776        };
777
778        // Get completions
779        let mut completions = Vec::new();
780        cb(&line_for_completion, &mut completions);
781
782        if completions.is_empty() {
783            self.terminal.beep();
784            self.completion_state = None;
785            return Ok(false);
786        }
787
788        // Update completion state
789        if let Some(ref mut comp_state) = self.completion_state {
790            // Already in completion mode - cycle to next
791            comp_state.current_index = (comp_state.current_index + 1) % completions.len();
792
793            if let Some(completion) = completions.get(comp_state.current_index) {
794                self.buffer.set(completion);
795                self.refresh_line()?;
796            }
797        } else {
798            // First tab - start completion mode
799            self.completion_state = Some(CompletionState {
800                original_line: line_for_completion,
801                current_index: 0,
802            });
803
804            // Show first completion
805            if let Some(first) = completions.first() {
806                self.buffer.set(first);
807                self.refresh_line()?;
808            }
809        }
810
811        Ok(true)
812    }
813
814    fn accept_completion(&mut self) {
815        self.completion_state = None;
816    }
817
818    fn handle_history(&mut self, direction: isize) -> io::Result<()> {
819        let state = G.lock().unwrap();
820        let history_len = state.history.entries.len();
821
822        if history_len == 0 {
823            return Ok(());
824        }
825
826        // Save current line on first history access
827        if self.history_index == 0 && self.saved_line.is_none() {
828            self.saved_line = Some(self.buffer.as_string());
829        }
830
831        // Update history index
832        if direction > 0 {
833            if self.history_index < history_len {
834                self.history_index += 1;
835            }
836        } else if self.history_index > 0 {
837            self.history_index -= 1;
838        }
839
840        // Load history entry or restore saved line
841        if self.history_index == 0 {
842            if let Some(saved) = &self.saved_line {
843                self.buffer.set(saved);
844            }
845        } else if let Some(entry) = state.history.get(self.history_index) {
846            self.buffer.set(entry);
847        }
848
849        drop(state);
850        self.refresh_line()
851    }
852
853    fn handle_escape_sequence(&mut self) -> io::Result<()> {
854        let seq = [
855            self.terminal.read_byte_nonblocking()?,
856            self.terminal.read_byte_nonblocking()?,
857        ];
858
859        let action: Option<fn(&mut Self) -> io::Result<()>> = match seq {
860            [Some(b'['), Some(b'A')] => Some(|s| s.handle_history(1)),
861            [Some(b'['), Some(b'B')] => Some(|s| s.handle_history(-1)),
862            [Some(b'['), Some(b'C')] => Some(|s| {
863                s.buffer.move_right();
864                Ok(())
865            }),
866            [Some(b'['), Some(b'D')] => Some(|s| {
867                s.buffer.move_left();
868                Ok(())
869            }),
870            [Some(b'['), Some(b'H')] | [Some(b'O'), Some(b'H')] => Some(|s| {
871                s.buffer.move_home();
872                Ok(())
873            }),
874            [Some(b'['), Some(b'F')] | [Some(b'O'), Some(b'F')] => Some(|s| {
875                s.buffer.move_end();
876                Ok(())
877            }),
878            _ => None,
879        };
880
881        if let Some(f) = action {
882            f(self)?;
883            self.refresh_line()?;
884        }
885
886        // Special case for delete
887        if matches!(seq, [Some(b'['), Some(b'3')])
888            && matches!(self.terminal.read_byte_nonblocking()?, Some(b'~'))
889            && self.buffer.delete()
890        {
891            self.refresh_line()?;
892        }
893
894        Ok(())
895    }
896
897    /// Process a single input character/byte
898    fn process_key(&mut self, c: u8) -> io::Result<Option<String>> {
899        // Handle completion state
900        if self.completion_state.is_some() && c != Key::Tab as u8 {
901            self.accept_completion();
902        }
903
904        match c {
905            c if c == Key::Enter as u8 => Ok(Some(self.buffer.as_string())),
906            c if c == Key::CtrlC as u8 => Err(io::Error::new(io::ErrorKind::Interrupted, "")),
907            c if c == Key::CtrlD as u8 => {
908                if self.buffer.chars.is_empty() {
909                    Ok(None)
910                } else {
911                    if self.buffer.delete() {
912                        self.refresh_line()?;
913                    }
914                    Err(io::Error::new(
915                        io::ErrorKind::WouldBlock,
916                        "More input needed",
917                    ))
918                }
919            }
920            c if c == Key::Tab as u8 => {
921                self.handle_completion()?;
922                Err(io::Error::new(
923                    io::ErrorKind::WouldBlock,
924                    "More input needed",
925                ))
926            }
927            c if c == Key::Backspace as u8 || c == Key::CtrlH as u8 => key_action!(self, {
928                self.buffer.backspace();
929            }),
930            c if c == Key::CtrlU as u8 => key_action!(self, self.buffer.clear()),
931            c if c == Key::CtrlK as u8 => key_action!(self, self.buffer.delete_to_end()),
932            c if c == Key::CtrlW as u8 => key_action!(self, self.buffer.delete_word()),
933            c if c == Key::CtrlA as u8 => key_action!(self, self.buffer.move_home()),
934            c if c == Key::CtrlE as u8 => key_action!(self, self.buffer.move_end()),
935            c if c == Key::CtrlB as u8 => key_action!(self, {
936                self.buffer.move_left();
937            }),
938            c if c == Key::CtrlF as u8 => key_action!(self, {
939                self.buffer.move_right();
940            }),
941            c if c == Key::CtrlP as u8 => {
942                self.handle_history(1)?;
943                Err(io::Error::new(
944                    io::ErrorKind::WouldBlock,
945                    "More input needed",
946                ))
947            }
948            c if c == Key::CtrlN as u8 => {
949                self.handle_history(-1)?;
950                Err(io::Error::new(
951                    io::ErrorKind::WouldBlock,
952                    "More input needed",
953                ))
954            }
955            c if c == Key::CtrlL as u8 => {
956                self.terminal.clear_screen()?;
957                self.old_rows = 0;
958                self.cursor_row_offset = 0;
959                self.refresh_line()?;
960                Err(io::Error::new(
961                    io::ErrorKind::WouldBlock,
962                    "More input needed",
963                ))
964            }
965            c if c == Key::CtrlT as u8 => {
966                // Transpose chars
967                if self.buffer.pos > 0 && self.buffer.chars.len() > 1 {
968                    if self.buffer.pos == self.buffer.chars.len() {
969                        self.buffer
970                            .chars
971                            .swap(self.buffer.pos - 2, self.buffer.pos - 1);
972                    } else {
973                        self.buffer.chars.swap(self.buffer.pos - 1, self.buffer.pos);
974                        self.buffer.pos += 1;
975                    }
976                    self.refresh_line()?;
977                }
978                Err(io::Error::new(
979                    io::ErrorKind::WouldBlock,
980                    "More input needed",
981                ))
982            }
983            c if c == Key::Esc as u8 => {
984                self.handle_escape_sequence()?;
985                Err(io::Error::new(
986                    io::ErrorKind::WouldBlock,
987                    "More input needed",
988                ))
989            }
990            c if c >= 32 && c < 127 => {
991                // Printable ASCII
992                if self.buffer.insert(c as char) {
993                    self.refresh_line()?;
994                } else {
995                    self.terminal.beep();
996                }
997                Err(io::Error::new(
998                    io::ErrorKind::WouldBlock,
999                    "More input needed",
1000                ))
1001            }
1002            c if c >= 128 => {
1003                // UTF-8 handling
1004                let mut utf8_buf = vec![c];
1005
1006                // Determine how many bytes we need
1007                let bytes_needed = if c & 0xE0 == 0xC0 {
1008                    2
1009                } else if c & 0xF0 == 0xE0 {
1010                    3
1011                } else if c & 0xF8 == 0xF0 {
1012                    4
1013                } else {
1014                    1 // Invalid UTF-8 start byte
1015                };
1016
1017                // Read remaining bytes
1018                for _ in 1..bytes_needed {
1019                    if let Ok(Some(next_byte)) = self.terminal.read_byte() {
1020                        if next_byte & 0xC0 == 0x80 {
1021                            utf8_buf.push(next_byte);
1022                        } else {
1023                            break; // Invalid continuation byte
1024                        }
1025                    } else {
1026                        break;
1027                    }
1028                }
1029
1030                // Try to decode
1031                if utf8_buf.len() == bytes_needed {
1032                    if let Ok(s) = std::str::from_utf8(&utf8_buf) {
1033                        if let Some(ch) = s.chars().next() {
1034                            if self.buffer.insert(ch) {
1035                                self.refresh_line()?;
1036                            } else {
1037                                self.terminal.beep();
1038                            }
1039                        }
1040                    }
1041                } else {
1042                    self.terminal.beep();
1043                }
1044                Err(io::Error::new(
1045                    io::ErrorKind::WouldBlock,
1046                    "More input needed",
1047                ))
1048            }
1049            _ => Err(io::Error::new(
1050                io::ErrorKind::WouldBlock,
1051                "More input needed",
1052            )),
1053        }
1054    }
1055}
1056
1057/// Return true if the terminal name is in the list of terminals we know
1058/// are not able to understand basic escape sequences.
1059fn is_unsupported_term() -> bool {
1060    let unsupported = ["dumb", "cons25", "emacs"];
1061    if let Ok(term) = env::var("TERM") {
1062        unsupported.iter().any(|&t| term.eq_ignore_ascii_case(t))
1063    } else {
1064        false
1065    }
1066}
1067
1068// Public API
1069
1070/// The high level function that is the main API of the linenoise library.
1071/// This function checks if the terminal has basic capabilities, just checking
1072/// for a blacklist of stupid terminals, and later either calls the line editing
1073/// function or uses dummy `fgets()` so that you will be able to type something
1074/// even in the most desperate of conditions.
1075pub fn linenoise(prompt: &str) -> Option<String> {
1076    let terminal = Terminal::new(libc::STDIN_FILENO, libc::STDOUT_FILENO);
1077
1078    if !terminal.is_tty() {
1079        return linenoise_no_tty();
1080    }
1081
1082    if is_unsupported_term() {
1083        return linenoise_unsupported_term(prompt);
1084    }
1085
1086    // Use the multiplexed API internally
1087    let mut state = match LinenoiseState::edit_start(-1, -1, prompt) {
1088        Ok(s) => s,
1089        Err(_) => return None,
1090    };
1091
1092    // Read until we get a result
1093    loop {
1094        match state.edit_feed() {
1095            Ok(Some(line)) => {
1096                let _ = state.edit_stop();
1097                return Some(line);
1098            }
1099            Ok(None) => {
1100                let _ = state.edit_stop();
1101                return None;
1102            }
1103            Err(e) if e.kind() == io::ErrorKind::Interrupted => {
1104                let _ = state.edit_stop();
1105                return None;
1106            }
1107            Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
1108                // Need more input, continue
1109                continue;
1110            }
1111            Err(_) => {
1112                let _ = state.edit_stop();
1113                return None;
1114            }
1115        }
1116    }
1117}
1118
1119/// For when we are not a TTY
1120fn linenoise_no_tty() -> Option<String> {
1121    let mut line = String::new();
1122    match io::stdin().read_line(&mut line) {
1123        Ok(0) => None, // EOF
1124        Ok(_) => {
1125            // Remove trailing newline
1126            if line.ends_with('\n') {
1127                line.pop();
1128                if line.ends_with('\r') {
1129                    line.pop();
1130                }
1131            }
1132            Some(line)
1133        }
1134        Err(_) => None,
1135    }
1136}
1137
1138/// For unsupported terminals provide basic functionality
1139fn linenoise_unsupported_term(prompt: &str) -> Option<String> {
1140    print!("{prompt}");
1141    let _ = io::stdout().flush();
1142
1143    linenoise_no_tty()
1144}
1145
1146/// Toggle multi line mode.
1147pub fn linenoise_set_multi_line(ml: bool) {
1148    G.lock().unwrap().multi_line = ml;
1149}
1150
1151/// Enable mask mode. When it is enabled, instead of the input that
1152/// the user is typing, the terminal will just display a corresponding
1153/// number of asterisks, like "***". This is useful for passwords and
1154/// other secrets that should not be displayed.
1155pub fn linenoise_mask_mode_enable() {
1156    G.lock().unwrap().mask_mode = true;
1157}
1158
1159/// Disable mask mode.
1160pub fn linenoise_mask_mode_disable() {
1161    G.lock().unwrap().mask_mode = false;
1162}
1163
1164/// Register a callback function to be called for tab-completion.
1165pub fn linenoise_set_completion_callback(cb: CompletionCallback) {
1166    G.lock().unwrap().completion_callback = Some(cb);
1167}
1168
1169/// Registers a hints function to be called to show hints to the user
1170/// at the right of the prompt.
1171pub fn linenoise_set_hints_callback(cb: HintsCallback) {
1172    G.lock().unwrap().hints_callback = Some(cb);
1173}
1174
1175/// This is the API call to add a new entry to the linenoise history.
1176pub fn linenoise_history_add(line: &str) -> bool {
1177    G.lock().unwrap().history.add(line)
1178}
1179
1180/// Set the maximum length for the history. This function can be called
1181/// even if there is already some history, the function will make sure
1182/// to retain just the latest `len` elements if the new history length
1183/// value is smaller than the amount of items already inside the history.
1184pub fn linenoise_history_set_max_len(len: usize) -> bool {
1185    if len < 1 {
1186        return false;
1187    }
1188    let mut state = G.lock().unwrap();
1189    state.history.max_len = len;
1190    while state.history.entries.len() > len {
1191        state.history.entries.pop_front();
1192    }
1193    true
1194}
1195
1196/// Save the history to the specified file.
1197pub fn linenoise_history_save(filename: &str) -> io::Result<()> {
1198    let state = G.lock().unwrap();
1199    let mut file = File::create(filename)?;
1200    for entry in &state.history.entries {
1201        writeln!(file, "{entry}")?;
1202    }
1203    Ok(())
1204}
1205
1206/// Load the history from the specified file. If the file does not exist
1207/// then no operation is performed.
1208///
1209/// If file exists then it returns `Ok()` on success and an error on fail.
1210pub fn linenoise_history_load(filename: &str) -> io::Result<()> {
1211    let file = match File::open(filename) {
1212        Ok(f) => f,
1213        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
1214        Err(e) => return Err(e),
1215    };
1216
1217    let reader = BufReader::new(file);
1218    let mut state = G.lock().unwrap();
1219
1220    #[allow(clippy::manual_flatten)]
1221    for line in reader.lines() {
1222        if let Ok(line) = line {
1223            let trimmed = line.trim_end();
1224            if !trimmed.is_empty() {
1225                state.history.add(trimmed);
1226            }
1227        }
1228    }
1229
1230    Ok(())
1231}
1232
1233/// Clear the screen. Used to handle Ctrl+L
1234pub fn linenoise_clear_screen() {
1235    let terminal = Terminal::new(libc::STDIN_FILENO, libc::STDOUT_FILENO);
1236    let _ = terminal.clear_screen();
1237}
1238
1239/// This special mode is used by linenoise in order to print scan codes
1240/// on screen for debugging/development purposes. It is implemented by
1241/// the linenoise example program using the `--keycodes` option.
1242pub fn linenoise_print_key_codes() {
1243    let terminal = Terminal::new(libc::STDIN_FILENO, libc::STDOUT_FILENO);
1244
1245    println!("Linenoise key codes debugging mode.");
1246    println!("Press keys to see scan codes. Type 'quit' to exit.");
1247
1248    let _guard = match terminal.enable_raw_mode() {
1249        Ok(guard) => guard,
1250        Err(_) => return,
1251    };
1252
1253    let mut quit_buf = [0u8; 4];
1254
1255    loop {
1256        if let Ok(Some(c)) = terminal.read_byte() {
1257            // Shift buffer
1258            quit_buf[0] = quit_buf[1];
1259            quit_buf[1] = quit_buf[2];
1260            quit_buf[2] = quit_buf[3];
1261            quit_buf[3] = c;
1262
1263            // Build the output string first to avoid any control char interpretation
1264            let mut output = format!(
1265                "'{}'  {:#04x}",
1266                if c >= 32 && c < 127 { c as char } else { '?' },
1267                c
1268            );
1269
1270            // Append name if known
1271            match c {
1272                1 => output.push_str(" (ctrl-a)"),
1273                2 => output.push_str(" (ctrl-b)"),
1274                3 => output.push_str(" (ctrl-c)"),
1275                4 => output.push_str(" (ctrl-d)"),
1276                5 => output.push_str(" (ctrl-e)"),
1277                6 => output.push_str(" (ctrl-f)"),
1278                8 => output.push_str(" (ctrl-h)"),
1279                9 => output.push_str(" (tab)"),
1280                11 => output.push_str(" (ctrl-k)"),
1281                12 => output.push_str(" (ctrl-l)"),
1282                13 => output.push_str(" (enter)"),
1283                14 => output.push_str(" (ctrl-n)"),
1284                16 => output.push_str(" (ctrl-p)"),
1285                20 => output.push_str(" (ctrl-t)"),
1286                21 => output.push_str(" (ctrl-u)"),
1287                23 => output.push_str(" (ctrl-w)"),
1288                27 => output.push_str(" (esc)"),
1289                127 => output.push_str(" (backspace)"),
1290                _ => {}
1291            }
1292
1293            // Use write to avoid println's processing, add explicit \r\n
1294            let _ = terminal.write(&format!("{}\r\n", output));
1295
1296            if &quit_buf == b"quit" {
1297                break;
1298            }
1299        }
1300    }
1301
1302    // _guard will be dropped here, terminal returns to cooked mode
1303    println!();
1304}
1305
1306/// Multiplexing support
1307pub struct LinenoiseState {
1308    editor: Editor,
1309    active: bool,
1310    _raw_guard: Option<RawModeGuard>,
1311}
1312
1313impl LinenoiseState {
1314    /// This function is part of the multiplexed API in linenoise, that is used in order
1315    /// to implement the blocking variant of the API but can also be called by the user
1316    /// directly in an event-driven program.
1317    pub fn edit_start(stdin_fd: RawFd, stdout_fd: RawFd, prompt: &str) -> io::Result<Self> {
1318        let ifd = if stdin_fd == -1 {
1319            libc::STDIN_FILENO
1320        } else {
1321            stdin_fd
1322        };
1323
1324        let ofd = if stdout_fd == -1 {
1325            libc::STDOUT_FILENO
1326        } else {
1327            stdout_fd
1328        };
1329
1330        let terminal = Terminal::new(ifd, ofd);
1331
1332        if !terminal.is_tty() || is_unsupported_term() {
1333            return Err(io::Error::other("Not supported"));
1334        }
1335
1336        let raw_guard = terminal.enable_raw_mode()?;
1337
1338        let mut editor = Editor::new(terminal, prompt);
1339
1340        // Reset editor state for new session
1341        editor.history_index = 0;
1342        editor.saved_line = None;
1343
1344        // Display initial prompt
1345        editor.refresh_line()?;
1346
1347        Ok(Self {
1348            editor,
1349            active: true,
1350            _raw_guard: Some(raw_guard),
1351        })
1352    }
1353
1354    /// Part of the multiplexed API. Call this function each time there is some data
1355    /// to read from the standard input file descriptor. In case of blocking operations
1356    /// this function can just be called in a loop, and block.
1357    pub fn edit_feed(&mut self) -> io::Result<Option<String>> {
1358        if !self.active {
1359            return Ok(None);
1360        }
1361
1362        // Try to read a byte
1363        match self.editor.terminal.read_byte()? {
1364            Some(c) => {
1365                match self.editor.process_key(c) {
1366                    Ok(result) => {
1367                        if result.is_some() {
1368                            // Move to new line before returning
1369                            let _ = self.editor.terminal.write("\r\n");
1370                            self.active = false;
1371                        }
1372                        Ok(result)
1373                    }
1374                    Err(e) if e.kind() == io::ErrorKind::Interrupted => {
1375                        self.active = false;
1376                        Err(e)
1377                    }
1378                    Err(e) => Err(e),
1379                }
1380            }
1381            None => {
1382                // EOF
1383                self.active = false;
1384                Ok(None)
1385            }
1386        }
1387    }
1388
1389    /// Part of the multiplexed API. At this point the user input is in the buffer,
1390    /// and we can restore the terminal in normal node.
1391    pub fn edit_stop(&mut self) -> io::Result<()> {
1392        if self.active {
1393            self.active = false;
1394            // Drop the guard to restore terminal
1395            self._raw_guard = None;
1396        }
1397        Ok(())
1398    }
1399
1400    /// Hide the current line, when using the multiplexed API.
1401    pub fn hide(&self) -> io::Result<()> {
1402        // Move to beginning of line and clear it
1403        self.editor.terminal.write("\r\x1b[0K")
1404    }
1405
1406    /// Show the current line, when using the multiplexed API.
1407    pub fn show(&mut self) -> io::Result<()> {
1408        // Instead of restoring cursor position which might be stale,
1409        // just move to beginning of line and refresh
1410        self.editor.terminal.write("\r")?;
1411        self.editor.refresh_line()
1412    }
1413
1414    pub fn get_fd(&self) -> RawFd {
1415        self.editor.terminal.ifd
1416    }
1417}