Skip to content

configurable keymaps #136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,39 @@ opencode -c /path/to/project
| ------------------ | ------------------- |
| `Backspace` or `q` | Return to chat page |

#### Configuring Keymaps

You can remap keys in the `keymaps` section of the configuration file, for example:

```json
{
"keymaps": {
"logs": {
"keys": ["ctrl+w", "ctrl+e"],
"keymap_display": "ctrl+w/e",
"command_display": "look at my logs"
Comment on lines +289 to +290
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
"keymap_display": "ctrl+w/e",
"command_display": "look at my logs"
"help_key": "ctrl+w/e",
"help_description": "look at my logs"

I believe using help_* is a bit more descriptive here?

}
}
}
```

This will change the keymap directive in the help menu to `ctrl+w/e` and the command help text in that menu to `look at my logs`.

This example showcases using two keymaps for the same function, but you can use any number of keymaps.

The full list of remappable commands is:
- `logs`
- `quit`
- `help`
- `switch_session`
- `file_picker`
- `commands`
- `switch_theme`
- `models`
- `help_esc`
- `return_key`
- `logs_key_return_key`

## AI Assistant Tools

OpenCode's AI assistant has access to various tools to help with coding tasks:
Expand Down
53 changes: 53 additions & 0 deletions cmd/schema/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,5 +303,58 @@ func generateSchema() map[string]any {
},
}

// Add keymap definitions & configuration
keymapDefs := map[string]struct {
DefaultKeys []string
DefaultKeymapDisplay string
DefaultCommandDisplay string
}{
"logs": {config.DefaultKeymaps.Logs.Keys, config.DefaultKeymaps.Logs.KeymapDisplay, config.DefaultKeymaps.Logs.CommandDisplay},
"quit": {config.DefaultKeymaps.Quit.Keys, config.DefaultKeymaps.Quit.KeymapDisplay, config.DefaultKeymaps.Quit.CommandDisplay},
"help": {config.DefaultKeymaps.Help.Keys, config.DefaultKeymaps.Help.KeymapDisplay, config.DefaultKeymaps.Help.CommandDisplay},
"switch_session": {config.DefaultKeymaps.SwitchSession.Keys, config.DefaultKeymaps.SwitchSession.KeymapDisplay, config.DefaultKeymaps.SwitchSession.CommandDisplay},
"commands": {config.DefaultKeymaps.Commands.Keys, config.DefaultKeymaps.Commands.KeymapDisplay, config.DefaultKeymaps.Commands.CommandDisplay},
"switch_theme": {config.DefaultKeymaps.SwitchTheme.Keys, config.DefaultKeymaps.SwitchTheme.KeymapDisplay, config.DefaultKeymaps.SwitchTheme.CommandDisplay},
"help_esc": {config.DefaultKeymaps.HelpEsc.Keys, config.DefaultKeymaps.HelpEsc.KeymapDisplay, config.DefaultKeymaps.HelpEsc.CommandDisplay},
"return_key": {config.DefaultKeymaps.ReturnKey.Keys, config.DefaultKeymaps.ReturnKey.KeymapDisplay, config.DefaultKeymaps.ReturnKey.CommandDisplay},
"models": {config.DefaultKeymaps.Models.Keys, config.DefaultKeymaps.Models.KeymapDisplay, config.DefaultKeymaps.Models.CommandDisplay},
"logs_key_return_key": {config.DefaultKeymaps.LogsKeyReturnKey.Keys, config.DefaultKeymaps.LogsKeyReturnKey.KeymapDisplay, config.DefaultKeymaps.LogsKeyReturnKey.CommandDisplay},
}

keymapProperties := map[string]any{}
for name, def := range keymapDefs {
keymapProperties[name] = map[string]any{
"type": "object",
"description": def.DefaultCommandDisplay,
"properties": map[string]any{
"keys": map[string]any{
"type": "array",
"description": "Keyboard shortcuts",
"items": map[string]any{
"type": "string",
},
"default": def.DefaultKeys,
},
"keymap_display": map[string]any{
"type": "string",
"description": "UI label",
"default": def.DefaultKeymapDisplay,
},
"command_display": map[string]any{
"type": "string",
"description": "UI label",
"default": def.DefaultCommandDisplay,
},
},
"required": []string{"keys", "keymap_display", "command_display"},
}
}

schema["properties"].(map[string]any)["keymaps"] = map[string]any{
"type": "object",
"description": "Keymap configurations",
"properties": keymapProperties,
}

return schema
}
127 changes: 127 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,27 @@ type LSPConfig struct {
Options any `json:"options"`
}

type KeymapConfig struct {
Keys []string `json:"keys" mapstructure:"keys"`
KeymapDisplay string `json:"keymap_display" mapstructure:"keymap_display"`
CommandDisplay string `json:"command_display" mapstructure:"command_display"`
}

// Keymap defines a keymap for the TUI.
type Keymaps struct {
Logs KeymapConfig `json:"logs"`
Quit KeymapConfig `json:"quit"`
Help KeymapConfig `json:"help"`
SwitchSession KeymapConfig `json:"switch_session" mapstructure:"switch_session"`
Commands KeymapConfig `json:"commands"`
FilePicker KeymapConfig `json:"file_picker" mapstructure:"file_picker"`
SwitchTheme KeymapConfig `json:"switch_theme" mapstructure:"switch_theme"`
HelpEsc KeymapConfig `json:"help_esc" mapstructure:"help_esc"`
ReturnKey KeymapConfig `json:"return_key" mapstructure:"return_key"`
LogsKeyReturnKey KeymapConfig `json:"logs_key_return_key" mapstructure:"logs_key_return_key"`
Models KeymapConfig `json:"models"`
}

// TUIConfig defines the configuration for the Terminal User Interface.
type TUIConfig struct {
Theme string `json:"theme,omitempty"`
Expand All @@ -79,6 +100,7 @@ type Config struct {
MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
LSP map[string]LSPConfig `json:"lsp,omitempty"`
Keymaps Keymaps `json:"keymaps,omitzero"`
Agents map[AgentName]Agent `json:"agents"`
Debug bool `json:"debug,omitempty"`
DebugLSP bool `json:"debugLSP,omitempty"`
Expand Down Expand Up @@ -109,6 +131,20 @@ var defaultContextPaths = []string{
"OPENCODE.local.md",
}

var DefaultKeymaps = Keymaps{
Logs: KeymapConfig{Keys: []string{"ctrl+l"}, KeymapDisplay: "ctrl+l", CommandDisplay: "logs"},
Quit: KeymapConfig{Keys: []string{"ctrl+c"}, KeymapDisplay: "ctrl+c", CommandDisplay: "quit"},
Help: KeymapConfig{Keys: []string{"ctrl+_"}, KeymapDisplay: "ctrl+?", CommandDisplay: "toggle help"},
SwitchSession: KeymapConfig{Keys: []string{"ctrl+a"}, KeymapDisplay: "ctrl+a", CommandDisplay: "switch session"},
Commands: KeymapConfig{Keys: []string{"ctrl+k"}, KeymapDisplay: "ctrl+k", CommandDisplay: "commands"},
FilePicker: KeymapConfig{Keys: []string{"ctrl+f"}, KeymapDisplay: "ctrl+f", CommandDisplay: "select files to upload"},
Models: KeymapConfig{Keys: []string{"ctrl+o"}, KeymapDisplay: "ctrl+o", CommandDisplay: "model selection"},
SwitchTheme: KeymapConfig{Keys: []string{"ctrl+t"}, KeymapDisplay: "ctrl+t", CommandDisplay: "switch theme"},
HelpEsc: KeymapConfig{Keys: []string{"?"}, KeymapDisplay: "?", CommandDisplay: "toggle help"},
ReturnKey: KeymapConfig{Keys: []string{"esc"}, KeymapDisplay: "esc", CommandDisplay: "close"},
LogsKeyReturnKey: KeymapConfig{Keys: []string{"esc", "q", "backspace"}, KeymapDisplay: "esc/q", CommandDisplay: "go back"},
}

// Global configuration instance
var cfg *Config

Expand All @@ -125,6 +161,7 @@ func Load(workingDir string, debug bool) (*Config, error) {
MCPServers: make(map[string]MCPServer),
Providers: make(map[models.ModelProvider]Provider),
LSP: make(map[string]LSPConfig),
Keymaps: DefaultKeymaps,
}

configureViper()
Expand Down Expand Up @@ -194,6 +231,7 @@ func Load(workingDir string, debug bool) (*Config, error) {
Model: cfg.Agents[AgentTitle].Model,
MaxTokens: 80,
}

return cfg, nil
}

Expand All @@ -212,6 +250,7 @@ func configureViper() {
func setDefaults(debug bool) {
viper.SetDefault("data.directory", defaultDataDirectory)
viper.SetDefault("contextPaths", defaultContextPaths)
viper.SetDefault("keymaps", DefaultKeymaps)
viper.SetDefault("tui.theme", "opencode")

if debug {
Expand Down Expand Up @@ -517,6 +556,91 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
return nil
}

func validateKeymaps(keymaps *Keymaps) error {
if keymaps == nil {
return fmt.Errorf("keymaps configuration is nil")
}

validateKeymap := func(name string, k KeymapConfig) error {
if len(k.Keys) == 0 {
return fmt.Errorf("%s keymap has no keys configured", name)
}
if k.KeymapDisplay == "" {
return fmt.Errorf("%s keymap has no display value", name)
}
if k.CommandDisplay == "" {
return fmt.Errorf("%s keymap has no command display value", name)
}
return nil
}

if err := validateKeymap("logs", keymaps.Logs); err != nil {
return err
}
if err := validateKeymap("quit", keymaps.Quit); err != nil {
return err
}
if err := validateKeymap("help", keymaps.Help); err != nil {
return err
}
if err := validateKeymap("switch_session", keymaps.SwitchSession); err != nil {
return err
}
if err := validateKeymap("commands", keymaps.Commands); err != nil {
return err
}
if err := validateKeymap("models", keymaps.Models); err != nil {
return err
}
if err := validateKeymap("help_esc", keymaps.HelpEsc); err != nil {
return err
}
if err := validateKeymap("return_key", keymaps.ReturnKey); err != nil {
return err
}
if err := validateKeymap("logs_key_return_key", keymaps.LogsKeyReturnKey); err != nil {
return err
}

keyBindings := make(map[string]string)
checkDuplicates := func(name string, k KeymapConfig) error {
for _, key := range k.Keys {
if existing, exists := keyBindings[key]; exists {
return fmt.Errorf("duplicate key binding '%s' found in %s and %s", key, existing, name)
}
keyBindings[key] = name
}
return nil
}

if err := checkDuplicates("logs", keymaps.Logs); err != nil {
return err
}
if err := checkDuplicates("quit", keymaps.Quit); err != nil {
return err
}
if err := checkDuplicates("help", keymaps.Help); err != nil {
return err
}
if err := checkDuplicates("switch_session", keymaps.SwitchSession); err != nil {
return err
}
if err := checkDuplicates("commands", keymaps.Commands); err != nil {
return err
}
if err := checkDuplicates("models", keymaps.Models); err != nil {
return err
}
if err := checkDuplicates("help_esc", keymaps.HelpEsc); err != nil {
return err
}
if err := checkDuplicates("return_key", keymaps.ReturnKey); err != nil {
return err
}

return nil
}

// Validate checks if the configuration is valid and applies defaults where needed.
func Validate() error {
if cfg == nil {
Expand Down Expand Up @@ -548,6 +672,9 @@ func Validate() error {
}
}

if err := validateKeymaps(&cfg.Keymaps); err != nil {
return fmt.Errorf("keymap validation failed: %w", err)
}
return nil
}

Expand Down
Loading