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