diff --git a/internal/app/ui/keys.go b/internal/app/ui/keys.go index e86dd89..c65c403 100644 --- a/internal/app/ui/keys.go +++ b/internal/app/ui/keys.go @@ -6,6 +6,7 @@ import "charm.land/bubbles/v2/key" type KeyMap struct { Quit key.Binding Interrupt key.Binding + Clear key.Binding } // DefaultKeyMap returns the default keybindings. @@ -19,5 +20,9 @@ func DefaultKeyMap() KeyMap { key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "interrupt"), ), + Clear: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc esc", "clear"), + ), } } diff --git a/internal/app/ui/model.go b/internal/app/ui/model.go index 1257241..3efde13 100644 --- a/internal/app/ui/model.go +++ b/internal/app/ui/model.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "strings" + "time" "charm.land/bubbles/v2/key" "charm.land/bubbles/v2/spinner" @@ -27,6 +28,7 @@ type State int const ( StateInput State = iota StateExecuting + StatePending ) // ReadyMsg signals the ui that execution is done and it should prompt. @@ -41,6 +43,10 @@ type QuitRequestMsg struct{} // ConfirmQuitMsg is used internally to finalize quitting. type ConfirmQuitMsg struct{} +// seqTimeoutMsg is sent after 500ms to cancel a pending key +// seq if the second key was not pressed on time. +type seqTimeoutMsg struct{ seq int } + type cancel func(ctx context.Context) error type execute func(query string) tea.Cmd @@ -56,6 +62,7 @@ type Model struct { prevUserInput string version string highlighter func(string) string + escSeq int keys KeyMap styles Styles @@ -168,7 +175,17 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m.handleInput() + case seqTimeoutMsg: + if msg.seq == m.escSeq { + m.state = StateInput + } + return m, nil + case tea.KeyMsg: + if m.state == StatePending && !key.Matches(msg, m.keys.Clear) { + m.state = StateInput + } + if key.Matches(msg, m.keys.Quit) { return m, func() tea.Msg { return QuitRequestMsg{} @@ -189,6 +206,22 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.input.Reset() return m, nil } + if key.Matches(msg, m.keys.Clear) { + if m.state == StateExecuting { + return m, nil + } + if m.state == StatePending { + m.state = StateInput + m.input.Reset() + return m, nil + } + m.state = StatePending + m.escSeq++ + n := m.escSeq + return m, tea.Tick(500*time.Millisecond, func(t time.Time) tea.Msg { + return seqTimeoutMsg{seq: n} + }) + } } // Route to input only if in input state, avoiding capturing keystrokes while executing. diff --git a/pgxspecial/dbcommands/util_test.go b/pgxspecial/dbcommands/util_test.go index 2439c13..ef4f7b9 100644 --- a/pgxspecial/dbcommands/util_test.go +++ b/pgxspecial/dbcommands/util_test.go @@ -330,7 +330,6 @@ func RequiresRowResult(t *testing.T, r pgxspecial.SpecialCommandResult) pgxspeci t.Fatalf("expected rows result, got %T", r) } - rowsResult, ok := r.(pgxspecial.RowResult) if !ok { t.Fatalf("expected RowsResult, got %T", r) diff --git a/pgxspecial/export.go b/pgxspecial/export.go index 26f05f7..7455f64 100644 --- a/pgxspecial/export.go +++ b/pgxspecial/export.go @@ -6,7 +6,7 @@ func Export() []CommandExport { for key, cmd := range commandRegistry { // key contains the command name with the alias // \quit, '\q, \exit has the same command, only differs by key/alias - cmd := New(key, cmd.Syntax, cmd.Description) + cmd := New(key, cmd.Syntax, cmd.Description) cmds = append(cmds, cmd) } return cmds diff --git a/pgxspecial/registry_test.go b/pgxspecial/registry_test.go index 07a98cf..9c9b40e 100644 --- a/pgxspecial/registry_test.go +++ b/pgxspecial/registry_test.go @@ -162,7 +162,6 @@ func TestExecuteCommand(t *testing.T) { t.Errorf("Expected result kind to be rows, got: %T", result) } - rows := result.(pgxspecial.RowResult).Rows defer rows.Close() @@ -202,7 +201,6 @@ func TestRegisterCommandAlias(t *testing.T) { t.Errorf("Expected result kind to be rows, got: %T", result) } - rows := result.(pgxspecial.RowResult).Rows defer rows.Close()