Recipe: Command Palettes & Prompts
TWrite provides a headless prompt system (PromptState) capable of driving bottom-line inputs (such as :goto-line) and floating top command palettes (such as an F1 or Ctrl+P quick picker).
1. Two Prompt Shapes
Prompts are configured using PromptPlacement:
PromptPlacement::BottomBar: Single line anchored to the bottom border with optional leading prefix (for example,:or/).PromptPlacement::TopPalette: Centered floating modal overlay with item list and fuzzy search filtering.
2. Opening and Routing a Prompt
A hook opens a prompt via ctx.prompt.open(...), and routes incoming keystrokes through ctx.prompt.handle_key(event):
#![allow(unused)] fn main() { use twrite::{ EditorHook, HookContext, HookOutcome, KeyCode, KeyEvent, PromptAction, PromptPlacement, PromptSpec, }; pub struct GotoLineHook; impl EditorHook for GotoLineHook { fn on_key(&mut self, ctx: &mut HookContext, event: &KeyEvent) -> HookOutcome { // Open on Ctrl+G: if event.code == KeyCode::Char('g') && event.modifiers.ctrl { ctx.prompt.open( PromptSpec::new("goto-line", ":", "Enter line number", PromptPlacement::BottomBar, false), "", ); return HookOutcome::Consumed; } // When our prompt is open, route keys to it: if ctx.prompt.spec().is_some_and(|s| s.id == "goto-line") { match ctx.prompt.handle_key(event) { PromptAction::Submitted(input) => { if let Ok(line_num) = input.trim().parse::<usize>() { let target_row = line_num.saturating_sub(1); ctx.buffer.set_cursor_point(twrite::Point::new(target_row, 0)); ctx.prompt.close(); } else { ctx.prompt.set_message("Invalid line number".to_string()); } return HookOutcome::Consumed; } PromptAction::Cancelled => { return HookOutcome::Consumed; } PromptAction::Editing => { return HookOutcome::Consumed; } PromptAction::Ignored => {} } } HookOutcome::PassThrough } } }
3. Fuzzy-Filtered Command Palette
To build a floating F1 command palette with live fuzzy filtering, populate item rows whenever the input changes:
#![allow(unused)] fn main() { use twrite::prompt::{PromptItem, fuzzy_filter}; use twrite::{ EditorHook, HookContext, HookOutcome, KeyCode, KeyEvent, PromptAction, PromptPlacement, PromptSpec, }; pub struct CommandPaletteHook { available_commands: Vec<PromptItem>, } impl CommandPaletteHook { pub fn new() -> Self { Self { available_commands: vec![ PromptItem::with_hint("Toggle Line Numbers", "Alt+L"), PromptItem::with_hint("Enable Markdown", "Alt+M"), PromptItem::with_hint("Save File", "Ctrl+S"), PromptItem::with_hint("Find in Page", "Ctrl+F"), ], } } fn update_filtered_items(&self, ctx: &mut HookContext) { let query = ctx.prompt.input(); let ranked = fuzzy_filter(&self.available_commands, query); let items: Vec<PromptItem> = ranked .into_iter() .map(|(idx, _score)| self.available_commands[idx].clone()) .collect(); ctx.prompt.set_items(items); } } impl EditorHook for CommandPaletteHook { fn on_key(&mut self, ctx: &mut HookContext, event: &KeyEvent) -> HookOutcome { if event.code == KeyCode::F(1) || (event.code == KeyCode::Char('p') && event.modifiers.ctrl) { ctx.prompt.open( PromptSpec::new( "command-palette", ">", "Type a command...", PromptPlacement::TopPalette, true, ), "", ); self.update_filtered_items(ctx); return HookOutcome::Consumed; } if ctx.prompt.spec().is_some_and(|s| s.id == "command-palette") { match ctx.prompt.handle_key(event) { PromptAction::Editing => { self.update_filtered_items(ctx); return HookOutcome::Consumed; } PromptAction::Submitted(_input) => { if let Some(selected) = ctx.prompt.selected_item() { println!("Executing command: {}", selected.label); } ctx.prompt.close(); return HookOutcome::Consumed; } PromptAction::Cancelled => return HookOutcome::Consumed, PromptAction::Ignored => {} } } HookOutcome::PassThrough } } }
4. Built-in Features of All Prompts
Every open prompt automatically includes:
- Vertical bar cursor: Thin cursor with full UTF-8 character boundary safety.
- Word navigation & deletion:
Ctrl+Left/Ctrl+Right,Ctrl+Backspace/Ctrl+Delete,Ctrl+K. - History navigation:
UpandDownrecall previously submitted inputs (unless item rows are visible). - Tab completion: Pressing
Tabfills the input with the highlighted row label. - Escape: Automatically cancels and closes the prompt.