Skip to content

Commit

Permalink
Add interactive mode (call without subcommand) (#220)
Browse files Browse the repository at this point in the history
* add interactive mode when no args #219
* update README for #219

Signed-off-by: Denis Vaumoron <dvaumoron@gmail.com>
  • Loading branch information
dvaumoron authored Aug 3, 2024
1 parent 681a311 commit 7dc2c10
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 37 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,13 @@ echo "source '~/.tenv.completion.fish'" >> ~/.zshrc
| `at` (`atmos`) | [ATMOS_](#atmos-env-vars) | [Atmos](https://atmos.tools) |


<details><summary><b>tenv</b></summary><br>

Without subcommand `tenv` display interactive menus to manage tools and their versions.

</details>


<details><summary><b>tenv &lt;tool&gt; install [version]</b></summary><br>

Install a requested version of the tool (into `TENV_ROOT` directory from `<TOOL>_REMOTE` url).
Expand Down
16 changes: 16 additions & 0 deletions cmd/tenv/tenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package main

import (
"fmt"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -73,6 +74,7 @@ func main() {
}

hclParser := hclparse.NewParser()
manageNoArgsCmd(&conf, builders, hclParser) // call os.Exit when necessary
manageHiddenCallCmd(&conf, builders, hclParser) // proxy call use os.Exit when called

if err = initRootCmd(&conf, builders, hclParser).Execute(); err != nil {
Expand Down Expand Up @@ -157,6 +159,20 @@ func initRootCmd(conf *config.Config, builders map[string]builder.BuilderFunc, h
return rootCmd
}

func manageNoArgsCmd(conf *config.Config, builders map[string]builder.BuilderFunc, hclParser *hclparse.Parser) {
if len(os.Args) > 1 {
return
}

if err := toolUI(conf, builders, hclParser); err != nil {
fmt.Println(err.Error())

os.Exit(1)
}

os.Exit(0)
}

func manageHiddenCallCmd(conf *config.Config, builders map[string]builder.BuilderFunc, hclParser *hclparse.Parser) {
if len(os.Args) < 3 || os.Args[1] != cmdconst.CallSubCmd {
return
Expand Down
221 changes: 193 additions & 28 deletions cmd/tenv/textui.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package main

import (
"cmp"
"fmt"
"io"
"slices"
Expand All @@ -27,9 +28,12 @@ import (
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/hashicorp/hcl/v2/hclparse"

"github.com/tofuutils/tenv/v2/config"
"github.com/tofuutils/tenv/v2/pkg/loghelper"
"github.com/tofuutils/tenv/v2/versionmanager"
"github.com/tofuutils/tenv/v2/versionmanager/builder"
"github.com/tofuutils/tenv/v2/versionmanager/semantic"
)

Expand All @@ -52,6 +56,10 @@ func (i item) FilterValue() string {
return string(i)
}

func cmpItem(a list.Item, b list.Item) int {
return cmp.Compare(a.FilterValue(), b.FilterValue())
}

type itemDelegate struct {
choices map[string]struct{}
}
Expand All @@ -73,10 +81,43 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
fmt.Fprint(w, line)
}

type manageItemDelegate struct {
choices map[string]struct{}
installed map[string]struct{}
}

func (d manageItemDelegate) Height() int { return 1 }
func (d manageItemDelegate) Spacing() int { return 0 }
func (d manageItemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil }
func (d manageItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
version, selectedStr := listItem.FilterValue(), " "
_, selected := d.choices[version]
_, installed := d.installed[version]
if selected {
// display what will be done
if installed {
selectedStr = "U"
} else {
selectedStr = "I"
}
} else {
if installed {
selectedStr = "X"
}
}

line := loghelper.Concat("[", selectedStr, "] ", version)

if index == m.Index() {
line = selectedItemStyle.Render(line)
}

fmt.Fprint(w, line)
}

type itemModel struct {
choices map[string]struct{}
list list.Model
manager versionmanager.VersionManager
quitting bool
}

Expand Down Expand Up @@ -133,6 +174,127 @@ func (m itemModel) View() string {
return "\n" + m.list.View()
}

func toolUI(conf *config.Config, builders map[string]builder.BuilderFunc, hclParser *hclparse.Parser) error {
conf.InitDisplayer(false)

items := make([]list.Item, 0, len(builders))
for tool := range builders {
items = append(items, item(tool))
}
slices.SortFunc(items, cmpItem)

// shared object
selection := map[string]struct{}{}

delegate := itemDelegate{
choices: selection,
}

l := list.New(items, delegate, defaultWidth, listHeight)
l.Title = "Which tool do you want to manage ?"
l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
l.Styles.Title = titleStyle
l.Styles.PaginationStyle = paginationStyle
l.Styles.HelpStyle = helpStyle

l.AdditionalFullHelpKeys = additionalFullHelpKeys
l.AdditionalShortHelpKeys = additionalShortHelpKeys

m := itemModel{
choices: selection,
list: l,
}

_, err := tea.NewProgram(m).Run()
if err != nil {
return err
}

if len(m.choices) == 0 {
loghelper.StdDisplay("No selected tool")

return nil
}

for selected := range selection {
err = manageUI(builders[selected](conf, hclParser))
if err != nil {
return err
}
}

return nil
}

func manageUI(versionManager versionmanager.VersionManager) error {
installed := versionManager.LocalSet()

remoteVersions, err := versionManager.ListRemote(true)
if err != nil {
return err
}

items := make([]list.Item, 0, len(remoteVersions))
for _, remoteVersion := range remoteVersions {
items = append(items, item(remoteVersion))
}

// shared object
selection := map[string]struct{}{}

delegate := manageItemDelegate{
choices: selection,
installed: installed,
}

l := list.New(items, delegate, defaultWidth, listHeight)
l.Title = loghelper.Concat("Which ", versionManager.FolderName, " version(s) do you want to install(I) or uninstall(U) ? (X mark already installed)")
l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
l.Styles.Title = titleStyle
l.Styles.PaginationStyle = paginationStyle
l.Styles.HelpStyle = helpStyle

l.AdditionalFullHelpKeys = additionalFullHelpKeys
l.AdditionalShortHelpKeys = additionalShortHelpKeys

m := itemModel{
choices: selection,
list: l,
}

_, err = tea.NewProgram(m).Run()
if err != nil {
return err
}

if len(m.choices) == 0 {
loghelper.StdDisplay(loghelper.Concat("No selected ", versionManager.FolderName, " versions"))

return nil
}

toInstall := make([]string, 0, len(m.choices))
toUninstall := make([]string, 0, len(m.choices))
for version := range m.choices {
if _, installed := installed[version]; installed {
toUninstall = append(toUninstall, version)
} else {
toInstall = append(toInstall, version)
}
}
slices.SortFunc(toInstall, semantic.CmpVersion)
slices.SortFunc(toUninstall, semantic.CmpVersion)

err = versionManager.UninstallMultiple(toUninstall)
if err != nil {
return nil
}

return versionManager.InstallMultiple(toInstall)
}

func uninstallUI(versionManager versionmanager.VersionManager) error {
datedVersions, err := versionManager.ListLocal(false)
if err != nil {
Expand All @@ -152,42 +314,19 @@ func uninstallUI(versionManager versionmanager.VersionManager) error {
}

l := list.New(items, delegate, defaultWidth, listHeight)
l.Title = "Which version(s) do you want to uninstall ?"
l.Title = loghelper.Concat("Which ", versionManager.FolderName, " version(s) do you want to uninstall ?")
l.SetShowStatusBar(false)
l.SetFilteringEnabled(false)
l.Styles.Title = titleStyle
l.Styles.PaginationStyle = paginationStyle
l.Styles.HelpStyle = helpStyle

l.AdditionalFullHelpKeys = func() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("space"),
key.WithHelp("space", "select item"),
),
key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "validate uninstallation"),
),
}
}
l.AdditionalShortHelpKeys = func() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("space"),
key.WithHelp("space", "select"),
),
key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "validate"),
),
}
}
l.AdditionalFullHelpKeys = additionalFullHelpKeys
l.AdditionalShortHelpKeys = additionalShortHelpKeys

m := itemModel{
choices: selection,
list: l,
manager: versionManager,
}

_, err = tea.NewProgram(m).Run()
Expand All @@ -207,5 +346,31 @@ func uninstallUI(versionManager versionmanager.VersionManager) error {
}
slices.SortFunc(selected, semantic.CmpVersion)

return m.manager.UninstallMultiple(selected)
return versionManager.UninstallMultiple(selected)
}

func additionalFullHelpKeys() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("space"),
key.WithHelp("space", "select item"),
),
key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "validate uninstallation"),
),
}
}

func additionalShortHelpKeys() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("space"),
key.WithHelp("space", "select"),
),
key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "validate"),
),
}
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/x/ansi v0.1.4 // indirect
github.com/charmbracelet/x/input v0.1.2 // indirect
github.com/charmbracelet/x/input v0.1.3 // indirect
github.com/charmbracelet/x/term v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.1.2 // indirect
github.com/cloudflare/circl v1.3.9 // indirect
Expand All @@ -40,7 +40,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/N
github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/input v0.1.2 h1:QJAZr33eOhDowkkEQ24rsJy4Llxlm+fRDf/cQrmqJa0=
github.com/charmbracelet/x/input v0.1.2/go.mod h1:LGBim0maUY4Pitjn/4fHnuXb4KirU3DODsyuHuXdOyA=
github.com/charmbracelet/x/input v0.1.3 h1:oy4TMhyGQsYs/WWJwu1ELUMFnjiUAXwtDf048fHbCkg=
github.com/charmbracelet/x/input v0.1.3/go.mod h1:1gaCOyw1KI9e2j00j/BBZ4ErzRZqa05w0Ghn83yIhKU=
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg=
Expand Down Expand Up @@ -75,8 +75,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
Expand Down Expand Up @@ -121,8 +121,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
Expand Down
Loading

0 comments on commit 7dc2c10

Please sign in to comment.