Skip to content

Commit

Permalink
Add dynamic prompt support via ExternalPrinter
Browse files Browse the repository at this point in the history
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 kkawakam#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 kkawakam#417

Related kkawakam#208, kkawakam#372, kkawakam#369, kkawakam#417, kkawakam#598, kkawakam#696
  • Loading branch information
dmlary committed Apr 6, 2024
1 parent bd63fea commit bc64fb3
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 35 deletions.
25 changes: 25 additions & 0 deletions examples/dynamic_prompt.rs
Original file line number Diff line number Diff line change
@@ -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}");
}
}
6 changes: 5 additions & 1 deletion src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub enum Status {

pub fn execute<H: Helper>(
cmd: Cmd,
s: &mut State<'_, '_, H>,
s: &mut State<'_, H>,
input_state: &InputState,
kill_ring: &mut KillRing,
config: &Config,
Expand Down Expand Up @@ -229,6 +229,10 @@ pub fn execute<H: Helper>(
s.move_cursor_to_end()?;
return Err(error::ReadlineError::Interrupted);
}
Cmd::SetPrompt(prompt) => {
s.set_prompt(prompt);
s.refresh_line()?;
}
_ => {
// Ignore the character typed.
}
Expand Down
61 changes: 36 additions & 25 deletions src/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Terminal as Term>::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,
Expand All @@ -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 <Terminal as Term>::Writer,
prompt: &'prompt str,
prompt: impl Into<String>,
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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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<()> {
Expand All @@ -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);
Expand Down Expand Up @@ -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<ValidationResult> {
if let Some(validator) = self.helper {
self.changes.begin();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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();
Expand All @@ -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);
Expand All @@ -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()
Expand Down Expand Up @@ -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(),
Expand Down
5 changes: 5 additions & 0 deletions src/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub type Result<T> = result::Result<T, ReadlineError>;
/// Completes the line/word
fn complete_line<H: Helper>(
rdr: &mut <Terminal as Term>::Reader,
s: &mut State<'_, '_, H>,
s: &mut State<'_, H>,
input_state: &mut InputState,
config: &Config,
) -> Result<Option<Cmd>> {
Expand Down Expand Up @@ -263,7 +263,7 @@ fn complete_line<H: Helper>(
}

/// Completes the current hint
fn complete_hint_line<H: Helper>(s: &mut State<'_, '_, H>) -> Result<()> {
fn complete_hint_line<H: Helper>(s: &mut State<'_, H>) -> Result<()> {
let hint = match s.hint.as_ref() {
Some(hint) => hint,
None => return Ok(()),
Expand All @@ -281,7 +281,7 @@ fn complete_hint_line<H: Helper>(s: &mut State<'_, '_, H>) -> Result<()> {

fn page_completions<C: Candidate, H: Helper>(
rdr: &mut <Terminal as Term>::Reader,
s: &mut State<'_, '_, H>,
s: &mut State<'_, H>,
input_state: &mut InputState,
candidates: &[C],
) -> Result<Option<Cmd>> {
Expand Down Expand Up @@ -362,7 +362,7 @@ fn page_completions<C: Candidate, H: Helper>(
/// Incremental search
fn reverse_incremental_search<H: Helper, I: History>(
rdr: &mut <Terminal as Term>::Reader,
s: &mut State<'_, '_, H>,
s: &mut State<'_, H>,
input_state: &mut InputState,
history: &I,
) -> Result<Option<Cmd>> {
Expand Down
3 changes: 3 additions & 0 deletions src/tty/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub trait RawMode: Sized {
pub enum Event {
KeyPress(KeyEvent),
ExternalPrint(String),
SetPrompt(String),
}

/// Translate bytes read from stdin to keys.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/tty/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
33 changes: 28 additions & 5 deletions src/tty/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,9 @@ impl TtyIn {
}

// (native receiver with a selectable file descriptor, actual message receiver)
type PipeReader = Arc<Mutex<(File, mpsc::Receiver<String>)>>;
type PipeReader = Arc<Mutex<(File, mpsc::Receiver<ExternalPrinterMsg>)>>;
// (native sender, actual message sender)
type PipeWriter = (Arc<Mutex<File>>, SyncSender<String>);
type PipeWriter = (Arc<Mutex<File>>, SyncSender<ExternalPrinterMsg>);

/// Console input reader
pub struct PosixRawReader {
Expand Down Expand Up @@ -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)),
};
}
}
}
Expand Down Expand Up @@ -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::<ExternalPrinterMsg>(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);
Expand Down Expand Up @@ -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()?;
Expand All @@ -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))]
Expand Down
4 changes: 4 additions & 0 deletions src/tty/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down

0 comments on commit bc64fb3

Please sign in to comment.