Skip to main content

rustpython_vm/
readline.rs

1//! Readline interface for REPLs
2//!
3//! This module provides a common interface for reading lines from the console, with support for history and completion.
4//! It uses the [`rustyline`] crate on non-WASM platforms and a custom implementation on WASM platforms.
5
6use std::{io, path::Path};
7
8type OtherError = Box<dyn core::error::Error>;
9type OtherResult<T> = Result<T, OtherError>;
10
11pub enum ReadlineResult {
12    Line(String),
13    Eof,
14    Interrupt,
15    Io(std::io::Error),
16    #[cfg(unix)]
17    OsError(nix::Error),
18    Other(OtherError),
19}
20
21#[allow(unused)]
22mod basic_readline {
23    use super::*;
24
25    pub trait Helper {}
26    impl<T> Helper for T {}
27
28    pub struct Readline<H: Helper> {
29        helper: H,
30    }
31
32    impl<H: Helper> Readline<H> {
33        pub const fn new(helper: H) -> Self {
34            Self { helper }
35        }
36
37        pub fn load_history(&mut self, _path: &Path) -> OtherResult<()> {
38            Ok(())
39        }
40
41        pub fn save_history(&mut self, _path: &Path) -> OtherResult<()> {
42            Ok(())
43        }
44
45        pub fn add_history_entry(&mut self, _entry: &str) -> OtherResult<()> {
46            Ok(())
47        }
48
49        pub fn readline(&mut self, prompt: &str) -> ReadlineResult {
50            use std::io::prelude::*;
51            print!("{prompt}");
52            if let Err(e) = io::stdout().flush() {
53                return ReadlineResult::Io(e);
54            }
55
56            let next_line = io::stdin().lock().lines().next();
57            match next_line {
58                Some(Ok(line)) => ReadlineResult::Line(line),
59                None => ReadlineResult::Eof,
60                Some(Err(e)) if e.kind() == io::ErrorKind::Interrupted => ReadlineResult::Interrupt,
61                Some(Err(e)) => ReadlineResult::Io(e),
62            }
63        }
64    }
65}
66
67#[cfg(not(target_arch = "wasm32"))]
68mod rustyline_readline {
69    use super::*;
70
71    pub trait Helper: rustyline::Helper {}
72    impl<T: rustyline::Helper> Helper for T {}
73
74    /// Readline: the REPL
75    pub struct Readline<H: Helper> {
76        repl: rustyline::Editor<H, rustyline::history::DefaultHistory>,
77    }
78
79    #[cfg(windows)]
80    const EOF_CHAR: &str = "\u{001A}";
81
82    impl<H: Helper> Readline<H> {
83        pub fn new(helper: H) -> Self {
84            use rustyline::*;
85            let mut repl = Editor::with_config(
86                Config::builder()
87                    .completion_type(CompletionType::List)
88                    .tab_stop(8)
89                    .bracketed_paste(false) // multi-line paste
90                    .build(),
91            )
92            .expect("failed to initialize line editor");
93            repl.set_helper(Some(helper));
94
95            // Bind CTRL + Z to insert EOF character on Windows
96            #[cfg(windows)]
97            {
98                repl.bind_sequence(
99                    KeyEvent::new('z', Modifiers::CTRL),
100                    EventHandler::Simple(Cmd::Insert(1, EOF_CHAR.into())),
101                );
102            }
103
104            Self { repl }
105        }
106
107        pub fn load_history(&mut self, path: &Path) -> OtherResult<()> {
108            self.repl.load_history(path)?;
109            Ok(())
110        }
111
112        pub fn save_history(&mut self, path: &Path) -> OtherResult<()> {
113            if !path.exists()
114                && let Some(parent) = path.parent()
115            {
116                std::fs::create_dir_all(parent)?;
117            }
118            self.repl.save_history(path)?;
119            Ok(())
120        }
121
122        pub fn add_history_entry(&mut self, entry: &str) -> OtherResult<()> {
123            self.repl.add_history_entry(entry)?;
124            Ok(())
125        }
126
127        pub fn readline(&mut self, prompt: &str) -> ReadlineResult {
128            use rustyline::error::ReadlineError;
129            loop {
130                break match self.repl.readline(prompt) {
131                    Ok(line) => {
132                        // Check for CTRL + Z on Windows
133                        #[cfg(windows)]
134                        {
135                            use std::io::IsTerminal;
136
137                            let trimmed = line.trim_end_matches(&['\r', '\n'][..]);
138                            if trimmed == EOF_CHAR && io::stdin().is_terminal() {
139                                return ReadlineResult::Eof;
140                            }
141                        }
142                        ReadlineResult::Line(line)
143                    }
144                    Err(ReadlineError::Interrupted) => ReadlineResult::Interrupt,
145                    Err(ReadlineError::Eof) => ReadlineResult::Eof,
146                    Err(ReadlineError::Io(e)) => ReadlineResult::Io(e),
147                    Err(ReadlineError::Signal(_)) => continue,
148                    #[cfg(unix)]
149                    Err(ReadlineError::Errno(num)) => ReadlineResult::OsError(num),
150                    Err(e) => ReadlineResult::Other(e.into()),
151                };
152            }
153        }
154    }
155}
156
157#[cfg(target_arch = "wasm32")]
158use basic_readline as readline_inner;
159#[cfg(not(target_arch = "wasm32"))]
160use rustyline_readline as readline_inner;
161
162pub use readline_inner::Helper;
163
164pub struct Readline<H: Helper>(readline_inner::Readline<H>);
165
166impl<H: Helper> Readline<H> {
167    pub fn new(helper: H) -> Self {
168        Self(readline_inner::Readline::new(helper))
169    }
170
171    pub fn load_history(&mut self, path: &Path) -> OtherResult<()> {
172        self.0.load_history(path)
173    }
174
175    pub fn save_history(&mut self, path: &Path) -> OtherResult<()> {
176        self.0.save_history(path)
177    }
178
179    pub fn add_history_entry(&mut self, entry: &str) -> OtherResult<()> {
180        self.0.add_history_entry(entry)
181    }
182
183    pub fn readline(&mut self, prompt: &str) -> ReadlineResult {
184        self.0.readline(prompt)
185    }
186}