Skip to content

Commit

Permalink
feat!: Implement non-interactive mode
Browse files Browse the repository at this point in the history
  • Loading branch information
Allaman committed Jul 12, 2024
1 parent 486f793 commit 761e6a7
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 68 deletions.
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ chmod +x werkzeugkasten
./werkzeugkasten
```

You could also integrate werkzeugkasten in your golden (Docker) image. ⚠️ Keep possible security implications in mind.

## How it works

Werkzeugkasten is basically a wrapper around the excellent [eget](https://github.com/zyedidia/eget) that does the heavy lifting and is responsible for downloading the chosen tools. Eget itself is downloaded as binary via `net/http` call and decompression/extracting logic. The awesome [charmbracelet](https://github.com/charmbracelet) tools [huh](https://github.com/charmbracelet/huh), [log](https://github.com/charmbracelet/log), and [lipgloss](https://github.com/charmbracelet/lipgloss) are used for a modern look and feel. By default, the latest release of a tool is downloaded (see [Configuration](#configuration)).
Expand All @@ -50,21 +52,38 @@ Werkzeugkasten is basically a wrapper around the excellent [eget](https://github

Werkzeugkasten is not intended to replace package managers (such as apt, brew, ...) or configuration management tools (such as Ansible, ...). It is also not intended to be used non-interactively.

## Configuration
## Usage

```
❯ werkzeugkasten -h
Usage of werkzeugkasten:
❯ werkzeugkasten -help
Usage: werkzeugkasten [flags]
Flags:
-accessible
Enable accessibility mode
Enable accessibility mode for interactiveuse
-debug
Enable debug output
-help
Print help message
-installDir string
Where to install the tools (default ".")
-list
Print all available tools
-tool value
Specify multiple tools to install programmatically (e.g., -tool kustomize -tool task)
-version
Print version
```

Werkzeugkasten supports an interactive mode and a non-interactive mode.

- `werkzeugkasten` will start the interactive mode where you select your tools you want to install in a menu.

- `werkzeugkasten -list` will print all available tools.

- `werkzeugkasten -tool age -tool kustomize` will download age and kustomize.

## Configuration

Besides CLI flags, further configuration is possible with environment variables. Since Werkzeugkasten is designed to run on minimal systems, I cannot rely on having an editor available for writing configuration files.

Set a tool's version/tag explicitly:
Expand Down
34 changes: 31 additions & 3 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,37 @@ import (
"flag"
"fmt"
"os"
"strings"
)

type cliConfig struct {
debug bool
accessible bool
accessible bool
debug bool
downloadDir string
list bool
toolList toolList
}
type toolList []string

func (s *toolList) String() string {
return strings.Join(*s, ", ")
}

func (s *toolList) Set(value string) error {
*s = append(*s, value)
return nil
}

func cli() cliConfig {
var cliFlags cliConfig
var toolList toolList
helpFlag := flag.Bool("help", false, "Print help message")
versionFlag := flag.Bool("version", false, "Print version")
debugFlag := flag.Bool("debug", false, "Enable debug output")
accessibleFlag := flag.Bool("accessible", false, "Enable accessibility mode")
accessibleFlag := flag.Bool("accessible", false, "Enable accessibility mode for interactiveuse")
downloadDirFlag := flag.String("installDir", ".", "Where to install the tools")
listFlag := flag.Bool("list", false, "Print all available tools")
flag.Var(&toolList, "tool", "Specify multiple tools to install programmatically (e.g., -tool kustomize -tool task)")
flag.Parse()
if *helpFlag {
fmt.Println("Usage: werkzeugkasten [flags]")
Expand All @@ -28,11 +46,21 @@ func cli() cliConfig {
logger.Print(version)
os.Exit(0)
}
if *listFlag {
cliFlags.list = true
}
if *debugFlag {
cliFlags.debug = true
}
if *accessibleFlag {
cliFlags.accessible = true
}
if *downloadDirFlag != "" {
cliFlags.downloadDir = *downloadDirFlag
}
cliFlags.toolList = []string{}
if len(toolList) > 0 {
cliFlags.toolList = toolList
}
return cliFlags
}
16 changes: 15 additions & 1 deletion eget.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type egetConfig struct {
version string
}

func newDefaultConfig() egetConfig {
func newDefaultEgetConfig() egetConfig {
url := "https://github.com/zyedidia/eget/releases/download/v%s/eget-%s-%s_%s.tar.gz"
return egetConfig{arch: runtime.GOARCH, os: runtime.GOOS, url: url, version: "1.3.4"}
}
Expand Down Expand Up @@ -178,3 +178,17 @@ func makeExecutable(filePath string) error {
func rename(source, dest string) error {
return os.Rename(source, dest)
}

func installEget(installDir string) {
egetConfig := newDefaultEgetConfig()
if os.Getenv("WK_EGET_VERSION") != "" {
version := os.Getenv("WK_EGET_VERSION")
logger.Debug("setting eget version", "version", version)
egetConfig.version = version
}
err := downloadEgetBinary(installDir, egetConfig)
if err != nil {
logger.Error("could not download eget binary", "error", err)
os.Exit(1)
}
}
29 changes: 28 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,32 @@ func main() {
logger.SetReportCaller(true)
logger.SetLevel(log.DebugLevel)
}
startUI(cfg)
tools, err := createToolData()
if err != nil {
logger.Fatal("could not parse tools data", "error", err)
}
logger.Debug("download dir", "dir", cfg.downloadDir)

installDir, err := normalizePath(cfg.downloadDir)
if err != nil {
logger.Fatal("could not normalize path")
}
if cfg.list {
printTools(tools)
os.Exit(0)
}
// interactive mode
if len(cfg.toolList) == 0 {
startUI(cfg, tools)
} else {
// non-interactive mode
installEget(cfg.downloadDir)
for _, toolName := range cfg.toolList {
err = downloadToolWithEget(installDir, tools.Tools[toolName])
if err != nil {
logger.Warn("could not download tool", "tool", toolName, "error", err)
continue
}
}
}
}
27 changes: 27 additions & 0 deletions tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"path"
"path/filepath"
"runtime"
"slices"
"strings"
"text/tabwriter"

"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -117,3 +119,28 @@ func downloadToolWithEget(workingdir string, tool Tool) error {
}
return nil
}

func sortTools(tools Tools) []string {
sortedTools := make([]string, 0, len(tools.Tools))
for k := range tools.Tools {
sortedTools = append(sortedTools, k)
}
slices.Sort(sortedTools)
return sortedTools
}

func printTools(tools Tools) {
w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0)
fmt.Fprintln(w, "Key\tURL\tDescription")
sortedTools := sortTools(tools)
for _, tool := range sortedTools {
identifier := tools.Tools[tool].Identifier
url := fmt.Sprintf("https://github.com/%s", identifier)
// handle packages that are not installed from GitHub
if strings.HasPrefix(identifier, "https") {
url = tools.Tools[tool].Identifier
}
fmt.Fprintf(w, "%s\t%s\t%s\n", tool, url, tools.Tools[tool].Description)
}
w.Flush()
}
73 changes: 14 additions & 59 deletions ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package main
import (
"errors"
"fmt"
"os"
"sort"
"strings"

"github.com/charmbracelet/huh"
Expand All @@ -13,7 +11,7 @@ import (
)

var (
workingDir string
// workingDir string
selectedTools []string
)

Expand Down Expand Up @@ -50,11 +48,7 @@ func formatToolString(name string, tool Tool) string {
}

func createToolOptions(tools Tools) []huh.Option[string] {
sortedTools := make([]string, 0, len(tools.Tools))
for k := range tools.Tools {
sortedTools = append(sortedTools, k)
}
sort.Strings(sortedTools)
sortedTools := sortTools(tools)
options := make([]huh.Option[string], 0, len(tools.Tools))
for _, name := range sortedTools {
tool := tools.Tools[name]
Expand All @@ -64,29 +58,12 @@ func createToolOptions(tools Tools) []huh.Option[string] {
return options
}

func createForm(cfg cliConfig, tools Tools) *huh.Form {
func createForm(tools Tools) *huh.Form {
form := huh.NewForm(
huh.NewGroup(
huh.NewNote().
Title("Your Werkzeugkasten").
Description("Installing awesome tools with ease!"),

huh.NewInput().
Title("Where should your tools be installed?").
Description("Provide an absolute or relative path where your tools should be downloaded to. All directories are created if needed.").
Prompt("#").
Validate(func(str string) error {
if str == "" {
return errors.New("you must provide a path")
}
return nil
}).
Value(&workingDir),
),
huh.NewGroup(
huh.NewMultiSelect[string]().
Title("Which tools do you want to install?").
Description("Chose one or more tools to be downloaded to the specified path.").
Description("Chose one or more tools to be downloaded.").
Options(createToolOptions(tools)...).
Validate(func(t []string) error {
if len(t) == 0 {
Expand All @@ -96,59 +73,37 @@ func createForm(cfg cliConfig, tools Tools) *huh.Form {
}).
Value(&selectedTools),
),
).WithAccessible(cfg.accessible)
)

return form
}

func process(tools Tools) func() {
func processSelectedTools(cfg cliConfig, tools Tools) func() {
return func() {
installDir, err := normalizePath(workingDir)
if err != nil {
logger.Error("could not normalize path")
os.Exit(1)
}

config := newDefaultConfig()
if os.Getenv("WK_EGET_VERSION") != "" {
version := os.Getenv("WK_EGET_VERSION")
logger.Debug("setting eget version", "version", version)
config.version = version
}
err = downloadEgetBinary(installDir, config)
if err != nil {
logger.Error("could not download eget binary", "error", err)
os.Exit(1)
}
installEget(cfg.downloadDir)
for _, t := range selectedTools {
err = downloadToolWithEget(installDir, tools.Tools[t])
err := downloadToolWithEget(cfg.downloadDir, tools.Tools[t])
if err != nil {
logger.Warn("could not download tool", "tool", t, "error", err)
continue
}
}
logger.Info(fmt.Sprintf("Run 'export PATH=$PATH:%s' to add your tools to the PATH", installDir))
logger.Info(fmt.Sprintf("Run 'export PATH=$PATH:%s' to add your tools to the PATH", cfg.downloadDir))
}
}

func startUI(cfg cliConfig) {
tools, err := createToolData()
if err != nil {
logger.Error("could not parse tools data", "error", err)
os.Exit(1)
}

form := createForm(cfg, tools)

func startUI(cfg cliConfig, tools Tools) {
form := createForm(tools)
form.WithAccessible(cfg.accessible)
form.WithTheme(theme)

err = form.Run()
err := form.Run()

if err != nil {
logger.Fatal(err)
}

start := process(tools)
start := processSelectedTools(cfg, tools)

_ = spinner.New().Title("Downloading tools ...").Accessible(cfg.accessible).Action(start).Run()
}

0 comments on commit 761e6a7

Please sign in to comment.