diff --git a/cmd/ssm-browse/main.go b/cmd/ssm-browse/main.go index f08af42..6b215e9 100644 --- a/cmd/ssm-browse/main.go +++ b/cmd/ssm-browse/main.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ssm" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/lmika/awstools/internal/common/ui/commandctrl" "github.com/lmika/awstools/internal/common/ui/logging" "github.com/lmika/awstools/internal/ssm-browse/controllers" "github.com/lmika/awstools/internal/ssm-browse/providers/awsssm" @@ -33,7 +34,17 @@ func main() { service := ssmparameters.NewService(provider) ctrl := controllers.New(service) - model := ui.NewModel(ctrl) + + cmdController := commandctrl.NewCommandController() + cmdController.AddCommands(&commandctrl.CommandContext{ + Commands: map[string]commandctrl.Command{ + "cd": func(args []string) tea.Cmd { + return ctrl.ChangePrefix(args[0]) + }, + }, + }) + + model := ui.NewModel(ctrl, cmdController) p := tea.NewProgram(model, tea.WithAltScreen()) diff --git a/internal/common/ui/commandctrl/commandctrl.go b/internal/common/ui/commandctrl/commandctrl.go index 92a384a..4332023 100644 --- a/internal/common/ui/commandctrl/commandctrl.go +++ b/internal/common/ui/commandctrl/commandctrl.go @@ -9,15 +9,20 @@ import ( ) type CommandController struct { - commands map[string]Command + commandList *CommandContext } -func NewCommandController(commands map[string]Command) *CommandController { +func NewCommandController() *CommandController { return &CommandController{ - commands: commands, + commandList: nil, } } +func (c *CommandController) AddCommands(ctx *CommandContext) { + ctx.parent = c.commandList + c.commandList = ctx +} + func (c *CommandController) Prompt() tea.Cmd { return func() tea.Msg { return events.PromptForInputMsg{ @@ -36,10 +41,19 @@ func (c *CommandController) Execute(commandInput string) tea.Cmd { } tokens := shellwords.Split(input) - command, ok := c.commands[tokens[0]] - if !ok { + command := c.lookupCommand(tokens[0]) + if command == nil { return events.SetStatus("no such command: " + tokens[0]) } - return command(tokens) + return command(tokens[1:]) } + +func (c *CommandController) lookupCommand(name string) Command { + for ctx := c.commandList; ctx != nil; ctx = ctx.parent { + if cmd, ok := ctx.Commands[name]; ok { + return cmd + } + } + return nil +} \ No newline at end of file diff --git a/internal/common/ui/commandctrl/types.go b/internal/common/ui/commandctrl/types.go index ca6d9ca..0cf1a9e 100644 --- a/internal/common/ui/commandctrl/types.go +++ b/internal/common/ui/commandctrl/types.go @@ -9,3 +9,9 @@ func NoArgCommand(cmd tea.Cmd) Command { return cmd } } + +type CommandContext struct { + Commands map[string]Command + + parent *CommandContext +} diff --git a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go index 2862a36..d4ee696 100644 --- a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go +++ b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go @@ -19,16 +19,16 @@ type StatusAndPrompt struct { width int } -func New(model layout.ResizingModel, initialMsg string) StatusAndPrompt { +func New(model layout.ResizingModel, initialMsg string) *StatusAndPrompt { textInput := textinput.New() - return StatusAndPrompt{model: model, statusMessage: initialMsg, textInput: textInput} + return &StatusAndPrompt{model: model, statusMessage: initialMsg, textInput: textInput} } -func (s StatusAndPrompt) Init() tea.Cmd { +func (s *StatusAndPrompt) Init() tea.Cmd { return s.model.Init() } -func (s StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case events.ErrorMsg: s.statusMessage = "Error: " + msg.Error() @@ -80,18 +80,22 @@ func (s StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return s, cmd } -func (s StatusAndPrompt) View() string { +func (s *StatusAndPrompt) InPrompt() bool { + return s.pendingInput != nil +} + +func (s *StatusAndPrompt) View() string { return lipgloss.JoinVertical(lipgloss.Top, s.model.View(), s.viewStatus()) } -func (s StatusAndPrompt) Resize(w, h int) layout.ResizingModel { +func (s *StatusAndPrompt) Resize(w, h int) layout.ResizingModel { s.width = w submodelHeight := h - lipgloss.Height(s.viewStatus()) s.model = s.model.Resize(w, submodelHeight) return s } -func (s StatusAndPrompt) viewStatus() string { +func (s *StatusAndPrompt) viewStatus() string { if s.pendingInput != nil { return s.textInput.View() } diff --git a/internal/ssm-browse/controllers/events.go b/internal/ssm-browse/controllers/events.go index d629930..30f125b 100644 --- a/internal/ssm-browse/controllers/events.go +++ b/internal/ssm-browse/controllers/events.go @@ -3,5 +3,6 @@ package controllers import "github.com/lmika/awstools/internal/ssm-browse/models" type NewParameterListMsg struct { + Prefix string Parameters *models.SSMParameters } diff --git a/internal/ssm-browse/controllers/ssmcontroller.go b/internal/ssm-browse/controllers/ssmcontroller.go index bfe5f5d..2ee83b6 100644 --- a/internal/ssm-browse/controllers/ssmcontroller.go +++ b/internal/ssm-browse/controllers/ssmcontroller.go @@ -5,27 +5,53 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/lmika/awstools/internal/common/ui/events" "github.com/lmika/awstools/internal/ssm-browse/services/ssmparameters" + "sync" ) type SSMController struct { service *ssmparameters.Service + + // state + mutex *sync.Mutex + prefix string } func New(service *ssmparameters.Service) *SSMController { return &SSMController{ service: service, + prefix: "/", + mutex: new(sync.Mutex), } } func (c *SSMController) Fetch() tea.Cmd { return func() tea.Msg { - res, err := c.service.List(context.Background()) + res, err := c.service.List(context.Background(), c.prefix) if err != nil { return events.Error(err) } return NewParameterListMsg{ + Prefix: c.prefix, Parameters: res, } } } + +func (c *SSMController) ChangePrefix(newPrefix string) tea.Cmd { + return func() tea.Msg { + res, err := c.service.List(context.Background(), newPrefix) + if err != nil { + return events.Error(err) + } + + c.mutex.Lock() + defer c.mutex.Unlock() + c.prefix = newPrefix + + return NewParameterListMsg{ + Prefix: c.prefix, + Parameters: res, + } + } +} \ No newline at end of file diff --git a/internal/ssm-browse/models/models.go b/internal/ssm-browse/models/models.go index 777e6c8..524eeba 100644 --- a/internal/ssm-browse/models/models.go +++ b/internal/ssm-browse/models/models.go @@ -2,6 +2,7 @@ package models type SSMParameters struct { Items []SSMParameter + NextToken string } type SSMParameter struct { diff --git a/internal/ssm-browse/providers/awsssm/provider.go b/internal/ssm-browse/providers/awsssm/provider.go index 15f3fec..049b5d9 100644 --- a/internal/ssm-browse/providers/awsssm/provider.go +++ b/internal/ssm-browse/providers/awsssm/provider.go @@ -6,6 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/lmika/awstools/internal/ssm-browse/models" "github.com/pkg/errors" + "log" ) type Provider struct { @@ -18,9 +19,16 @@ func NewProvider(client *ssm.Client) *Provider { } } -func (p *Provider) List(ctx context.Context) (*models.SSMParameters, error) { +func (p *Provider) List(ctx context.Context, prefix string, nextToken string) (*models.SSMParameters, error) { + log.Printf("new prefix: %v", prefix) + + var nextTokenStr *string = nil + if nextToken != "" { + nextTokenStr = aws.String(nextToken) + } pars, err := p.client.GetParametersByPath(ctx, &ssm.GetParametersByPathInput{ - Path: aws.String("/"), + Path: aws.String(prefix), + NextToken: nextTokenStr, MaxResults: 10, Recursive: true, }) @@ -30,10 +38,11 @@ func (p *Provider) List(ctx context.Context) (*models.SSMParameters, error) { res := &models.SSMParameters{ Items: make([]models.SSMParameter, len(pars.Parameters)), + NextToken: aws.ToString(pars.NextToken), } for i, p := range pars.Parameters { res.Items[i] = models.SSMParameter{ - Name: aws.ToString(p.Name), + Name: aws.ToString(p.Name), Value: aws.ToString(p.Value), } } diff --git a/internal/ssm-browse/services/ssmparameters/iface.go b/internal/ssm-browse/services/ssmparameters/iface.go index ed3eb47..f873f72 100644 --- a/internal/ssm-browse/services/ssmparameters/iface.go +++ b/internal/ssm-browse/services/ssmparameters/iface.go @@ -6,5 +6,5 @@ import ( ) type SSMProvider interface { - List(ctx context.Context) (*models.SSMParameters, error) + List(ctx context.Context, prefix string, nextToken string) (*models.SSMParameters, error) } diff --git a/internal/ssm-browse/services/ssmparameters/service.go b/internal/ssm-browse/services/ssmparameters/service.go index 16b9c7c..521490b 100644 --- a/internal/ssm-browse/services/ssmparameters/service.go +++ b/internal/ssm-browse/services/ssmparameters/service.go @@ -15,6 +15,22 @@ func NewService(provider SSMProvider) *Service { } } -func (s *Service) List(ctx context.Context) (*models.SSMParameters, error) { - return s.provider.List(ctx) +func (s *Service) List(ctx context.Context, prefix string) (*models.SSMParameters, error) { + var items []models.SSMParameter + var nextToken string + + for { + page, err := s.provider.List(ctx, prefix, nextToken) + if err != nil { + return nil, err + } + + items = append(items, page.Items...) + nextToken = page.NextToken + if len(items) >= 50 || nextToken == "" { + break + } + } + + return &models.SSMParameters{Items: items, NextToken: nextToken}, nil } \ No newline at end of file diff --git a/internal/ssm-browse/ui/model.go b/internal/ssm-browse/ui/model.go index d11585a..4de9dea 100644 --- a/internal/ssm-browse/ui/model.go +++ b/internal/ssm-browse/ui/model.go @@ -2,6 +2,7 @@ package ui import ( tea "github.com/charmbracelet/bubbletea" + "github.com/lmika/awstools/internal/common/ui/commandctrl" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt" "github.com/lmika/awstools/internal/ssm-browse/controllers" @@ -9,26 +10,29 @@ import ( ) type Model struct { - controller *controllers.SSMController + cmdController *commandctrl.CommandController + controller *controllers.SSMController + statusAndPrompt *statusandprompt.StatusAndPrompt - root tea.Model + root tea.Model ssmList *ssmlist.Model } -func NewModel(controller *controllers.SSMController) Model { +func NewModel(controller *controllers.SSMController, cmdController *commandctrl.CommandController) Model { ssmList := ssmlist.New() - root := layout.FullScreen( - statusandprompt.New(ssmList, "Hello SSM"), - ) + statusAndPrompt := statusandprompt.New(ssmList, "Hello SSM") + + root := layout.FullScreen(statusAndPrompt) return Model{ - controller: controller, - root: root, - ssmList: ssmList, + controller: controller, + cmdController: cmdController, + root: root, + statusAndPrompt: statusAndPrompt, + ssmList: ssmList, } } - func (m Model) Init() tea.Cmd { return m.controller.Fetch() } @@ -36,11 +40,19 @@ func (m Model) Init() tea.Cmd { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case controllers.NewParameterListMsg: + m.ssmList.SetPrefix(msg.Prefix) m.ssmList.SetParameters(msg.Parameters) case tea.KeyMsg: - switch msg.String() { - case "ctrl+c", "q": - return m, tea.Quit + if !m.statusAndPrompt.InPrompt() { + switch msg.String() { + // TEMP + case ":": + return m, m.cmdController.Prompt() + // END TEMP + + case "ctrl+c", "q": + return m, tea.Quit + } } } @@ -52,4 +64,3 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) View() string { return m.root.View() } - diff --git a/internal/ssm-browse/ui/ssmlist/ssmlist.go b/internal/ssm-browse/ui/ssmlist/ssmlist.go index ae2d0c6..ca563a3 100644 --- a/internal/ssm-browse/ui/ssmlist/ssmlist.go +++ b/internal/ssm-browse/ui/ssmlist/ssmlist.go @@ -19,7 +19,7 @@ type Model struct { } func New() *Model { - frameTitle := frame.NewFrameTitle("SSM", true) + frameTitle := frame.NewFrameTitle("SSM: /", true) table := table.New([]string{"name", "type", "value"}, 0, 0) return &Model{ @@ -28,6 +28,10 @@ func New() *Model { } } +func (m *Model) SetPrefix(newPrefix string) { + m.frameTitle.SetTitle("SSM: " + newPrefix) +} + func (m *Model) SetParameters(parameters *models.SSMParameters) { m.parameters = parameters cols := []string{"name", "type", "value"} diff --git a/internal/ssm-browse/ui/ssmlist/tblmodel.go b/internal/ssm-browse/ui/ssmlist/tblmodel.go index c9f78aa..d7c0d7e 100644 --- a/internal/ssm-browse/ui/ssmlist/tblmodel.go +++ b/internal/ssm-browse/ui/ssmlist/tblmodel.go @@ -5,6 +5,7 @@ import ( table "github.com/calyptia/go-bubble-table" "github.com/lmika/awstools/internal/ssm-browse/models" "io" + "strings" ) type itemTableRow struct { @@ -12,7 +13,8 @@ type itemTableRow struct { } func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) { - line := fmt.Sprintf("%s\t%s\t%s", mtr.item.Name, "String", mtr.item.Value) + firstLine := strings.SplitN(mtr.item.Value, "\n", 2)[0] + line := fmt.Sprintf("%s\t%s\t%s", mtr.item.Name, "String", firstLine) if index == model.Cursor() { fmt.Fprintln(w, model.Styles.SelectedRow.Render(line))