Skip to content

Commit

Permalink
feat: support customize keybindings (#79)
Browse files Browse the repository at this point in the history
* feat: customize key bindings

* fix

* tweak
  • Loading branch information
j178 authored Sep 14, 2023
1 parent 65b903a commit 676b3cf
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 74 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,42 @@ go install github.com/j178/chatgpt/cmd/chatgpt@latest
| `ctrl+d` | Submit text when in multi-line mode |
| `enter` | Insert a new line when in multi-line mode |


### Custom Key Bindings

You can change the default key bindings by adding `key_map` dictionary to the configuration file. For example:

```jsonc
{
"api_key": "sk-xxxxxx",
"endpoint": "https://api.openai.com/v1",
"prompts": {
// ...
},
// Default conversation parameters
"conversation": {
// ...
},
"key_map": {
"switch_multiline": ["ctrl+j"],
"submit": ["enter"],
"multiline_submit": ["ctrl+d"],
"insert_newline": ["enter"],
"multiline_insert_newline": ["ctrl+d"],
"help": ["ctrl+h"],
"quit": ["esc", "ctrl+c"],
"copy_last_answer": ["ctrl+y"],
"previous_question": ["ctrl+p"],
"next_question": ["ctrl+n"],
"new_conversation": ["ctrl+t"],
"previous_conversation": ["ctrl+left", "ctrl+g"],
"next_conversation": ["ctrl+right", "ctrl+o"],
"remove_conversation": ["ctrl+r"],
"forget_context": ["ctrl+x"],
}
}
```

</details>

## Advanced usage
Expand Down
40 changes: 40 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ type ConversationConfig struct {
MaxTokens int `json:"max_tokens"`
}

type KeyMapConfig struct {
SwitchMultiline []string `json:"switch_multiline"`
Submit []string `json:"submit,omitempty"`
MultilineSubmit []string `json:"multiline_submit,omitempty"`
InsertNewline []string `json:"insert_newline,omitempty"`
MultilineInsertNewLine []string `json:"multiline_insert_newline,omitempty"`
Help []string `json:"help,omitempty"`
Quit []string `json:"quit,omitempty"`
CopyLastAnswer []string `json:"copy_last_answer,omitempty"`
PreviousQuestion []string `json:"previous_question,omitempty"`
NextQuestion []string `json:"next_question,omitempty"`
NewConversation []string `json:"new_conversation,omitempty"`
PreviousConversation []string `json:"previous_conversation,omitempty"`
NextConversation []string `json:"next_conversation,omitempty"`
RemoveConversation []string `json:"remove_conversation,omitempty"`
ForgetContext []string `json:"forget_context,omitempty"`
}

type GlobalConfig struct {
APIKey string `json:"api_key"`
Endpoint string `json:"endpoint"`
Expand All @@ -30,6 +48,7 @@ type GlobalConfig struct {
OrgID string `json:"org_id,omitempty"`
Prompts map[string]string `json:"prompts"`
Conversation ConversationConfig `json:"conversation"` // Default conversation config
KeyMap KeyMapConfig `json:"key_map"`
}

func (c *GlobalConfig) LookupPrompt(key string) string {
Expand Down Expand Up @@ -87,6 +106,26 @@ func readOrWriteConfig(conf *GlobalConfig) error {
return nil
}

func defaultKeyMapConfig() KeyMapConfig {
return KeyMapConfig{
SwitchMultiline: []string{"ctrl+j"},
Submit: []string{"enter"},
InsertNewline: []string{"ctrl+d"},
MultilineSubmit: []string{"ctrl+d"},
MultilineInsertNewLine: []string{"enter"},
Help: []string{"ctrl+h"},
Quit: []string{"esc", "ctrl+c"},
CopyLastAnswer: []string{"ctrl+y"},
PreviousQuestion: []string{"ctrl+p"},
NextQuestion: []string{"ctrl+n"},
NewConversation: []string{"ctrl+t"},
PreviousConversation: []string{"ctrl+left", "ctrl+g"},
NextConversation: []string{"ctrl+right", "ctrl+o"},
RemoveConversation: []string{"ctrl+r"},
ForgetContext: []string{"ctrl+x"},
}
}

func InitConfig() (GlobalConfig, error) {
conf := GlobalConfig{
APIType: openai.APITypeOpenAI,
Expand All @@ -104,6 +143,7 @@ func InitConfig() (GlobalConfig, error) {
Temperature: 0,
MaxTokens: 1024,
},
KeyMap: defaultKeyMapConfig(),
}
err := readOrWriteConfig(&conf)
if err != nil {
Expand Down
85 changes: 28 additions & 57 deletions ui/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@ import (
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/bubbles/viewport"

"github.com/j178/chatgpt"
)

type InputMode int

const (
InputModelSingleLine InputMode = iota
InputModelMultiLine
)

func newBinding(keys []string, help string) key.Binding {
return key.NewBinding(key.WithKeys(keys...), key.WithHelp(keys[0], help))
}

type keyMap struct {
SwitchMultiline key.Binding
Submit key.Binding
ShowHelp key.Binding
HideHelp key.Binding
ToggleHelp key.Binding
Quit key.Binding
Copy key.Binding
PrevHistory key.Binding
Expand All @@ -25,7 +37,7 @@ type keyMap struct {
}

func (k keyMap) ShortHelp() []key.Binding {
return []key.Binding{k.ShowHelp}
return []key.Binding{k.ToggleHelp}
}

func (k keyMap) FullHelp() [][]key.Binding {
Expand All @@ -43,30 +55,20 @@ func (k keyMap) FullHelp() [][]key.Binding {
}
}

func defaultKeyMap() keyMap {
func newKeyMap(conf chatgpt.KeyMapConfig) keyMap {
return keyMap{
SwitchMultiline: key.NewBinding(key.WithKeys("ctrl+j"), key.WithHelp("ctrl+j", "multiline mode")),
Submit: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")),
ShowHelp: key.NewBinding(key.WithKeys("ctrl+h"), key.WithHelp("ctrl+h", "show help")),
HideHelp: key.NewBinding(key.WithKeys("ctrl+h"), key.WithHelp("ctrl+h", "hide help")),
Quit: key.NewBinding(key.WithKeys("esc", "ctrl+c"), key.WithHelp("esc", "quit")),
Copy: key.NewBinding(key.WithKeys("ctrl+y"), key.WithHelp("ctrl+y", "copy last answer")),
PrevHistory: key.NewBinding(key.WithKeys("ctrl+p"), key.WithHelp("ctrl+p", "previous question")),
NextHistory: key.NewBinding(key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "next question")),
NewConversation: key.NewBinding(key.WithKeys("ctrl+t"), key.WithHelp("ctrl+t", "new conversation")),
ForgetContext: key.NewBinding(key.WithKeys("ctrl+x"), key.WithHelp("ctrl+x", "forget context")),
RemoveConversation: key.NewBinding(
key.WithKeys("ctrl+r"),
key.WithHelp("ctrl+r", "remove current conversation"),
),
PrevConversation: key.NewBinding(
key.WithKeys("ctrl+left", "ctrl+g"),
key.WithHelp("ctrl+left", "previous conversation"),
),
NextConversation: key.NewBinding(
key.WithKeys("ctrl+right", "ctrl+o"),
key.WithHelp("ctrl+right", "next conversation"),
),
SwitchMultiline: newBinding(conf.SwitchMultiline, "multiline mode"),
Submit: newBinding(conf.Submit, "submit"),
ToggleHelp: newBinding(conf.Help, "toggle help"),
Quit: newBinding(conf.Quit, "quit"),
Copy: newBinding(conf.CopyLastAnswer, "copy last answer"),
PrevHistory: newBinding(conf.PreviousQuestion, "previous question"),
NextHistory: newBinding(conf.NextQuestion, "next question"),
NewConversation: newBinding(conf.NewConversation, "new conversation"),
ForgetContext: newBinding(conf.ForgetContext, "forget context"),
RemoveConversation: newBinding(conf.RemoveConversation, "remove current conversation"),
PrevConversation: newBinding(conf.PreviousConversation, "previous conversation"),
NextConversation: newBinding(conf.NextConversation, "next conversation"),
ViewPortKeys: viewport.KeyMap{
PageDown: key.NewBinding(
key.WithKeys("pgdown"),
Expand Down Expand Up @@ -115,34 +117,3 @@ func defaultKeyMap() keyMap {
},
}
}

type InputMode int

const (
InputModelSingleLine InputMode = iota
InputModelMultiLine
)

func UseSingleLineInputMode(m *Model) {
m.inputMode = InputModelSingleLine
m.keymap.SwitchMultiline = key.NewBinding(key.WithKeys("ctrl+j"), key.WithHelp("ctrl+j", "multiline mode"))
m.keymap.Submit = key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit"))
m.keymap.TextAreaKeys.InsertNewline = key.NewBinding(
key.WithKeys("ctrl+d"),
key.WithHelp("ctrl+d", "insert new line"),
)
m.viewport.KeyMap = m.keymap.ViewPortKeys
m.textarea.KeyMap = m.keymap.TextAreaKeys
}

func UseMultiLineInputMode(m *Model) {
m.inputMode = InputModelMultiLine
m.keymap.SwitchMultiline = key.NewBinding(key.WithKeys("ctrl+j"), key.WithHelp("ctrl+j", "single line mode"))
m.keymap.Submit = key.NewBinding(key.WithKeys("ctrl+d"), key.WithHelp("ctrl+d", "submit"))
m.keymap.TextAreaKeys.InsertNewline = key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "insert new line"),
)
m.viewport.KeyMap = m.keymap.ViewPortKeys
m.textarea.KeyMap = m.keymap.TextAreaKeys
}
51 changes: 34 additions & 17 deletions ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func InitialModel(
glamour.WithWordWrap(0), // we do hard-wrapping ourselves
)

keymap := defaultKeyMap()
keymap := newKeyMap(conf.KeyMap)
m := Model{
textarea: ta,
viewport: vp,
Expand All @@ -92,11 +92,11 @@ func InitialModel(
globalConf: conf,
chatgpt: chatgpt,
conversations: conversations,
historyIdx: conversations.Curr().Len(),
keymap: keymap,
renderer: renderer,
}
m.historyIdx = m.conversations.Curr().Len()
UseSingleLineInputMode(&m)
m = m.SetInputMode(InputModelSingleLine)
return m
}

Expand Down Expand Up @@ -146,7 +146,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keymap.ShowHelp, m.keymap.HideHelp):
case key.Matches(msg, m.keymap.ToggleHelp):
m.help.ShowAll = !m.help.ShowAll
m.viewport.Height = m.height - m.textarea.Height() - lipgloss.Height(m.RenderFooter())
m.viewport.SetContent(m.RenderConversation(m.viewport.Width))
Expand Down Expand Up @@ -235,16 +235,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.historyIdx = m.conversations.Curr().Len()
case key.Matches(msg, m.keymap.SwitchMultiline):
if m.inputMode == InputModelSingleLine {
UseMultiLineInputMode(&m)
m.textarea.ShowLineNumbers = true
m.textarea.SetHeight(2)
m.viewport.Height = m.height - m.textarea.Height() - lipgloss.Height(m.RenderFooter())
m = m.SetInputMode(InputModelMultiLine)
} else {
UseSingleLineInputMode(&m)
m.textarea.ShowLineNumbers = false
m.textarea.SetHeight(1)
m.viewport.Height = m.height - m.textarea.Height() - lipgloss.Height(m.RenderFooter())
m = m.SetInputMode(InputModelSingleLine)
}
m.viewport.Height = m.height - m.textarea.Height() - lipgloss.Height(m.RenderFooter())
m.viewport.SetContent(m.RenderConversation(m.viewport.Width))
case key.Matches(msg, m.keymap.Copy):
if m.answering || m.conversations.Curr().LastAnswer() == "" {
Expand Down Expand Up @@ -326,16 +321,38 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}

func (m Model) SetInputMode(mode InputMode) Model {
keys := m.globalConf.KeyMap
if mode == InputModelMultiLine {
m.keymap.SwitchMultiline = newBinding(keys.SwitchMultiline, "single line mode")
m.keymap.Submit = newBinding(keys.MultilineSubmit, "submit")
m.keymap.TextAreaKeys.InsertNewline = newBinding(keys.MultilineInsertNewLine, "insert new line")
m.inputMode = InputModelMultiLine
m.textarea.ShowLineNumbers = true
m.textarea.SetHeight(2)
} else {
m.keymap.SwitchMultiline = newBinding(keys.SwitchMultiline, "multiline mode")
m.keymap.Submit = newBinding(keys.Submit, "submit")
m.keymap.TextAreaKeys.InsertNewline = newBinding(keys.InsertNewline, "insert new line")
m.inputMode = InputModelSingleLine
m.textarea.ShowLineNumbers = false
m.textarea.SetHeight(1)
}
m.viewport.KeyMap = m.keymap.ViewPortKeys
m.textarea.KeyMap = m.keymap.TextAreaKeys
return m
}

var (
senderStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("5"))
botStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6"))
errorStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("1"))
footerStyle = lipgloss.NewStyle().
Height(1).
BorderTop(true).
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("8")).
Faint(true)
Height(1).
BorderTop(true).
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("8")).
Faint(true)
)

func (m Model) RenderConversation(maxWidth int) string {
Expand Down

0 comments on commit 676b3cf

Please sign in to comment.