Initial commit

This commit is contained in:
2024-08-30 11:25:28 +12:00
commit 0cd89c0707
31 changed files with 1470 additions and 0 deletions

57
cmd/clear.go Normal file
View File

@@ -0,0 +1,57 @@
package cmd
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/samber/lo"
"github.com/spf13/cobra"
"hub.cybercinch.nz/guisea/go-template/internal/app"
"hub.cybercinch.nz/guisea/go-template/internal/color"
"hub.cybercinch.nz/guisea/go-template/internal/filesystem"
"hub.cybercinch.nz/guisea/go-template/internal/icon"
"hub.cybercinch.nz/guisea/go-template/internal/util"
"hub.cybercinch.nz/guisea/go-template/internal/where"
)
type clearTarget struct {
name string
clear func() error
}
// Specify what can be cleared
var clearTargets = []clearTarget{
{"cache", func() error {
return filesystem.Api().RemoveAll(where.Cache())
}},
{"logs", func() error {
return filesystem.Api().RemoveAll(where.Logs())
}},
}
func init() {
rootCmd.AddCommand(clearCmd)
for _, n := range clearTargets {
clearCmd.Flags().BoolP(n.name, string(n.name[0]), false, "clear "+n.name)
}
}
var clearCmd = &cobra.Command{
Use: "clear",
Short: "Clears sidelined files produced by the " + app.Name,
Run: func(cmd *cobra.Command, args []string) {
successStyle := lipgloss.NewStyle().Foreground(color.Green).Render
var didSomething bool
for _, n := range clearTargets {
if lo.Must(cmd.Flags().GetBool(n.name)) {
handleErr(n.clear())
fmt.Printf("%s %s cleared\n", successStyle(icon.Check), util.Capitalize(n.name))
didSomething = true
}
}
if !didSomething {
_ = cmd.Help()
}
},
}

284
cmd/config.go Normal file
View File

@@ -0,0 +1,284 @@
package cmd
import (
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strconv"
"unsafe"
"github.com/charmbracelet/lipgloss"
"hub.cybercinch.nz/guisea/go-template/internal/style"
"hub.cybercinch.nz/guisea/go-template/internal/app"
levenshtein "github.com/ka-weihe/fast-levenshtein"
"github.com/samber/lo"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"hub.cybercinch.nz/guisea/go-template/internal/color"
"hub.cybercinch.nz/guisea/go-template/internal/config"
"hub.cybercinch.nz/guisea/go-template/internal/filesystem"
"hub.cybercinch.nz/guisea/go-template/internal/icon"
"hub.cybercinch.nz/guisea/go-template/internal/where"
)
// errUnknownKey will generate error for key that was not found and will provide a hint
func errUnknownKey(key string) error {
closest := lo.MinBy(lo.Keys(config.Default), func(a string, b string) bool {
return levenshtein.Distance(key, a) < levenshtein.Distance(key, b)
})
msg := fmt.Sprintf(
"unknown key %s, did you mean %s?",
lipgloss.NewStyle().Foreground(color.Red).Render(key),
lipgloss.NewStyle().Foreground(color.Yellow).Render(closest),
)
return errors.New(msg)
}
func completionConfigKeys(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return lo.Keys(config.Default), cobra.ShellCompDirectiveNoFileComp
}
func init() {
rootCmd.AddCommand(configCmd)
}
var configCmd = &cobra.Command{
Use: "config",
Short: "Various config commands",
}
func init() {
configCmd.AddCommand(configInfoCmd)
configInfoCmd.Flags().StringP("key", "k", "", "The key to get the value for")
_ = configInfoCmd.RegisterFlagCompletionFunc("key", completionConfigKeys)
}
var configInfoCmd = &cobra.Command{
Use: "info",
Short: "Show the info for each config field with description",
Run: func(cmd *cobra.Command, args []string) {
var (
key = lo.Must(cmd.Flags().GetString("key"))
fields = lo.Values(config.Default)
)
if key != "" {
if field, ok := config.Default[key]; ok {
fields = []*config.Field{field}
} else {
handleErr(errUnknownKey(key))
}
}
sort.Slice(fields, func(i, j int) bool {
return fields[i].Key < fields[j].Key
})
for i, field := range fields {
fmt.Println(field.Pretty())
if i < len(fields)-1 {
fmt.Println()
}
}
},
}
func init() {
configCmd.AddCommand(configSetCmd)
configSetCmd.Flags().StringP("key", "k", "", "The key to set the value for")
lo.Must0(configSetCmd.MarkFlagRequired("key"))
_ = configSetCmd.RegisterFlagCompletionFunc("key", completionConfigKeys)
configSetCmd.Flags().StringP("value", "v", "", "The value to set. Leave empty to use default")
}
var configSetCmd = &cobra.Command{
Use: "set",
Short: "Set a config value",
Run: func(cmd *cobra.Command, args []string) {
var (
key = lo.Must(cmd.Flags().GetString("key"))
value = lo.Must(cmd.Flags().GetString("value"))
)
if _, ok := config.Default[key]; !ok {
handleErr(errUnknownKey(key))
}
var v any
if lo.IsEmpty(value) {
v = config.Default[key].DefaultValue
goto write
}
switch config.Default[key].DefaultValue.(type) {
case string:
v = value
case int:
parsedInt, err := strconv.ParseInt(value, 10, 64)
if err != nil {
handleErr(fmt.Errorf("invalid integer value: %s", value))
}
v = int(parsedInt)
case bool:
parsedBool, err := strconv.ParseBool(value)
if err != nil {
handleErr(fmt.Errorf("invalid boolean value: %s", value))
}
v = parsedBool
}
write:
viper.Set(key, v)
switch err := viper.WriteConfig(); err.(type) {
case viper.ConfigFileNotFoundError:
handleErr(viper.SafeWriteConfig())
default:
handleErr(err)
}
fmt.Printf(
"%s set %s to %s\n",
style.Success(icon.Check),
lipgloss.NewStyle().Foreground(color.Purple).Render(key),
lipgloss.NewStyle().Foreground(color.Yellow).Render(fmt.Sprint(v)),
)
},
}
func init() {
configCmd.AddCommand(configGetCmd)
configGetCmd.Flags().StringP("key", "k", "", "The key to get the value for")
lo.Must0(configGetCmd.MarkFlagRequired("key"))
_ = configGetCmd.RegisterFlagCompletionFunc("key", completionConfigKeys)
}
var configGetCmd = &cobra.Command{
Use: "get",
Short: "Get a config value",
Run: func(cmd *cobra.Command, args []string) {
var (
key = lo.Must(cmd.Flags().GetString("key"))
)
if _, ok := config.Default[key]; !ok {
handleErr(errUnknownKey(key))
}
fmt.Println(viper.Get(key))
},
}
func init() {
configCmd.AddCommand(configEnvCmd)
}
func fastBoolConv(b bool) int {
return int(*(*byte)(unsafe.Pointer(&b)))
}
var configEnvCmd = &cobra.Command{
Use: "env",
Short: "Show the env for each config field",
Run: func(cmd *cobra.Command, args []string) {
fields := lo.Values(config.Default)
fields = append(fields, &config.Field{Key: where.EnvConfigPath})
slices.SortStableFunc(fields, func(a *config.Field, b *config.Field) int {
return fastBoolConv(a.Key < b.Key)
})
for _, field := range fields {
envValue, isSet := os.LookupEnv(field.Env())
var value string
if isSet {
value = envValue
} else {
value = lipgloss.NewStyle().Faint(true).Render("unset")
}
_, err := fmt.Fprintf(cmd.OutOrStdout(), "%s=%s\n", field.Env(), value)
handleErr(err)
}
},
}
func init() {
configCmd.AddCommand(configWriteCmd)
configWriteCmd.Flags().BoolP("force", "f", false, "Force overwrite of existing config file")
}
var configWriteCmd = &cobra.Command{
Use: "write",
Short: "Write current config to the file",
Run: func(cmd *cobra.Command, args []string) {
var (
force = lo.Must(cmd.Flags().GetBool("force"))
configFilePath = filepath.Join(
where.Config(),
fmt.Sprintf("%s.%s", app.Name, config.ConfigFormat),
)
)
if force {
exists, err := filesystem.Api().Exists(configFilePath)
handleErr(err)
if exists {
err := filesystem.Api().Remove(configFilePath)
handleErr(err)
}
}
handleErr(viper.SafeWriteConfig())
fmt.Printf(
"%s wrote config to %s\n",
style.Success(icon.Check),
configFilePath,
)
},
}
func init() {
configCmd.AddCommand(configDeleteCmd)
}
var configDeleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete the config file",
Aliases: []string{"remove"},
Run: func(cmd *cobra.Command, args []string) {
configFilePath := filepath.Join(where.Config(), fmt.Sprintf("%s.%s", app.Name, config.ConfigFormat))
exists, err := filesystem.Api().Exists(configFilePath)
handleErr(err)
if !exists {
fmt.Printf(
"%s nothing to delete\n",
style.Success(icon.Check),
)
return
}
err = filesystem.Api().Remove(configFilePath)
handleErr(err)
fmt.Printf(
"%s deleted config\n",
style.Success(icon.Check),
)
},
}

75
cmd/root.go Normal file
View File

@@ -0,0 +1,75 @@
package cmd
import (
"fmt"
"github.com/charmbracelet/log"
"hub.cybercinch.nz/guisea/go-template/internal/style"
"os"
"strings"
cc "github.com/ivanpirog/coloredcobra"
"github.com/samber/lo"
"github.com/spf13/cobra"
"hub.cybercinch.nz/guisea/go-template/internal/app"
"hub.cybercinch.nz/guisea/go-template/internal/filesystem"
"hub.cybercinch.nz/guisea/go-template/internal/icon"
"hub.cybercinch.nz/guisea/go-template/internal/where"
)
func init() {
rootCmd.Flags().BoolP("version", "v", false, app.Name+" version")
}
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: strings.ToLower(app.Name),
Short: app.DescriptionShort,
Long: app.DescriptionLong,
Run: func(cmd *cobra.Command, args []string) {
if lo.Must(cmd.Flags().GetBool("version")) {
versionCmd.Run(versionCmd, args)
} else {
_ = cmd.Help()
}
},
}
func Execute() {
// Setup colored cobra
cc.Init(&cc.Config{
RootCmd: rootCmd,
Headings: cc.HiCyan + cc.Bold + cc.Underline,
Commands: cc.HiYellow + cc.Bold,
Example: cc.Italic,
ExecName: cc.Bold,
Flags: cc.Bold,
FlagsDataType: cc.Italic + cc.HiBlue,
NoExtraNewlines: true,
NoBottomNewline: true,
})
// Clears temp files on each run.
// It should not affect startup time since it's being run as goroutine.
go func() {
_ = filesystem.Api().RemoveAll(where.Temp())
}()
_ = rootCmd.Execute()
}
// handleErr will stop program execution and logger error to the stderr
// if err is not nil
func handleErr(err error) {
if err == nil {
return
}
log.Error(err)
_, _ = fmt.Fprintf(
os.Stderr,
"%s %s\n",
style.Failure(icon.Cross),
strings.Trim(err.Error(), " \n"),
)
os.Exit(1)
}

58
cmd/version.go Normal file
View File

@@ -0,0 +1,58 @@
package cmd
import (
"github.com/charmbracelet/lipgloss"
"html/template"
"runtime"
"github.com/samber/lo"
"hub.cybercinch.nz/guisea/go-template/internal/app"
"hub.cybercinch.nz/guisea/go-template/internal/color"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(versionCmd)
versionCmd.Flags().BoolP("short", "s", false, "print the version number only")
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of the " + app.Name,
Run: func(cmd *cobra.Command, args []string) {
if lo.Must(cmd.Flags().GetBool("short")) {
_, err := cmd.OutOrStdout().Write([]byte(app.Version + "\n"))
handleErr(err)
return
}
versionInfo := struct {
Version string
OS string
Arch string
App string
Compiler string
}{
Version: app.Version,
App: app.Name,
OS: runtime.GOOS,
Arch: runtime.GOARCH,
Compiler: runtime.Compiler,
}
t, err := template.New("version").Funcs(map[string]any{
"faint": lipgloss.NewStyle().Faint(true).Render,
"bold": lipgloss.NewStyle().Bold(true).Render,
"magenta": lipgloss.NewStyle().Foreground(color.Purple).Render,
}).Parse(`{{ magenta "▇▇▇" }} {{ magenta .App }}
{{ faint "Version" }} {{ bold .Version }}
{{ faint "Platform" }} {{ bold .OS }}/{{ bold .Arch }}
{{ faint "Compiler" }} {{ bold .Compiler }}
`)
handleErr(err)
handleErr(t.Execute(cmd.OutOrStdout(), versionInfo))
},
}

68
cmd/where.go Normal file
View File

@@ -0,0 +1,68 @@
package cmd
import (
"github.com/charmbracelet/lipgloss"
"os"
"github.com/samber/lo"
"github.com/spf13/cobra"
"hub.cybercinch.nz/guisea/go-template/internal/app"
"hub.cybercinch.nz/guisea/go-template/internal/color"
"hub.cybercinch.nz/guisea/go-template/internal/where"
)
type whereTarget struct {
name string
where func() string
argShort, argLong string
}
// Specify what paths to show
var wherePaths = []whereTarget{
{"Config", where.Config, "c", "config"},
{"Logs", where.Logs, "l", "logs"},
}
func init() {
rootCmd.AddCommand(whereCmd)
for _, n := range wherePaths {
if n.argShort != "" {
whereCmd.Flags().BoolP(n.argLong, n.argShort, false, n.name+" path")
} else {
whereCmd.Flags().Bool(n.argLong, false, n.name+" path")
}
}
whereCmd.MarkFlagsMutuallyExclusive(lo.Map(wherePaths, func(t whereTarget, _ int) string {
return t.argLong
})...)
whereCmd.SetOut(os.Stdout)
}
var whereCmd = &cobra.Command{
Use: "where",
Short: "Show the paths for a files related to the " + app.Name,
Run: func(cmd *cobra.Command, args []string) {
headerStyle := lipgloss.NewStyle().Foreground(color.HiPurple).Bold(true).Render
argStyle := lipgloss.NewStyle().Foreground(color.Yellow).Render
for _, n := range wherePaths {
if lo.Must(cmd.Flags().GetBool(n.argLong)) {
cmd.Println(n.where())
return
}
}
for i, n := range wherePaths {
cmd.Printf("%s %s\n", headerStyle(n.name+"?"), argStyle("--"+n.argLong))
cmd.Println(n.where())
if i < len(wherePaths)-1 {
cmd.Println()
}
}
},
}