Skip to content

Comments

feat(tui): add configurable readline-style text transformations#6778

Open
aspiers wants to merge 1 commit intoanomalyco:devfrom
aspiers:readline-additions
Open

feat(tui): add configurable readline-style text transformations#6778
aspiers wants to merge 1 commit intoanomalyco:devfrom
aspiers:readline-additions

Conversation

@aspiers
Copy link
Contributor

@aspiers aspiers commented Jan 3, 2026

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 or punctuation: 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, and alt+d delete commands now save deleted text to a kill buffer
  • ctrl+y inserts 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 TextareaRenderable which has limited built-in actions
  • Word boundary detection uses [A-Za-z0-9] (matching Readline's isalnum() and Emacs' word syntax class), extracted into word.ts for testability
  • Tests import and exercise the real implementation directly

Known limitations

M-f and M-b (word navigation) are handled natively by opentui and are unaffected by this PR — they still treat _ as a word character. Making word boundary behaviour fully consistent across navigation and transformation would require a separate PR overriding opentui's native M-f/M-b handling.

Files

  • packages/opencode/src/config/config.ts - Added keybind schema entries
  • packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx - Implemented shortcuts
  • packages/opencode/src/cli/cmd/tui/component/prompt/word.ts - Word boundary and transformation functions
  • 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 - 32 tests against real implementation

@aspiers
Copy link
Contributor Author

aspiers commented Jan 3, 2026

I'm guessing the test failures are due to #6674, rather than anything to do with this PR.

@aspiers aspiers force-pushed the readline-additions branch from cc3e56b to c71c6be Compare January 5, 2026 10:16
@aspiers
Copy link
Contributor Author

aspiers commented Jan 5, 2026

Tests passing after rebase.

@ariane-emory
Copy link
Contributor

ariane-emory commented Jan 18, 2026

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.

@github-actions
Copy link
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@aspiers aspiers mentioned this pull request Jan 18, 2026
@aspiers
Copy link
Contributor Author

aspiers commented Jan 18, 2026

@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!

@aspiers
Copy link
Contributor Author

aspiers commented Jan 18, 2026

I've submitted #9234 as the feature request for this PR.

@ariane-emory
Copy link
Contributor

ariane-emory commented Jan 18, 2026

@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!

@kommander
Copy link
Collaborator

Does this behave for CJK? The opentui native core already calculates word boundaries and could provide such actions.

@aspiers
Copy link
Contributor Author

aspiers commented Jan 19, 2026

Thanks @ariane-emory and @kommander great question! I will look into that when I get a chance.

@ariane-emory
Copy link
Contributor

ariane-emory commented Jan 29, 2026

I have been using this branch for almost a month now in my own fork's integration branches.

Having downcase-word available has proven to be quite nice: I use Handy for dictation, and it is somewhat prone to capitalizing words that should not be capitalized (e.g., KV.JSON when I'd meant kv.json), so having this function available has made it easier to correct that error on Handy's part. For disabled users like me, who may have difficulty typing sometimes, this PR is a genuine improvement to OpenCode's accessibility.

I have not retrained myself into the habit of regularly using all the new key commands provided by this branch, but those that I have successfully trained myself into the habit of using do seem to be working appropriately.

So, LGTM and also FGTM (feels good to me).

@thdxr thdxr force-pushed the dev branch 3 times, most recently from f1ae801 to 08fa7f7 Compare January 30, 2026 14:37
@aspiers aspiers force-pushed the readline-additions branch 2 times, most recently from da3de15 to a18b09c Compare February 2, 2026 09:15
@aspiers
Copy link
Contributor Author

aspiers commented Feb 2, 2026

@kommander commented on Jan 19, 2026, 12:57 AM GMT+13:

Does this behave for CJK? The opentui native core already calculates word boundaries and could provide such actions.

I asked Opus about this, and it said:

The CJK word boundary handling was already broken before this PR. The PR's getWordBoundariesForTransformation function uses the same whitespace-based approach that opentui's native getNextWordBoundary()/getPrevWordBoundary() uses.

So the PR doesn't make CJK support any worse - it was never working properly in the first place. The text transformation features (uppercase/lowercase/capitalize word) behave consistently with the existing word navigation/deletion features.

CJK support would be a separate improvement that could use Intl.Segmenter, but that's outside the scope of this PR.

Although of course it could be wrong, and I'm more inclined to trust a human than AI 😅

@ariane-emory
Copy link
Contributor

ariane-emory commented Feb 20, 2026

@aspiers The word boundary behaviour does not align with that of readline/emacs.

(the * character is used to indicate the position of point in the following examples)

If I have this text:

MERGED*-branches.md (the result after first using alt+u at the start of the string merged-branches.md)

... and then I strike alt+u again, the result is:

MERGED-BRANCHES.MD*

In contrast, striking the same keys in readline/emacs produces:

MERGED-BRANCHES*.md

In short, . should be a word boundary character.

@aspiers
Copy link
Contributor Author

aspiers commented Feb 20, 2026

Good catch, thanks!

@aspiers
Copy link
Contributor Author

aspiers commented Feb 22, 2026

@ariane-emory Fixed in the latest push. The root cause was using /\s/ (whitespace only) as the word boundary test instead of /\w/ (alphanumeric + underscore). The fix switches to proper Readline/Emacs forward-word semantics: skip non-word chars (/\W/), then advance through word chars (/\w/). So -, ., and other punctuation now correctly act as word boundaries.

The same bug was present in the input_delete_word_backward (alt+d) handler and has been fixed there too.

New tests added covering the exact example from your comment (MERGED*-branches.mdMERGED-BRANCHES*.md then .md remaining) plus -, ., and _ boundary cases.

@aspiers aspiers force-pushed the readline-additions branch 2 times, most recently from 6121652 to 2382f2c Compare February 23, 2026 11:22
@aspiers
Copy link
Contributor Author

aspiers commented Feb 23, 2026

The CI failures here are not caused by this PR. Both e2e (windows) and test (linux) are failing on all PRs right now — e.g. fix/github-copilot-claude-prefill-issue-13768 has the same failures. The root cause is a pre-existing NotFoundError: Session not found in session/index.ts:382 on the Windows e2e runner, and test (linux) is just a gate that fails when e2e (windows) does.

@aspiers
Copy link
Contributor Author

aspiers commented Feb 23, 2026

This PR has been updated to address the word boundary issue raised by @ariane-emory. Here's a summary of what changed:

Word boundary fix

The initial implementation used /\s/ (whitespace only) as the word boundary test. After @ariane-emory's report I investigated the exact semantics of Readline and Emacs more carefully and found two bugs that affected both my fix and the independent fix @ariane-emory submitted in their own fork:

  1. Wrong word character definition. Both fixes used JS /\w/ (alphanumeric + underscore), but Readline uses isalnum() and Emacs uses its word syntax class — neither includes _. So foo_bar is two words, not one. Fixed by using /[A-Za-z0-9]/.

  2. Incorrect fallback at end of buffer. Both fixes fell back to the previous word when the cursor was past the last word. Verified with emacs --batch: upcase-word/downcase-word at end of buffer is simply a no-op. The function now returns null in that case and the handler skips the action.

The isWordChar helper is from @ariane-emory's fix — it's cleaner than repeated inline regexes. I also picked up the missing input_transpose_characters entry in types.gen.ts that their PR caught.

Tests

32 unit tests now cover isWordChar, word boundary detection (including punctuation and _ as boundaries, and the no-op at end of buffer), and the case transformation functions.

@aspiers
Copy link
Contributor Author

aspiers commented Feb 23, 2026

Following up on my previous comment: the "fallback to previous word" behaviour when there's no next word was accidentally removed by an AI assistant during refactoring, and has now been restored — it was an intentional improvement over Emacs' silent no-op, useful when the cursor is at the end of the input and you want to transform the word you just typed. The word character definition fix (/[A-Za-z0-9]/ instead of /\w/) is still in place.

@aspiers
Copy link
Contributor Author

aspiers commented Feb 23, 2026

Gah, Sonnet was too dumb to force-push the right changes. Trying again ...

@aspiers
Copy link
Contributor Author

aspiers commented Feb 23, 2026

Oh it's even worse, the agent was copying the implementation into the test file and testing that! 🤦🏼‍♂️ 😱 😠 I really thought Sonnet 4.6 would be better than this...

Without this patch, users must manually retype text to change case or
paste deleted content in the TUI prompt, and some keybindings conflict
with other TUI functions.

This is a problem because it slows down text editing and limits the
ability to customize keybindings to match user preferences.

This patch solves the problem by adding configurable shortcuts for
lowercase, uppercase, capitalize word, transpose characters, and yank
operations, along with a kill buffer for storing deleted text.
@aspiers
Copy link
Contributor Author

aspiers commented Feb 23, 2026

All fixed now.

Note: M-f and M-b (word navigation) are handled natively by opentui and are unaffected by this PR — they still treat _ as a word character. Making word boundary behaviour fully consistent across navigation and transformation would require a separate PR overriding opentui's native M-f/M-b handling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add configurable readline-style text transformations to TUI prompt

3 participants