From 65b0691bc0e401133c23610f72182a408e19247b Mon Sep 17 00:00:00 2001 From: Ritik Pal Date: Mon, 28 Apr 2025 23:44:43 +0530 Subject: [PATCH 1/2] feat: add hotkey validation system with comprehensive tests --- internal/config/hotkeys.go | 212 ++++++++++++++++++++++++++++ internal/config/hotkeys_test.go | 243 ++++++++++++++++++++++++++++++++ 2 files changed, 455 insertions(+) create mode 100644 internal/config/hotkeys.go create mode 100644 internal/config/hotkeys_test.go diff --git a/internal/config/hotkeys.go b/internal/config/hotkeys.go new file mode 100644 index 0000000..ad9966a --- /dev/null +++ b/internal/config/hotkeys.go @@ -0,0 +1,212 @@ +package config + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/key" +) + +// HotkeyConfig defines the structure for all remappable hotkeys +type HotkeyConfig struct { + // Global hotkeys + Logs string `yaml:"logs"` // Default: "ctrl+l" + Quit string `yaml:"quit"` // Default: "ctrl+c" + Help string `yaml:"help"` // Default: "ctrl+?" + SwitchSession string `yaml:"switch_session"` // Default: "ctrl+a" + Commands string `yaml:"commands"` // Default: "ctrl+k" + + // Chat page hotkeys + NewSession string `yaml:"new_session"` // Default: "ctrl+n" + Cancel string `yaml:"cancel"` // Default: "esc" + + // Message navigation hotkeys + PageDown string `yaml:"page_down"` // Default: "pgdown" + PageUp string `yaml:"page_up"` // Default: "pgup" + HalfPageUp string `yaml:"half_page_up"` // Default: "ctrl+u" + HalfPageDown string `yaml:"half_page_down"` // Default: "ctrl+d" + + // Dialog navigation hotkeys + Up string `yaml:"up"` // Default: "up" + Down string `yaml:"down"` // Default: "down" + Enter string `yaml:"enter"` // Default: "enter" + Escape string `yaml:"escape"` // Default: "esc" + J string `yaml:"j"` // Default: "j" + K string `yaml:"k"` // Default: "k" + Left string `yaml:"left"` // Default: "left" + Right string `yaml:"right"` // Default: "right" + Tab string `yaml:"tab"` // Default: "tab" +} + +// DefaultHotkeyConfig returns the default hotkey configuration +func DefaultHotkeyConfig() HotkeyConfig { + return HotkeyConfig{ + Logs: "ctrl+l", + Quit: "ctrl+c", + Help: "ctrl+?", + SwitchSession: "ctrl+a", + Commands: "ctrl+k", + NewSession: "ctrl+n", + Cancel: "esc", + PageDown: "pgdown", + PageUp: "pgup", + HalfPageUp: "ctrl+u", + HalfPageDown: "ctrl+d", + Up: "up", + Down: "down", + Enter: "enter", + Escape: "esc", + J: "j", + K: "k", + Left: "left", + Right: "right", + Tab: "tab", + } +} + +// GetKeyBinding creates a new key.Binding from the given key string +func GetKeyBinding(keyStr string, helpKey, helpDesc string) key.Binding { + return key.NewBinding( + key.WithKeys(keyStr), + key.WithHelp(helpKey, helpDesc), + ) +} + +// ValidateHotkey validates a hotkey string +func ValidateHotkey(keyStr string) error { + if keyStr == "" { + return fmt.Errorf("hotkey cannot be empty") + } + + // Split into parts for modifier+key combinations + parts := strings.Split(keyStr, "+") + + // Check each part + for i, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + return fmt.Errorf("invalid hotkey format: empty part in '%s'", keyStr) + } + + // Last part is the key, others are modifiers + if i == len(parts)-1 { + // Validate key + if !isValidKey(part) { + return fmt.Errorf("invalid key '%s' in hotkey '%s'", part, keyStr) + } + } else { + // Validate modifier + if !isValidModifier(part) { + return fmt.Errorf("invalid modifier '%s' in hotkey '%s'", part, keyStr) + } + } + } + + return nil +} + +// ValidateHotkeyConfig validates all hotkeys in the configuration +func ValidateHotkeyConfig(config HotkeyConfig) error { + // Validate global hotkeys + if err := ValidateHotkey(config.Logs); err != nil { + return fmt.Errorf("logs hotkey: %w", err) + } + if err := ValidateHotkey(config.Quit); err != nil { + return fmt.Errorf("quit hotkey: %w", err) + } + if err := ValidateHotkey(config.Help); err != nil { + return fmt.Errorf("help hotkey: %w", err) + } + if err := ValidateHotkey(config.SwitchSession); err != nil { + return fmt.Errorf("switch_session hotkey: %w", err) + } + if err := ValidateHotkey(config.Commands); err != nil { + return fmt.Errorf("commands hotkey: %w", err) + } + + // Validate chat page hotkeys + if err := ValidateHotkey(config.NewSession); err != nil { + return fmt.Errorf("new_session hotkey: %w", err) + } + if err := ValidateHotkey(config.Cancel); err != nil { + return fmt.Errorf("cancel hotkey: %w", err) + } + + // Validate message navigation hotkeys + if err := ValidateHotkey(config.PageDown); err != nil { + return fmt.Errorf("page_down hotkey: %w", err) + } + if err := ValidateHotkey(config.PageUp); err != nil { + return fmt.Errorf("page_up hotkey: %w", err) + } + if err := ValidateHotkey(config.HalfPageUp); err != nil { + return fmt.Errorf("half_page_up hotkey: %w", err) + } + if err := ValidateHotkey(config.HalfPageDown); err != nil { + return fmt.Errorf("half_page_down hotkey: %w", err) + } + + // Validate dialog navigation hotkeys + if err := ValidateHotkey(config.Up); err != nil { + return fmt.Errorf("up hotkey: %w", err) + } + if err := ValidateHotkey(config.Down); err != nil { + return fmt.Errorf("down hotkey: %w", err) + } + if err := ValidateHotkey(config.Enter); err != nil { + return fmt.Errorf("enter hotkey: %w", err) + } + if err := ValidateHotkey(config.Escape); err != nil { + return fmt.Errorf("escape hotkey: %w", err) + } + if err := ValidateHotkey(config.J); err != nil { + return fmt.Errorf("j hotkey: %w", err) + } + if err := ValidateHotkey(config.K); err != nil { + return fmt.Errorf("k hotkey: %w", err) + } + if err := ValidateHotkey(config.Left); err != nil { + return fmt.Errorf("left hotkey: %w", err) + } + if err := ValidateHotkey(config.Right); err != nil { + return fmt.Errorf("right hotkey: %w", err) + } + if err := ValidateHotkey(config.Tab); err != nil { + return fmt.Errorf("tab hotkey: %w", err) + } + + return nil +} + +// isValidKey checks if a key is valid +func isValidKey(key string) bool { + // List of valid keys + validKeys := map[string]bool{ + "a": true, "b": true, "c": true, "d": true, "e": true, "f": true, "g": true, + "h": true, "i": true, "j": true, "k": true, "l": true, "m": true, "n": true, + "o": true, "p": true, "q": true, "r": true, "s": true, "t": true, "u": true, + "v": true, "w": true, "x": true, "y": true, "z": true, + "0": true, "1": true, "2": true, "3": true, "4": true, "5": true, "6": true, + "7": true, "8": true, "9": true, + "up": true, "down": true, "left": true, "right": true, + "enter": true, "space": true, "tab": true, "esc": true, "backspace": true, + "delete": true, "home": true, "end": true, "pgup": true, "pgdown": true, + "f1": true, "f2": true, "f3": true, "f4": true, "f5": true, "f6": true, + "f7": true, "f8": true, "f9": true, "f10": true, "f11": true, "f12": true, + "?": true, "!": true, "@": true, "#": true, "$": true, "%": true, "^": true, + "&": true, "*": true, "(": true, ")": true, "-": true, "_": true, "=": true, + "+": true, "[": true, "]": true, "{": true, "}": true, "\\": true, "|": true, + ";": true, ":": true, "'": true, "\"": true, ",": true, "<": true, ".": true, + ">": true, "/": true, "`": true, "~": true, + } + + return validKeys[strings.ToLower(key)] +} + +// isValidModifier checks if a modifier is valid +func isValidModifier(modifier string) bool { + validModifiers := map[string]bool{ + "ctrl": true, "alt": true, "shift": true, "cmd": true, "super": true, + } + return validModifiers[strings.ToLower(modifier)] +} \ No newline at end of file diff --git a/internal/config/hotkeys_test.go b/internal/config/hotkeys_test.go new file mode 100644 index 0000000..e8510a0 --- /dev/null +++ b/internal/config/hotkeys_test.go @@ -0,0 +1,243 @@ +package config + +import ( + "testing" +) + +func TestValidateHotkey(t *testing.T) { + tests := []struct { + name string + hotkey string + wantErr bool + }{ + { + name: "valid single key", + hotkey: "a", + wantErr: false, + }, + { + name: "valid modifier+key", + hotkey: "ctrl+l", + wantErr: false, + }, + { + name: "valid multiple modifiers", + hotkey: "ctrl+alt+delete", + wantErr: false, + }, + { + name: "empty hotkey", + hotkey: "", + wantErr: true, + }, + { + name: "invalid modifier", + hotkey: "invalid+l", + wantErr: true, + }, + { + name: "invalid key", + hotkey: "ctrl+invalid", + wantErr: true, + }, + { + name: "empty part", + hotkey: "ctrl++l", + wantErr: true, + }, + { + name: "whitespace", + hotkey: "ctrl + l", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateHotkey(tt.hotkey) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateHotkey() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestValidateHotkeyConfig(t *testing.T) { + tests := []struct { + name string + config HotkeyConfig + wantErr bool + }{ + { + name: "valid config", + config: HotkeyConfig{ + Logs: "ctrl+l", + Quit: "ctrl+c", + Help: "ctrl+?", + SwitchSession: "ctrl+a", + Commands: "ctrl+k", + NewSession: "ctrl+n", + Cancel: "esc", + PageDown: "pgdown", + PageUp: "pgup", + HalfPageUp: "ctrl+u", + HalfPageDown: "ctrl+d", + Up: "up", + Down: "down", + Enter: "enter", + Escape: "esc", + J: "j", + K: "k", + Left: "left", + Right: "right", + Tab: "tab", + }, + wantErr: false, + }, + { + name: "invalid config - empty hotkey", + config: HotkeyConfig{ + Logs: "", + Quit: "ctrl+c", + Help: "ctrl+?", + SwitchSession: "ctrl+a", + Commands: "ctrl+k", + NewSession: "ctrl+n", + Cancel: "esc", + PageDown: "pgdown", + PageUp: "pgup", + HalfPageUp: "ctrl+u", + HalfPageDown: "ctrl+d", + Up: "up", + Down: "down", + Enter: "enter", + Escape: "esc", + J: "j", + K: "k", + Left: "left", + Right: "right", + Tab: "tab", + }, + wantErr: true, + }, + { + name: "invalid config - invalid modifier", + config: HotkeyConfig{ + Logs: "invalid+l", + Quit: "ctrl+c", + Help: "ctrl+?", + SwitchSession: "ctrl+a", + Commands: "ctrl+k", + NewSession: "ctrl+n", + Cancel: "esc", + PageDown: "pgdown", + PageUp: "pgup", + HalfPageUp: "ctrl+u", + HalfPageDown: "ctrl+d", + Up: "up", + Down: "down", + Enter: "enter", + Escape: "esc", + J: "j", + K: "k", + Left: "left", + Right: "right", + Tab: "tab", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateHotkeyConfig(tt.config) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateHotkeyConfig() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestIsValidKey(t *testing.T) { + tests := []struct { + name string + key string + want bool + }{ + { + name: "valid letter", + key: "a", + want: true, + }, + { + name: "valid number", + key: "1", + want: true, + }, + { + name: "valid special key", + key: "enter", + want: true, + }, + { + name: "valid special character", + key: "?", + want: true, + }, + { + name: "invalid key", + key: "invalid", + want: false, + }, + { + name: "empty key", + key: "", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isValidKey(tt.key); got != tt.want { + t.Errorf("isValidKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsValidModifier(t *testing.T) { + tests := []struct { + name string + modifier string + want bool + }{ + { + name: "valid modifier", + modifier: "ctrl", + want: true, + }, + { + name: "valid modifier uppercase", + modifier: "CTRL", + want: true, + }, + { + name: "invalid modifier", + modifier: "invalid", + want: false, + }, + { + name: "empty modifier", + modifier: "", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isValidModifier(tt.modifier); got != tt.want { + t.Errorf("isValidModifier() = %v, want %v", got, tt.want) + } + }) + } +} \ No newline at end of file From 099dca60a559a1de977ac157d05afa2cc3aa28bd Mon Sep 17 00:00:00 2001 From: Ritik Pal Date: Mon, 28 Apr 2025 23:56:52 +0530 Subject: [PATCH 2/2] conflict fixed --- config.yaml | 31 +++++++++++ internal/tui/components/chat/list.go | 41 ++++++++------ internal/tui/components/dialog/commands.go | 59 +++++++++++--------- internal/tui/components/dialog/init.go | 41 ++++++++++++++ internal/tui/components/dialog/quit.go | 50 +++++++++-------- internal/tui/components/dialog/session.go | 59 +++++++++++--------- internal/tui/page/chat.go | 23 ++++---- internal/tui/tui.go | 62 ++++++++++++---------- 8 files changed, 240 insertions(+), 126 deletions(-) create mode 100644 config.yaml diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..5ca4243 --- /dev/null +++ b/config.yaml @@ -0,0 +1,31 @@ +# OpenCode Hotkey Configuration +# Modify these values to customize your keyboard shortcuts +# Format: "key" or "modifier+key" (e.g., "ctrl+l", "alt+enter") + +# Global hotkeys +logs: "ctrl+l" # View logs +quit: "ctrl+c" # Quit application +help: "ctrl+?" # Toggle help +switch_session: "ctrl+a" # Switch between sessions +commands: "ctrl+k" # Show commands + +# Chat page hotkeys +new_session: "ctrl+n" # Create new session +cancel: "esc" # Cancel current action + +# Message navigation hotkeys +page_down: "pgdown" # Page down +page_up: "pgup" # Page up +half_page_up: "ctrl+u" # Half page up +half_page_down: "ctrl+d" # Half page down + +# Dialog navigation hotkeys +up: "up" # Move up +down: "down" # Move down +enter: "enter" # Confirm selection +escape: "esc" # Close/cancel +j: "j" # Next item +k: "k" # Previous item +left: "left" # Move left +right: "right" # Move right +tab: "tab" # Switch options \ No newline at end of file diff --git a/internal/tui/components/chat/list.go b/internal/tui/components/chat/list.go index fa7332d..63fa6d3 100644 --- a/internal/tui/components/chat/list.go +++ b/internal/tui/components/chat/list.go @@ -16,6 +16,7 @@ import ( "github.com/opencode-ai/opencode/internal/session" "github.com/opencode-ai/opencode/internal/tui/styles" "github.com/opencode-ai/opencode/internal/tui/util" + "github.com/opencode-ai/opencode/internal/config" ) type cacheItem struct { @@ -43,23 +44,29 @@ type MessageKeys struct { HalfPageDown key.Binding } -var messageKeys = MessageKeys{ - PageDown: key.NewBinding( - key.WithKeys("pgdown"), - key.WithHelp("f/pgdn", "page down"), - ), - PageUp: key.NewBinding( - key.WithKeys("pgup"), - key.WithHelp("b/pgup", "page up"), - ), - HalfPageUp: key.NewBinding( - key.WithKeys("ctrl+u"), - key.WithHelp("ctrl+u", "½ page up"), - ), - HalfPageDown: key.NewBinding( - key.WithKeys("ctrl+d", "ctrl+d"), - key.WithHelp("ctrl+d", "½ page down"), - ), +func NewMessageKeys(hotkeys config.HotkeyConfig) MessageKeys { + return MessageKeys{ + PageDown: config.GetKeyBinding( + hotkeys.PageDown, + hotkeys.PageDown, + "page down", + ), + PageUp: config.GetKeyBinding( + hotkeys.PageUp, + hotkeys.PageUp, + "page up", + ), + HalfPageUp: config.GetKeyBinding( + hotkeys.HalfPageUp, + hotkeys.HalfPageUp, + "½ page up", + ), + HalfPageDown: config.GetKeyBinding( + hotkeys.HalfPageDown, + hotkeys.HalfPageDown, + "½ page down", + ), + } } func (m *messagesCmp) Init() tea.Cmd { diff --git a/internal/tui/components/dialog/commands.go b/internal/tui/components/dialog/commands.go index 5a1888c..1aa6c40 100644 --- a/internal/tui/components/dialog/commands.go +++ b/internal/tui/components/dialog/commands.go @@ -7,6 +7,7 @@ import ( "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" "github.com/opencode-ai/opencode/internal/tui/util" + "github.com/opencode-ai/opencode/internal/tui/config" ) // Command represents a command that can be executed @@ -50,31 +51,39 @@ type commandKeyMap struct { K key.Binding } -var commandKeys = commandKeyMap{ - Up: key.NewBinding( - key.WithKeys("up"), - key.WithHelp("↑", "previous command"), - ), - Down: key.NewBinding( - key.WithKeys("down"), - key.WithHelp("↓", "next command"), - ), - Enter: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("enter", "select command"), - ), - Escape: key.NewBinding( - key.WithKeys("esc"), - key.WithHelp("esc", "close"), - ), - J: key.NewBinding( - key.WithKeys("j"), - key.WithHelp("j", "next command"), - ), - K: key.NewBinding( - key.WithKeys("k"), - key.WithHelp("k", "previous command"), - ), +func NewCommandKeyMap(hotkeys config.HotkeyConfig) commandKeyMap { + return commandKeyMap{ + Up: config.GetKeyBinding( + hotkeys.Up, + "↑", + "previous command", + ), + Down: config.GetKeyBinding( + hotkeys.Down, + "↓", + "next command", + ), + Enter: config.GetKeyBinding( + hotkeys.Enter, + hotkeys.Enter, + "select command", + ), + Escape: config.GetKeyBinding( + hotkeys.Escape, + hotkeys.Escape, + "close", + ), + J: config.GetKeyBinding( + hotkeys.J, + hotkeys.J, + "next command", + ), + K: config.GetKeyBinding( + hotkeys.K, + hotkeys.K, + "previous command", + ), + } } func (c *commandDialogCmp) Init() tea.Cmd { diff --git a/internal/tui/components/dialog/init.go b/internal/tui/components/dialog/init.go index bfe2323..b531ea4 100644 --- a/internal/tui/components/dialog/init.go +++ b/internal/tui/components/dialog/init.go @@ -7,6 +7,7 @@ import ( "github.com/opencode-ai/opencode/internal/tui/styles" "github.com/opencode-ai/opencode/internal/tui/util" + "github.com/opencode-ai/opencode/internal/tui/config" ) // InitDialogCmp is a component that asks the user if they want to initialize the project. @@ -34,6 +35,46 @@ type initDialogKeyMap struct { N key.Binding } +func NewInitDialogKeyMap(hotkeys config.HotkeyConfig) initDialogKeyMap { + return initDialogKeyMap{ + Tab: config.GetKeyBinding( + hotkeys.Tab, + hotkeys.Tab, + "toggle selection", + ), + Left: config.GetKeyBinding( + hotkeys.Left, + "←", + "toggle selection", + ), + Right: config.GetKeyBinding( + hotkeys.Right, + "→", + "toggle selection", + ), + Enter: config.GetKeyBinding( + hotkeys.Enter, + hotkeys.Enter, + "confirm", + ), + Escape: config.GetKeyBinding( + hotkeys.Escape, + hotkeys.Escape, + "cancel", + ), + Y: config.GetKeyBinding( + "y", + "y", + "yes", + ), + N: config.GetKeyBinding( + "n", + "n", + "no", + ), + } +} + // ShortHelp implements key.Map. func (k initDialogKeyMap) ShortHelp() []key.Binding { return []key.Binding{ diff --git a/internal/tui/components/dialog/quit.go b/internal/tui/components/dialog/quit.go index 38c7dc1..9fcd539 100644 --- a/internal/tui/components/dialog/quit.go +++ b/internal/tui/components/dialog/quit.go @@ -9,6 +9,7 @@ import ( "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" "github.com/opencode-ai/opencode/internal/tui/util" + "github.com/opencode-ai/opencode/internal/tui/config" ) const question = "Are you sure you want to quit?" @@ -32,27 +33,34 @@ type helpMapping struct { Tab key.Binding } -var helpKeys = helpMapping{ - LeftRight: key.NewBinding( - key.WithKeys("left", "right"), - key.WithHelp("←/→", "switch options"), - ), - EnterSpace: key.NewBinding( - key.WithKeys("enter", " "), - key.WithHelp("enter/space", "confirm"), - ), - Yes: key.NewBinding( - key.WithKeys("y", "Y"), - key.WithHelp("y/Y", "yes"), - ), - No: key.NewBinding( - key.WithKeys("n", "N"), - key.WithHelp("n/N", "no"), - ), - Tab: key.NewBinding( - key.WithKeys("tab"), - key.WithHelp("tab", "switch options"), - ), +func NewHelpMapping(hotkeys config.HotkeyConfig) helpMapping { + return helpMapping{ + LeftRight: config.GetKeyBinding( + hotkeys.Left+","+hotkeys.Right, + "←/→", + "switch options", + ), + EnterSpace: config.GetKeyBinding( + hotkeys.Enter+",space", + "enter/space", + "confirm", + ), + Yes: config.GetKeyBinding( + "y,Y", + "y/Y", + "yes", + ), + No: config.GetKeyBinding( + "n,N", + "n/N", + "no", + ), + Tab: config.GetKeyBinding( + hotkeys.Tab, + hotkeys.Tab, + "switch options", + ), + } } func (q *quitDialogCmp) Init() tea.Cmd { diff --git a/internal/tui/components/dialog/session.go b/internal/tui/components/dialog/session.go index 90a0735..a370991 100644 --- a/internal/tui/components/dialog/session.go +++ b/internal/tui/components/dialog/session.go @@ -8,6 +8,7 @@ import ( "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" "github.com/opencode-ai/opencode/internal/tui/util" + "github.com/opencode-ai/opencode/internal/config" ) // SessionSelectedMsg is sent when a session is selected @@ -43,31 +44,39 @@ type sessionKeyMap struct { K key.Binding } -var sessionKeys = sessionKeyMap{ - Up: key.NewBinding( - key.WithKeys("up"), - key.WithHelp("↑", "previous session"), - ), - Down: key.NewBinding( - key.WithKeys("down"), - key.WithHelp("↓", "next session"), - ), - Enter: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("enter", "select session"), - ), - Escape: key.NewBinding( - key.WithKeys("esc"), - key.WithHelp("esc", "close"), - ), - J: key.NewBinding( - key.WithKeys("j"), - key.WithHelp("j", "next session"), - ), - K: key.NewBinding( - key.WithKeys("k"), - key.WithHelp("k", "previous session"), - ), +func NewSessionKeyMap(hotkeys config.HotkeyConfig) sessionKeyMap { + return sessionKeyMap{ + Up: config.GetKeyBinding( + hotkeys.Up, + "↑", + "previous session", + ), + Down: config.GetKeyBinding( + hotkeys.Down, + "↓", + "next session", + ), + Enter: config.GetKeyBinding( + hotkeys.Enter, + hotkeys.Enter, + "select session", + ), + Escape: config.GetKeyBinding( + hotkeys.Escape, + hotkeys.Escape, + "close", + ), + J: config.GetKeyBinding( + hotkeys.J, + hotkeys.J, + "next session", + ), + K: config.GetKeyBinding( + hotkeys.K, + hotkeys.K, + "previous session", + ), + } } func (s *sessionDialogCmp) Init() tea.Cmd { diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index e801d73..546bd08 100644 --- a/internal/tui/page/chat.go +++ b/internal/tui/page/chat.go @@ -10,6 +10,7 @@ import ( "github.com/opencode-ai/opencode/internal/tui/components/chat" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/util" + "github.com/opencode-ai/opencode/internal/config" ) var ChatPage PageID = "chat" @@ -27,15 +28,19 @@ type ChatKeyMap struct { Cancel key.Binding } -var keyMap = ChatKeyMap{ - NewSession: key.NewBinding( - key.WithKeys("ctrl+n"), - key.WithHelp("ctrl+n", "new session"), - ), - Cancel: key.NewBinding( - key.WithKeys("esc"), - key.WithHelp("esc", "cancel"), - ), +func NewChatKeyMap(hotkeys config.HotkeyConfig) ChatKeyMap { + return ChatKeyMap{ + NewSession: config.GetKeyBinding( + hotkeys.NewSession, + hotkeys.NewSession, + "new session", + ), + Cancel: config.GetKeyBinding( + hotkeys.Cancel, + hotkeys.Cancel, + "cancel", + ), + } } func (p *chatPage) Init() tea.Cmd { diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 186f812..01ac893 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -29,35 +29,39 @@ type keyMap struct { Models key.Binding } -var keys = keyMap{ - Logs: key.NewBinding( - key.WithKeys("ctrl+l"), - key.WithHelp("ctrl+l", "logs"), - ), - - Quit: key.NewBinding( - key.WithKeys("ctrl+c"), - key.WithHelp("ctrl+c", "quit"), - ), - Help: key.NewBinding( - key.WithKeys("ctrl+_"), - key.WithHelp("ctrl+?", "toggle help"), - ), - - SwitchSession: key.NewBinding( - key.WithKeys("ctrl+a"), - key.WithHelp("ctrl+a", "switch session"), - ), - - Commands: key.NewBinding( - key.WithKeys("ctrl+k"), - key.WithHelp("ctrl+k", "commands"), - ), - - Models: key.NewBinding( - key.WithKeys("ctrl+o"), - key.WithHelp("ctrl+o", "model selection"), - ), +func NewKeyMap(hotkeys config.HotkeyConfig) keyMap { + return keyMap{ + Logs: config.GetKeyBinding( + hotkeys.Logs, + hotkeys.Logs, + "logs", + ), + Quit: config.GetKeyBinding( + hotkeys.Quit, + hotkeys.Quit, + "quit", + ), + Help: config.GetKeyBinding( + hotkeys.Help, + hotkeys.Help, + "toggle help", + ), + SwitchSession: config.GetKeyBinding( + hotkeys.SwitchSession, + hotkeys.SwitchSession, + "switch session", + ), + Commands: config.GetKeyBinding( + hotkeys.Commands, + hotkeys.Commands, + "commands", + ), + Models: config.GetKeyBinding( + hotkeys.Models, + hotkeys.Models, + "model selection", + ), + } } var helpEsc = key.NewBinding(