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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl"
|
"lmika.dev/cmd/dynamo-browse/internal/common/ui/commandctrl"
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/common/ui/events"
|
"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)
|
resChan := make(chan string)
|
||||||
|
cancelChan := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
commandctrl.PostMsg(ctx, events.PromptForInput(prompt, nil, func(value string) tea.Msg {
|
commandctrl.PostMsg(ctx, events.PromptForInputMsg{
|
||||||
resChan <- value
|
Prompt: prompt,
|
||||||
return nil
|
OnDone: func(value string) tea.Msg {
|
||||||
}))
|
resChan <- value
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
OnCancel: func() tea.Msg {
|
||||||
|
cancelChan <- struct{}{}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case value := <-resChan:
|
case value := <-resChan:
|
||||||
return value, nil
|
return value, nil
|
||||||
|
case <-cancelChan:
|
||||||
|
return nil, nil
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, ctx.Err()
|
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) {
|
func (m *uiModule) uiPromptTable(ctx context.Context, args ucl.CallArgs) (any, error) {
|
||||||
tables, err := m.tableService.ListTables(context.Background())
|
tables, err := m.tableService.ListTables(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -166,13 +209,14 @@ func moduleUI(
|
||||||
return ucl.Module{
|
return ucl.Module{
|
||||||
Name: "ui",
|
Name: "ui",
|
||||||
Builtins: map[string]ucl.BuiltinHandler{
|
Builtins: map[string]ucl.BuiltinHandler{
|
||||||
"command": m.uiCommand,
|
"command": m.uiCommand,
|
||||||
"prompt": m.uiPrompt,
|
"prompt": m.uiPrompt,
|
||||||
"prompt-table": m.uiPromptTable,
|
"prompt-table": m.uiPromptTable,
|
||||||
"confirm": m.uiConfirm,
|
"prompt-keypress": m.uiInKey,
|
||||||
"query": m.uiQuery,
|
"confirm": m.uiConfirm,
|
||||||
"filter": m.uiFilter,
|
"query": m.uiQuery,
|
||||||
"bind": m.uiBind,
|
"filter": m.uiFilter,
|
||||||
|
"bind": m.uiBind,
|
||||||
},
|
},
|
||||||
}, m.ckb
|
}, m.ckb
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package events
|
package events
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services"
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services"
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Error(err error) tea.Msg {
|
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 {
|
func Confirm(prompt string, onResult func(yes bool) tea.Msg) tea.Msg {
|
||||||
return PromptForInput(prompt, nil, func(value string) tea.Msg {
|
return PromptForInputMsg{
|
||||||
return onResult(value == "y")
|
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 {
|
func ConfirmYes(prompt string, onYes func() tea.Msg) tea.Msg {
|
||||||
|
|
|
||||||
|
|
@ -27,3 +27,10 @@ type PromptForInputMsg struct {
|
||||||
OnCancel func() tea.Msg
|
OnCancel func() tea.Msg
|
||||||
OnTabComplete func(value string) (string, bool)
|
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
|
package statusandprompt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
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/common/ui/events"
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
||||||
"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
"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
|
// 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
|
spinner spinner.Model
|
||||||
spinnerVisible bool
|
spinnerVisible bool
|
||||||
pendingInput *pendingInputState
|
pendingInput *pendingInputState
|
||||||
|
pendingKeyState *pendingKeyState
|
||||||
textInput textinput.Model
|
textInput textinput.Model
|
||||||
width, height int
|
width, height int
|
||||||
lastModeLineHeight int
|
lastModeLineHeight int
|
||||||
|
|
@ -85,7 +87,7 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
s.statusMessage = msg.StatusMessage()
|
s.statusMessage = msg.StatusMessage()
|
||||||
case events.PromptForInputMsg:
|
case events.PromptForInputMsg:
|
||||||
if s.pendingInput != nil {
|
if s.pendingInput != nil || s.pendingKeyState != nil {
|
||||||
// ignore, already in an input
|
// ignore, already in an input
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +96,40 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
s.textInput.Focus()
|
s.textInput.Focus()
|
||||||
s.textInput.SetValue("")
|
s.textInput.SetValue("")
|
||||||
s.pendingInput = newPendingInputState(msg)
|
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:
|
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 {
|
if s.pendingInput != nil {
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
case tea.KeyCtrlC, tea.KeyEsc:
|
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 {
|
func (s *StatusAndPrompt) InPrompt() bool {
|
||||||
return s.pendingInput != nil
|
return s.pendingInput != nil || s.pendingKeyState != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StatusAndPrompt) View() string {
|
func (s *StatusAndPrompt) View() string {
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,7 @@ func newPendingInputState(msg events.PromptForInputMsg) *pendingInputState {
|
||||||
type PasteboardProvider interface {
|
type PasteboardProvider interface {
|
||||||
ReadText() (string, bool)
|
ReadText() (string, bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type pendingKeyState struct {
|
||||||
|
originalMsg events.PromptForKeyMsg
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue