Add debounce before exit when using non-leader exit command (#759)

This commit is contained in:
Yihui Khuu
2025-07-09 09:53:38 +10:00
committed by GitHub
parent cfc715bd48
commit 7893b84614
2 changed files with 53 additions and 3 deletions

View File

@@ -32,15 +32,27 @@ import (
// InterruptDebounceTimeoutMsg is sent when the interrupt key debounce timeout expires
type InterruptDebounceTimeoutMsg struct{}
// ExitDebounceTimeoutMsg is sent when the exit key debounce timeout expires
type ExitDebounceTimeoutMsg struct{}
// InterruptKeyState tracks the state of interrupt key presses for debouncing
type InterruptKeyState int
// ExitKeyState tracks the state of exit key presses for debouncing
type ExitKeyState int
const (
InterruptKeyIdle InterruptKeyState = iota
InterruptKeyFirstPress
)
const (
ExitKeyIdle ExitKeyState = iota
ExitKeyFirstPress
)
const interruptDebounceTimeout = 1 * time.Second
const exitDebounceTimeout = 1 * time.Second
const fileViewerFullWidthCutoff = 160
type appModel struct {
@@ -59,6 +71,7 @@ type appModel struct {
isLeaderSequence bool
toastManager *toast.ToastManager
interruptKeyState InterruptKeyState
exitKeyState ExitKeyState
lastScroll time.Time
messagesRight bool
fileViewer fileviewer.Model
@@ -271,7 +284,26 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
// 7. Check again for commands that don't require leader (excluding interrupt when busy)
// 7. Handle exit key debounce for app exit when using non-leader command
exitCommand := a.app.Commands[commands.AppExitCommand]
if exitCommand.Matches(msg, a.isLeaderSequence) {
switch a.exitKeyState {
case ExitKeyIdle:
// First exit key press - start debounce timer
a.exitKeyState = ExitKeyFirstPress
a.editor.SetExitKeyInDebounce(true)
return a, tea.Tick(exitDebounceTimeout, func(t time.Time) tea.Msg {
return ExitDebounceTimeoutMsg{}
})
case ExitKeyFirstPress:
// Second exit key press within timeout - actually exit
a.exitKeyState = ExitKeyIdle
a.editor.SetExitKeyInDebounce(false)
return a, util.CmdHandler(commands.ExecuteCommandMsg(exitCommand))
}
}
// 8. Check again for commands that don't require leader (excluding interrupt when busy and exit when in debounce)
matches := a.app.Commands.Matches(msg, a.isLeaderSequence)
if len(matches) > 0 {
// Skip interrupt key if we're in debounce mode and app is busy
@@ -281,7 +313,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
}
// 7. Fallback to editor. This is for other characters
// 9. Fallback to editor. This is for other characters
// like backspace, tab, etc.
updatedEditor, cmd := a.editor.Update(msg)
a.editor = updatedEditor.(chat.EditorComponent)
@@ -499,6 +531,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Reset interrupt key state after timeout
a.interruptKeyState = InterruptKeyIdle
a.editor.SetInterruptKeyInDebounce(false)
case ExitDebounceTimeoutMsg:
// Reset exit key state after timeout
a.exitKeyState = ExitKeyIdle
a.editor.SetExitKeyInDebounce(false)
case dialog.FindSelectedMsg:
return a.openFile(msg.FilePath)
}
@@ -1015,6 +1051,7 @@ func NewModel(app *app.App) tea.Model {
fileCompletionActive: false,
toastManager: toast.NewToastManager(),
interruptKeyState: InterruptKeyIdle,
exitKeyState: ExitKeyIdle,
fileViewer: fileviewer.New(app),
messagesRight: app.State.MessagesRight,
}