From bc64fb3ef43688e58403fc2f5b000ad9979d09b3 Mon Sep 17 00:00:00 2001 From: "David M. Lary" Date: Sat, 6 Apr 2024 11:56:23 -0500 Subject: [PATCH] Add dynamic prompt support via ExternalPrinter rustyline doesn't currently support changing the prompt while in the core readline loop. There are a number of open PRs and issues for this functionality, but all of them appear to be stalled for more than a year. Looking at #696 and 4ec26e8, the traditional appoach to this is to provide a reference to a trait object (`Prompt` or `ToString`), but with that appoach there's no way to cause the prompt to be redrawn for a change without user input. This means for these appoaches the prompt could change without being displayed to the user. There's an existing mechanism to allow another async task/thread to push input into the core readline loop, the `ExternalPrinter`. In this commit, I expand `ExternalPrinter` to add `set_prompt()`. With various plumbing, this function results in `wait_for_input` to return `Cmd::SetPrompt(String)`. One of key change here is `State.prompt` changes from `&str` to `String`. There is a performance hit here from the copy, but rustyline would need to prompt and receive input hundreds of times per second for the copy to have a noticable performance inpact. Added examples/dynamic_prompt.rs to demonstrate the functionality. Closes #417 Related #208, #372, #369, #417, #598, #696 --- examples/dynamic_prompt.rs | 25 ++++++++++++++++ src/command.rs | 6 +++- src/edit.rs | 61 ++++++++++++++++++++++---------------- src/keymap.rs | 5 ++++ src/lib.rs | 8 ++--- src/tty/mod.rs | 3 ++ src/tty/test.rs | 4 +++ src/tty/unix.rs | 33 +++++++++++++++++---- src/tty/windows.rs | 4 +++ 9 files changed, 114 insertions(+), 35 deletions(-) create mode 100644 examples/dynamic_prompt.rs diff --git a/examples/dynamic_prompt.rs b/examples/dynamic_prompt.rs new file mode 100644 index 000000000..2ef14ed9c --- /dev/null +++ b/examples/dynamic_prompt.rs @@ -0,0 +1,25 @@ +use std::thread; +use std::time::Duration; + +use rustyline::{DefaultEditor, ExternalPrinter, Result}; + +fn main() -> Result<()> { + let mut rl = DefaultEditor::new()?; + let mut printer = rl.create_external_printer()?; + thread::spawn(move || { + let mut i = 0usize; + loop { + printer + .set_prompt(format!("prompt {:02}>", i)) + .expect("set prompt successfully"); + thread::sleep(Duration::from_secs(1)); + i += 1; + } + }); + + loop { + let line = rl.readline("> ")?; + rl.add_history_entry(line.as_str())?; + println!("Line: {line}"); + } +} diff --git a/src/command.rs b/src/command.rs index f0185e2cb..182c699a7 100644 --- a/src/command.rs +++ b/src/command.rs @@ -16,7 +16,7 @@ pub enum Status { pub fn execute( cmd: Cmd, - s: &mut State<'_, '_, H>, + s: &mut State<'_, H>, input_state: &InputState, kill_ring: &mut KillRing, config: &Config, @@ -229,6 +229,10 @@ pub fn execute( s.move_cursor_to_end()?; return Err(error::ReadlineError::Interrupted); } + Cmd::SetPrompt(prompt) => { + s.set_prompt(prompt); + s.refresh_line()?; + } _ => { // Ignore the character typed. } diff --git a/src/edit.rs b/src/edit.rs index e6881eb44..75c3c5e0a 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -23,9 +23,9 @@ use crate::KillRing; /// Represent the state during line editing. /// Implement rendering. -pub struct State<'out, 'prompt, H: Helper> { +pub struct State<'out, H: Helper> { pub out: &'out mut ::Writer, - prompt: &'prompt str, // Prompt to display (rl_prompt) + prompt: String, // Prompt to display (rl_prompt) prompt_size: Position, // Prompt Unicode/visible width and height pub line: LineBuffer, // Edited line buffer pub layout: Layout, @@ -45,14 +45,15 @@ enum Info<'m> { Msg(Option<&'m str>), } -impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { +impl<'out, 'prompt, H: Helper> State<'out, H> { pub fn new( out: &'out mut ::Writer, - prompt: &'prompt str, + prompt: impl Into, helper: Option<&'out H>, ctx: Context<'out>, - ) -> State<'out, 'prompt, H> { - let prompt_size = out.calculate_position(prompt, Position::default()); + ) -> State<'out, H> { + let prompt: String = prompt.into(); + let prompt_size = out.calculate_position(&prompt, Position::default()); State { out, prompt, @@ -97,7 +98,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { { self.prompt_size = self .out - .calculate_position(self.prompt, Position::default()); + .calculate_position(&self.prompt, Position::default()); self.refresh_line()?; } continue; @@ -131,8 +132,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { return Ok(()); } if self.highlight_char() { - let prompt_size = self.prompt_size; - self.refresh(self.prompt, prompt_size, true, Info::NoHint)?; + self.refresh(None, true, Info::NoHint)?; } else { self.out.move_cursor(self.layout.cursor, cursor)?; self.layout.prompt_size = self.prompt_size; @@ -158,8 +158,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { fn refresh( &mut self, - prompt: &str, - prompt_size: Position, + prompt: Option<&str>, default_prompt: bool, info: Info<'_>, ) -> Result<()> { @@ -174,6 +173,17 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { None }; + // if a prompt was specified, calculate the size of it, otherwise use + // the default promp & size. + let (prompt, prompt_size): (&str, Position) = if let Some(prompt) = prompt { + ( + prompt, + self.out.calculate_position(prompt, Position::default()), + ) + } else { + (&self.prompt, self.prompt_size) + }; + let new_layout = self .out .compute_layout(prompt_size, default_prompt, &self.line, info); @@ -228,6 +238,11 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { self.layout.default_prompt } + pub fn set_prompt(&mut self, prompt: String) { + self.prompt_size = self.out.calculate_position(&prompt, Position::default()); + self.prompt = prompt; + } + pub fn validate(&mut self) -> Result { if let Some(validator) = self.helper { self.changes.begin(); @@ -256,32 +271,29 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } } -impl<'out, 'prompt, H: Helper> Invoke for State<'out, 'prompt, H> { +impl<'out, H: Helper> Invoke for State<'out, H> { fn input(&self) -> &str { self.line.as_str() } } -impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { +impl<'out, H: Helper> Refresher for State<'out, H> { fn refresh_line(&mut self) -> Result<()> { - let prompt_size = self.prompt_size; self.hint(); self.highlight_char(); - self.refresh(self.prompt, prompt_size, true, Info::Hint) + self.refresh(None, true, Info::Hint) } fn refresh_line_with_msg(&mut self, msg: Option<&str>) -> Result<()> { - let prompt_size = self.prompt_size; self.hint = None; self.highlight_char(); - self.refresh(self.prompt, prompt_size, true, Info::Msg(msg)) + self.refresh(None, true, Info::Msg(msg)) } fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()> { - let prompt_size = self.out.calculate_position(prompt, Position::default()); self.hint(); self.highlight_char(); - self.refresh(prompt, prompt_size, false, Info::Hint) + self.refresh(Some(prompt), false, Info::Hint) } fn doing_insert(&mut self) { @@ -328,7 +340,7 @@ impl<'out, 'prompt, H: Helper> Refresher for State<'out, 'prompt, H> { } } -impl<'out, 'prompt, H: Helper> fmt::Debug for State<'out, 'prompt, H> { +impl<'out, H: Helper> fmt::Debug for State<'out, H> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("State") .field("prompt", &self.prompt) @@ -341,7 +353,7 @@ impl<'out, 'prompt, H: Helper> fmt::Debug for State<'out, 'prompt, H> { } } -impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { +impl<'out, H: Helper> State<'out, H> { pub fn clear_screen(&mut self) -> Result<()> { self.out.clear_screen()?; self.layout.cursor = Position::default(); @@ -353,7 +365,6 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { pub fn edit_insert(&mut self, ch: char, n: RepeatCount) -> Result<()> { if let Some(push) = self.line.insert(ch, n, &mut self.changes) { if push { - let prompt_size = self.prompt_size; let no_previous_hint = self.hint.is_none(); self.hint(); let width = ch.width().unwrap_or(0); @@ -371,7 +382,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { let bits = ch.encode_utf8(&mut self.byte_buffer); self.out.write_and_flush(bits) } else { - self.refresh(self.prompt, prompt_size, true, Info::Hint) + self.refresh(None, true, Info::Hint) } } else { self.refresh_line() @@ -751,10 +762,10 @@ pub fn init_state<'out, H: Helper>( pos: usize, helper: Option<&'out H>, history: &'out crate::history::DefaultHistory, -) -> State<'out, 'static, H> { +) -> State<'out, H> { State { out, - prompt: "", + prompt: "".to_string(), prompt_size: Position::default(), line: LineBuffer::init(line, pos), layout: Layout::default(), diff --git a/src/keymap.rs b/src/keymap.rs index b7303df84..f77c4ed6e 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -86,6 +86,8 @@ pub enum Cmd { SelfInsert(RepeatCount, char), /// Suspend signal (Ctrl-Z on unix platform) Suspend, + /// change the prompt + SetPrompt(String), /// transpose-chars TransposeChars, /// transpose-words @@ -439,6 +441,9 @@ impl<'b> InputState<'b> { tty::Event::ExternalPrint(msg) => { wrt.external_print(msg)?; } + tty::Event::SetPrompt(prompt) => { + return Ok(Cmd::SetPrompt(prompt)); + } } } } diff --git a/src/lib.rs b/src/lib.rs index 9f0cee529..32334c90e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,7 +76,7 @@ pub type Result = result::Result; /// Completes the line/word fn complete_line( rdr: &mut ::Reader, - s: &mut State<'_, '_, H>, + s: &mut State<'_, H>, input_state: &mut InputState, config: &Config, ) -> Result> { @@ -263,7 +263,7 @@ fn complete_line( } /// Completes the current hint -fn complete_hint_line(s: &mut State<'_, '_, H>) -> Result<()> { +fn complete_hint_line(s: &mut State<'_, H>) -> Result<()> { let hint = match s.hint.as_ref() { Some(hint) => hint, None => return Ok(()), @@ -281,7 +281,7 @@ fn complete_hint_line(s: &mut State<'_, '_, H>) -> Result<()> { fn page_completions( rdr: &mut ::Reader, - s: &mut State<'_, '_, H>, + s: &mut State<'_, H>, input_state: &mut InputState, candidates: &[C], ) -> Result> { @@ -362,7 +362,7 @@ fn page_completions( /// Incremental search fn reverse_incremental_search( rdr: &mut ::Reader, - s: &mut State<'_, '_, H>, + s: &mut State<'_, H>, input_state: &mut InputState, history: &I, ) -> Result> { diff --git a/src/tty/mod.rs b/src/tty/mod.rs index 62484f042..cbed9dc26 100644 --- a/src/tty/mod.rs +++ b/src/tty/mod.rs @@ -19,6 +19,7 @@ pub trait RawMode: Sized { pub enum Event { KeyPress(KeyEvent), ExternalPrint(String), + SetPrompt(String), } /// Translate bytes read from stdin to keys. @@ -214,6 +215,8 @@ fn width(s: &str, esc_seq: &mut u8) -> usize { pub trait ExternalPrinter { /// Print message to stdout fn print(&mut self, msg: String) -> Result<()>; + /// Change the prompt + fn set_prompt(&mut self, prompt: String) -> Result<()>; } /// Terminal contract diff --git a/src/tty/test.rs b/src/tty/test.rs index a7fa01fc8..4f80efc46 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -159,6 +159,10 @@ impl ExternalPrinter for DummyExternalPrinter { fn print(&mut self, _msg: String) -> Result<()> { Ok(()) } + + fn set_prompt(&mut self, _prompt: String) -> Result<()> { + Ok(()) + } } pub type Terminal = DummyTerminal; diff --git a/src/tty/unix.rs b/src/tty/unix.rs index c0c8a0d24..96fab55d4 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -183,9 +183,9 @@ impl TtyIn { } // (native receiver with a selectable file descriptor, actual message receiver) -type PipeReader = Arc)>>; +type PipeReader = Arc)>>; // (native sender, actual message sender) -type PipeWriter = (Arc>, SyncSender); +type PipeWriter = (Arc>, SyncSender); /// Console input reader pub struct PosixRawReader { @@ -763,7 +763,10 @@ impl PosixRawReader { let mut buf = [0; 1]; guard.0.read_exact(&mut buf)?; if let Ok(msg) = guard.1.try_recv() { - return Ok(Event::ExternalPrint(msg)); + return match msg { + ExternalPrinterMsg::Print(str) => Ok(Event::ExternalPrint(str)), + ExternalPrinterMsg::SetPrompt(prompt) => Ok(Event::SetPrompt(prompt)), + }; } } } @@ -1451,7 +1454,7 @@ impl Term for PosixTerminal { return Err(nix::Error::ENOTTY.into()); } use nix::unistd::pipe; - let (sender, receiver) = mpsc::sync_channel(1); // TODO validate: bound + let (sender, receiver) = mpsc::sync_channel::(1); // TODO validate: bound let (r, w) = pipe()?; let reader = Arc::new(Mutex::new((r.into(), receiver))); let writer = (Arc::new(Mutex::new(w.into())), sender); @@ -1501,7 +1504,7 @@ impl super::ExternalPrinter for ExternalPrinter { } else if let Ok(mut writer) = self.writer.0.lock() { self.writer .1 - .send(msg) + .send(ExternalPrinterMsg::Print(msg)) .map_err(|_| io::Error::from(ErrorKind::Other))?; // FIXME writer.write_all(&[b'm'])?; writer.flush()?; @@ -1510,6 +1513,26 @@ impl super::ExternalPrinter for ExternalPrinter { } Ok(()) } + + fn set_prompt(&mut self, prompt: String) -> Result<()> { + if let Ok(mut writer) = self.writer.0.lock() { + self.writer + .1 + .send(ExternalPrinterMsg::SetPrompt(prompt)) + .map_err(|_| io::Error::from(ErrorKind::Other))?; // FIXME + writer.write_all(&[b'm'])?; + writer.flush()?; + } else { + return Err(io::Error::from(ErrorKind::Other).into()); // FIXME + } + Ok(()) + } +} + +#[derive(Debug)] +enum ExternalPrinterMsg { + Print(String), + SetPrompt(String), } #[cfg(not(test))] diff --git a/src/tty/windows.rs b/src/tty/windows.rs index fd2a06333..565e63f9b 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -913,6 +913,10 @@ impl super::ExternalPrinter for ExternalPrinter { Ok(check(unsafe { threading::SetEvent(self.event) })?) } } + + fn set_prompt(&mut self, prompt: String) -> Result<()> { + unimplemented!() + } } #[derive(Debug)]