Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dynamic prompt support via ExternalPrinter #772

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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_prompt()?;
}
_ => {
// Ignore the character typed.
}
Expand Down
65 changes: 40 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,33 @@ 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 refresh_prompt(&mut self) -> Result<()> {
self.refresh(None, true, Info::Hint)
}

fn doing_insert(&mut self) {
Expand Down Expand Up @@ -328,7 +344,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 +357,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 +369,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 +386,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 +766,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
8 changes: 8 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 @@ -378,6 +380,9 @@ pub trait Refresher {
fn refresh_line_with_msg(&mut self, msg: Option<&str>) -> Result<()>;
/// Same as `refresh_line` but with a dynamic prompt.
fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()>;
/// Lightweight `refresh_line()` used when the default prompt has been
/// changed.
fn refresh_prompt(&mut self) -> Result<()>;
/// Vi only, switch to insert mode.
fn doing_insert(&mut self);
/// Vi only, switch to command mode.
Expand Down Expand Up @@ -439,6 +444,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