feat(tui): add configurable readline-style text transformations
Summary
Adds missing Emacs/Readline-style text editing shortcuts to the TUI (terminal) prompt input, improving text editing efficiency in the terminal.
Closes #9234.
Changes
New shortcuts
| Shortcut | Action |
|---|---|
alt+u |
Uppercase word from cursor |
alt+l |
Lowercase word from cursor |
alt+c |
Capitalize word from cursor |
ctrl+y |
Yank (paste) last killed text |
ctrl+t |
Transpose characters (requires rebind) |
User-facing behavior
Case transformations (alt+u, alt+l, alt+c)
- When cursor is on a word character: transforms the word starting from cursor position
- When cursor is on whitespace: transforms the next word
- When no next word exists: transforms the previous word
- With text selection: transforms the selected text
- After transformation: cursor moves to end of the transformed word
Kill ring support (ctrl+y)
- The
ctrl+k,ctrl+u,ctrl+w, andalt+ddelete commands now save deleted text to a kill buffer -
ctrl+yinserts the contents of the kill buffer at the current cursor position - Simple single-entry buffer (full kill ring can be added later)
Transpose characters (ctrl+t)
- Swaps the character under the cursor with the character immediately preceding it
- At beginning of line: swaps first two characters
- At end of line: swaps last two characters
- Cursor advances one position to the right (unless already at end of line)
Configuration
All new shortcuts are configurable via opencode.json:
{
"keybinds": {
"input_lowercase_word": "alt+l",
"input_uppercase_word": "alt+u",
"input_capitalize_word": "alt+c",
"input_yank": "ctrl+y",
"input_transpose_characters": "ctrl+t"
}
}
Note on ctrl+t conflict:
By default, ctrl+t is bound to variant_cycle (cycle model variants). To use ctrl+t for transpose characters, you can rebind, e.g.:
{
"keybinds": {
"variant_cycle": "<leader>v",
"input_transpose_characters": "ctrl+t"
}
}
Technical details
- TUI uses OpenTUI's
TextareaRenderablewhich has limited built-in actions - Implementation adds helper functions for word boundary detection matching Readline behavior
- Tests added for word boundary detection and transformation functions
Files
-
packages/opencode/src/config/config.ts- Added keybind schema entries -
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx- Implemented shortcuts -
packages/web/src/content/docs/keybinds.mdx- Updated documentation -
packages/sdk/js/src/v2/gen/types.gen.ts- Regenerated from schema -
packages/opencode/test/tui/text-transform.test.ts- Added 17 tests
I'm guessing the test failures are due to #6674, rather than anything to do with this PR.
Tests passing after rebase.
Adding these key commands could indeed be helpful, I would be in favour of that. However, the impression that I've gotten from the maintainers over the course of some of my own recent PRs is that they'd generally prefer not to make changes to the default keybindings in the TUI and would generally prefer that new key commands instead be bound to "none" by default, leaving it to the user to bind them to keys in their own opencode.jsonc file should they so choose.
Sometimes I'll accidentally type words in a prompt in in the wrong order, so having equivalents to emacs' kill-word and transpose-words available could also be nice.
Thanks for your contribution!
This PR doesn't have a linked issue. All PRs must reference an existing issue.
Please:
- Open an issue describing the bug/feature (if one doesn't exist)
- Add
Fixes #<number>orCloses #<number>to this PR description
See CONTRIBUTING.md for details.
@ariane-emory Thanks a lot for the feedback. If the maintainers request removal of the default bindings then of course I'll do it. However in the absence of a request I'd prefer to keep them, since I think removal violates the Principle of Least Surprise - at this point there are multiple decades of precedent for these key bindings in all modern shells plus anything which uses readline(3).
I definitely agree with your suggestion regarding kill-word and transpose-words!
I've submitted #9234 as the feature request for this PR.
@aspiers You'll get no complaints from me: is's your PR, do it your way, and I would certainly love to see these key commands. FWIW, my interpretation of the behaviour I've seen on some of my own past PRs (which is of course merely an interpretation, and could be incorrect) was that it was also based on the application of the Principle of Least Surprise, just seen from the other side: users would be surprised if keys that did nothing in Opencode yesterday suddenly did something today.
Speaking as someone whose emacs keybindings elisp file Is closer to 5 hundred lines long than 4, I'm certainly not too concerned with what the defaults are (if any) and am certainly more than happy to configure the bindings to my own tastes. In my book, the fact that the can be rebound to suit my own weird preferences is the the more important aspect.
Setting the topic of defaults aside, this looks like great work so far. I haven't yet tested the branch out (I may find time to do so tomorrow), but upon a cursory read-through, it all looks pretty good to me! Best of luck guiding the PR in to a safe landing!
Does this behave for CJK? The opentui native core already calculates word boundaries and could provide such actions.