Added ui:prompt-keypress to support single key presses
All checks were successful
ci / build (push) Successful in 3m41s
All checks were successful
ci / build (push) Successful in 3m41s
Have also fixed a bug in ui:prompt which was keeping the script running when the prompt was being cancelled
This commit is contained in:
parent
022cec7393
commit
8dafa6fa8f
|
|
@ -2,6 +2,7 @@ package cmdpacks
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/common/ui/events"
|
||||
|
|
@ -38,16 +39,26 @@ func (m *uiModule) uiPrompt(ctx context.Context, args ucl.CallArgs) (any, error)
|
|||
}
|
||||
|
||||
resChan := make(chan string)
|
||||
cancelChan := make(chan struct{})
|
||||
go func() {
|
||||
commandctrl.PostMsg(ctx, events.PromptForInput(prompt, nil, func(value string) tea.Msg {
|
||||
resChan <- value
|
||||
return nil
|
||||
}))
|
||||
commandctrl.PostMsg(ctx, events.PromptForInputMsg{
|
||||
Prompt: prompt,
|
||||
OnDone: func(value string) tea.Msg {
|
||||
resChan <- value
|
||||
return nil
|
||||
},
|
||||
OnCancel: func() tea.Msg {
|
||||
cancelChan <- struct{}{}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}()
|
||||
|
||||
select {
|
||||
case value := <-resChan:
|
||||
return value, nil
|
||||
case <-cancelChan:
|
||||
return nil, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
|
@ -75,6 +86,38 @@ func (m *uiModule) uiConfirm(ctx context.Context, args ucl.CallArgs) (any, error
|
|||
}
|
||||
}
|
||||
|
||||
func (m *uiModule) uiInKey(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
var prompt string
|
||||
if err := args.Bind(&prompt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resChan := make(chan string)
|
||||
cancelChan := make(chan struct{})
|
||||
go func() {
|
||||
commandctrl.PostMsg(ctx, events.PromptForKeyMsg{
|
||||
Prompt: prompt,
|
||||
OnDone: func(value string) tea.Msg {
|
||||
resChan <- value
|
||||
return nil
|
||||
},
|
||||
OnCancel: func() tea.Msg {
|
||||
cancelChan <- struct{}{}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}()
|
||||
|
||||
select {
|
||||
case value := <-resChan:
|
||||
return value, nil
|
||||
case <-cancelChan:
|
||||
return nil, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *uiModule) uiPromptTable(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||
tables, err := m.tableService.ListTables(context.Background())
|
||||
if err != nil {
|
||||
|
|
@ -166,13 +209,14 @@ func moduleUI(
|
|||
return ucl.Module{
|
||||
Name: "ui",
|
||||
Builtins: map[string]ucl.BuiltinHandler{
|
||||
"command": m.uiCommand,
|
||||
"prompt": m.uiPrompt,
|
||||
"prompt-table": m.uiPromptTable,
|
||||
"confirm": m.uiConfirm,
|
||||
"query": m.uiQuery,
|
||||
"filter": m.uiFilter,
|
||||
"bind": m.uiBind,
|
||||
"command": m.uiCommand,
|
||||
"prompt": m.uiPrompt,
|
||||
"prompt-table": m.uiPromptTable,
|
||||
"prompt-keypress": m.uiInKey,
|
||||
"confirm": m.uiConfirm,
|
||||
"query": m.uiQuery,
|
||||
"filter": m.uiFilter,
|
||||
"bind": m.uiBind,
|
||||
},
|
||||
}, m.ckb
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package events
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services"
|
||||
"log"
|
||||
)
|
||||
|
||||
func Error(err error) tea.Msg {
|
||||
|
|
@ -31,10 +32,23 @@ func PromptForInput(prompt string, history services.HistoryProvider, onDone func
|
|||
}
|
||||
}
|
||||
|
||||
func PromptForKey(prompt string, onDone func(key string) tea.Msg) tea.Msg {
|
||||
return PromptForKeyMsg{
|
||||
Prompt: prompt,
|
||||
OnDone: onDone,
|
||||
}
|
||||
}
|
||||
|
||||
func Confirm(prompt string, onResult func(yes bool) tea.Msg) tea.Msg {
|
||||
return PromptForInput(prompt, nil, func(value string) tea.Msg {
|
||||
return onResult(value == "y")
|
||||
})
|
||||
return PromptForInputMsg{
|
||||
Prompt: prompt,
|
||||
OnDone: func(value string) tea.Msg {
|
||||
return onResult(value == "y")
|
||||
},
|
||||
OnCancel: func() tea.Msg {
|
||||
return onResult(false)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ConfirmYes(prompt string, onYes func() tea.Msg) tea.Msg {
|
||||
|
|
|
|||
|
|
@ -27,3 +27,10 @@ type PromptForInputMsg struct {
|
|||
OnCancel func() tea.Msg
|
||||
OnTabComplete func(value string) (string, bool)
|
||||
}
|
||||
|
||||
// PromptForKey indicates that the context is requesting a single key press
|
||||
type PromptForKeyMsg struct {
|
||||
Prompt string
|
||||
OnDone func(key string) tea.Msg
|
||||
OnCancel func() tea.Msg
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package statusandprompt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
|
@ -9,7 +11,6 @@ import (
|
|||
"lmika.dev/cmd/dynamo-browse/internal/common/ui/events"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StatusAndPrompt is a resizing model which displays a submodel and a status bar. When the start prompt
|
||||
|
|
@ -24,6 +25,7 @@ type StatusAndPrompt struct {
|
|||
spinner spinner.Model
|
||||
spinnerVisible bool
|
||||
pendingInput *pendingInputState
|
||||
pendingKeyState *pendingKeyState
|
||||
textInput textinput.Model
|
||||
width, height int
|
||||
lastModeLineHeight int
|
||||
|
|
@ -85,7 +87,7 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
s.statusMessage = msg.StatusMessage()
|
||||
case events.PromptForInputMsg:
|
||||
if s.pendingInput != nil {
|
||||
if s.pendingInput != nil || s.pendingKeyState != nil {
|
||||
// ignore, already in an input
|
||||
return s, nil
|
||||
}
|
||||
|
|
@ -94,7 +96,40 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
s.textInput.Focus()
|
||||
s.textInput.SetValue("")
|
||||
s.pendingInput = newPendingInputState(msg)
|
||||
case events.PromptForKeyMsg:
|
||||
if s.pendingInput != nil || s.pendingKeyState != nil {
|
||||
// ignore, already in an input
|
||||
return s, nil
|
||||
}
|
||||
|
||||
s.statusMessage = msg.Prompt
|
||||
s.pendingKeyState = &pendingKeyState{msg}
|
||||
case tea.KeyMsg:
|
||||
if s.pendingKeyState != nil {
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlC, tea.KeyEsc:
|
||||
if s.pendingKeyState.originalMsg.OnCancel != nil {
|
||||
pendingKeyState := s.pendingKeyState
|
||||
cc.Add(func() tea.Msg {
|
||||
m := pendingKeyState.originalMsg.OnCancel()
|
||||
return m
|
||||
})
|
||||
}
|
||||
s.pendingKeyState = nil
|
||||
default:
|
||||
if s.pendingKeyState.originalMsg.OnDone != nil {
|
||||
pendingKeyState := s.pendingKeyState
|
||||
cc.Add(func() tea.Msg {
|
||||
m := pendingKeyState.originalMsg.OnDone(msg.String())
|
||||
return m
|
||||
})
|
||||
}
|
||||
s.pendingKeyState = nil
|
||||
}
|
||||
s.statusMessage = ""
|
||||
return s, cc.Cmd()
|
||||
}
|
||||
|
||||
if s.pendingInput != nil {
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlC, tea.KeyEsc:
|
||||
|
|
@ -187,7 +222,7 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
|
||||
func (s *StatusAndPrompt) InPrompt() bool {
|
||||
return s.pendingInput != nil
|
||||
return s.pendingInput != nil || s.pendingKeyState != nil
|
||||
}
|
||||
|
||||
func (s *StatusAndPrompt) View() string {
|
||||
|
|
|
|||
|
|
@ -14,3 +14,7 @@ func newPendingInputState(msg events.PromptForInputMsg) *pendingInputState {
|
|||
type PasteboardProvider interface {
|
||||
ReadText() (string, bool)
|
||||
}
|
||||
|
||||
type pendingKeyState struct {
|
||||
originalMsg events.PromptForKeyMsg
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue