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

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