ssm-browse: added cd command
Also came up with an approach for dealing with commands that will probably work with contexts
This commit is contained in:
		
							parent
							
								
									0b745a6dfa
								
							
						
					
					
						commit
						f6f06eb22d
					
				|  | @ -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()) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  | @ -9,3 +9,9 @@ func NoArgCommand(cmd tea.Cmd) Command { | |||
| 		return cmd | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type CommandContext struct { | ||||
| 	Commands map[string]Command | ||||
| 
 | ||||
| 	parent   *CommandContext | ||||
| } | ||||
|  |  | |||
|  | @ -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() | ||||
| 	} | ||||
|  |  | |||
|  | @ -3,5 +3,6 @@ package controllers | |||
| import "github.com/lmika/awstools/internal/ssm-browse/models" | ||||
| 
 | ||||
| type NewParameterListMsg struct { | ||||
| 	Prefix string | ||||
| 	Parameters *models.SSMParameters | ||||
| } | ||||
|  |  | |||
|  | @ -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, | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -2,6 +2,7 @@ package models | |||
| 
 | ||||
| type SSMParameters struct { | ||||
| 	Items []SSMParameter | ||||
| 	NextToken string | ||||
| } | ||||
| 
 | ||||
| type SSMParameter struct { | ||||
|  |  | |||
|  | @ -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), | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  | @ -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() | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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"} | ||||
|  |  | |||
|  | @ -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)) | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue