Skip to content
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
15 changes: 15 additions & 0 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ pub fn execute<H: Helper, P: Prompt + ?Sized>(
Cmd::Insert(n, text) => {
s.edit_yank(input_state, &text, Anchor::Before, n)?;
}
Cmd::Macro(macro_str) => {
s.start_macro(macro_str);
}
Cmd::MacroClearLine(macro_str) => {
// Save the current line content for restoration after macro execution
if !s.line.is_empty() {
let line_content = s.line.as_str().to_string();
s.macro_player_mut().set_pending_restore(line_content);
}
// Clear the current line
s.edit_kill(&Movement::WholeLine, kill_ring)?;
s.changes.end();
// Then start the macro
s.start_macro(macro_str);
}
Cmd::Move(Movement::BeginningOfLine) => {
// Move to the beginning of line.
s.edit_move_home()?;
Expand Down
27 changes: 27 additions & 0 deletions src/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word};
use crate::keymap::{InputState, Invoke, Refresher};
use crate::layout::{cwidh, Layout, Position, Unit};
use crate::line_buffer::{DeleteListener, Direction, LineBuffer, NoListener, WordAction, MAX_LINE};
use crate::macro_player::MacroPlayer;
use crate::tty::{Renderer, Term, Terminal};
use crate::undo::Changeset;
use crate::validate::{ValidationContext, ValidationResult};
Expand All @@ -34,6 +35,7 @@ pub struct State<'out, 'prompt, H: Helper, P: Prompt + ?Sized> {
pub ctx: Context<'out>, // Give access to history for `hinter`
pub hint: Option<Box<dyn Hint>>, // last hint displayed
pub highlight_char: bool, // `true` if a char has been highlighted
macro_player: MacroPlayer, // Macro player for keystroke replay
}

enum Info<'m> {
Expand Down Expand Up @@ -73,9 +75,24 @@ impl<'out, 'prompt, H: Helper, P: Prompt + ?Sized> State<'out, 'prompt, H, P> {
ctx,
hint: None,
highlight_char: false,
macro_player: MacroPlayer::default(),
}
}

/// Start macro replay
///
/// Initiates replay of a macro string character-by-character.
/// Each character in the macro will be processed as if the user typed it,
/// with `\n` characters triggering `AcceptLine` to submit the input.
pub fn start_macro(&mut self, macro_str: String) {
self.macro_player.start(macro_str);
}

/// Get mutable access to the macro player
pub fn macro_player_mut(&mut self) -> &mut MacroPlayer {
&mut self.macro_player
}

pub fn highlighter(&self) -> Option<&dyn Highlighter> {
if self.out.colors_enabled() {
self.helper.map(|h| h as &dyn Highlighter)
Expand All @@ -92,6 +109,15 @@ impl<'out, 'prompt, H: Helper, P: Prompt + ?Sized> State<'out, 'prompt, H, P> {
ignore_external_print: bool,
) -> Result<Cmd> {
loop {
// Check if we're replaying a macro
if let Some(ch) = self.macro_player.next() {
// Convert character to appropriate command
return Ok(match ch {
'\n' => Cmd::AcceptLine,
c => Cmd::SelfInsert(1, c),
});
}

let rc = input_state.next_cmd(rdr, self, single_esc_abort, ignore_external_print);
if let Err(ReadlineError::Signal(signal)) = rc {
match signal {
Expand Down Expand Up @@ -833,6 +859,7 @@ pub fn init_state<'out, H: Helper>(
ctx: Context::new(history),
hint: Some(Box::new("hint".to_owned())),
highlight_char: false,
macro_player: MacroPlayer::default(),
}
}

Expand Down
27 changes: 27 additions & 0 deletions src/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,32 @@ pub enum Cmd {
/// of the current input
accept_in_the_middle: bool,
},
/// Execute a macro (replay keystrokes)
///
/// Replays the given string character-by-character as if the user typed each
/// character. Newline characters (`\n`) are converted to `AcceptLine` commands
/// to automatically submit the input.
Macro(String),
/// Execute a macro after clearing the current line
///
/// Clears the current line content, then replays the given string
/// character-by-character as if the user typed each character.
/// Newline characters (`\n`) are converted to `AcceptLine` commands
/// to automatically submit the input.
///
/// The cleared line content is saved and can be restored by the application
/// on the next readline call. See [`Editor::take_pending_restore`] for details.
///
/// # Example
/// ```ignore
/// // Check for pending restore before each readline
/// let result = if let Some(restore) = rl.take_pending_restore() {
/// rl.readline_with_initial("> ", (&restore, ""))
/// } else {
/// rl.readline("> ")
/// };
/// ```
MacroClearLine(String),
}

impl Cmd {
Expand All @@ -141,6 +167,7 @@ impl Cmd {
| Self::Suspend
| Self::Yank(..)
| Self::YankPop => false,
Self::Macro(_) | Self::MacroClearLine(_) => true,
_ => true,
}
}
Expand Down
36 changes: 36 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ mod kill_ring;
mod layout;
pub mod line_buffer;
mod prompt;
mod macro_player;
#[cfg(feature = "with-sqlite-history")]
pub mod sqlite_history;
mod tty;
Expand Down Expand Up @@ -593,6 +594,7 @@ pub struct Editor<H: Helper, I: History> {
kill_ring: KillRing,
config: Config,
custom_bindings: Bindings,
pending_restore: Option<String>,
}

/// Default editor with no helper and `DefaultHistory`
Expand Down Expand Up @@ -623,9 +625,37 @@ impl<H: Helper, I: History> Editor<H, I> {
kill_ring: KillRing::new(60),
config,
custom_bindings: Bindings::new(),
pending_restore: None,
})
}

/// Set content to be restored on the next readline call
///
/// This is used by MacroClearLine to restore the cleared line after the macro executes.
/// Generally, applications don't need to call this directly.
pub fn set_pending_restore(&mut self, content: String) {
self.pending_restore = Some(content);
}

/// Get and clear any pending restore content
///
/// Returns the saved content that should be restored on the next readline call.
/// This is set by [`Cmd::MacroClearLine`] when it clears the current line.
///
/// Applications should check this before each readline call and use
/// [`Editor::readline_with_initial`] to restore the content:
///
/// ```ignore
/// let result = if let Some(restore) = rl.take_pending_restore() {
/// rl.readline_with_initial("> ", (&restore, ""))
/// } else {
/// rl.readline("> ")
/// };
/// ```
pub fn take_pending_restore(&mut self) -> Option<String> {
self.pending_restore.take()
}

/// This method will read a line from STDIN and will display a `prompt`.
///
/// `prompt` should not be styled (in case the terminal doesn't support
Expand Down Expand Up @@ -804,6 +834,12 @@ impl<H: Helper, I: History> Editor<H, I> {
let _ = original_mode; // silent warning
}
self.buffer = rdr.unbuffer();

// Transfer pending_restore from MacroPlayer to Editor for next readline call
if let Some(restore_content) = s.macro_player_mut().take_pending_restore() {
self.pending_restore = Some(restore_content);
}

Ok(s.line.into_string())
}

Expand Down
62 changes: 62 additions & 0 deletions src/macro_player.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Macro: replays keystroke sequences

#[derive(Debug, Default)]
pub struct MacroPlayer {
buffer: Vec<char>,
position: usize,
pending_restore: Option<String>,
}

impl MacroPlayer {
/// Start playing a new macro, stripping \r characters
pub fn start(&mut self, macro_str: String) {
self.buffer = macro_str.chars().filter(|&c| c != '\r').collect();
self.position = 0;
}

/// Set content to be restored on the next readline call
///
/// This is called by [`Cmd::MacroClearLine`] to save the cleared line content.
/// The content is transferred to [`Editor`] at the end of the readline session.
pub fn set_pending_restore(&mut self, content: String) {
self.pending_restore = Some(content);
}

/// Get and clear any pending restore content
///
/// This is called internally to transfer the pending restore to [`Editor`]
/// at the end of the readline session.
pub fn take_pending_restore(&mut self) -> Option<String> {
self.pending_restore.take()
}
}

impl Iterator for MacroPlayer {
type Item = char;

fn next(&mut self) -> Option<Self::Item> {
if self.position < self.buffer.len() {
let ch = self.buffer[self.position];
self.position += 1;
Some(ch)
} else {
None
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_strips_carriage_returns() {
let mut player = MacroPlayer::default();
player.start("a\r\nb".to_string());

assert_eq!(player.next(), Some('a'));
assert_eq!(player.next(), Some('\n'));
assert_eq!(player.next(), Some('b'));
assert_eq!(player.next(), None);
}
}