Skip to content

Commit

Permalink
✨ make components.Select() generic (#4811)
Browse files Browse the repository at this point in the history
The Go `components` 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) HumanName() 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/rawlist/main.go
```

---------

Signed-off-by: Salim Afiune Maya <afiune@mondoo.com>
  • Loading branch information
afiune authored Nov 4, 2024
1 parent 8e42171 commit d69318b
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 84 deletions.
5 changes: 3 additions & 2 deletions apps/cnquery/cmd/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Expand Down
58 changes: 58 additions & 0 deletions cli/components/README.md
Original file line number Diff line number Diff line change
@@ -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
```

26 changes: 26 additions & 0 deletions cli/components/_examples/list/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
22 changes: 22 additions & 0 deletions cli/components/_examples/selector/main.go
Original file line number Diff line number Diff line change
@@ -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])
}
38 changes: 0 additions & 38 deletions cli/components/assetlist.go

This file was deleted.

44 changes: 0 additions & 44 deletions cli/components/assetselect.go

This file was deleted.

62 changes: 62 additions & 0 deletions cli/components/list_raw_items.go
Original file line number Diff line number Diff line change
@@ -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()
}
64 changes: 64 additions & 0 deletions cli/components/selector.go
Original file line number Diff line number Diff line change
@@ -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
}
22 changes: 22 additions & 0 deletions providers-sdk/v1/inventory/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down

0 comments on commit d69318b

Please sign in to comment.