diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index f56a40b..9cddadb 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -10,18 +10,21 @@ import ( "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/common/ui/osstyle" "github.com/lmika/awstools/internal/dynamo-browse/controllers" "github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo" "github.com/lmika/awstools/internal/dynamo-browse/services/tables" "github.com/lmika/awstools/internal/dynamo-browse/ui" "github.com/lmika/gopkgs/cli" "log" + "net" "os" ) func main() { var flagTable = flag.String("t", "", "dynamodb table name") - var flagLocal = flag.Bool("local", false, "local endpoint") + var flagLocal = flag.String("local", "", "local endpoint") + var flagDebug = flag.String("debug", "", "file to log debug messages") flag.Parse() ctx := context.Background() @@ -32,9 +35,19 @@ func main() { } var dynamoClient *dynamodb.Client - if *flagLocal { + if *flagLocal != "" { + host, port, err := net.SplitHostPort(*flagLocal) + if err != nil { + cli.Fatalf("invalid address '%v': %v", *flagLocal, err) + } + if host == "" { + host = "localhost" + } + if port == "" { + port = "8000" + } dynamoClient = dynamodb.NewFromConfig(cfg, - dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:18000"))) + dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL(fmt.Sprintf("http://%v:%v", host, port)))) } else { dynamoClient = dynamodb.NewFromConfig(cfg) } @@ -50,14 +63,28 @@ func main() { commandController := commandctrl.NewCommandController() model := ui.NewModel(tableReadController, tableWriteController, commandController) - // Pre-determine if layout has dark background. This prevents calls for creating a list to hang. - lipgloss.HasDarkBackground() - p := tea.NewProgram(model, tea.WithAltScreen()) - closeFn := logging.EnableLogging() + closeFn := logging.EnableLogging(*flagDebug) defer closeFn() + // Pre-determine if layout has dark background. This prevents calls for creating a list to hang. + if lipgloss.HasDarkBackground() { + if colorScheme := osstyle.CurrentColorScheme(); colorScheme == osstyle.ColorSchemeLightMode { + log.Printf("terminal reads dark but really in light mode") + lipgloss.SetHasDarkBackground(true) + } else { + log.Printf("in dark background") + } + } else { + if colorScheme := osstyle.CurrentColorScheme(); colorScheme == osstyle.ColorSchemeDarkMode { + log.Printf("terminal reads light but really in dark mode") + lipgloss.SetHasDarkBackground(true) + } else { + log.Printf("cannot detect system darkmode") + } + } + log.Println("launching") if err := p.Start(); err != nil { fmt.Printf("Alas, there's been an error: %v", err) diff --git a/go.mod b/go.mod index f77add4..fa569c3 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc // indirect - github.com/lmika/go-bubble-table v0.2.2-0.20220608033210-61eeb29a6239 // indirect + github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/go.sum b/go.sum index c730d98..b0ee31b 100644 --- a/go.sum +++ b/go.sum @@ -102,6 +102,8 @@ github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e h1:0QkUe2ejnT/i+xbgGy github.com/lmika/events v0.0.0-20200906102219-a2269cd4394e/go.mod h1:qtkBmNC9OfD0STtOR9sF55pQchjIfNlC3gzm4n8CrqM= github.com/lmika/go-bubble-table v0.2.2-0.20220608033210-61eeb29a6239 h1:GGw5pZtEFnHtD7kKdWsiwgcIwZTnok60sShrHVYz4ok= github.com/lmika/go-bubble-table v0.2.2-0.20220608033210-61eeb29a6239/go.mod h1:0RT1upgKZ6qZ6B1SqseE3wWsPjSQRv/G/HjpYK8jNsg= +github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538 h1:dtMPRNoDqDnnP3HgOvYhswcJVSqdISkYlCtGOjTqg6Q= +github.com/lmika/go-bubble-table v0.2.2-0.20220616114432-6bbb2995e538/go.mod h1:0RT1upgKZ6qZ6B1SqseE3wWsPjSQRv/G/HjpYK8jNsg= github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890 h1:mwl/exYV/WkBMeShqK7q+B2w2r+b0vP1TSA7clBn9kI= github.com/lmika/gopkgs v0.0.0-20211210041137-0dc91e939890/go.mod h1:FH6OJSvYcJ9xY8CGs9yGgR89kMCK1UimuUQ6kE5YuJQ= github.com/lmika/shellwords v0.0.0-20140714114018-ce258dd729fe h1:1UXS/6OFkbi6JrihPykmYO1VtsABB02QQ+YmYYzTY18= diff --git a/internal/common/ui/events/commands.go b/internal/common/ui/events/commands.go index 99f70a7..19857b4 100644 --- a/internal/common/ui/events/commands.go +++ b/internal/common/ui/events/commands.go @@ -31,6 +31,15 @@ func PromptForInput(prompt string, onDone func(value string) tea.Cmd) tea.Cmd { } } +func Confirm(prompt string, onYes func() tea.Cmd) tea.Cmd { + return PromptForInput(prompt, func(value string) tea.Cmd { + if value == "y" { + return onYes() + } + return nil + }) +} + type MessageWithStatus interface { StatusMessage() string } diff --git a/internal/common/ui/logging/debug.go b/internal/common/ui/logging/debug.go index 12ddea1..7ff1fc1 100644 --- a/internal/common/ui/logging/debug.go +++ b/internal/common/ui/logging/debug.go @@ -6,15 +6,18 @@ import ( "os" ) -func EnableLogging() (closeFn func()) { - tempFile, err := os.CreateTemp("", "debug.log") - if err != nil { - fmt.Println("fatal:", err) - os.Exit(1) +func EnableLogging(logFile string) (closeFn func()) { + if logFile == "" { + tempFile, err := os.CreateTemp("", "debug.log") + if err != nil { + fmt.Println("fatal:", err) + os.Exit(1) + } + tempFile.Close() + logFile = tempFile.Name() } - tempFile.Close() - f, err := tea.LogToFile(tempFile.Name(), "debug") + f, err := tea.LogToFile(logFile, "debug") if err != nil { fmt.Println("fatal:", err) os.Exit(1) diff --git a/internal/common/ui/osstyle/osstyle.go b/internal/common/ui/osstyle/osstyle.go new file mode 100644 index 0000000..e053f0b --- /dev/null +++ b/internal/common/ui/osstyle/osstyle.go @@ -0,0 +1,18 @@ +package osstyle + +type ColorScheme int + +const ( + ColorSchemeUnknown ColorScheme = iota + ColorSchemeLightMode + ColorSchemeDarkMode +) + +var getOSColorScheme func() ColorScheme = nil + +func CurrentColorScheme() ColorScheme { + if getOSColorScheme == nil { + return ColorSchemeUnknown + } + return getOSColorScheme() +} diff --git a/internal/common/ui/osstyle/osstyle_darwin.go b/internal/common/ui/osstyle/osstyle_darwin.go new file mode 100644 index 0000000..ef39429 --- /dev/null +++ b/internal/common/ui/osstyle/osstyle_darwin.go @@ -0,0 +1,27 @@ +package osstyle + +import ( + "log" + "os/exec" +) + +// Usage: https://stefan.sofa-rockers.org/2018/10/23/macos-dark-mode-terminal-vim/ +func darwinGetOSColorScheme() ColorScheme { + d, err := exec.Command("defaults", "read", "-g", "AppleInterfaceStyle").Output() + if err != nil { + log.Printf("cannot get current OS color scheme: %v", err) + return ColorSchemeUnknown + } + + switch string(d) { + case "Dark\n": + return ColorSchemeDarkMode + case "Light\n": + return ColorSchemeLightMode + } + return ColorSchemeUnknown +} + +func init() { + getOSColorScheme = darwinGetOSColorScheme +} diff --git a/internal/dynamo-browse/controllers/events.go b/internal/dynamo-browse/controllers/events.go index 441f05c..a460f5d 100644 --- a/internal/dynamo-browse/controllers/events.go +++ b/internal/dynamo-browse/controllers/events.go @@ -1,18 +1,18 @@ package controllers import ( - "fmt" - tea "github.com/charmbracelet/bubbletea" "github.com/lmika/awstools/internal/dynamo-browse/models" ) type NewResultSet struct { - ResultSet *models.ResultSet + ResultSet *models.ResultSet + statusMessage string } func (rs NewResultSet) StatusMessage() string { - return fmt.Sprintf("%d items returned", len(rs.ResultSet.Items())) + //return fmt.Sprintf("%d items returned", len(rs.ResultSet.Items())) + return rs.statusMessage } type SetReadWrite struct { diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index 07bbea8..01048e5 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -3,6 +3,7 @@ package controllers import ( "context" "encoding/csv" + "fmt" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/awstools/internal/common/ui/events" "github.com/lmika/awstools/internal/dynamo-browse/models" @@ -126,16 +127,23 @@ func (c *TableReadController) doScan(ctx context.Context, resultSet *models.Resu return c.setResultSetAndFilter(newResultSet, c.state.Filter()) } -//func (c *TableReadController) ResultSet() *models.ResultSet { -// c.mutex.Lock() -// defer c.mutex.Unlock() -// -// return c.resultSet -//} - func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, filter string) tea.Msg { c.state.setResultSetAndFilter(resultSet, filter) - return NewResultSet{resultSet} + + var statusMessage string + if filter != "" { + var filteredCount int + for i := range resultSet.Items() { + if !resultSet.Hidden(i) { + filteredCount += 1 + } + } + statusMessage = fmt.Sprintf("%d of %d items returned", filteredCount, len(resultSet.Items())) + } else { + statusMessage = fmt.Sprintf("%d items returned", len(resultSet.Items())) + } + + return NewResultSet{resultSet, statusMessage} } func (c *TableReadController) Unmark() tea.Cmd { diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index 7c90d90..60ea800 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -61,7 +61,7 @@ func (twc *TableWriteController) NewItem() tea.Cmd { Dirty: true, }) }) - return NewResultSet{twc.state.ResultSet()} + return NewResultSet{twc.state.ResultSet(), "New item added"} } return keyPrompts.next() diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go index 971e99d..fbdfdb4 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go @@ -128,9 +128,7 @@ func (m *Model) setLeftmostDisplayedColumn(newCol int) { } else { m.colOffset = newCol } - // TEMP - m.table.GoDown() - m.table.GoUp() + m.table.UpdateView() } func (m *Model) View() string { @@ -172,15 +170,6 @@ func (m *Model) rebuildTable() { m.rows = newRows newTbl.SetRows(newRows) - /* - for newTbl.Cursor() != m.table.Cursor() { - if newTbl.Cursor() < m.table.Cursor() { - newTbl.GoDown() - } else if newTbl.Cursor() > m.table.Cursor() { - newTbl.GoUp() - } - } - */ m.table = newTbl } diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go index 69d599f..2ecfafc 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/tblmodel.go @@ -12,7 +12,7 @@ import ( var ( markedRowStyle = lipgloss.NewStyle(). - Background(lipgloss.AdaptiveColor{Dark: "#e1e1e1", Light: "#414141"}) + Background(lipgloss.AdaptiveColor{Light: "#e1e1e1", Dark: "#414141"}) dirtyRowStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#e13131")) newRowStyle = lipgloss.NewStyle(). @@ -60,6 +60,8 @@ func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) { if mi := r.MetaInfo(); mi != "" { sb.WriteString(metaInfoStyle.Render(mi)) } + } else { + sb.WriteString(metaInfoStyle.Render("~")) } } diff --git a/internal/ssm-browse/controllers/ssmcontroller.go b/internal/ssm-browse/controllers/ssmcontroller.go index 4af7335..5f2a6b7 100644 --- a/internal/ssm-browse/controllers/ssmcontroller.go +++ b/internal/ssm-browse/controllers/ssmcontroller.go @@ -77,3 +77,24 @@ func (c *SSMController) Clone(param models.SSMParameter) tea.Cmd { } }) } + +func (c *SSMController) DeleteParameter(param models.SSMParameter) tea.Cmd { + return events.Confirm("delete parameter? ", func() tea.Cmd { + return func() tea.Msg { + ctx := context.Background() + if err := c.service.Delete(ctx, param); err != nil { + return events.Error(err) + } + + res, err := c.service.List(context.Background(), c.prefix) + if err != nil { + return events.Error(err) + } + + return NewParameterListMsg{ + Prefix: c.prefix, + Parameters: res, + } + } + }) +} diff --git a/internal/ssm-browse/providers/awsssm/provider.go b/internal/ssm-browse/providers/awsssm/provider.go index c7e72bb..ab5cd81 100644 --- a/internal/ssm-browse/providers/awsssm/provider.go +++ b/internal/ssm-browse/providers/awsssm/provider.go @@ -56,9 +56,9 @@ outer: func (p *Provider) Put(ctx context.Context, param models.SSMParameter, override bool) error { in := &ssm.PutParameterInput{ - Name: aws.String(param.Name), - Type: param.Type, - Value: aws.String(param.Value), + Name: aws.String(param.Name), + Type: param.Type, + Value: aws.String(param.Value), Overwrite: override, } if param.Type == types.ParameterTypeSecureString { @@ -71,4 +71,14 @@ func (p *Provider) Put(ctx context.Context, param models.SSMParameter, override } return nil -} \ No newline at end of file +} + +func (p *Provider) Delete(ctx context.Context, param models.SSMParameter) error { + _, err := p.client.DeleteParameter(ctx, &ssm.DeleteParameterInput{ + Name: aws.String(param.Name), + }) + if err != nil { + return errors.Wrap(err, "unable to delete SSM parameter") + } + return nil +} diff --git a/internal/ssm-browse/services/ssmparameters/iface.go b/internal/ssm-browse/services/ssmparameters/iface.go index 406d733..5a1249a 100644 --- a/internal/ssm-browse/services/ssmparameters/iface.go +++ b/internal/ssm-browse/services/ssmparameters/iface.go @@ -8,4 +8,5 @@ import ( type SSMProvider interface { List(ctx context.Context, prefix string, maxCount int) (*models.SSMParameters, error) Put(ctx context.Context, param models.SSMParameter, override bool) error + Delete(ctx context.Context, param models.SSMParameter) error } diff --git a/internal/ssm-browse/services/ssmparameters/service.go b/internal/ssm-browse/services/ssmparameters/service.go index 40f2042..19e8903 100644 --- a/internal/ssm-browse/services/ssmparameters/service.go +++ b/internal/ssm-browse/services/ssmparameters/service.go @@ -21,9 +21,13 @@ func (s *Service) List(ctx context.Context, prefix string) (*models.SSMParameter func (s *Service) Clone(ctx context.Context, param models.SSMParameter, newName string) error { newParam := models.SSMParameter{ - Name: newName, - Type: param.Type, + Name: newName, + Type: param.Type, Value: param.Value, } return s.provider.Put(ctx, newParam, false) -} \ No newline at end of file +} + +func (s *Service) Delete(ctx context.Context, param models.SSMParameter) error { + return s.provider.Delete(ctx, param) +} diff --git a/internal/ssm-browse/ui/model.go b/internal/ssm-browse/ui/model.go index e01fa2d..d3c315e 100644 --- a/internal/ssm-browse/ui/model.go +++ b/internal/ssm-browse/ui/model.go @@ -37,6 +37,12 @@ func NewModel(controller *controllers.SSMController, cmdController *commandctrl. } return events.SetError(errors.New("no parameter selected")) }, + "delete": func(args []string) tea.Cmd { + if currentParam := ssmList.CurrentParameter(); currentParam != nil { + return controller.DeleteParameter(*currentParam) + } + return events.SetError(errors.New("no parameter selected")) + }, }, }) diff --git a/test/cmd/load-test-table/main.go b/test/cmd/load-test-table/main.go index a09d047..51a1137 100644 --- a/test/cmd/load-test-table/main.go +++ b/test/cmd/load-test-table/main.go @@ -2,24 +2,23 @@ package main import ( "context" - "github.com/brianvoe/gofakeit/v6" - "github.com/google/uuid" - "log" - "strconv" - + "fmt" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/brianvoe/gofakeit/v6" + "github.com/google/uuid" "github.com/lmika/awstools/internal/dynamo-browse/models" "github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo" "github.com/lmika/awstools/internal/dynamo-browse/services/tables" "github.com/lmika/gopkgs/cli" + "log" ) func main() { ctx := context.Background() - tableName := "awstools-test" + tableName := "business-addresses" totalItems := 5000 cfg, err := config.LoadDefaultConfig(ctx) @@ -67,21 +66,18 @@ func main() { for i := 0; i < totalItems; i++ { key := uuid.New().String() if err := tableService.Put(ctx, tableInfo, models.Item{ - "pk": &types.AttributeValueMemberS{Value: key}, - "sk": &types.AttributeValueMemberS{Value: key}, - "name": &types.AttributeValueMemberS{Value: gofakeit.Name()}, - "address": &types.AttributeValueMemberS{Value: gofakeit.Address().Address}, - "city": &types.AttributeValueMemberS{Value: gofakeit.Address().City}, - "phone": &types.AttributeValueMemberN{Value: gofakeit.Phone()}, - "web": &types.AttributeValueMemberS{Value: gofakeit.URL()}, - "inOffice": &types.AttributeValueMemberBOOL{Value: gofakeit.Bool()}, + "pk": &types.AttributeValueMemberS{Value: key}, + "sk": &types.AttributeValueMemberS{Value: key}, + "name": &types.AttributeValueMemberS{Value: gofakeit.Name()}, + "address": &types.AttributeValueMemberS{Value: gofakeit.Address().Address}, + "city": &types.AttributeValueMemberS{Value: gofakeit.Address().City}, + "phone": &types.AttributeValueMemberN{Value: gofakeit.Phone()}, + "web": &types.AttributeValueMemberS{Value: gofakeit.URL()}, + "officeOpened": &types.AttributeValueMemberBOOL{Value: gofakeit.Bool()}, "ratings": &types.AttributeValueMemberL{Value: []types.AttributeValue{ - &types.AttributeValueMemberS{Value: gofakeit.Adverb()}, - &types.AttributeValueMemberN{Value: "12.34"}, - }}, - "values": &types.AttributeValueMemberM{Value: map[string]types.AttributeValue{ - "adverb": &types.AttributeValueMemberS{Value: gofakeit.Adverb()}, - "int": &types.AttributeValueMemberN{Value: strconv.Itoa(int(gofakeit.Int32()))}, + &types.AttributeValueMemberN{Value: fmt.Sprint(gofakeit.IntRange(0, 5))}, + &types.AttributeValueMemberN{Value: fmt.Sprint(gofakeit.IntRange(0, 5))}, + &types.AttributeValueMemberN{Value: fmt.Sprint(gofakeit.IntRange(0, 5))}, }}, }); err != nil { log.Fatalln(err)