From b44300535c02e58aa9c72edaf41754baa7372712 Mon Sep 17 00:00:00 2001 From: Ulyssa Date: Sun, 26 Jul 2026 13:16:35 -0400 Subject: [PATCH] Allow `InputState` implementers to hint how the cursor should be shown --- crates/keybindings/src/lib.rs | 52 ++++++++++++++-------- crates/modalkit-ratatui/examples/editor.rs | 4 +- crates/modalkit-ratatui/src/lib.rs | 9 ++-- crates/modalkit/src/editing/cursor/mod.rs | 25 +++++++++++ crates/modalkit/src/editing/key.rs | 16 ++++--- crates/modalkit/src/env/emacs/mod.rs | 6 +++ crates/modalkit/src/env/mixed.rs | 8 ++-- crates/modalkit/src/env/vim/keybindings.rs | 18 ++++---- crates/modalkit/src/env/vim/mod.rs | 13 ++++-- crates/scansion/src/lib.rs | 5 ++- 10 files changed, 110 insertions(+), 46 deletions(-) diff --git a/crates/keybindings/src/lib.rs b/crates/keybindings/src/lib.rs index ffcd674..e25a2f2 100644 --- a/crates/keybindings/src/lib.rs +++ b/crates/keybindings/src/lib.rs @@ -174,6 +174,14 @@ pub trait InputState { /// The output context type returned along with actions. type Output: Clone + Default; + /// Implementation-specific type for indicating how to render the cursor. + type CursorHint: Default; + + /// Return a hint for how to render the cursor. + fn get_cursor_hint(&self) -> Self::CursorHint { + Self::CursorHint::default() + } + /// Reset any action-specific state. fn reset(&mut self); @@ -225,11 +233,6 @@ impl InputKey for char { pub trait InputKeyState>: InputState { /// Update the context as needed after a `Key` has matched an [EdgeEvent]. fn event(&mut self, event: &EdgeEvent, key: &Key) {} - - /// Return a character to show at the current cursor position. - fn get_cursor_indicator(&self) -> Option { - None - } } /// Trait for the input modes specific to a consumer. @@ -358,7 +361,7 @@ pub trait InputBindings> { } /// Trait for objects that can process input keys using previously mapped bindings. -pub trait BindingMachine +pub trait BindingMachine where K: InputKey, { @@ -384,7 +387,7 @@ where fn reset_mode(&mut self); /// Returns a character to show for the cursor. - fn get_cursor_indicator(&self) -> Option; + fn get_cursor_hint(&self) -> H; /// Repeat a recent sequence of tracked actions, and optionally override their original /// contexts using [InputState::merge]. The repeated sequence will be inserted at @@ -427,6 +430,7 @@ pub struct EmptyKeyState {} impl InputState for EmptyKeyState { type Output = Self; + type CursorHint = (); fn merge(original: Self, _: &Self) -> Self { original @@ -1254,6 +1258,11 @@ where self.actions.push_back(pair); } + /// Returns an implementation-defined hint for how to render the input cursor. + pub fn get_cursor_hint(&self) -> ::CursorHint { + self.ctx.get_cursor_hint() + } + /// Returns the mode we've most recently entered. /// /// Modes reached via [Fallthrough](EdgeEvent::Fallthrough) will not change what this returns. @@ -1267,8 +1276,14 @@ where } } -impl BindingMachine::Output> - for ModalMachine +impl + BindingMachine< + Key, + S::A, + S::Sequence, + ::Output, + ::CursorHint, + > for ModalMachine where Key: InputKey, S: Step, @@ -1378,8 +1393,8 @@ where self.state.show(&self.ctx) } - fn get_cursor_indicator(&self) -> Option { - self.ctx.get_cursor_indicator() + fn get_cursor_hint(&self) -> ::CursorHint { + self.ctx.get_cursor_hint() } fn repeat(&mut self, seq: S::Sequence, ctx: Option<::Output>) { @@ -1710,6 +1725,11 @@ mod tests { impl InputState for TestContext { type Output = Self; + type CursorHint = Option; + + fn get_cursor_hint(&self) -> Option { + self.temp.cursor + } fn merge(mut original: Self, other: &Self) -> Self { if other.temp.count.is_some() { @@ -1768,10 +1788,6 @@ mod tests { }, } } - - fn get_cursor_indicator(&self) -> Option { - self.temp.cursor - } } impl Step for TestStep { @@ -3113,13 +3129,13 @@ mod tests { // No cursor indicator yet. assert_eq!(tm.mode(), TestMode::Insert); - assert_eq!(tm.get_cursor_indicator(), None); + assert_eq!(tm.get_cursor_hint(), None); // Set cursor indicator while we wait for the register. tm.input_key(ctl!('r')); assert_eq!(tm.pop(), None); assert_eq!(tm.mode(), TestMode::Insert); - assert_eq!(tm.get_cursor_indicator(), Some('^')); + assert_eq!(tm.get_cursor_hint(), Some('^')); // Set cursor indicator while we wait for the register. ctx.temp.register = Some('a'); @@ -3127,6 +3143,6 @@ mod tests { tm.input_key(key!('a')); assert_pop2!(tm, TestAction::Paste, ctx); assert_eq!(tm.mode(), TestMode::Insert); - assert_eq!(tm.get_cursor_indicator(), None); + assert_eq!(tm.get_cursor_hint(), None); } } diff --git a/crates/modalkit-ratatui/examples/editor.rs b/crates/modalkit-ratatui/examples/editor.rs index 7ac9e6f..69e0870 100644 --- a/crates/modalkit-ratatui/examples/editor.rs +++ b/crates/modalkit-ratatui/examples/editor.rs @@ -738,13 +738,13 @@ impl Editor { let area = f.area(); let modestr = bindings.show_mode(); - let cursor = bindings.get_cursor_indicator(); + let cursor = bindings.get_cursor_hint(); let dialogstr = bindings.show_dialog(area.width as usize, area.height as usize); let screen = Screen::new(store).show_dialog(dialogstr).show_mode(modestr).borders(true); f.render_stateful_widget(screen, area, sstate); - render_cursor(f, sstate, cursor); + render_cursor(f, sstate, &cursor); })?; if sstate.hide_term_cursor() { term.hide_cursor()?; diff --git a/crates/modalkit-ratatui/src/lib.rs b/crates/modalkit-ratatui/src/lib.rs index 1e9002c..614683e 100644 --- a/crates/modalkit-ratatui/src/lib.rs +++ b/crates/modalkit-ratatui/src/lib.rs @@ -108,7 +108,10 @@ use crossterm::{ }; use modalkit::actions::Action; -use modalkit::editing::{application::ApplicationInfo, completion::CompletionList, store::Store}; +use modalkit::editing::application::ApplicationInfo; +use modalkit::editing::completion::CompletionList; +use modalkit::editing::cursor::CursorStyle; +use modalkit::editing::store::Store; use modalkit::errors::{EditResult, UIResult}; use modalkit::prelude::*; @@ -258,9 +261,9 @@ pub trait Window: WindowOps + Sized { } /// Position and draw a terminal cursor. -pub fn render_cursor(f: &mut Frame, widget: &T, cursor: Option) { +pub fn render_cursor(f: &mut Frame, widget: &T, cursor: &CursorStyle) { if let Some((cx, cy)) = widget.get_term_cursor() { - if let Some(c) = cursor { + if let Some(c) = cursor.get_indicator() { let style = Style::default().fg(Color::Green); let span = Span::styled(c.to_string(), style); let para = Paragraph::new(span); diff --git a/crates/modalkit/src/editing/cursor/mod.rs b/crates/modalkit/src/editing/cursor/mod.rs index ee69c08..0fae4e8 100644 --- a/crates/modalkit/src/editing/cursor/mod.rs +++ b/crates/modalkit/src/editing/cursor/mod.rs @@ -263,6 +263,31 @@ pub(crate) fn block_cursors(a: &Cursor, b: &Cursor) -> (Cursor, Cursor) { (Cursor::new(lstart, lcol), Cursor::new(lend, rcol).goal(rgoal)) } +/// The style in which to render the editor's cursor. +#[derive(Clone, Default)] +#[non_exhaustive] +pub struct CursorStyle { + pub(crate) indicator: Option, + pub(crate) insert: Option, +} + +impl CursorStyle { + /// Returns whether a specific character should be shown for the cursor. + /// + /// For example, when pressing `^V` in Vim, a caret (`^`) is used for the cursor. + pub fn get_indicator(&self) -> Option { + self.indicator + } + + /// Returns whether the cursor is being used for inserting text. + /// + /// This allows consumers to show the cursor differently in their UI if + /// they wish, such as using a blinking cursor when in Insert mode. + pub fn get_insert_style(&self) -> Option { + self.insert + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/modalkit/src/editing/key.rs b/crates/modalkit/src/editing/key.rs index e2e0e71..d410242 100644 --- a/crates/modalkit/src/editing/key.rs +++ b/crates/modalkit/src/editing/key.rs @@ -28,9 +28,11 @@ use std::borrow::Cow; use std::collections::VecDeque; use crate::actions::MacroAction; +use crate::editing::cursor::CursorStyle; use crate::errors::{EditError, EditResult}; use crate::key::MacroError; -use crate::keybindings::{dialog::Dialog, BindingMachine, InputKey}; +use crate::keybindings::dialog::Dialog; +use crate::keybindings::{BindingMachine, InputKey}; use crate::prelude::*; use super::{ @@ -47,7 +49,7 @@ pub struct KeyManager where K: InputKey, { - bindings: Box>, + bindings: Box>, keystack: VecDeque, recording: Option<(Register, bool)>, @@ -62,7 +64,9 @@ where K: InputKey, { /// Create a new instance. - pub fn new + 'static>(bindings: B) -> Self { + pub fn new + 'static>( + bindings: B, + ) -> Self { let bindings = Box::new(bindings); Self { @@ -147,7 +151,7 @@ where } } -impl BindingMachine for KeyManager +impl BindingMachine for KeyManager where K: InputKey + ToString, { @@ -201,8 +205,8 @@ where self.bindings.reset_mode() } - fn get_cursor_indicator(&self) -> Option { - self.bindings.get_cursor_indicator() + fn get_cursor_hint(&self) -> CursorStyle { + self.bindings.get_cursor_hint() } fn repeat(&mut self, seq: S, other: Option) { diff --git a/crates/modalkit/src/env/emacs/mod.rs b/crates/modalkit/src/env/emacs/mod.rs index 1f5e08d..7fdacff 100644 --- a/crates/modalkit/src/env/emacs/mod.rs +++ b/crates/modalkit/src/env/emacs/mod.rs @@ -9,6 +9,7 @@ use std::marker::PhantomData; use crate::{ actions::{Action, InsertTextAction, PromptAction}, + editing::cursor::CursorStyle, key::TerminalKey, keybindings::{ EdgeEvent, @@ -208,6 +209,11 @@ impl Clone for EmacsState { impl InputState for EmacsState { type Output = EditContext; + type CursorHint = CursorStyle; + + fn get_cursor_hint(&self) -> Self::CursorHint { + CursorStyle { indicator: None, insert: Some(self.persist.insert) } + } fn merge(original: EditContext, overrides: &EditContext) -> EditContext { let mut builder = EditContextBuilder::from(original); diff --git a/crates/modalkit/src/env/mixed.rs b/crates/modalkit/src/env/mixed.rs index 2d96ba3..3f73407 100644 --- a/crates/modalkit/src/env/mixed.rs +++ b/crates/modalkit/src/env/mixed.rs @@ -10,6 +10,7 @@ use crate::{ actions::Action, editing::application::{ApplicationInfo, EmptyInfo}, editing::context::EditContext, + editing::cursor::CursorStyle, key::TerminalKey, keybindings::{dialog::Dialog, BindingMachine, InputKey, Step}, prelude::RepeatType, @@ -86,7 +87,8 @@ where } } -impl BindingMachine, RepeatType, EditContext> for MixedBindings +impl BindingMachine, RepeatType, EditContext, CursorStyle> + for MixedBindings where K: InputKey, I: ApplicationInfo, @@ -117,8 +119,8 @@ where delegate_bindings!(self, BindingMachine::reset_mode) } - fn get_cursor_indicator(&self) -> Option { - delegate_bindings!(self, BindingMachine::get_cursor_indicator) + fn get_cursor_hint(&self) -> CursorStyle { + delegate_bindings!(self, BindingMachine::get_cursor_hint) } fn repeat(&mut self, rt: RepeatType, other: Option) { diff --git a/crates/modalkit/src/env/vim/keybindings.rs b/crates/modalkit/src/env/vim/keybindings.rs index 012285b..552a5f8 100644 --- a/crates/modalkit/src/env/vim/keybindings.rs +++ b/crates/modalkit/src/env/vim/keybindings.rs @@ -3901,10 +3901,10 @@ mod tests { // Type a digraph. vm.input_key(ctl!('k')); - assert_eq!(vm.get_cursor_indicator(), Some('?')); + assert_eq!(vm.get_cursor_hint().get_indicator(), Some('?')); vm.input_key(key!('L')); - assert_eq!(vm.get_cursor_indicator(), Some('L')); + assert_eq!(vm.get_cursor_hint().get_indicator(), Some('L')); ctx.ch.digraph1 = Some('L'); ctx.ch.digraph2 = Some('i'); @@ -3914,7 +3914,7 @@ mod tests { // Type a literal. vm.input_key(ctl!('v')); - assert_eq!(vm.get_cursor_indicator(), Some('^')); + assert_eq!(vm.get_cursor_hint().get_indicator(), Some('^')); ctx.ch.digraph1 = None; ctx.ch.digraph2 = None; @@ -3964,20 +3964,20 @@ mod tests { ctx.action.register_append = false; vm.input_key(ctl!('r')); assert_eq!(vm.pop(), None); - assert_eq!(vm.get_cursor_indicator(), Some('"')); + assert_eq!(vm.get_cursor_hint().get_indicator(), Some('"')); vm.input_key(key!('z')); assert_pop1!(vm, Action::from(it), ctx); assert_eq!(vm.mode(), VimMode::Insert); - assert_eq!(vm.get_cursor_indicator(), None); + assert_eq!(vm.get_cursor_hint().get_indicator(), None); // Pressing ^R^C should go back to Normal mode. ctx.action.register = None; vm.input_key(ctl!('r')); assert_eq!(vm.pop(), None); - assert_eq!(vm.get_cursor_indicator(), Some('"')); + assert_eq!(vm.get_cursor_hint().get_indicator(), Some('"')); vm.input_key(ctl!('c')); assert_insert_exit!(vm, ctx); - assert_eq!(vm.get_cursor_indicator(), None); + assert_eq!(vm.get_cursor_hint().get_indicator(), None); } #[test] @@ -4253,13 +4253,13 @@ mod tests { vm.input_key(ctl!('v')); assert_eq!(vm.pop(), None); assert_eq!(vm.mode(), VimMode::Insert); - assert_eq!(vm.get_cursor_indicator(), Some('^')); + assert_eq!(vm.get_cursor_hint().get_indicator(), Some('^')); ctx.ch.hex = Some(0x1B); vm.input_key(key!(KeyCode::Esc)); assert_pop2!(vm, TYPE_CONTEXTUAL, ctx); assert_eq!(vm.mode(), VimMode::Insert); - assert_eq!(vm.get_cursor_indicator(), None); + assert_eq!(vm.get_cursor_hint().get_indicator(), None); // Test that typing in a full octal sequence works. ctx.ch.hex = Some(0x7F); diff --git a/crates/modalkit/src/env/vim/mod.rs b/crates/modalkit/src/env/vim/mod.rs index 767216a..1939fdb 100644 --- a/crates/modalkit/src/env/vim/mod.rs +++ b/crates/modalkit/src/env/vim/mod.rs @@ -9,6 +9,7 @@ use std::marker::PhantomData; use crate::{ actions::{Action, CursorAction, EditAction, EditorAction, HistoryAction, InsertTextAction}, + editing::cursor::CursorStyle, key::TerminalKey, keybindings::{ EdgeEvent, @@ -359,6 +360,14 @@ impl Clone for VimState { impl InputState for VimState { type Output = EditContext; + type CursorHint = CursorStyle; + + fn get_cursor_hint(&self) -> Self::CursorHint { + CursorStyle { + indicator: self.action.cursor, + insert: self.persist.insert, + } + } fn merge(original: EditContext, overrides: &EditContext) -> EditContext { let mut builder = EditContextBuilder::from(original); @@ -454,10 +463,6 @@ impl InputKeyState for VimState }, } } - - fn get_cursor_indicator(&self) -> Option { - self.action.cursor - } } impl From> for EditContext { diff --git a/crates/scansion/src/lib.rs b/crates/scansion/src/lib.rs index 54e1b8e..2b3e6a4 100644 --- a/crates/scansion/src/lib.rs +++ b/crates/scansion/src/lib.rs @@ -79,6 +79,7 @@ use modalkit::actions::{ use modalkit::editing::{ application::{ApplicationContentId, ApplicationInfo, ApplicationWindowId}, context::{EditContext, Resolve}, + cursor::CursorStyle, history::HistoryList, key::KeyManager, rope::EditRope, @@ -206,7 +207,9 @@ where I: ApplicationInfo, { /// Create a new instance. - pub fn new, RepeatType, EditContext> + 'static>( + pub fn new< + B: BindingMachine, RepeatType, EditContext, CursorStyle> + 'static, + >( bindings: B, ) -> Result where