Editing text: keys, search and presets¶
A text_area and an input edit through
rs-rich-interact's edit::Buffer, and what their keys do is a keymap of
named actions from rich_interact::edit_keys. This page covers the options
on top: rebinding the keys, the Emacs and Vim presets, search with a find
bar, word wrap, the cursor line, the caret's shape, tabs, the undo history,
and an input's validation message. Nothing here changes an editor unless
it is asked for: a text_area with none of these options behaves as it
always has.
cargo run -p rs-rich-intuituive --features vim --example textarea runs a
gallery of them, one tab each (F1 to F7): ratatui-textarea's examples
rebuilt.
Keys are actions¶
The default keymap (edit_keys::editor_keymap()) is the table in
Text areas, with each key named by its action:
left, word-left, home, top, page-up, select-all, deselect,
copy, cut, undo, redo, newline, indent, backspace,
delete-word, delete-to-start, delete-to-end, find-next and the rest.
A few actions have no key by default, to be bound: delete-next-word,
paragraph-up and paragraph-down, set-mark, and the kill ring's
kill-to-end, kill-to-start, kill-word, kill-next-word,
kill-previous-word, yank and yank-pop.
text_area(notes)
.rebind_key("yank", "ctrl+y") // Ctrl+Y leaves `redo`
.rebind_key("kill-to-end", "ctrl+k") // and takes `delete-to-end`'s key
.rebind_key("delete-to-end", "") // no key at all
.rebind_key("paragraph-down", "alt+n")
.rebind_key(action, keys)moves keys onto an action on this editor only; a key moved onto one action leaves the action that had it..edit_keymap(keymap)replaces the whole keymap with one of your own built from rs-rich-interact'sKeymap.- A key with modifiers that is bound to nothing as it is does what it does without them: Shift selects (Shift+→, Ctrl+Shift+←), Alt+↑ is ↑. A character typed is text unless a binding names it.
- The context is
text_area, so a configuration file's installed overrides reach every editor:text_area.undo = ctrl+z, ctrl+/.
The Emacs preset¶
.emacs_keys() takes edit_keys::emacs_keymap(): Emacs's and readline's
keys beside the arrows.
| Keys | What they do |
|---|---|
| Ctrl+F, Ctrl+B, Ctrl+N, Ctrl+P | right, left, down, up |
| Alt+F, Alt+B | a word right, left |
| Ctrl+A, Ctrl+E; Alt+<, Alt+> | the line's start and end; the text's |
| Alt+V, Ctrl+V; Alt+{, Alt+} | a page up and down; a paragraph |
| Ctrl+Space, Ctrl+G | set the mark (moves select from it); drop it |
| Ctrl+H, Ctrl+D | delete back, forward |
| Ctrl+K, Ctrl+U | kill to the line's end (at its end, the line break), to its start |
| Ctrl+W, Alt+D, Alt+Backspace | kill the selection or a word back, a word forward, to the word's start |
| Ctrl+Y, Alt+Y | yank the last kill; swap it for an older one |
| Alt+W | copy (and put it on the kill ring) |
| Ctrl+/, Ctrl+_; Ctrl+Shift+Z | undo; redo |
| Ctrl+S, Ctrl+R | the next and previous match of a search |
Kills one after another join: Ctrl+K twice takes a line and its break, and Ctrl+Y puts both back. The ring keeps sixteen kills. Copying with Ctrl+C (or Alt+W) puts the text on the ring too, so it can be yanked.
The Vim preset (the vim feature)¶
fn vim() -> Node {
let mode = signal(String::new());
column([
text_area(signal(CODE.to_string()))
.vim()
.mode_status(mode)
.line_numbers()
.autofocus()
.panel("vim"),
text!("[reverse] {mode} [/] [dim]dw · 3x · yyp · cw · . repeats").fixed(1),
])
}
.vim() (or .vim_inserting(), for a field typed into first) gives the
editor rs-rich-interact's vim::Vim, a key mode: a preset that sees
each key before the keymap and keeps its own state.
- Modes: normal, insert (
i a I A o O,s S C,cwith a motion), visual (v) and visual line (V); Esc goes back to normal. - Motions:
h j k land the arrows,w b e,0 ^ $,gg G(with a count, that line),{ },f t F Tand a character,n N(the search's matches), Ctrl+D/U and Ctrl+F/B. - Operators:
d c y > <with a motion, doubled for lines (dd,cc,yy,>>), or on a visual selection;x X D C s S J r p P uand Ctrl+R. - Counts before a command or a motion (
3dw,d3w,5j), and.repeats the last change, typing included.
Deleted and yanked text goes on the buffer's kill ring, which p puts
back. The caret is a block in normal mode and a bar while inserting.
.mode_status(signal) writes NORMAL, INSERT, VISUAL or V-LINE, and
the keys of a command not yet complete (NORMAL 2d), for a status line.
:, / and keys Vim does not know go on to the app's bindings, so they
can open a command line or the find bar.
╭─ vim ────────────────────────────────────────────────────╮
│1 │ fn main() { │
│2 │ for word in words { │
│3 │ println!("{word}"); │
│4 │ } │
│5 │ } │
│6 │ │
╰──────────────────────────────────────────────────────────╯
NORMAL dw · 3x · yyp · cw · . repeats
Vim is a plugin over a generic hook: .key_mode(mode) takes any
edit_keys::KeyMode, so another modal preset is a type of your own.
Search and the find bar¶
fn editor() -> Node {
let code = signal(CODE.to_string());
let search = TextSearch::new();
let finding = signal(false);
let area = text_area(code)
.language("rust")
.line_numbers()
.cursor_line()
.tab_width(4)
.search(search)
.autofocus()
.panel("main.rs");
let bar = switch(
move || finding.get(),
move |open| {
if open {
search_bar(search).autofocus()
} else {
label("[dim]Ctrl+F finds · F3 next match")
}
},
)
.fixed(1);
let (area_id, bar_id) = (area.id(), bar.id());
column([area, bar])
.on_key("ctrl+f", move |cx| {
finding.set(true);
cx.focus_within(bar_id);
})
.on_key("escape", move |cx| {
finding.set(false);
cx.focus_within(area_id);
})
}
TextSearch::new() is a search the app drives (it is Copy; make it in
the app's closure). .search(search) gives it to a text area:
search.set("fish"),search.set_regex(true)(fancy-regex's syntax, look-around included) andsearch.set_ignore_case(true)set what is found; a new pattern selects the first match from the caret, as an editor's incremental search does. Matches are found line by line, so one never spans a line break.search.next()andsearch.previous()select the next or previous match, round past the ends; in the area F3 and Shift+F3 do the same.search.found()isSome((3, 12))(the third of twelve; 0 when the selection is not a match), andsearch.error()says why a regular expression does not compile. Read in a node, they update.- Every match in view is drawn in the theme's
text_area.searchstyle, the selected one intext_area.search.current.
search_bar(search) is a find bar over it: Find, an input bound to the
pattern, where it is (2/3, no matches, or the error), and .* and Aa
while those flags are on. In it, Enter and ↓ go to the next match, ↑ to the
one before, Alt+R takes the pattern as a regular expression and Alt+C
matches either case. Its styles are search_bar.label, search_bar.status
and search_bar.error.
╭─ main.rs ────────────────────────────────────────────────╮
│1 │ fn main() { │
│2 │ let words = ["one", "fish", "two", "fish"]; │
│3 │ for word in words { │
╰──────────────────────────────────────────────────────────╯
Find fish 1/2
Wrapping, the cursor line and the caret¶
.wrap_mode(WrapMode::Word)breaks long lines after a space, so words stay whole (a word longer than the row breaks where it must);WrapMode::Graphemeis the default character wrap andWrapMode::Nonescrolls sideways, as.soft_wrap(false)does.WrapModeisintuituive::interact::edit::WrapMode..cursor_line()draws the caret's line, all its rows, in the theme'stext_area.cursor_linestyle: underlined unless the theme names it..caret_shape(CaretShape::Bar)(orBlock,Underline, fromintuituive::interact::caret) sets the terminal's cursor to that shape (DECSCUSR) while the editor has the focus. The app puts back the user's own shape when the focus moves to a widget with none, when it hands the terminal over and when it ends; an app that never sets a shape never sends one. Any widget can ask for one throughWidget::caret_shape..draw_caret()draws the caret in the cells instead of using the terminal's cursor, in the theme'stext_area.caretstyle (reversed, or underlined forCaretShape::Underline): it then shows in a screenshot, a page drawn from the cells, or a terminal with its cursor hidden.
Tabs and the undo history¶
.tab_width(n)sets how many cells a tab takes (4 by default) and how many spaces Tab indents with;.hard_tabs()indents with a tab character instead..undo_limit(Some(n))keeps the lastnundo steps (1000 by default;Nonekeeps them all). A typed run is one step.
Validating an input¶
fn single_line() -> Node {
let number = signal("3.14".to_string());
column([
input(number)
.label("A number")
.validate(|text| {
text.trim()
.parse::<f64>()
.is_err()
.then(|| "not a number".to_string())
})
.autofocus()
.panel("a number")
.fixed(4),
text!("[dim]value: {number}").fixed(1),
])
}
.validate(|text| Option<String>) checks an input's text: while it
returns a message, the node is in error (styled :error by a stylesheet,
as error_when does), the message shows under the text in the
theme's input.error style when the input has two rows (beside it, at the
right, when it has one), and assistive technology is given it: linear mode
reads Age, text box: x, invalid: a number, and a DOM renderer gets
it as aria-description.
Node::error_message(|| Option<String>) does the same for any node, beside
error_when (either makes it invalid); AccessNode::error holds the
message.