From 5dd0e39b0545f91862a8f0d12f05db6f886bff30 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Sun, 26 May 2019 22:39:50 -0700 Subject: [PATCH] termwiz: improve line editor Move the cursor to the correct column when emoji are input. Add some docs. --- termwiz/src/lineedit/mod.rs | 80 ++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/termwiz/src/lineedit/mod.rs b/termwiz/src/lineedit/mod.rs index c0b0f74db..fa0df0bd1 100644 --- a/termwiz/src/lineedit/mod.rs +++ b/termwiz/src/lineedit/mod.rs @@ -1,16 +1,71 @@ +//! The `LineEditor` struct provides line editing facilities similar +//! to those in the unix shell. +//! It is recommended that a direct `Terminal` instance be used to +//! construct the `LineEditor` (rather than a `BufferedTerminal`), +//! and to disable mouse input: +//! +//! ``` +//! use failure::{err_msg, Fallible}; +//! use termwiz::caps::{Capabilities, ProbeHintsBuilder}; +//! use termwiz::lineedit::LineEditor; +//! use termwiz::terminal::new_terminal; +//! +//! fn main() -> Fallible<()> { +//! // Disable mouse input in the line editor +//! let hints = ProbeHintsBuilder::new_from_env() +//! .mouse_reporting(Some(false)) +//! .build() +//! .map_err(err_msg)?; +//! let caps = Capabilities::new_with_hints(hints)?; +//! let terminal = new_terminal(caps)?; +//! let mut editor = LineEditor::new(terminal); +//! +//! let line = editor.read_line()?; +//! println!("read line: {}", line); +//! +//! Ok(()) +//! } +//! ``` +//! +//! ## Key Bindings +//! +//! The following key bindings are supported: +//! +//! Keystroke | Action +//! --------- | ------ +//! Ctrl-A, Home | Move cursor to the beginning of the line +//! Ctrl-E, End | Move cursor to the end of the line +//! Ctrl-B, Left | Move cursor one grapheme to the left +//! Ctrl-F, Right | Move cursor one grapheme to the right +//! Ctrl-H, Backspace | Delete the grapheme to the left of the cursor +//! Ctrl-J, Ctrl-M, Enter | Finish line editing and accept the current line use crate::input::{InputEvent, KeyCode, KeyEvent, Modifiers}; use crate::surface::{Change, Position}; use crate::terminal::Terminal; use failure::Fallible; use unicode_segmentation::GraphemeCursor; +use unicode_width::UnicodeWidthStr; pub struct LineEditor { terminal: T, line: String, + /// byte index into the UTF-8 string data of the insertion + /// point. This is NOT the number of graphemes! cursor: usize, } impl LineEditor { + /// Create a new line editor. + /// It is recommended that the terminal be created this way: + /// ``` + /// // Disable mouse input in the line editor + /// let hints = ProbeHintsBuilder::new_from_env() + /// .mouse_reporting(Some(false)) + /// .build() + /// .map_err(err_msg)?; + /// let caps = Capabilities::new_with_hints(hints)?; + /// let terminal = new_terminal(caps)?; + /// ``` pub fn new(terminal: T) -> Self { Self { terminal, @@ -20,6 +75,14 @@ impl LineEditor { } fn render(&mut self) -> Fallible<()> { + // In order to position the terminal cursor at the right spot, + // we need to compute how many graphemes away from the start of + // the line the current insertion point is. We can do this by + // slicing into the string and requesting its unicode width. + // It might feel more right to count the number of graphemes in + // the string, but this doesn't render correctly for glyphs that + // are double-width. Nothing about unicode is easy :-/ + let grapheme_count = UnicodeWidthStr::width(&self.line[0..self.cursor]); self.terminal.render(&[ Change::CursorPosition { x: Position::Absolute(0), @@ -28,13 +91,16 @@ impl LineEditor { Change::ClearToEndOfScreen(Default::default()), Change::Text(self.line.clone()), Change::CursorPosition { - x: Position::Absolute(self.cursor), + x: Position::Absolute(grapheme_count), y: Position::NoChange, }, ])?; Ok(()) } + /// Enter line editing mode. + /// Control is not returned to the caller until a line has been + /// accepted, or until an error is detected. pub fn read_line(&mut self) -> Fallible { self.terminal.set_raw_mode()?; let res = self.read_line_impl(); @@ -65,6 +131,10 @@ impl LineEditor { break; } InputEvent::Key(KeyEvent { + key: KeyCode::Char('H'), + modifiers: Modifiers::CTRL, + }) + | InputEvent::Key(KeyEvent { key: KeyCode::Backspace, modifiers: Modifiers::NONE, }) => { @@ -75,6 +145,10 @@ impl LineEditor { } } InputEvent::Key(KeyEvent { + key: KeyCode::Char('B'), + modifiers: Modifiers::CTRL, + }) + | InputEvent::Key(KeyEvent { key: KeyCode::LeftArrow, modifiers: Modifiers::NONE, }) => { @@ -108,6 +182,10 @@ impl LineEditor { } } InputEvent::Key(KeyEvent { + key: KeyCode::Char('F'), + modifiers: Modifiers::CTRL, + }) + | InputEvent::Key(KeyEvent { key: KeyCode::RightArrow, modifiers: Modifiers::NONE, }) => {