Skip to content
Draft
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
78 changes: 78 additions & 0 deletions examples/chords.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Example demonstrating multi-keystroke chord support in reedline
//
// This example shows how to configure key chords (multi-key sequences)
// that trigger actions. For example, pressing Ctrl+X followed by Ctrl+C
// can be bound to a specific event.

use crossterm::event::{KeyCode, KeyModifiers};
use reedline::{
default_emacs_keybindings, DefaultPrompt, Emacs, KeyCombination, Reedline, ReedlineEvent,
Signal,
};
use std::io;

/// Helper to create a KeyCombination for Ctrl+<char>
fn ctrl(c: char) -> KeyCombination {
KeyCombination {
modifier: KeyModifiers::CONTROL,
key_code: KeyCode::Char(c),
}
}

fn main() -> io::Result<()> {
println!("Reedline Chord Example");
println!("======================");
println!();
println!("This example demonstrates multi-keystroke chord bindings.");
println!();
println!("Available chords:");
println!(" Ctrl+X Ctrl+C - Quit");
println!(" Ctrl+X Ctrl+Y Ctrl+Z Ctrl+Z Ctrl+Y - Report that nothing happens");
println!();
println!("Regular keys and single-key bindings still work normally.");
println!("You may also type 'exit' or press Ctrl+D to quit.");
println!();

// Start with the default Emacs keybindings
let mut keybindings = default_emacs_keybindings();

// Add chord bindings
// Ctrl+X Ctrl+C: Quit
keybindings.add_sequence_binding(&[ctrl('x'), ctrl('c')], ReedlineEvent::CtrlD);

// Ctrl+X Ctrl+Y Ctrl+Z Ctrl+Z Ctrl+Y: Quit
keybindings.add_sequence_binding(
&[ctrl('x'), ctrl('y'), ctrl('z'), ctrl('z'), ctrl('y')],
ReedlineEvent::ExecuteHostCommand(String::from("Nothing happens")),
);

// Create the Emacs edit mode with our custom keybindings
let edit_mode = Box::new(Emacs::new(keybindings));

// Create the line editor
let mut line_editor = Reedline::create().with_edit_mode(edit_mode);

let prompt = DefaultPrompt::default();

loop {
let sig = line_editor.read_line(&prompt)?;
match sig {
Signal::Success(buffer) => {
if buffer == "exit" {
println!("Goodbye!");
break;
}
println!("You typed: {buffer}");
}
Signal::CtrlD => {
println!("\nQuitting.");
break;
}
Signal::CtrlC => {
println!("Ctrl+C pressed");
}
}
}

Ok(())
}
122 changes: 76 additions & 46 deletions src/edit_mode/emacs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use crate::{
edit_mode::{
keybindings::{
add_common_control_bindings, add_common_edit_bindings, add_common_navigation_bindings,
add_common_selection_bindings, edit_bind, Keybindings,
add_common_selection_bindings, edit_bind, KeyBindingTarget, KeyCombination,
Keybindings,
},
EditMode,
},
Expand Down Expand Up @@ -104,12 +105,16 @@ pub fn default_emacs_keybindings() -> Keybindings {

/// This parses the incoming Events like a emacs style-editor
pub struct Emacs {
/// Cache for multi-key sequences (chords); will be empty when not in a known chord
cache: Vec<KeyCombination>,
/// Keybindings for this mode
keybindings: Keybindings,
}

impl Default for Emacs {
fn default() -> Self {
Emacs {
cache: Vec::new(),
keybindings: default_emacs_keybindings(),
}
}
Expand All @@ -120,50 +125,10 @@ impl EditMode for Emacs {
match event.into() {
Event::Key(KeyEvent {
code, modifiers, ..
}) => match (modifiers, code) {
(modifier, KeyCode::Char(c)) => {
// Note. The modifier can also be a combination of modifiers, for
// example:
// KeyModifiers::CONTROL | KeyModifiers::ALT
// KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT
//
// Mixed modifiers are used by non american keyboards that have extra
// keys like 'alt gr'. Keep this in mind if in the future there are
// cases where an event is not being captured
let c = match modifier {
KeyModifiers::NONE => c,
_ => c.to_ascii_lowercase(),
};

self.keybindings
.find_binding(modifier, KeyCode::Char(c))
.unwrap_or_else(|| {
if modifier == KeyModifiers::NONE
|| modifier == KeyModifiers::SHIFT
|| modifier == KeyModifiers::CONTROL | KeyModifiers::ALT
|| modifier
== KeyModifiers::CONTROL
| KeyModifiers::ALT
| KeyModifiers::SHIFT
{
ReedlineEvent::Edit(vec![EditCommand::InsertChar(
if modifier == KeyModifiers::SHIFT {
c.to_ascii_uppercase()
} else {
c
},
)])
} else {
ReedlineEvent::None
}
})
}
_ => self
.keybindings
.find_binding(modifiers, code)
.unwrap_or(ReedlineEvent::None),
},

}) => self.parse_key_event(KeyCombination {
key_code: code,
modifier: modifiers,
}),
Event::Mouse(_) => ReedlineEvent::Mouse,
Event::Resize(width, height) => ReedlineEvent::Resize(width, height),
Event::FocusGained => ReedlineEvent::None,
Expand All @@ -182,7 +147,72 @@ impl EditMode for Emacs {
impl Emacs {
/// Emacs style input parsing constructor if you want to use custom keybindings
pub const fn new(keybindings: Keybindings) -> Self {
Emacs { keybindings }
Emacs {
keybindings,
cache: Vec::new(),
}
}

fn parse_key_event(&mut self, combo: KeyCombination) -> ReedlineEvent {
self.cache.push(normalize_key_combo(&combo));

match self.keybindings.find_sequence_binding(&self.cache) {
Some(KeyBindingTarget::Event(event)) => {
// Found a complete binding, clear the cache and return the event
self.cache.clear();
event
}
Some(KeyBindingTarget::ChordPrefix) => {
// Partial match, wait for the next key
ReedlineEvent::None
}
None => {
// No match, clear the cache.
self.cache.clear();

// Check fallback condition of just inserting a normal character.
match combo.key_code {
KeyCode::Char(c) => {
if combo.modifier == KeyModifiers::NONE
|| combo.modifier == KeyModifiers::SHIFT
|| combo.modifier == KeyModifiers::CONTROL | KeyModifiers::ALT
|| combo.modifier
== KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT
{
ReedlineEvent::Edit(vec![EditCommand::InsertChar(c)])
} else {
ReedlineEvent::None
}
}
_ => ReedlineEvent::None,
}
}
}
}
}

fn normalize_key_combo(combo: &KeyCombination) -> KeyCombination {
match combo.key_code {
KeyCode::Char(c) => {
// Note. The modifier can also be a combination of modifiers, for
// example:
// KeyModifiers::CONTROL | KeyModifiers::ALT
// KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT
//
// Mixed modifiers are used by non american keyboards that have extra
// keys like 'alt gr'. Keep this in mind if in the future there are
// cases where an event is not being captured
let code = match combo.modifier {
KeyModifiers::NONE => combo.key_code,
_ => KeyCode::Char(c.to_ascii_lowercase()),
};

KeyCombination {
modifier: combo.modifier,
key_code: code,
}
}
_ => combo.clone(),
}
}

Expand Down
Loading
Loading