From 0cd89c07073e084fe67f43477f9bbe67b28c6e27 Mon Sep 17 00:00:00 2001 From: Aaron Guise Date: Fri, 30 Aug 2024 11:25:28 +1200 Subject: [PATCH] Initial commit --- .github/workflows/build.yml | 28 ++++ .gitignore | 115 +++++++++++++ .goreleaser.yaml | 25 +++ .semrelrc | 13 ++ README.md | 85 ++++++++++ cmd/clear.go | 57 +++++++ cmd/config.go | 284 +++++++++++++++++++++++++++++++++ cmd/root.go | 75 +++++++++ cmd/version.go | 58 +++++++ cmd/where.go | 68 ++++++++ go.mod | 48 ++++++ go.sum | 116 ++++++++++++++ internal/app/meta.go | 15 ++ internal/color/color.go | 25 +++ internal/config/default.go | 66 ++++++++ internal/config/field.go | 90 +++++++++++ internal/config/init.go | 38 +++++ internal/config/key/keys.go | 7 + internal/filesystem/api.go | 8 + internal/filesystem/init.go | 5 + internal/filesystem/set.go | 18 +++ internal/filesystem/wrapper.go | 5 + internal/icon/icon.go | 13 ++ internal/logger/init.go | 48 ++++++ internal/style/style.go | 12 ++ internal/util/strings.go | 11 ++ internal/where/env.go | 10 ++ internal/where/util.go | 14 ++ internal/where/where.go | 68 ++++++++ justfile | 17 ++ main.go | 28 ++++ 31 files changed, 1470 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 .goreleaser.yaml create mode 100644 .semrelrc create mode 100644 README.md create mode 100644 cmd/clear.go create mode 100644 cmd/config.go create mode 100644 cmd/root.go create mode 100644 cmd/version.go create mode 100644 cmd/where.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/app/meta.go create mode 100644 internal/color/color.go create mode 100644 internal/config/default.go create mode 100644 internal/config/field.go create mode 100644 internal/config/init.go create mode 100644 internal/config/key/keys.go create mode 100644 internal/filesystem/api.go create mode 100644 internal/filesystem/init.go create mode 100644 internal/filesystem/set.go create mode 100644 internal/filesystem/wrapper.go create mode 100644 internal/icon/icon.go create mode 100644 internal/logger/init.go create mode 100644 internal/style/style.go create mode 100644 internal/util/strings.go create mode 100644 internal/where/env.go create mode 100644 internal/where/util.go create mode 100644 internal/where/where.go create mode 100755 justfile create mode 100644 main.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b60ccef --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,28 @@ +name: CI +on: + push: + branches: + - "**" + tags: + - "!**" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + - name: Copy go-semantic-release plugins and release + run: | + /usr/local/bin/semantic-release --version-file \ + --changelog .generated-go-semantic-release-changelog.md \ + --hooks goreleaser \ + --provider=gitea + env: + GITEA_TOKEN: ${{ secrets.G_TOKEN }} + GITEA_HOST: ${{ secrets.G_SERVER_URL}} + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ec0d94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,115 @@ +######### +# macOS # +######### + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + + + + +###### +# Go # +###### + +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +bin/ +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +########### +# Windows # +########### + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + + + + + +######### +# Linux # +######### + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +############# +# JetBrains # +############# + +.idea diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..fde6eb3 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,25 @@ +builds: + - env: + - CGO_ENABLED=0 + targets: + - linux_amd64 + - linux_arm64 + - darwin_amd64 + - darwin_arm64 + - linux_arm + - windows_amd64 + main: main.go + ldflags: + - -extldflags '-static' + - -s -w -X hub.cybercinch.nz/guisea/go-template/internal/app/meta.Version={{.Version}} + +gitea_urls: + api: https://hub.cybercinch.nz/api/v1 + download: https://hub.cybercinch.nz + +archives: + - format: binary + name_template: '{{ .Binary }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}' + +checksum: + name_template: '{{ .ProjectName }}_v{{ .Version }}_checksums.txt' diff --git a/.semrelrc b/.semrelrc new file mode 100644 index 0000000..d6c3eb4 --- /dev/null +++ b/.semrelrc @@ -0,0 +1,13 @@ +{ + "plugins": { + "provider": { + "name": "gitea" + }, + "changelog-generator": { + "name": "default", + "options": { + "emojis": "true" + } + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f42556 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# Go CLI Project Template ☄️ + +Powerful template for Go CLI applications with advanced config management + +Screenshot 2022-10-05 at 11 14 32 + + +## Features + +- Advanced config management with [viper](https://github.com/spf13/viper) and + useful config commands such as setting config values directly from CLI (like this `config set -k logs.write -v true`), + reading env variables and file-based configuration (either TOML or YAML). Also, configuration is self-documented, type `config info` to show every config field available with description for each. + +- Cache & Temp files management with `clear` command + +- Polished CLI experience with [cobra](https://github.com/spf13/cobra) + [coloredcobra](https://github.com/ivanpirog/coloredcobra) to make things look pretty + +- [Afero](https://github.com/spf13/afero) filesystem for various fs utils, abstractions and in-memory fs for testing. + For example, instead of `os.Remove("file")` use `filesystem.Api().Remove("file")` + +- Easy to use path management with `where` package + +- Logging to file + +- Icons! + +- Predefined lipgloss colors + +## How to use + +Press this shiny green button on top + +Screenshot 2022-09-30 at 13 37 30 + +Then you would probably want to rename go mod name from `hub.cybercinch.nz/guisea/go-template` to something else. +To do this you could use your IDE refactor features or run [just](https://github.com/casey/just) target. + +```shell +just rename github.com/username/repo +``` + +This will prompt you to type a new name and will replace every occurence of the old go mod name with the new one. + +## Further usage + +### Changing name of the app + +Change the value of the constant `Name` at [internal/app/meta.go](https://hub.cybercinch.nz/guisea/go-template/src/branch/main/internal/app/meta.go) + +### Changing config file format from TOML from YAML + +Change the value of the constant `ConfigFormat` at [internal/config/init.go](https://hub.cybercinch.nz/guisea/go-template/src/branch/main/internal/config/init.go) + +### Declaring new config fields + +Firstly, declare a field key name as a constant inside [internal/config/key/keys.go](https://hub.cybercinch.nz/guisea/go-template/src/branch/main/internal/config/key/keys.go) + +Then put them inside [config/default.go](https://hub.cybercinch.nz/guisea/go-template/src/branch/main/internal/config/default.go) (take a predefined fields for logging as a reference) + +For example + +```go +// key/keys.go + +const IconType = "icon.type" +``` + +```go +// config/default.go + +{ + constant.IconType, // config field key + "emoji", // default value + "What type of icons to use", // description +} +``` + +### Accessing config fields + +For the example above it would be `viper.GetString(key.EmojiType)`. See [viper](https://github.com/spf13/viper) for more information + + +## Something is not clear? + +Please, [open an issue](https://hub.cybercinch.nz/guisea/go-template/issues/new) so I could document it diff --git a/cmd/clear.go b/cmd/clear.go new file mode 100644 index 0000000..c1f6f1b --- /dev/null +++ b/cmd/clear.go @@ -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() + } + }, +} diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..f7f7088 --- /dev/null +++ b/cmd/config.go @@ -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), + ) + }, +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..ebed51e --- /dev/null +++ b/cmd/root.go @@ -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) +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..0acde85 --- /dev/null +++ b/cmd/version.go @@ -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)) + }, +} diff --git a/cmd/where.go b/cmd/where.go new file mode 100644 index 0000000..fb5f4b1 --- /dev/null +++ b/cmd/where.go @@ -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() + } + } + }, +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7b8c89a --- /dev/null +++ b/go.mod @@ -0,0 +1,48 @@ +module hub.cybercinch.nz/guisea/go-template + +go 1.22.1 + +require ( + github.com/charmbracelet/lipgloss v0.13.0 + github.com/charmbracelet/log v0.4.0 + github.com/ivanpirog/coloredcobra v1.0.1 + github.com/ka-weihe/fast-levenshtein v0.0.0-20201227151214-4c99ee36a1ba + github.com/samber/lo v1.47.0 + github.com/spf13/afero v1.11.0 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + go.uber.org/multierr v1.11.0 // indirect +) + +require ( + github.com/fatih/color v1.17.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9df68e9 --- /dev/null +++ b/go.sum @@ -0,0 +1,116 @@ +github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM= +github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= +github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dgryski/trifles v0.0.0-20200830180326-aaf60a07f6a3 h1:JibukGTEjdN4VMX7YHmXQsLr/gPURUbetlH4E6KvHSU= +github.com/dgryski/trifles v0.0.0-20200830180326-aaf60a07f6a3/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/ivanpirog/coloredcobra v1.0.1 h1:aURSdEmlR90/tSiWS0dMjdwOvCVUeYLfltLfbgNxrN4= +github.com/ivanpirog/coloredcobra v1.0.1/go.mod h1:iho4nEKcnwZFiniGSdcgdvRgZNjxm+h20acv8vqmN6Q= +github.com/ka-weihe/fast-levenshtein v0.0.0-20201227151214-4c99ee36a1ba h1:keZ4vJpYOVm6yrjLzZ6QgozbEBaT0GjfH30ihbO67+4= +github.com/ka-weihe/fast-levenshtein v0.0.0-20201227151214-4c99ee36a1ba/go.mod h1:kaXTPU4xitQT0rfT7/i9O9Gm8acSh3DXr0p4y3vKqiE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/meta.go b/internal/app/meta.go new file mode 100644 index 0000000..1376785 --- /dev/null +++ b/internal/app/meta.go @@ -0,0 +1,15 @@ +package app + +// Version is the version of the application +const Version = "0.0.1" + +const ( + // App is the name of the application + Name = "app" + + // DescriptionShort short description of the app + DescriptionShort = Name + " description" + + // DescriptionLong long description of the app + DescriptionLong = Name + " v" + Version +) diff --git a/internal/color/color.go b/internal/color/color.go new file mode 100644 index 0000000..215117f --- /dev/null +++ b/internal/color/color.go @@ -0,0 +1,25 @@ +package color + +import "github.com/charmbracelet/lipgloss" + +const ( + Red = lipgloss.Color("1") + Green = lipgloss.Color("2") + Yellow = lipgloss.Color("3") + Blue = lipgloss.Color("4") + Purple = lipgloss.Color("5") + Cyan = lipgloss.Color("6") + White = lipgloss.Color("7") + Black = lipgloss.Color("8") +) + +const ( + HiRed = lipgloss.Color("9") + HiGreen = lipgloss.Color("10") + HiYellow = lipgloss.Color("11") + HiBlue = lipgloss.Color("12") + HiPurple = lipgloss.Color("13") + HiCyan = lipgloss.Color("14") + HiWhite = lipgloss.Color("15") + HiBlack = lipgloss.Color("16") +) diff --git a/internal/config/default.go b/internal/config/default.go new file mode 100644 index 0000000..c185df7 --- /dev/null +++ b/internal/config/default.go @@ -0,0 +1,66 @@ +package config + +import ( + "github.com/spf13/viper" + "hub.cybercinch.nz/guisea/go-template/internal/config/key" +) + +// fields is the config fields with their default values and descriptions +var fields = []*Field{ + // LOGS + { + key.LogsWrite, + true, + "Write logs to file", + }, + { + key.LogsLevel, + "info", + `Logs level. +Available options are: (from less to most verbose) +fatal, error, warn, info, debug`, + }, + { + key.LogsReportCaller, + false, + "Whether the logger should report the caller location.", + }, + //{ + // key.TenantId, + // "some_client_id", + // "The TenantID to use with Xero.", + //}, + //{ + // key.BankAccountId, + // "a-random-bank-account-id", + // "Guid for the xero account to use.", + //}, + // END LOGS + // BEANSTALK + //{ + // key.BeanstalkServer, + // "localhost", + // "The Beanstalk Server to use", + //}, + //{ + // key.BeanstalkPort, + // "11300", + // "The port to communicate with Beanstalk server", + //}, + //{ + // key.BeanstalkTube, + // "visa-transactions", + // "The tube to use to find our transactions in.", + //}, +} + +func setDefaults() { + Default = make(map[string]*Field, len(fields)) + for _, f := range fields { + Default[f.Key] = f + viper.SetDefault(f.Key, f.DefaultValue) + viper.MustBindEnv(f.Key) + } +} + +var Default map[string]*Field diff --git a/internal/config/field.go b/internal/config/field.go new file mode 100644 index 0000000..a9e633a --- /dev/null +++ b/internal/config/field.go @@ -0,0 +1,90 @@ +package config + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "text/template" + + "github.com/charmbracelet/lipgloss" + + "github.com/samber/lo" + "github.com/spf13/viper" + "hub.cybercinch.nz/guisea/go-template/internal/app" + + "hub.cybercinch.nz/guisea/go-template/internal/color" +) + +type Field struct { + Key string + DefaultValue any + Description string +} + +// typeName returns the type of the field without reflection +func (f *Field) typeName() string { + switch f.DefaultValue.(type) { + case string: + return "string" + case int: + return "int" + case bool: + return "bool" + case []string: + return "[]string" + case []int: + return "[]int" + default: + return "unknown" + } +} + +var prettyTemplate = lo.Must(template.New("pretty").Funcs(template.FuncMap{ + "faint": lipgloss.NewStyle().Faint(true).Render, + "bold": lipgloss.NewStyle().Bold(true).Render, + "purple": lipgloss.NewStyle().Foreground(color.Purple).Render, + "blue": lipgloss.NewStyle().Foreground(color.Blue).Render, + "cyan": lipgloss.NewStyle().Foreground(color.Cyan).Render, + "value": func(k string) any { return viper.Get(k) }, + "hl": func(v any) string { + switch value := v.(type) { + case bool: + b := strconv.FormatBool(value) + if value { + return lipgloss.NewStyle().Foreground(color.Green).Render(b) + } + + return lipgloss.NewStyle().Foreground(color.Red).Render(b) + case string: + return lipgloss.NewStyle().Foreground(color.Yellow).Render(value) + default: + return fmt.Sprint(value) + } + }, + "typename": func(v any) string { return reflect.TypeOf(v).String() }, +}).Parse(`{{ faint .Description }} +{{ blue "Key:" }} {{ purple .Key }} +{{ blue "Env:" }} {{ .Env }} +{{ blue "Value:" }} {{ hl (value .Key) }} +{{ blue "Default:" }} {{ hl (.DefaultValue) }} +{{ blue "Type:" }} {{ typename .DefaultValue }}`)) + +func (f *Field) Pretty() string { + var b strings.Builder + + lo.Must0(prettyTemplate.Execute(&b, f)) + + return b.String() +} + +func (f *Field) Env() string { + env := strings.ToUpper(EnvKeyReplacer.Replace(f.Key)) + appPrefix := strings.ToUpper(app.Name + "_") + + if strings.HasPrefix(env, appPrefix) { + return env + } + + return appPrefix + env +} diff --git a/internal/config/init.go b/internal/config/init.go new file mode 100644 index 0000000..4b8b64e --- /dev/null +++ b/internal/config/init.go @@ -0,0 +1,38 @@ +package config + +import ( + "strings" + + "github.com/spf13/viper" + "hub.cybercinch.nz/guisea/go-template/internal/app" + "hub.cybercinch.nz/guisea/go-template/internal/filesystem" + "hub.cybercinch.nz/guisea/go-template/internal/where" +) + +// ConfigFormat is the format of the config file +// Available options are: json, yaml, toml +const ConfigFormat = "yaml" + +var EnvKeyReplacer = strings.NewReplacer(".", "_") + +func Init() error { + viper.SetConfigName(app.Name) + viper.SetConfigType(ConfigFormat) + viper.SetFs(filesystem.Api()) + viper.AddConfigPath(where.Config()) + viper.SetTypeByDefaultValue(true) + viper.SetEnvPrefix(app.Name) + viper.SetEnvKeyReplacer(EnvKeyReplacer) + + setDefaults() + + err := viper.ReadInConfig() + + switch err.(type) { + case viper.ConfigFileNotFoundError: + // Use defaults then + return nil + default: + return err + } +} diff --git a/internal/config/key/keys.go b/internal/config/key/keys.go new file mode 100644 index 0000000..e1ba200 --- /dev/null +++ b/internal/config/key/keys.go @@ -0,0 +1,7 @@ +package key + +const ( + LogsWrite = "logs.write" + LogsLevel = "logs.level" + LogsReportCaller = "logs.show_caller" +) diff --git a/internal/filesystem/api.go b/internal/filesystem/api.go new file mode 100644 index 0000000..f053361 --- /dev/null +++ b/internal/filesystem/api.go @@ -0,0 +1,8 @@ +package filesystem + +import "github.com/spf13/afero" + +// Api returns the filesystem api +func Api() afero.Afero { + return wrapper +} diff --git a/internal/filesystem/init.go b/internal/filesystem/init.go new file mode 100644 index 0000000..446587d --- /dev/null +++ b/internal/filesystem/init.go @@ -0,0 +1,5 @@ +package filesystem + +func init() { + SetOsFs() +} diff --git a/internal/filesystem/set.go b/internal/filesystem/set.go new file mode 100644 index 0000000..620ce3a --- /dev/null +++ b/internal/filesystem/set.go @@ -0,0 +1,18 @@ +package filesystem + +import "github.com/spf13/afero" + +// SetOsFs sets the filesystem to the os filesystem +func SetOsFs() { + if wrapper.Fs == nil || wrapper.Fs.Name() != "os" { + wrapper.Fs = afero.NewOsFs() + } +} + +// SetMemMapFs sets the filesystem to the memory mapped filesystem +// Use this if you want to use the filesystem in a sandbox +func SetMemMapFs() { + if wrapper.Fs == nil || wrapper.Fs.Name() != "memmap" { + wrapper.Fs = afero.NewMemMapFs() + } +} diff --git a/internal/filesystem/wrapper.go b/internal/filesystem/wrapper.go new file mode 100644 index 0000000..e1ecae9 --- /dev/null +++ b/internal/filesystem/wrapper.go @@ -0,0 +1,5 @@ +package filesystem + +import "github.com/spf13/afero" + +var wrapper = afero.Afero{} diff --git a/internal/icon/icon.go b/internal/icon/icon.go new file mode 100644 index 0000000..7f0b62d --- /dev/null +++ b/internal/icon/icon.go @@ -0,0 +1,13 @@ +package icon + +const ( + Cross = "✖" + Check = "✔" + Arrow = "➜" + Info = "ℹ" + Star = "★" + Heart = "♥" + Warn = "⚠" + Gear = "⚙" + Ellipsis = "…" +) diff --git a/internal/logger/init.go b/internal/logger/init.go new file mode 100644 index 0000000..56021f1 --- /dev/null +++ b/internal/logger/init.go @@ -0,0 +1,48 @@ +package logger + +import ( + "errors" + "fmt" + "github.com/charmbracelet/log" + "github.com/spf13/viper" + "hub.cybercinch.nz/guisea/go-template/internal/config/key" + "os" + "path/filepath" + "time" + + "github.com/samber/lo" + "hub.cybercinch.nz/guisea/go-template/internal/filesystem" + "hub.cybercinch.nz/guisea/go-template/internal/where" +) + +func Init() error { + logsPath := where.Logs() + + if logsPath == "" { + return errors.New("logs path is not set") + } + + today := time.Now().Format("2006-01-02") + logFilePath := filepath.Join(logsPath, fmt.Sprintf("%s.log", today)) + if !lo.Must(filesystem.Api().Exists(logFilePath)) { + lo.Must(filesystem.Api().Create(logFilePath)) + } + + logFile, err := filesystem.Api().OpenFile(logFilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + return err + } + + logger := log.NewWithOptions(logFile, log.Options{ + TimeFormat: time.TimeOnly, + ReportTimestamp: true, + ReportCaller: viper.GetBool(key.LogsReportCaller), + }) + + level, _ := log.ParseLevel(key.LogsLevel) + logger.SetLevel(level) + + log.SetDefault(logger) + + return nil +} diff --git a/internal/style/style.go b/internal/style/style.go new file mode 100644 index 0000000..56e466d --- /dev/null +++ b/internal/style/style.go @@ -0,0 +1,12 @@ +package style + +import ( + "github.com/charmbracelet/lipgloss" + "hub.cybercinch.nz/guisea/go-template/internal/color" +) + +var ( + Success = lipgloss.NewStyle().Foreground(color.Green).Render + Failure = lipgloss.NewStyle().Foreground(color.Red).Render + Warning = lipgloss.NewStyle().Foreground(color.Yellow).Render +) diff --git a/internal/util/strings.go b/internal/util/strings.go new file mode 100644 index 0000000..8e12e3d --- /dev/null +++ b/internal/util/strings.go @@ -0,0 +1,11 @@ +package util + +import "strings" + +func Capitalize(s string) string { + if len(s) == 0 { + return s + } + + return strings.ToUpper(string(s[0])) + s[1:] +} diff --git a/internal/where/env.go b/internal/where/env.go new file mode 100644 index 0000000..7ba68a4 --- /dev/null +++ b/internal/where/env.go @@ -0,0 +1,10 @@ +package where + +import ( + "strings" + + "hub.cybercinch.nz/guisea/go-template/internal/app" +) + +// EnvConfigPath is the environment variable name for the config path +var EnvConfigPath = strings.ToUpper(app.Name) + "_CONFIG_PATH" diff --git a/internal/where/util.go b/internal/where/util.go new file mode 100644 index 0000000..853241f --- /dev/null +++ b/internal/where/util.go @@ -0,0 +1,14 @@ +package where + +import ( + "github.com/samber/lo" + "hub.cybercinch.nz/guisea/go-template/internal/filesystem" + "os" +) + +// mkdir creates a directory and all parent directories if they don't exist +// will return the path of the directory +func mkdir(path string) string { + lo.Must0(filesystem.Api().MkdirAll(path, os.ModePerm)) + return path +} diff --git a/internal/where/where.go b/internal/where/where.go new file mode 100644 index 0000000..86ee94f --- /dev/null +++ b/internal/where/where.go @@ -0,0 +1,68 @@ +package where + +import ( + "os" + "path/filepath" + "runtime" + + "hub.cybercinch.nz/guisea/go-template/internal/app" +) + +func home() string { + home, err := os.UserHomeDir() + if err == nil { + return home + } + + return "." +} + +// Config path +// Will create the directory if it doesn't exist +func Config() string { + var path string + + if customDir, present := os.LookupEnv(EnvConfigPath); present { + return mkdir(customDir) + } + + var userConfigDir string + + if runtime.GOOS == "darwin" { + userConfigDir = filepath.Join(home(), ".config") + } else { + var err error + userConfigDir, err = os.UserConfigDir() + if err != nil { + userConfigDir = filepath.Join(home(), ".config") + } + } + + path = filepath.Join(userConfigDir, app.Name) + return mkdir(path) +} + +// Logs path +// Will create the directory if it doesn't exist +func Logs() string { + return mkdir(filepath.Join(Cache(), "logs")) +} + +// Cache path +// Will create the directory if it doesn't exist +func Cache() string { + userCacheDir, err := os.UserCacheDir() + if err != nil { + userCacheDir = "." + } + + cacheDir := filepath.Join(userCacheDir, app.Name) + return mkdir(cacheDir) +} + +// Temp path +// Will create the directory if it doesn't exist +func Temp() string { + tempDir := filepath.Join(os.TempDir(), app.Name) + return mkdir(tempDir) +} diff --git a/justfile b/justfile new file mode 100755 index 0000000..c5cc2b0 --- /dev/null +++ b/justfile @@ -0,0 +1,17 @@ +#!/usr/bin/env just --justfile + +go-mod := `go list` +flags := '-ldflags="-s -w"' + +install: + go install {{flags}} + +build: + @echo -n "Building app ... " + @go build {{flags}} -o bin/ && echo "OK" || echo "FAILED" +update: + go get -u + go mod tidy -v + +rename new-go-mod: + @find . -type f -not -path './.git/*' -exec sed -i '' -e "s|{{go-mod}}|{{new-go-mod}}|g" >/dev/null 2>&1 {} \; \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..1fae8c4 --- /dev/null +++ b/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "github.com/charmbracelet/log" + "github.com/samber/lo" + "hub.cybercinch.nz/guisea/go-template/cmd" + "hub.cybercinch.nz/guisea/go-template/internal/config" + "hub.cybercinch.nz/guisea/go-template/internal/logger" + "os" +) + +func handlePanic() { + if err := recover(); err != nil { + log.Error("crashed", "err", err) + os.Exit(1) + } +} + +func main() { + defer handlePanic() + + // prepare config and logs + lo.Must0(config.Init()) + lo.Must0(logger.Init()) + + // run the app + cmd.Execute() +}