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

28
.github/workflows/build.yml vendored Normal file
View File

@@ -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}}

115
.gitignore vendored Normal file
View File

@@ -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

25
.goreleaser.yaml Normal file
View File

@@ -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'

13
.semrelrc Normal file
View File

@@ -0,0 +1,13 @@
{
"plugins": {
"provider": {
"name": "gitea"
},
"changelog-generator": {
"name": "default",
"options": {
"emojis": "true"
}
}
}
}

85
README.md Normal file
View File

@@ -0,0 +1,85 @@
# Go CLI Project Template ☄️
Powerful template for Go CLI applications with advanced config management
<img width="912" alt="Screenshot 2022-10-05 at 11 14 32" src="https://user-images.githubusercontent.com/62389790/194013247-897697ee-4b32-4b5d-9667-462fcc45e161.png">
## 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
<img width="203" alt="Screenshot 2022-09-30 at 13 37 30" src="https://user-images.githubusercontent.com/62389790/193252456-42b966a7-2679-4868-bf25-d862524733ee.png">
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

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()
}
}
},
}

48
go.mod Normal file
View File

@@ -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
)

116
go.sum Normal file
View File

@@ -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=

15
internal/app/meta.go Normal file
View File

@@ -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
)

25
internal/color/color.go Normal file
View File

@@ -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")
)

View File

@@ -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

90
internal/config/field.go Normal file
View File

@@ -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
}

38
internal/config/init.go Normal file
View File

@@ -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
}
}

View File

@@ -0,0 +1,7 @@
package key
const (
LogsWrite = "logs.write"
LogsLevel = "logs.level"
LogsReportCaller = "logs.show_caller"
)

View File

@@ -0,0 +1,8 @@
package filesystem
import "github.com/spf13/afero"
// Api returns the filesystem api
func Api() afero.Afero {
return wrapper
}

View File

@@ -0,0 +1,5 @@
package filesystem
func init() {
SetOsFs()
}

View File

@@ -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()
}
}

View File

@@ -0,0 +1,5 @@
package filesystem
import "github.com/spf13/afero"
var wrapper = afero.Afero{}

13
internal/icon/icon.go Normal file
View File

@@ -0,0 +1,13 @@
package icon
const (
Cross = "✖"
Check = "✔"
Arrow = "➜"
Info = ""
Star = "★"
Heart = "♥"
Warn = "⚠"
Gear = "⚙"
Ellipsis = "…"
)

48
internal/logger/init.go Normal file
View File

@@ -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
}

12
internal/style/style.go Normal file
View File

@@ -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
)

11
internal/util/strings.go Normal file
View File

@@ -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:]
}

10
internal/where/env.go Normal file
View File

@@ -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"

14
internal/where/util.go Normal file
View File

@@ -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
}

68
internal/where/where.go Normal file
View File

@@ -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)
}

17
justfile Executable file
View File

@@ -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 {} \;

28
main.go Normal file
View File

@@ -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()
}