diff --git a/apps/cnquery/cmd/shell.go b/apps/cnquery/cmd/shell.go index a9d6f63dc8..d9e79d6a5c 100644 --- a/apps/cnquery/cmd/shell.go +++ b/apps/cnquery/cmd/shell.go @@ -126,12 +126,13 @@ func StartShell(runtime *providers.Runtime, conf *ShellConfig) error { isTTY := isatty.IsTerminal(os.Stdout.Fd()) if isTTY { - selectedAsset := components.AssetSelect(invAssets) + selectedAsset := components.Select("Available assets", invAssets) if selectedAsset >= 0 { connectAsset = filteredAssets[selectedAsset] } } else { - fmt.Println(components.AssetList(theme.OperatingSystemTheme, invAssets)) + log.Info().Msgf("discovered %d assets(s)", len(invAssets)) + fmt.Println(components.List(theme.OperatingSystemTheme, invAssets)) log.Fatal().Msg("cannot connect to more than one asset, use --platform-id to select a specific asset") } } diff --git a/cli/components/README.md b/cli/components/README.md new file mode 100644 index 0000000000..691da44a96 --- /dev/null +++ b/cli/components/README.md @@ -0,0 +1,58 @@ +# `components` package + +This Go package has interactive helpers used by `cnquery` and `cnspec`. + +We use a powerful little TUI framework called [bubbletea](https://github.com/charmbracelet/bubbletea). + +## `Select` component + +Select is an interactive prompt that displays the provided message and displays a +list of items to be selected. + +e.g. +```go +type CustomString string + +func (s CustomString) Display() string { + return string(s) +} + +func main() { + customStrings := []CustomString{"first", "second", "third"} + selected := components.Select("Choose a string", customStrings) + fmt.Printf("You chose the %s string.\n", customStrings[selected]) +} +``` + +To execute this example: +``` +go run cli/components/_examples/selector/main.go +``` + +## `List` component + +List is a non-interactive function that lists items to the user. + +e.g. +```go +type CustomString string + +func (s CustomString) PrintableKeys() []string { + return []string{"string"} +} +func (s CustomString) PrintableValue(_ int) string { + return string(s) +} + +func main() { + customStrings := []CustomString{"first", "second", "third"} + list := components.List(theme.OperatingSystemTheme, customStrings) + fmt.Printf(list) +} +``` + +To execute this example: +``` +go run cli/components/_examples/list/main.go +``` + diff --git a/cli/components/_examples/list/main.go b/cli/components/_examples/list/main.go new file mode 100644 index 0000000000..316ca1be2d --- /dev/null +++ b/cli/components/_examples/list/main.go @@ -0,0 +1,26 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package main + +import ( + "fmt" + + "go.mondoo.com/cnquery/v11/cli/components" + "go.mondoo.com/cnquery/v11/cli/theme" +) + +type CustomString string + +func (s CustomString) PrintableKeys() []string { + return []string{"string"} +} +func (s CustomString) PrintableValue(_ int) string { + return string(s) +} + +func main() { + customStrings := []CustomString{"first", "second", "third"} + list := components.List(theme.OperatingSystemTheme, customStrings) + fmt.Printf(list) +} diff --git a/cli/components/_examples/selector/main.go b/cli/components/_examples/selector/main.go new file mode 100644 index 0000000000..0b835e764e --- /dev/null +++ b/cli/components/_examples/selector/main.go @@ -0,0 +1,22 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package main + +import ( + "fmt" + + "go.mondoo.com/cnquery/v11/cli/components" +) + +type CustomString string + +func (s CustomString) Display() string { + return string(s) +} + +func main() { + customStrings := []CustomString{"first", "second", "third"} + selected := components.Select("Choose a string", customStrings) + fmt.Printf("You chose the %s string.\n", customStrings[selected]) +} diff --git a/cli/components/assetlist.go b/cli/components/assetlist.go deleted file mode 100644 index a2f2316473..0000000000 --- a/cli/components/assetlist.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package components - -import ( - "strings" - - "github.com/rs/zerolog/log" - "go.mondoo.com/cnquery/v11/cli/theme" - "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" -) - -func AssetList(theme *theme.Theme, assetList []*inventory.Asset) string { - b := strings.Builder{} - - log.Info().Msgf("discovered %d asset(s)", len(assetList)) - - for i := range assetList { - assetObj := assetList[i] - - b.WriteString(theme.Primary("name:\t\t")) - b.WriteString(assetObj.HumanName()) - b.WriteRune('\n') - - if len(assetObj.PlatformIds) > 0 { - b.WriteString(theme.Primary("platform-id:\t")) - for j := range assetObj.PlatformIds { - b.WriteString(" " + assetObj.PlatformIds[j]) - } - } - - b.WriteRune('\n') - b.WriteRune('\n') - } - - return b.String() -} diff --git a/cli/components/assetselect.go b/cli/components/assetselect.go deleted file mode 100644 index 0301a772c3..0000000000 --- a/cli/components/assetselect.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Mondoo, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package components - -import ( - "fmt" - "os" - - tea "github.com/charmbracelet/bubbletea" - "github.com/rs/zerolog/log" - "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" -) - -func AssetSelect(assetList []*inventory.Asset) int { - list := make([]string, len(assetList)) - - // map asset name to list - for i := range assetList { - a := assetList[i] - name := a.Name - if a.Platform != nil { - name = fmt.Sprintf("%s (%s)", a.Name, a.Platform.Name) - } - list[i] = name - } - - selection := -1 // make sure we have an invalid index - model := NewListModel("Available assets", list, func(s int) { - selection = s - }) - _, err := tea.NewProgram(model, tea.WithInputTTY()).Run() - if err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } - - if selection == -1 { - return -1 - } - selected := assetList[selection] - log.Info().Int("selection", selection).Str("asset", selected.Name).Msg("selected asset") - return selection -} diff --git a/cli/components/list_raw_items.go b/cli/components/list_raw_items.go new file mode 100644 index 0000000000..5baaab1d22 --- /dev/null +++ b/cli/components/list_raw_items.go @@ -0,0 +1,62 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package components + +import ( + "fmt" + "strings" + "text/tabwriter" + + "go.mondoo.com/cnquery/v11/cli/theme" +) + +// ListableItem is the interface that a list need to implement so we can display its items. +type ListableItem interface { + // PrintableKeys returns the list of keys that will be printed. + PrintableKeys() []string + + // PrintableValue returns the key value based of the provided index. + PrintableValue(index int) string +} + +// List is a non-interactive function that lists items to the user. +// +// e.g. +// ```go +// +// type CustomString string +// +// func (s CustomString) PrintableKeys() []string { +// return []string{"string"} +// } +// func (s CustomString) PrintableValue(_ int) string { +// return string(s) +// } +// +// func main() { +// customStrings := []CustomString{"first", "second", "third"} +// list := components.List(theme.OperatingSystemTheme, customStrings) +// fmt.Printf(list) +// } +// +// ``` +func List[O ListableItem](theme *theme.Theme, list []O) string { + b := &strings.Builder{} + w := tabwriter.NewWriter(b, 1, 1, 1, ' ', tabwriter.TabIndent) + + for i := range list { + assetObj := list[i] + + for i, key := range assetObj.PrintableKeys() { + fmt.Fprint(w, theme.Primary(key, ":\t")) + fmt.Fprintln(w, assetObj.PrintableValue(i)) + } + + fmt.Fprintln(w, "") + } + + w.Flush() + + return b.String() +} diff --git a/cli/components/selector.go b/cli/components/selector.go new file mode 100644 index 0000000000..2f8694ec69 --- /dev/null +++ b/cli/components/selector.go @@ -0,0 +1,64 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package components + +import ( + "fmt" + "os" + + tea "github.com/charmbracelet/bubbletea" + "github.com/rs/zerolog/log" +) + +// SelectableItem is the interface that items need to implement so that we can select them. +type SelectableItem interface { + Display() string +} + +// SelectableItem is an interactive prompt that displays the provided message and displays a +// list of items to be selected. +// +// e.g. +// ```go +// +// type CustomString string +// +// func (s CustomString) Display() string { +// return string(s) +// } +// +// func main() { +// customStrings := []CustomString{"first", "second", "third"} +// selected := components.Select("Choose a string", customStrings) +// fmt.Printf("You chose the %s string.\n", customStrings[selected]) +// } +// +// ``` +func Select[S SelectableItem](msg string, items []S) int { + list := make([]string, len(items)) + + for i := range items { + list[i] = items[i].Display() + } + + selection := -1 // make sure we have an invalid index + model := NewListModel(msg, list, func(s int) { + selection = s + }) + _, err := tea.NewProgram(model, tea.WithInputTTY()).Run() + if err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } + + if selection == -1 { + return -1 + } + selected := items[selection] + log.Debug(). + Int("selection", selection). + Str("item", selected.Display()). + Msg("selected") + return selection +} diff --git a/providers-sdk/v1/inventory/asset.go b/providers-sdk/v1/inventory/asset.go index aacaed1bd3..8301e25900 100644 --- a/providers-sdk/v1/inventory/asset.go +++ b/providers-sdk/v1/inventory/asset.go @@ -13,6 +13,28 @@ import ( "github.com/rs/zerolog/log" ) +// Printable Keys and Values are used by the cli/components package. +var assetPrintableKeys = []string{"name", "platform-id"} + +func (a *Asset) PrintableKeys() []string { + return assetPrintableKeys +} +func (a *Asset) PrintableValue(index int) string { + switch assetPrintableKeys[index] { + case "name": + return a.Display() + case "platform-id": + return strings.Join(a.PlatformIds, " ") + default: + return a.String() + } +} + +// Display implements SelectableItem from the cli/components package. +func (a *Asset) Display() string { + return a.HumanName() +} + func (a *Asset) HumanName() string { if a == nil { return ""