Added command history (#45)

* Added command line history to the command, query and filter prompts.
* Added query planning debugging to the log.
* Fixed bug in query expression which was not treating true and false as boolean literals.
* Fixed a bug in the query planning logic which was incorrectly determine that an expression of the form sort_key ^= "string", with no partition key, could be executed as a query instead of a scan.
This commit is contained in:
Leon Mika 2023-01-26 21:46:31 +11:00 committed by GitHub
parent 700a1a2253
commit 54a120342e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 335 additions and 34 deletions

View file

@ -13,8 +13,10 @@ import (
"github.com/lmika/audax/internal/common/workspaces"
"github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/audax/internal/dynamo-browse/providers/inputhistorystore"
"github.com/lmika/audax/internal/dynamo-browse/providers/settingstore"
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
"github.com/lmika/audax/internal/dynamo-browse/services/inputhistory"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
keybindings_service "github.com/lmika/audax/internal/dynamo-browse/services/keybindings"
@ -81,6 +83,7 @@ func main() {
dynamoProvider := dynamo.NewProvider(dynamoClient)
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
settingStore := settingstore.New(ws)
inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws)
if *flagRO {
if err := settingStore.SetReadOnly(*flagRO); err != nil {
@ -98,10 +101,11 @@ func main() {
itemRendererService := itemrenderer.NewService(uiStyles.ItemView.FieldType, uiStyles.ItemView.MetaInfo)
scriptManagerService := scriptmanager.New()
jobsService := jobs.NewService(eventBus)
inputHistoryService := inputhistory.New(inputHistoryStore)
state := controllers.NewState()
jobsController := controllers.NewJobsController(jobsService, eventBus, false)
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, jobsController, eventBus, *flagTable)
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, jobsController, inputHistoryService, eventBus, *flagTable)
tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore)
columnsController := controllers.NewColumnsController(eventBus)
exportController := controllers.NewExportController(state, columnsController)
@ -112,7 +116,7 @@ func main() {
keyBindingService := keybindings_service.NewService(keyBindings)
keyBindingController := controllers.NewKeyBindingController(keyBindingService)
commandController := commandctrl.NewCommandController()
commandController := commandctrl.NewCommandController(inputHistoryService)
commandController.AddCommandLookupExtension(scriptController)
model := ui.NewModel(

View file

@ -32,7 +32,7 @@ func main() {
ctrl := controllers.NewLogFileController(service, flag.Arg(0))
cmdController := commandctrl.NewCommandController()
cmdController := commandctrl.NewCommandController(nil)
//cmdController.AddCommands(&commandctrl.CommandList{
// Commands: map[string]commandctrl.Command{
// "cd": func(args []string) tea.Cmd {

View file

@ -47,7 +47,7 @@ func main() {
ctrl := controllers.New(service)
cmdController := commandctrl.NewCommandController()
cmdController := commandctrl.NewCommandController(nil)
cmdController.AddCommands(&commandctrl.CommandList{
Commands: map[string]commandctrl.Command{
"cd": func(ec commandctrl.ExecContext, args []string) tea.Msg {

View file

@ -3,6 +3,7 @@ package commandctrl
import (
"bufio"
"bytes"
"context"
tea "github.com/charmbracelet/bubbletea"
"github.com/pkg/errors"
"log"
@ -14,13 +15,17 @@ import (
"github.com/lmika/shellwords"
)
const commandsCategory = "commands"
type CommandController struct {
historyProvider IterProvider
commandList *CommandList
lookupExtensions []CommandLookupExtension
}
func NewCommandController() *CommandController {
func NewCommandController(historyProvider IterProvider) *CommandController {
return &CommandController{
historyProvider: historyProvider,
commandList: nil,
lookupExtensions: nil,
}
@ -37,7 +42,8 @@ func (c *CommandController) AddCommandLookupExtension(ext CommandLookupExtension
func (c *CommandController) Prompt() tea.Msg {
return events.PromptForInputMsg{
Prompt: ":",
Prompt: ":",
History: c.historyProvider.Iter(context.Background(), commandsCategory),
OnDone: func(value string) tea.Msg {
return c.Execute(value)
},

View file

@ -1,7 +1,9 @@
package commandctrl_test
import (
"context"
"github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/dynamo-browse/services"
"testing"
"github.com/lmika/audax/internal/common/ui/commandctrl"
@ -10,7 +12,7 @@ import (
func TestCommandController_Prompt(t *testing.T) {
t.Run("prompt user for a command", func(t *testing.T) {
cmd := commandctrl.NewCommandController()
cmd := commandctrl.NewCommandController(mockIterProvider{})
res := cmd.Prompt()
@ -19,3 +21,10 @@ func TestCommandController_Prompt(t *testing.T) {
assert.Equal(t, ":", promptForInputMsg.Prompt)
})
}
type mockIterProvider struct {
}
func (m mockIterProvider) Iter(ctx context.Context, category string) services.HistoryProvider {
return nil
}

View file

@ -0,0 +1,10 @@
package commandctrl
import (
"context"
"github.com/lmika/audax/internal/dynamo-browse/services"
)
type IterProvider interface {
Iter(ctx context.Context, category string) services.HistoryProvider
}

View file

@ -2,6 +2,7 @@ package events
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/dynamo-browse/services"
"log"
)
@ -22,21 +23,22 @@ func SetTeaMessage(event tea.Msg) tea.Cmd {
}
}
func PromptForInput(prompt string, onDone func(value string) tea.Msg) tea.Msg {
func PromptForInput(prompt string, history services.HistoryProvider, onDone func(value string) tea.Msg) tea.Msg {
return PromptForInputMsg{
Prompt: prompt,
OnDone: onDone,
Prompt: prompt,
History: history,
OnDone: onDone,
}
}
func Confirm(prompt string, onResult func(yes bool) tea.Msg) tea.Msg {
return PromptForInput(prompt, func(value string) tea.Msg {
return PromptForInput(prompt, nil, func(value string) tea.Msg {
return onResult(value == "y")
})
}
func ConfirmYes(prompt string, onYes func() tea.Msg) tea.Msg {
return PromptForInput(prompt, func(value string) tea.Msg {
return PromptForInput(prompt, nil, func(value string) tea.Msg {
if value == "y" {
return onYes()
}

View file

@ -2,6 +2,7 @@ package events
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/dynamo-browse/services"
)
// Error indicates that an error occurred
@ -21,6 +22,7 @@ type ModeMessage string
// PromptForInput indicates that the context is requesting a line of input
type PromptForInputMsg struct {
Prompt string
History services.HistoryProvider
OnDone func(value string) tea.Msg
OnCancel func() tea.Msg
}

View file

@ -67,7 +67,7 @@ func (cc *ColumnsController) onNewResultSet(rs *models.ResultSet, op resultSetUp
}
func (cc *ColumnsController) AddColumn(afterIndex int) tea.Msg {
return events.PromptForInput("column expr: ", func(value string) tea.Msg {
return events.PromptForInput("column expr: ", nil, func(value string) tea.Msg {
colExpr, err := queryexpr.Parse(value)
if err != nil {
return events.Error(err)

View file

@ -11,6 +11,7 @@ import (
"github.com/lmika/audax/internal/dynamo-browse/models/attrcodec"
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
"github.com/lmika/audax/internal/dynamo-browse/models/serialisable"
"github.com/lmika/audax/internal/dynamo-browse/services/inputhistory"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/services/viewsnapshot"
bus "github.com/lmika/events"
@ -42,11 +43,17 @@ const (
MarkOpToggle
)
const (
queryInputHistoryCategory = "queries"
filterInputHistoryCategory = "filters"
)
type TableReadController struct {
tableService TableReadService
workspaceService *viewsnapshot.ViewSnapshotService
itemRendererService *itemrenderer.Service
jobController *JobsController
inputHistoryService *inputhistory.Service
eventBus *bus.Bus
tableName string
loadFromLastView bool
@ -63,6 +70,7 @@ func NewTableReadController(
workspaceService *viewsnapshot.ViewSnapshotService,
itemRendererService *itemrenderer.Service,
jobController *JobsController,
inputHistoryService *inputhistory.Service,
eventBus *bus.Bus,
tableName string,
) *TableReadController {
@ -72,6 +80,7 @@ func NewTableReadController(
workspaceService: workspaceService,
itemRendererService: itemRendererService,
jobController: jobController,
inputHistoryService: inputHistoryService,
eventBus: eventBus,
tableName: tableName,
mutex: new(sync.Mutex),
@ -136,7 +145,8 @@ func (c *TableReadController) ScanTable(name string) tea.Msg {
func (c *TableReadController) PromptForQuery() tea.Msg {
return events.PromptForInputMsg{
Prompt: "query: ",
Prompt: "query: ",
History: c.inputHistoryService.Iter(context.Background(), queryInputHistoryCategory),
OnDone: func(value string) tea.Msg {
resultSet := c.state.ResultSet()
if resultSet == nil {
@ -288,7 +298,8 @@ func (c *TableReadController) Mark(op MarkOp) tea.Msg {
func (c *TableReadController) Filter() tea.Msg {
return events.PromptForInputMsg{
Prompt: "filter: ",
Prompt: "filter: ",
History: c.inputHistoryService.Iter(context.Background(), filterInputHistoryCategory),
OnDone: func(value string) tea.Msg {
resultSet := c.state.ResultSet()
if resultSet == nil {

View file

@ -8,8 +8,10 @@ import (
"github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/audax/internal/dynamo-browse/models"
"github.com/lmika/audax/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/audax/internal/dynamo-browse/providers/inputhistorystore"
"github.com/lmika/audax/internal/dynamo-browse/providers/settingstore"
"github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore"
"github.com/lmika/audax/internal/dynamo-browse/services/inputhistory"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/services/jobs"
"github.com/lmika/audax/internal/dynamo-browse/services/scriptmanager"
@ -600,9 +602,12 @@ func newService(t *testing.T, cfg serviceConfig) *services {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
settingStore := settingstore.New(ws)
inputHistoryStore := inputhistorystore.NewInputHistoryStore(ws)
workspaceService := viewsnapshot.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
scriptService := scriptmanager.New()
inputHistoryService := inputhistory.New(inputHistoryStore)
client := testdynamo.SetupTestTable(t, testData)
@ -612,14 +617,14 @@ func newService(t *testing.T, cfg serviceConfig) *services {
state := controllers.NewState()
jobsController := controllers.NewJobsController(jobs.NewService(eventBus), eventBus, true)
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, jobsController, eventBus, cfg.tableName)
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, jobsController, inputHistoryService, eventBus, cfg.tableName)
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
settingsController := controllers.NewSettingsController(settingStore, eventBus)
columnsController := controllers.NewColumnsController(eventBus)
exportController := controllers.NewExportController(state, columnsController)
scriptController := controllers.NewScriptController(scriptService, readController, settingsController, eventBus)
commandController := commandctrl.NewCommandController()
commandController := commandctrl.NewCommandController(inputHistoryService)
commandController.AddCommandLookupExtension(scriptController)
if cfg.isReadOnly {

View file

@ -1,8 +1,48 @@
package models
import "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/lmika/audax/internal/dynamo-browse/models/itemrender"
)
type QueryExecutionPlan struct {
CanQuery bool
Expression expression.Expression
}
func (qep QueryExecutionPlan) Describe(dp DescribingPrinter) {
if qep.CanQuery {
dp.Println(" execute as: query")
} else {
dp.Println(" execute as: scan")
}
if keyCond := aws.ToString(qep.Expression.KeyCondition()); keyCond != "" {
dp.Printf(" key condition: %v", keyCond)
}
if cond := aws.ToString(qep.Expression.Condition()); cond != "" {
dp.Printf(" condition: %v", cond)
}
if filter := aws.ToString(qep.Expression.Filter()); filter != "" {
dp.Printf(" filter: %v", filter)
}
if names := qep.Expression.Names(); len(names) > 0 {
dp.Println(" names:")
for k, v := range names {
dp.Printf(" %v = %v", k, v)
}
}
if values := qep.Expression.Values(); len(values) > 0 {
dp.Println(" values:")
for k, v := range values {
r := itemrender.ToRenderer(v)
dp.Printf(" %v (%v) = %v", k, r.TypeName(), r.StringValue())
}
}
}
type DescribingPrinter interface {
Println(v ...any)
Printf(format string, v ...any)
}

View file

@ -81,11 +81,15 @@ type astPlaceholder struct {
}
type astLiteralValue struct {
StringVal *string `parser:"@String"`
IntVal *int64 `parser:"| @Int"`
StringVal *string `parser:"@String"`
IntVal *int64 `parser:"| @Int"`
TrueBoolValue bool `parser:"| @KwdTrue"`
FalseBoolValue bool `parser:"| @KwdFalse"`
}
var scanner = lexer.MustSimple([]lexer.SimpleRule{
{Name: "KwdTrue", Pattern: `true`},
{Name: "KwdFalse", Pattern: `false`},
{Name: "Eq", Pattern: `=|[\\^]=|[!]=`},
{Name: "Cmp", Pattern: `<[=]?|>[=]?`},
{Name: "String", Pattern: `"(\\"|[^"])*"`},

View file

@ -194,7 +194,7 @@ func (a irFieldBeginsWith) canBeExecutedAsQuery(info *models.TableInfo, qci *que
return false
}
if keyName == info.Keys.SortKey {
if keyName == info.Keys.SortKey && qci.hasSeenPrimaryKey(info) {
return qci.addKey(info, a.name.keyName())
}

View file

@ -143,6 +143,9 @@ func TestModExpr_Query(t *testing.T) {
scanCase("when request sk equals something", `sk="something"`, `#0 = :0`,
exprNameIsString(0, 0, "sk", "something"),
),
scanCase("when request sk starts with something", `sk^="something"`, `begins_with (#0, :0)`,
exprNameIsString(0, 0, "sk", "something"),
),
scanCase("with not equal", `sk != "something"`, `#0 <> :0`,
exprNameIsString(0, 0, "sk", "something"),
),
@ -158,6 +161,14 @@ func TestModExpr_Query(t *testing.T) {
scanCase("greater or equal to value", `num >= 100`, `#0 >= :0`,
exprNameIsNumber(0, 0, "num", "100"),
),
scanCase("is true", `bool = true`, `#0 = :0`,
exprName(0, "bool"),
exprValueIsBool(0, true),
),
scanCase("is false", `bool = false`, `#0 = :0`,
exprName(0, "bool"),
exprValueIsBool(0, false),
),
scanCase("with disjunctions",
`pk="prefix" or sk="another"`,
`(#0 = :0) OR (#1 = :1)`,
@ -846,6 +857,12 @@ func exprValueIsNumber(valIdx int, expected string) func(ss *scanScenario) {
}
}
func exprValueIsBool(valIdx int, expected bool) func(ss *scanScenario) {
return func(ss *scanScenario) {
ss.expectedValues[fmt.Sprintf(":%d", valIdx)] = &types.AttributeValueMemberBOOL{Value: expected}
}
}
func exprNameIsString(idx, valIdx int, name string, expected string) func(ss *scanScenario) {
return func(ss *scanScenario) {
ss.expectedNames[fmt.Sprintf("#%d", idx)] = name

View file

@ -51,6 +51,10 @@ func (a *astLiteralValue) goValue() (any, error) {
return s, nil
case a.IntVal != nil:
return *a.IntVal, nil
case a.TrueBoolValue:
return true, nil
case a.FalseBoolValue:
return false, nil
}
return nil, errors.New("unrecognised type")
}
@ -65,6 +69,10 @@ func (a *astLiteralValue) String() string {
return *a.StringVal
case a.IntVal != nil:
return strconv.FormatInt(*a.IntVal, 10)
case a.TrueBoolValue:
return "true"
case a.FalseBoolValue:
return "false"
}
return ""
}

View file

@ -0,0 +1,10 @@
package inputhistorystore
import "time"
type inputHistoryItem struct {
ID int `storm:"id,increment"`
Category string `storm:"index"`
Time time.Time
Item string
}

View file

@ -0,0 +1,49 @@
package inputhistorystore
import (
"context"
"github.com/asdine/storm"
"github.com/lmika/audax/internal/common/sliceutils"
"github.com/lmika/audax/internal/common/workspaces"
"github.com/pkg/errors"
"sort"
"time"
)
const inputHistoryStore = "InputHistoryStore"
type Store struct {
ws storm.Node
}
func NewInputHistoryStore(ws *workspaces.Workspace) *Store {
return &Store{
ws: ws.DB().From(inputHistoryStore),
}
}
// Items returns all items from a category ordered by the time
func (as *Store) Items(ctx context.Context, category string) ([]string, error) {
var items []inputHistoryItem
if err := as.ws.Find("Category", category, &items); err != nil {
if errors.Is(err, storm.ErrNotFound) {
return nil, nil
}
return nil, err
}
sort.Slice(items, func(i, j int) bool {
return items[i].Time.Before(items[j].Time)
})
return sliceutils.Map(items, func(t inputHistoryItem) string {
return t.Item
}), nil
}
func (as *Store) PutItem(ctx context.Context, category string, item string) error {
return as.ws.Save(&inputHistoryItem{
Time: time.Now(),
Category: category,
Item: item,
})
}

View file

@ -0,0 +1,13 @@
package services
type HistoryProvider interface {
// Len returns the number of historical items
Len() int
// Item returns the historical item at index 'idx', where items are chronologically ordered such that the
// item at 0 is the oldest item.
Item(idx int) string
// PutItem adds an item to the history
PutItem(item string)
}

View file

@ -0,0 +1,8 @@
package inputhistory
import "context"
type HistoryItemStore interface {
Items(ctx context.Context, category string) ([]string, error)
PutItem(ctx context.Context, category string, item string) error
}

View file

@ -0,0 +1,49 @@
package inputhistory
import (
"context"
"github.com/lmika/audax/internal/dynamo-browse/services"
"log"
"strings"
)
func (svc *Service) Iter(ctx context.Context, category string) services.HistoryProvider {
items, err := svc.store.Items(ctx, category)
if err != nil {
log.Printf("warn: cannot get iter for '%v': %v", category, err)
return nil
}
return &Iter{svc, items, category}
}
func (svc *Service) PutItem(ctx context.Context, category string, value string) error {
return svc.store.PutItem(ctx, category, value)
}
type Iter struct {
svc *Service
items []string
category string
}
func (i *Iter) Len() int {
return len(i.items)
}
func (i *Iter) Item(idx int) string {
return i.items[idx]
}
func (i *Iter) PutItem(item string) {
if strings.TrimSpace(item) == "" {
return
}
if len(i.items) > 0 && i.items[len(i.items)-1] == item {
return
}
if err := i.svc.PutItem(context.Background(), i.category, item); err != nil {
log.Printf("warn: cannot put input history: category = %v, value = %v, err = %v", i.category, item, err)
}
}

View file

@ -0,0 +1,9 @@
package inputhistory
type Service struct {
store HistoryItemStore
}
func New(store HistoryItemStore) *Service {
return &Service{store: store}
}

View file

@ -59,15 +59,18 @@ func (s *Service) doScan(
runAsQuery = plan.CanQuery
filterExpr = &plan.Expression
log.Printf("Running query over '%v'", tableInfo.Name)
plan.Describe(log.Default())
} else {
log.Printf("Performing scan over '%v'", tableInfo.Name)
}
var results []models.Item
var lastEvalKey map[string]types.AttributeValue
if runAsQuery {
log.Printf("executing query")
results, lastEvalKey, err = s.provider.QueryItems(ctx, tableInfo.Name, filterExpr, exclusiveStartKey, limit)
} else {
log.Printf("executing scan")
results, lastEvalKey, err = s.provider.ScanItems(ctx, tableInfo.Name, filterExpr, exclusiveStartKey, limit)
}

View file

@ -20,7 +20,7 @@ type StatusAndPrompt struct {
statusMessage string
spinner spinner.Model
spinnerVisible bool
pendingInput *events.PromptForInputMsg
pendingInput *pendingInputState
textInput textinput.Model
width, height int
lastModeLineHeight int
@ -83,15 +83,15 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
s.textInput.Prompt = msg.Prompt
s.textInput.Focus()
s.textInput.SetValue("")
s.pendingInput = &msg
s.pendingInput = newPendingInputState(msg)
case tea.KeyMsg:
if s.pendingInput != nil {
switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc:
if s.pendingInput.OnCancel != nil {
if s.pendingInput.originalMsg.OnCancel != nil {
pendingInput := s.pendingInput
cc.Add(func() tea.Msg {
m := pendingInput.OnCancel()
m := pendingInput.originalMsg.OnCancel()
return m
})
}
@ -100,18 +100,48 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
pendingInput := s.pendingInput
s.pendingInput = nil
return s, func() tea.Msg {
m := pendingInput.OnDone(s.textInput.Value())
return m
m := pendingInput.originalMsg.OnDone(s.textInput.Value())
return s, tea.Batch(
events.SetTeaMessage(m),
func() tea.Msg {
if historyProvider := pendingInput.originalMsg.History; historyProvider != nil {
if _, isErrMsg := m.(events.ErrorMsg); !isErrMsg {
historyProvider.PutItem(s.textInput.Value())
}
}
return nil
},
)
case tea.KeyUp:
if historyProvider := s.pendingInput.originalMsg.History; historyProvider != nil && historyProvider.Len() > 0 {
if s.pendingInput.historyIdx < 0 {
s.pendingInput.historyIdx = historyProvider.Len() - 1
} else if s.pendingInput.historyIdx > 0 {
s.pendingInput.historyIdx -= 1
} else {
s.pendingInput.historyIdx = 0
}
s.textInput.SetValue(historyProvider.Item(s.pendingInput.historyIdx))
s.textInput.SetCursor(len(s.textInput.Value()))
}
case tea.KeyDown:
if historyProvider := s.pendingInput.originalMsg.History; historyProvider != nil && historyProvider.Len() > 0 {
if s.pendingInput.historyIdx >= 0 && s.pendingInput.historyIdx < historyProvider.Len()-1 {
s.pendingInput.historyIdx += 1
}
s.textInput.SetValue(historyProvider.Item(s.pendingInput.historyIdx))
s.textInput.SetCursor(len(s.textInput.Value()))
}
default:
if msg.Type == tea.KeyRunes {
msg.Runes = sliceutils.Filter(msg.Runes, func(r rune) bool { return r != '\x0d' && r != '\x0a' })
}
newTextInput, cmd := s.textInput.Update(msg)
s.textInput = newTextInput
return s, cmd
}
newTextInput, cmd := s.textInput.Update(msg)
s.textInput = newTextInput
return s, cmd
} else {
s.statusMessage = ""
}

View file

@ -0,0 +1,12 @@
package statusandprompt
import "github.com/lmika/audax/internal/common/ui/events"
type pendingInputState struct {
originalMsg events.PromptForInputMsg
historyIdx int
}
func newPendingInputState(msg events.PromptForInputMsg) *pendingInputState {
return &pendingInputState{originalMsg: msg, historyIdx: -1}
}

View file

@ -56,7 +56,7 @@ func (c *SSMController) ChangePrefix(newPrefix string) tea.Msg {
}
func (c *SSMController) Clone(param models.SSMParameter) tea.Msg {
return events.PromptForInput("New key: ", func(value string) tea.Msg {
return events.PromptForInput("New key: ", nil, func(value string) tea.Msg {
return func() tea.Msg {
ctx := context.Background()
if err := c.service.Clone(ctx, param, value); err != nil {