Skip to main content

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