Skip to content

Commit 676b3cf

Browse files
authored
feat: support customize keybindings (#79)
* feat: customize key bindings * fix * tweak
1 parent 65b903a commit 676b3cf

File tree

4 files changed

+138
-74
lines changed

4 files changed

+138
-74
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,42 @@ go install github.com/j178/chatgpt/cmd/chatgpt@latest
128128
| `ctrl+d` | Submit text when in multi-line mode |
129129
| `enter` | Insert a new line when in multi-line mode |
130130

131+
132+
### Custom Key Bindings
133+
134+
You can change the default key bindings by adding `key_map` dictionary to the configuration file. For example:
135+
136+
```jsonc
137+
{
138+
"api_key": "sk-xxxxxx",
139+
"endpoint": "https://api.openai.com/v1",
140+
"prompts": {
141+
// ...
142+
},
143+
// Default conversation parameters
144+
"conversation": {
145+
// ...
146+
},
147+
"key_map": {
148+
"switch_multiline": ["ctrl+j"],
149+
"submit": ["enter"],
150+
"multiline_submit": ["ctrl+d"],
151+
"insert_newline": ["enter"],
152+
"multiline_insert_newline": ["ctrl+d"],
153+
"help": ["ctrl+h"],
154+
"quit": ["esc", "ctrl+c"],
155+
"copy_last_answer": ["ctrl+y"],
156+
"previous_question": ["ctrl+p"],
157+
"next_question": ["ctrl+n"],
158+
"new_conversation": ["ctrl+t"],
159+
"previous_conversation": ["ctrl+left", "ctrl+g"],
160+
"next_conversation": ["ctrl+right", "ctrl+o"],
161+
"remove_conversation": ["ctrl+r"],
162+
"forget_context": ["ctrl+x"],
163+
}
164+
}
165+
```
166+
131167
</details>
132168

133169
## Advanced usage

config.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ type ConversationConfig struct {
2121
MaxTokens int `json:"max_tokens"`
2222
}
2323

24+
type KeyMapConfig struct {
25+
SwitchMultiline []string `json:"switch_multiline"`
26+
Submit []string `json:"submit,omitempty"`
27+
MultilineSubmit []string `json:"multiline_submit,omitempty"`
28+
InsertNewline []string `json:"insert_newline,omitempty"`
29+
MultilineInsertNewLine []string `json:"multiline_insert_newline,omitempty"`
30+
Help []string `json:"help,omitempty"`
31+
Quit []string `json:"quit,omitempty"`
32+
CopyLastAnswer []string `json:"copy_last_answer,omitempty"`
33+
PreviousQuestion []string `json:"previous_question,omitempty"`
34+
NextQuestion []string `json:"next_question,omitempty"`
35+
NewConversation []string `json:"new_conversation,omitempty"`
36+
PreviousConversation []string `json:"previous_conversation,omitempty"`
37+
NextConversation []string `json:"next_conversation,omitempty"`
38+
RemoveConversation []string `json:"remove_conversation,omitempty"`
39+
ForgetContext []string `json:"forget_context,omitempty"`
40+
}
41+
2442
type GlobalConfig struct {
2543
APIKey string `json:"api_key"`
2644
Endpoint string `json:"endpoint"`
@@ -30,6 +48,7 @@ type GlobalConfig struct {
3048
OrgID string `json:"org_id,omitempty"`
3149
Prompts map[string]string `json:"prompts"`
3250
Conversation ConversationConfig `json:"conversation"` // Default conversation config
51+
KeyMap KeyMapConfig `json:"key_map"`
3352
}
3453

3554
func (c *GlobalConfig) LookupPrompt(key string) string {
@@ -87,6 +106,26 @@ func readOrWriteConfig(conf *GlobalConfig) error {
87106
return nil
88107
}
89108

109+
func defaultKeyMapConfig() KeyMapConfig {
110+
return KeyMapConfig{
111+
SwitchMultiline: []string{"ctrl+j"},
112+
Submit: []string{"enter"},
113+
InsertNewline: []string{"ctrl+d"},
114+
MultilineSubmit: []string{"ctrl+d"},
115+
MultilineInsertNewLine: []string{"enter"},
116+
Help: []string{"ctrl+h"},
117+
Quit: []string{"esc", "ctrl+c"},
118+
CopyLastAnswer: []string{"ctrl+y"},
119+
PreviousQuestion: []string{"ctrl+p"},
120+
NextQuestion: []string{"ctrl+n"},
121+
NewConversation: []string{"ctrl+t"},
122+
PreviousConversation: []string{"ctrl+left", "ctrl+g"},
123+
NextConversation: []string{"ctrl+right", "ctrl+o"},
124+
RemoveConversation: []string{"ctrl+r"},
125+
ForgetContext: []string{"ctrl+x"},
126+
}
127+
}
128+
90129
func InitConfig() (GlobalConfig, error) {
91130
conf := GlobalConfig{
92131
APIType: openai.APITypeOpenAI,
@@ -104,6 +143,7 @@ func InitConfig() (GlobalConfig, error) {
104143
Temperature: 0,
105144
MaxTokens: 1024,
106145
},
146+
KeyMap: defaultKeyMapConfig(),
107147
}
108148
err := readOrWriteConfig(&conf)
109149
if err != nil {

ui/keys.go

Lines changed: 28 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,25 @@ import (
44
"github.com/charmbracelet/bubbles/key"
55
"github.com/charmbracelet/bubbles/textarea"
66
"github.com/charmbracelet/bubbles/viewport"
7+
8+
"github.com/j178/chatgpt"
79
)
810

11+
type InputMode int
12+
13+
const (
14+
InputModelSingleLine InputMode = iota
15+
InputModelMultiLine
16+
)
17+
18+
func newBinding(keys []string, help string) key.Binding {
19+
return key.NewBinding(key.WithKeys(keys...), key.WithHelp(keys[0], help))
20+
}
21+
922
type keyMap struct {
1023
SwitchMultiline key.Binding
1124
Submit key.Binding
12-
ShowHelp key.Binding
13-
HideHelp key.Binding
25+
ToggleHelp key.Binding
1426
Quit key.Binding
1527
Copy key.Binding
1628
PrevHistory key.Binding
@@ -25,7 +37,7 @@ type keyMap struct {
2537
}
2638

2739
func (k keyMap) ShortHelp() []key.Binding {
28-
return []key.Binding{k.ShowHelp}
40+
return []key.Binding{k.ToggleHelp}
2941
}
3042

3143
func (k keyMap) FullHelp() [][]key.Binding {
@@ -43,30 +55,20 @@ func (k keyMap) FullHelp() [][]key.Binding {
4355
}
4456
}
4557

46-
func defaultKeyMap() keyMap {
58+
func newKeyMap(conf chatgpt.KeyMapConfig) keyMap {
4759
return keyMap{
48-
SwitchMultiline: key.NewBinding(key.WithKeys("ctrl+j"), key.WithHelp("ctrl+j", "multiline mode")),
49-
Submit: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")),
50-
ShowHelp: key.NewBinding(key.WithKeys("ctrl+h"), key.WithHelp("ctrl+h", "show help")),
51-
HideHelp: key.NewBinding(key.WithKeys("ctrl+h"), key.WithHelp("ctrl+h", "hide help")),
52-
Quit: key.NewBinding(key.WithKeys("esc", "ctrl+c"), key.WithHelp("esc", "quit")),
53-
Copy: key.NewBinding(key.WithKeys("ctrl+y"), key.WithHelp("ctrl+y", "copy last answer")),
54-
PrevHistory: key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "previous question")),
55-
NextHistory: key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next question")),
56-
NewConversation: key.NewBinding(key.WithKeys("ctrl+t"), key.WithHelp("ctrl+t", "new conversation")),
57-
ForgetContext: key.NewBinding(key.WithKeys("ctrl+x"), key.WithHelp("ctrl+x", "forget context")),
58-
RemoveConversation: key.NewBinding(
59-
key.WithKeys("ctrl+r"),
60-
key.WithHelp("ctrl+r", "remove current conversation"),
61-
),
62-
PrevConversation: key.NewBinding(
63-
key.WithKeys("ctrl+left", "ctrl+g"),
64-
key.WithHelp("ctrl+left", "previous conversation"),
65-
),
66-
NextConversation: key.NewBinding(
67-
key.WithKeys("ctrl+right", "ctrl+o"),
68-
key.WithHelp("ctrl+right", "next conversation"),
69-
),
60+
SwitchMultiline: newBinding(conf.SwitchMultiline, "multiline mode"),
61+
Submit: newBinding(conf.Submit, "submit"),
62+
ToggleHelp: newBinding(conf.Help, "toggle help"),
63+
Quit: newBinding(conf.Quit, "quit"),
64+
Copy: newBinding(conf.CopyLastAnswer, "copy last answer"),
65+
PrevHistory: newBinding(conf.PreviousQuestion, "previous question"),
66+
NextHistory: newBinding(conf.NextQuestion, "next question"),
67+
NewConversation: newBinding(conf.NewConversation, "new conversation"),
68+
ForgetContext: newBinding(conf.ForgetContext, "forget context"),
69+
RemoveConversation: newBinding(conf.RemoveConversation, "remove current conversation"),
70+
PrevConversation: newBinding(conf.PreviousConversation, "previous conversation"),
71+
NextConversation: newBinding(conf.NextConversation, "next conversation"),
7072
ViewPortKeys: viewport.KeyMap{
7173
PageDown: key.NewBinding(
7274
key.WithKeys("pgdown"),
@@ -115,34 +117,3 @@ func defaultKeyMap() keyMap {
115117
},
116118
}
117119
}
118-
119-
type InputMode int
120-
121-
const (
122-
InputModelSingleLine InputMode = iota
123-
InputModelMultiLine
124-
)
125-
126-
func UseSingleLineInputMode(m *Model) {
127-
m.inputMode = InputModelSingleLine
128-
m.keymap.SwitchMultiline = key.NewBinding(key.WithKeys("ctrl+j"), key.WithHelp("ctrl+j", "multiline mode"))
129-
m.keymap.Submit = key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit"))
130-
m.keymap.TextAreaKeys.InsertNewline = key.NewBinding(
131-
key.WithKeys("ctrl+d"),
132-
key.WithHelp("ctrl+d", "insert new line"),
133-
)
134-
m.viewport.KeyMap = m.keymap.ViewPortKeys
135-
m.textarea.KeyMap = m.keymap.TextAreaKeys
136-
}
137-
138-
func UseMultiLineInputMode(m *Model) {
139-
m.inputMode = InputModelMultiLine
140-
m.keymap.SwitchMultiline = key.NewBinding(key.WithKeys("ctrl+j"), key.WithHelp("ctrl+j", "single line mode"))
141-
m.keymap.Submit = key.NewBinding(key.WithKeys("ctrl+d"), key.WithHelp("ctrl+d", "submit"))
142-
m.keymap.TextAreaKeys.InsertNewline = key.NewBinding(
143-
key.WithKeys("enter"),
144-
key.WithHelp("enter", "insert new line"),
145-
)
146-
m.viewport.KeyMap = m.keymap.ViewPortKeys
147-
m.textarea.KeyMap = m.keymap.TextAreaKeys
148-
}

ui/ui.go

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func InitialModel(
8383
glamour.WithWordWrap(0), // we do hard-wrapping ourselves
8484
)
8585

86-
keymap := defaultKeyMap()
86+
keymap := newKeyMap(conf.KeyMap)
8787
m := Model{
8888
textarea: ta,
8989
viewport: vp,
@@ -92,11 +92,11 @@ func InitialModel(
9292
globalConf: conf,
9393
chatgpt: chatgpt,
9494
conversations: conversations,
95+
historyIdx: conversations.Curr().Len(),
9596
keymap: keymap,
9697
renderer: renderer,
9798
}
98-
m.historyIdx = m.conversations.Curr().Len()
99-
UseSingleLineInputMode(&m)
99+
m = m.SetInputMode(InputModelSingleLine)
100100
return m
101101
}
102102

@@ -146,7 +146,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
146146
}
147147
case tea.KeyMsg:
148148
switch {
149-
case key.Matches(msg, m.keymap.ShowHelp, m.keymap.HideHelp):
149+
case key.Matches(msg, m.keymap.ToggleHelp):
150150
m.help.ShowAll = !m.help.ShowAll
151151
m.viewport.Height = m.height - m.textarea.Height() - lipgloss.Height(m.RenderFooter())
152152
m.viewport.SetContent(m.RenderConversation(m.viewport.Width))
@@ -235,16 +235,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
235235
m.historyIdx = m.conversations.Curr().Len()
236236
case key.Matches(msg, m.keymap.SwitchMultiline):
237237
if m.inputMode == InputModelSingleLine {
238-
UseMultiLineInputMode(&m)
239-
m.textarea.ShowLineNumbers = true
240-
m.textarea.SetHeight(2)
241-
m.viewport.Height = m.height - m.textarea.Height() - lipgloss.Height(m.RenderFooter())
238+
m = m.SetInputMode(InputModelMultiLine)
242239
} else {
243-
UseSingleLineInputMode(&m)
244-
m.textarea.ShowLineNumbers = false
245-
m.textarea.SetHeight(1)
246-
m.viewport.Height = m.height - m.textarea.Height() - lipgloss.Height(m.RenderFooter())
240+
m = m.SetInputMode(InputModelSingleLine)
247241
}
242+
m.viewport.Height = m.height - m.textarea.Height() - lipgloss.Height(m.RenderFooter())
248243
m.viewport.SetContent(m.RenderConversation(m.viewport.Width))
249244
case key.Matches(msg, m.keymap.Copy):
250245
if m.answering || m.conversations.Curr().LastAnswer() == "" {
@@ -326,16 +321,38 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
326321
return m, tea.Batch(cmds...)
327322
}
328323

324+
func (m Model) SetInputMode(mode InputMode) Model {
325+
keys := m.globalConf.KeyMap
326+
if mode == InputModelMultiLine {
327+
m.keymap.SwitchMultiline = newBinding(keys.SwitchMultiline, "single line mode")
328+
m.keymap.Submit = newBinding(keys.MultilineSubmit, "submit")
329+
m.keymap.TextAreaKeys.InsertNewline = newBinding(keys.MultilineInsertNewLine, "insert new line")
330+
m.inputMode = InputModelMultiLine
331+
m.textarea.ShowLineNumbers = true
332+
m.textarea.SetHeight(2)
333+
} else {
334+
m.keymap.SwitchMultiline = newBinding(keys.SwitchMultiline, "multiline mode")
335+
m.keymap.Submit = newBinding(keys.Submit, "submit")
336+
m.keymap.TextAreaKeys.InsertNewline = newBinding(keys.InsertNewline, "insert new line")
337+
m.inputMode = InputModelSingleLine
338+
m.textarea.ShowLineNumbers = false
339+
m.textarea.SetHeight(1)
340+
}
341+
m.viewport.KeyMap = m.keymap.ViewPortKeys
342+
m.textarea.KeyMap = m.keymap.TextAreaKeys
343+
return m
344+
}
345+
329346
var (
330347
senderStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("5"))
331348
botStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6"))
332349
errorStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("1"))
333350
footerStyle = lipgloss.NewStyle().
334-
Height(1).
335-
BorderTop(true).
336-
BorderStyle(lipgloss.NormalBorder()).
337-
BorderForeground(lipgloss.Color("8")).
338-
Faint(true)
351+
Height(1).
352+
BorderTop(true).
353+
BorderStyle(lipgloss.NormalBorder()).
354+
BorderForeground(lipgloss.Color("8")).
355+
Faint(true)
339356
)
340357

341358
func (m Model) RenderConversation(maxWidth int) string {

0 commit comments

Comments
 (0)