Skip to content

Make shortcuts reconfigurable via the config file.#417

Open
eli-l wants to merge 1 commit into
Gitlawb:mainfrom
eli-l:feat/configurable-shortcuts
Open

Make shortcuts reconfigurable via the config file.#417
eli-l wants to merge 1 commit into
Gitlawb:mainfrom
eli-l:feat/configurable-shortcuts

Conversation

@eli-l

@eli-l eli-l commented Jul 2, 2026

Copy link
Copy Markdown

Summary

Allows to configure keyboard shortcuts mapping to prevent conflicts with terminal/shell.

Example config.json

  "keybindings": {
    "toggleDetailed": "ctrl+y",
    "toggleMouse": "ctrl+e",
    "cycleReasoning": "ctrl+m",
    "togglePlan": "ctrl+k",
    "toggleSidebar": "ctrl+u"
  }

Linked issue


Fixes #

Checklist

  • The linked issue already has the issue-approved label.
  • go build ./..., go vet ./..., and go test ./... pass locally.
  • gofmt clean.
  • Tests added/updated for the change (and run under -race where relevant).
  • UI changes include screenshots or a short recording where possible.

Summary by CodeRabbit

  • New Features

    • Added configurable keyboard shortcuts for the TUI, with support for remapping common actions like toggling details, mouse capture, plan view, and the sidebar.
    • Updated on-screen shortcut hints and help text to reflect your configured keys.
  • Bug Fixes

    • Shortcut settings now carry through app startup and runtime config, so custom keybindings apply consistently.
    • Sidebar toggle behavior now better respects user preference even when the sidebar is auto-hidden.

Copilot AI review requested due to automatic review settings July 2, 2026 22:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds configurable TUI keyboard shortcuts via config.json so users can avoid conflicts with terminal/shell key chords.

Changes:

  • Introduce config.KeyBindingsConfig and wire it through config resolution into TUI Options.
  • Add parsing/matching for user-defined keybinding strings and use them in model.Update dispatch.
  • Update the ? help overlay and idle hints to display remapped shortcut labels; add tests for parsing/help substitution.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
internal/tui/sidebar.go Adds sidebarToggleAllowed() to decouple sidebar toggle eligibility from sidebar content.
internal/tui/options.go Adds Options.KeyBindings to pass resolved bindings into the model.
internal/tui/model.go Uses configurable bindings for several actions; updates hint/notice text to reflect remaps.
internal/tui/keybindings.go New parsing/labeling/matching logic for configurable keybindings.
internal/tui/keybinding_help.go Switches help bindings list to be built per-model so configurable labels can be shown.
internal/tui/keybinding_help_test.go Updates tests to use the new buildKeybindingGroups() method.
internal/tui/binding_test.go Adds tests for parsing, matching, and config→binding pipeline.
internal/config/types.go Defines KeyBindingsConfig and includes it in FileConfig/resolved config types.
internal/config/resolver.go Merges/propagates KeyBindingsConfig through resolution and overrides.
internal/cli/app.go Passes resolved keybindings into TUI options for interactive runs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/tui/model.go
case keyCtrl(msg, 'c'):
return m.handleCtrlC()
case keyCtrl(msg, 'o'):
case m.keyMatch(m.keyBindings.toggleDetailed, msg, func(tea.KeyMsg) bool { return keyCtrl(msg, 'o') }):
Comment on lines +115 to +132
// ctrl+letter fast path — the terminal may report this as a control
// character code (e.g. ctrl+o → 0x0F), so use keyCtrl which handles
// both the code and ModCtrl representations.
if mod == tea.ModCtrl && p.code >= 'a' && p.code <= 'z' {
return func(msg tea.KeyMsg) bool {
return keyCtrl(msg, p.code)
}
}

if p.text != "" {
return func(msg tea.KeyMsg) bool {
return msg.Key().Text == p.text && msg.Key().Mod.Contains(mod)
}
}

return func(msg tea.KeyMsg) bool {
return msg.Key().Code == p.code && msg.Key().Mod.Contains(mod)
}
Comment on lines +166 to +171
if p.ctrl && len(keyPart) == 1 && keyPart[0] >= 'a' && keyPart[0] <= 'z' {
// ctrl+<letter> — store the code so the match is exact
p.code = []rune(keyPart)[0]
p.text = ""
return p
}
Comment on lines +207 to +210
// Single character, any modifier context
if len(keyPart) == 1 {
p.code = []rune(keyPart)[0]
}
Comment thread internal/tui/model.go
Comment on lines 1097 to 1100
if m.mouseReleased {
return m.appendSystemNotice("Mouse released — drag to select and copy text. Press Ctrl+E again to re-enable mouse interaction (clicks, right-click paste)."), nil
mouseKey := labelOr(m.keyBindings.toggleMouse, "Ctrl+E")
return m.appendSystemNotice(fmt.Sprintf("Mouse released — drag to select and copy text. Press %s again to re-enable mouse interaction (clicks, right-click paste).", mouseKey)), nil
}
Comment thread internal/tui/model.go
Comment on lines +1293 to 1297
case m.keyMatch(m.keyBindings.toggleSidebar, msg, func(tea.KeyMsg) bool { return keyCtrl(msg, 'b') }):
// Ctrl+B / Ctrl+U collapses / restores the right context sidebar. Only acts when
// the sidebar would otherwise be on screen (managed mode, wide enough,
// real conversation) so it's a no-op — not a confusing notice — on the
// home screen or a narrow terminal. Hiding reflows the chat to full
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds configurable TUI keybindings: new KeyBindingsConfig/KeyBindingDef types in config schema with JSON marshal/unmarshal support, resolver merging across config layers, a new keybinding parsing/matching/dispatch engine in the TUI, and integration into the model's key handlers, help overlay, sidebar toggle gating, and idle-hint text.

Changes

Configurable Keybindings Feature

Layer / File(s) Summary
KeyBindings config schema
internal/config/types.go
Adds KeyBindingDef/KeyBindingsConfig types, wires KeyBindings into FileConfig, ResolveOptions, ResolvedConfig, and JSON marshal/unmarshal paths.
Config merge and resolution
internal/config/resolver.go
Resolve populates ResolvedConfig.KeyBindings; mergeConfig, mergeProjectConfig, and applyOverrides merge bindings via new mergeKeyBindings helper that skips empty fields.
Keybinding parsing and dispatch engine
internal/tui/keybindings.go, internal/tui/binding_test.go
Adds parsedBinding, Label(), Matcher(), parseBinding, labelOr, resolveKeyBindings, builtinBindings, and model.keyMatch, with tests for parsing, matching, config pipeline, and dispatch.
Model, options, and CLI wiring
internal/tui/options.go, internal/tui/model.go, internal/cli/app.go
Options gains KeyBindings; model stores resolved bindings and routes toggleDetailed/toggleMouse/cycleReasoning/togglePlan/toggleSidebar through m.keyMatch; hint/notice text uses configured labels; app.go passes resolved bindings into TUI options.
Help overlay rebuild and sidebar gating
internal/tui/keybinding_help.go, internal/tui/keybinding_help_test.go, internal/tui/sidebar.go
Replaces static keybindingGroups with per-model buildKeybindingGroups(), updates column-width helper signature, updates related test, and adds sidebarToggleAllowed() gating.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Model
  participant keyMatch
  participant parsedBinding

  User->>Model: key press (e.g. Option+O)
  Model->>keyMatch: keyMatch(configuredBinding, defaultFn, msg)
  keyMatch->>parsedBinding: isZero check
  alt binding configured
    parsedBinding-->>keyMatch: Matcher()(msg) result
  else binding unset
    keyMatch->>Model: fall back to defaultFn(msg)
  end
  keyMatch-->>Model: matched true/false
  Model->>Model: dispatch action (toggleDetailed, toggleMouse, etc.)
Loading

Possibly related PRs

  • Gitlawb/zero#160: Related TUI key-handling work for toggling transcript details via Ctrl+O in the same model.go file.
  • Gitlawb/zero#229: Touches the same reasoning-effort cycling shortcut path in model.go now routed through configurable bindings.
  • Gitlawb/zero#300: Touches the same sidebar toggle keyboard shortcut surface now generalized via KeyBindings/keyMatch.

Suggested reviewers: anandh8x, Vasanthdev2004

Nothing broken, nothing red — just Ctrl+B now answers to a config file instead. Reviewers, do check the empty-binding fallback paths and the keyMatch defaultFn wiring carefully; that's where a silent regression would hide.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: making shortcuts configurable through the config file.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Warning

⚠️ This pull request shows signs of AI-generated slop (trivial_assertion). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/tui/model.go (1)

1293-1307: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Sidebar toggle still gated by sidebarAvailable(), not the new sidebarToggleAllowed().

The PR added sidebarToggleAllowed() in sidebar.go specifically so Ctrl+B can toggle the show/hide preference even when the sidebar is auto-hidden due to no content (sidebarHasContent() is false). But this handler still calls m.sidebarAvailable(), which includes that content check — so Ctrl+B remains a no-op whenever there are no agents/plan yet, exactly the behavior sidebarToggleAllowed was meant to fix. This also explains why golangci-lint flags sidebarToggleAllowed as unused in internal/tui/sidebar.go.

🐛 Proposed fix
-			if m.noBlockingModal() && m.sidebarAvailable() {
+			if m.noBlockingModal() && m.sidebarToggleAllowed() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/model.go` around lines 1293 - 1307, Ctrl+B is still using the
stricter sidebar availability check instead of the new toggle- अनुमति helper, so
the preference cannot be toggled when the sidebar is auto-hidden for empty
content. Update the key handler in m.keyMatch(m.keyBindings.toggleSidebar, ...)
to use m.sidebarToggleAllowed() rather than m.sidebarAvailable(), keeping the
existing noBlockingModal() guard and hide/show behavior intact. This will let
the new helper in sidebar.go be exercised and remove the unused-symbol lint
issue.

Source: Linters/SAST tools

♻️ Duplicate comments (1)
internal/tui/sidebar.go (1)

50-77: 🎯 Functional Correctness | 🟠 Major

Unused function — not yet wired into the toggle-sidebar key handler.

golangci-lint flags this as unused. Root cause and fix are noted in internal/tui/model.go's toggleSidebar case, which still calls sidebarAvailable() instead of this function.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/sidebar.go` around lines 50 - 77, The new sidebarToggleAllowed
helper is currently unused because the toggle-sidebar key handler still uses
sidebarAvailable. Update the toggleSidebar case in model.go to call
sidebarToggleAllowed instead, so the keybinding follows the intended allow/deny
rules while preserving the existing render-time sidebarAvailable checks. Keep
the change scoped to the toggle path and use the existing sidebarToggleAllowed
and sidebarAvailable symbols to ensure the wiring is correct.

Source: Linters/SAST tools

🧹 Nitpick comments (4)
internal/config/resolver.go (1)

707-724: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add merge-precedence tests for keybindings.

Logic is correct, but mergeKeyBindings is now invoked from four different layers (user, project, provider-command via mergeConfig, and CLI overrides). This kind of layered "last non-empty wins" merge is exactly where precedence bugs sneak in silently (e.g., project config unintentionally clobbering an intended override). Worth a couple of table-driven cases in resolver_test.go asserting override > project > user precedence for at least one field (e.g. ToggleSidebar).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/config/resolver.go` around lines 707 - 724, Add table-driven
merge-precedence tests for keybindings around mergeKeyBindings/mergeConfig in
resolver_test.go, since this path is used by user, project, provider-command,
and CLI layers. Cover at least one representative field such as ToggleSidebar
and assert last non-empty wins with override > project > user precedence. Make
the cases explicit so a project-level value cannot accidentally clobber a
higher-priority override.
internal/tui/keybindings.go (2)

251-257: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Drop builtinBindings — it's dead code and fails lint.

The var is all-zero and, per its own comment, is never matched through the Matcher path. golangci-lint flags it as unused, which will break CI. Remove it unless you intend to wire actual default matchers here.

♻️ Proposed removal
-// builtinBindings holds the default hardcoded bindings. keybinding config
-// overrides the built-in on a per-action basis when the field is non-empty.
-var builtinBindings = keyBindings{
-	// All zero: the model.go dispatch uses hardcoded key-case comparisons.
-	// These are the canonical defaults and are never matched through the
-	// parsedBinding.Matcher path — the config-override path is separate.
-}
-
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/keybindings.go` around lines 251 - 257, Remove the dead
`builtinBindings` variable from `keybindings.go`; it is an all-zero
`keyBindings` value that is never used by the `Matcher` path and is causing the
unused-variable lint failure. Keep the existing hardcoded dispatch behavior in
`model.go` and only retain default bindings here if you plan to actually wire
them into `parsedBinding.Matcher` and reference `builtinBindings` from the
keybinding lookup flow.

Source: Linters/SAST tools


203-214: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Silent fallback on unrecognized keys hides config typos.

Anything not matched (e.g. f5, ins, or a typo like ctlr+o) falls through with code == 0, so resolveKeyBindings treats it as "use default" with zero feedback. Users will think their remap took effect. Consider surfacing invalid keybinding strings (log/warn at resolve time) so misconfiguration is visible.

Want me to sketch a validation path that reports unparseable bindings?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/keybindings.go` around lines 203 - 214, The keybinding parser in
parseKeyBinding silently returns a zero code for unrecognized inputs, which
makes resolveKeyBindings treat invalid remaps as defaults without any feedback.
Update parseKeyBinding and/or resolveKeyBindings to detect unparseable key
strings (for example f5, ins, or typos like ctlr+o) and emit a warning or error
when a binding cannot be resolved, using the existing keybinding resolution path
to report the invalid entry instead of silently falling back.
internal/tui/binding_test.go (1)

64-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

This test can't fail — it only logs.

TestOptionOMatchesComposedCharacters has no assertion path; whether the matcher returns true or false it just t.Logfs (and the log only fires on the true branch). It documents macOS compose behavior but guards nothing. Either assert the intended behavior for ø (match or not-match) or convert it to a comment so it isn't mistaken for coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/binding_test.go` around lines 64 - 73,
TestOptionOMatchesComposedCharacters currently has no assertion and can never
fail, so it does not validate matcher behavior. Update the test to use matcher
against the tea.KeyPressMsg for ø with an explicit assertion for the intended
outcome, or remove the test logic and leave only a comment if it is meant to be
documentation. Use TestOptionOMatchesComposedCharacters, parseBinding, and
matcher to locate the check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/tui/keybindings.go`:
- Around line 124-132: The keybinding matcher in the shortcut predicate uses
Mod.Contains(mod), which allows modifier supersets to match and can trigger
unintended bindings. Update the anonymous functions returned from the text/code
branches in the keybinding logic to require exact modifier equality against the
configured mod, using the existing p.text and p.code checks as-is but replacing
the modifier containment test with an exact match.

---

Outside diff comments:
In `@internal/tui/model.go`:
- Around line 1293-1307: Ctrl+B is still using the stricter sidebar availability
check instead of the new toggle- अनुमति helper, so the preference cannot be
toggled when the sidebar is auto-hidden for empty content. Update the key
handler in m.keyMatch(m.keyBindings.toggleSidebar, ...) to use
m.sidebarToggleAllowed() rather than m.sidebarAvailable(), keeping the existing
noBlockingModal() guard and hide/show behavior intact. This will let the new
helper in sidebar.go be exercised and remove the unused-symbol lint issue.

---

Duplicate comments:
In `@internal/tui/sidebar.go`:
- Around line 50-77: The new sidebarToggleAllowed helper is currently unused
because the toggle-sidebar key handler still uses sidebarAvailable. Update the
toggleSidebar case in model.go to call sidebarToggleAllowed instead, so the
keybinding follows the intended allow/deny rules while preserving the existing
render-time sidebarAvailable checks. Keep the change scoped to the toggle path
and use the existing sidebarToggleAllowed and sidebarAvailable symbols to ensure
the wiring is correct.

---

Nitpick comments:
In `@internal/config/resolver.go`:
- Around line 707-724: Add table-driven merge-precedence tests for keybindings
around mergeKeyBindings/mergeConfig in resolver_test.go, since this path is used
by user, project, provider-command, and CLI layers. Cover at least one
representative field such as ToggleSidebar and assert last non-empty wins with
override > project > user precedence. Make the cases explicit so a project-level
value cannot accidentally clobber a higher-priority override.

In `@internal/tui/binding_test.go`:
- Around line 64-73: TestOptionOMatchesComposedCharacters currently has no
assertion and can never fail, so it does not validate matcher behavior. Update
the test to use matcher against the tea.KeyPressMsg for ø with an explicit
assertion for the intended outcome, or remove the test logic and leave only a
comment if it is meant to be documentation. Use
TestOptionOMatchesComposedCharacters, parseBinding, and matcher to locate the
check.

In `@internal/tui/keybindings.go`:
- Around line 251-257: Remove the dead `builtinBindings` variable from
`keybindings.go`; it is an all-zero `keyBindings` value that is never used by
the `Matcher` path and is causing the unused-variable lint failure. Keep the
existing hardcoded dispatch behavior in `model.go` and only retain default
bindings here if you plan to actually wire them into `parsedBinding.Matcher` and
reference `builtinBindings` from the keybinding lookup flow.
- Around line 203-214: The keybinding parser in parseKeyBinding silently returns
a zero code for unrecognized inputs, which makes resolveKeyBindings treat
invalid remaps as defaults without any feedback. Update parseKeyBinding and/or
resolveKeyBindings to detect unparseable key strings (for example f5, ins, or
typos like ctlr+o) and emit a warning or error when a binding cannot be
resolved, using the existing keybinding resolution path to report the invalid
entry instead of silently falling back.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f8d8eb71-9291-4d24-becd-38223ba09735

📥 Commits

Reviewing files that changed from the base of the PR and between 952788f and 668bb62.

📒 Files selected for processing (10)
  • internal/cli/app.go
  • internal/config/resolver.go
  • internal/config/types.go
  • internal/tui/binding_test.go
  • internal/tui/keybinding_help.go
  • internal/tui/keybinding_help_test.go
  • internal/tui/keybindings.go
  • internal/tui/model.go
  • internal/tui/options.go
  • internal/tui/sidebar.go

Comment on lines +124 to +132
if p.text != "" {
return func(msg tea.KeyMsg) bool {
return msg.Key().Text == p.text && msg.Key().Mod.Contains(mod)
}
}

return func(msg tea.KeyMsg) bool {
return msg.Key().Code == p.code && msg.Key().Mod.Contains(mod)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

charm bubbletea v2 KeyMod Contains method semantics

💡 Result:

In Bubble Tea v2, the KeyMod type—which represents keyboard modifier states such as Ctrl, Alt, and Shift—is implemented as a bitmask (an alias for a type from the underlying input handling library) [1]. While some legacy or pattern-based documentation might suggest checking modifiers using bitwise operations (e.g., key.Mod & tea.ModCtrl != 0) [2][3], the official v2 upgrade guide specifically references the Contains method for modifier checking [4]. The KeyMod type provides a Contains method that accepts a specific modifier constant to determine if that modifier is active [4]. Example usage: if msg.Mod.Contains(tea.ModAlt) { // Alt key was held } The v2 update significantly changed key handling [4][5]. Modifiers, which were previously separate boolean fields (e.g., msg.Alt), have been consolidated into the msg.Mod field [4]. You should use msg.Mod.Contains() to verify the presence of specific modifiers like tea.ModShift, tea.ModAlt, tea.ModCtrl, or tea.ModMeta [4][1]. For simple key combinations, you can also use the msg.String() method (which returns strings like "ctrl+c" or "shift+enter") or msg.Keystroke() for more precise modifier-inclusive string representations [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file outline ==\n'
ast-grep outline internal/tui/keybindings.go --view expanded || true

printf '\n== relevant lines ==\n'
sed -n '1,220p' internal/tui/keybindings.go | cat -n

printf '\n== dependency references ==\n'
rg -n "bubbletea|tea\.Mod|KeyMod|Contains\(" go.mod go.sum internal -S || true

printf '\n== local tests/usages for key matching ==\n'
rg -n "msg\.Key\(\)\.(Text|Code).*Contains|Contains\(mod\)" internal -S || true

Repository: Gitlawb/zero

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== keybindings slice ==\n'
sed -n '100,170p' internal/tui/keybindings.go | cat -n

printf '\n== go.mod bubbletea lines ==\n'
rg -n 'bubbletea|charmbracelet' go.mod go.sum -S

printf '\n== any local keybinding tests/usages ==\n'
rg -n 'KeyMsg|KeyMod|Contains\(mod\)|msg\.Key\(\)\.(Text|Code)' internal -S

Repository: Gitlawb/zero

Length of output: 8633


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '== Bubble Tea module dir ==\n'
go list -f '{{.Dir}}' charm.land/bubbletea/v2 2>/dev/null || true

printf '\n== Bubble Tea Contains source if cached ==\n'
moddir="$(go list -f '{{.Dir}}' charm.land/bubbletea/v2 2>/dev/null || true)"
if [ -n "$moddir" ] && [ -f "$moddir/mod.go" ]; then
  rg -n 'func .*Contains|type KeyMod|ModAlt|ModShift|ModCtrl' "$moddir/mod.go" -n -S
  sed -n '1,220p' "$moddir/mod.go" | cat -n
fi

printf '\n== binding tests ==\n'
sed -n '1,260p' internal/tui/binding_test.go | cat -n

printf '\n== keybindings matcher-related docs ==\n'
sed -n '80,150p' internal/tui/keybindings.go | cat -n

Repository: Gitlawb/zero

Length of output: 9479


Exact-match the configured modifiers here. Mod.Contains(mod) accepts supersets, so an alt+o binding can also fire on ctrl+alt+o. That widens the shortcut surface and can reintroduce collisions; use == mod for these branches.

🔒️ Proposed exact-match
 	if p.text != "" {
 		return func(msg tea.KeyMsg) bool {
-			return msg.Key().Text == p.text && msg.Key().Mod.Contains(mod)
+			return msg.Key().Text == p.text && msg.Key().Mod == mod
 		}
 	}

 	return func(msg tea.KeyMsg) bool {
-		return msg.Key().Code == p.code && msg.Key().Mod.Contains(mod)
+		return msg.Key().Code == p.code && msg.Key().Mod == mod
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if p.text != "" {
return func(msg tea.KeyMsg) bool {
return msg.Key().Text == p.text && msg.Key().Mod.Contains(mod)
}
}
return func(msg tea.KeyMsg) bool {
return msg.Key().Code == p.code && msg.Key().Mod.Contains(mod)
}
if p.text != "" {
return func(msg tea.KeyMsg) bool {
return msg.Key().Text == p.text && msg.Key().Mod == mod
}
}
return func(msg tea.KeyMsg) bool {
return msg.Key().Code == p.code && msg.Key().Mod == mod
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/keybindings.go` around lines 124 - 132, The keybinding matcher
in the shortcut predicate uses Mod.Contains(mod), which allows modifier
supersets to match and can trigger unintended bindings. Update the anonymous
functions returned from the text/code branches in the keybinding logic to
require exact modifier equality against the configured mod, using the existing
p.text and p.code checks as-is but replacing the modifier containment test with
an exact match.

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice feature, and the defaults are regression-safe — with no custom config every binding falls through to the identical hardcoded chord, and malformed config fails safe (no panics). Two things block merge:

1. CI will fail on the gofmt gate. internal/tui/model.go, keybindings.go, and binding_test.go aren't gofmt-clean (the CI "Check formatting" step runs gofmt -l . and exits non-zero on any output). Please run gofmt -w.

2. CodeRabbit's change-request is valid and unaddressed. The Ctrl+B handler still gates on m.sidebarAvailable() rather than the new m.sidebarToggleAllowed(), which is never called — dead code — so the toggle-while-auto-hidden feature this PR adds doesn't actually work.

Non-blocking correctness gaps for configured (non-default) keys: Mod.Contains matches supersets (a configured alt+o also fires on alt+shift+o; a plain-key binding fires while modifiers are held) — consider exact ==; and single-char parsing keys on byte length, so a 2-byte UTF-8 key (e.g. é) won't bind. builtinBindings is also unused. Defaults are unaffected by all of these.

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.

3 participants