diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index 7fd58685..3f5d27a7 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -5,6 +5,7 @@ title: Changelog ## [Unreleased](https://github.com/lets-cli/lets/releases/tag/v0.0.X) +* `[Added]` Add `lets self config path` and `lets self config edit` for user settings. Issue [#370](https://github.com/lets-cli/lets/issues/370) * `[Added]` Show interactive download progress for remote configs and remote mixins. Issue [#360](https://github.com/lets-cli/lets/issues/360) * `[Fixed]` Make `--no-cache` re-download remote mixins for local and remote configs. Issue [#365](https://github.com/lets-cli/lets/issues/365) * `[Added]` Remote config support: `lets -c https://url` downloads and caches config to `~/.config/lets/remote-configs/`. Use `--no-cache` to force re-download. diff --git a/docs/docs/settings.md b/docs/docs/settings.md index 02c281f7..98dc7297 100644 --- a/docs/docs/settings.md +++ b/docs/docs/settings.md @@ -17,6 +17,18 @@ Use settings for things like colored output, theming, or update notifications. D This file is per-user and applies to all projects on the machine. +Print the path: + +```bash +lets self config path +``` + +Open the file in `$EDITOR`: + +```bash +lets self config edit +``` + ## Precedence Settings are resolved in this order: diff --git a/internal/cmd/root_test.go b/internal/cmd/root_test.go index c3ac84f5..c0bee6e4 100644 --- a/internal/cmd/root_test.go +++ b/internal/cmd/root_test.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "errors" + "os" + "path/filepath" "strings" "testing" @@ -218,7 +220,7 @@ func TestSelfCmd(t *testing.T) { t.Run("should use help func when run without args", func(t *testing.T) { rootCmd := CreateRootCommand("v0.0.0-test", "") rootCmd.SetArgs([]string{"self"}) - initSelfCmd(rootCmd, "v0.0.0-test", func(string) error { return nil }) + initSelfCmd(rootCmd, "v0.0.0-test", func(string) error { return nil }, func(string) error { return nil }) called := false rootCmd.SetHelpFunc(func(c *cobra.Command, args []string) { @@ -236,6 +238,68 @@ func TestSelfCmd(t *testing.T) { } }) + t.Run("should print user config path", func(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + bufOut := new(bytes.Buffer) + + rootCmd := CreateRootCommand("v0.0.0-test", "") + rootCmd.SetArgs([]string{"self", "config", "path"}) + rootCmd.SetOut(bufOut) + rootCmd.SetErr(bufOut) + initSelfCmd(rootCmd, "v0.0.0-test", func(string) error { return nil }, func(string) error { return nil }) + + if err := rootCmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expected := filepath.Join(home, ".config", "lets", "config.yaml") + "\n" + if bufOut.String() != expected { + t.Fatalf("expected %q, got %q", expected, bufOut.String()) + } + }) + + t.Run("should open user config in editor", func(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + bufOut := new(bytes.Buffer) + called := false + gotPath := "" + + rootCmd := CreateRootCommand("v0.0.0-test", "") + rootCmd.SetArgs([]string{"self", "config", "edit"}) + rootCmd.SetOut(bufOut) + rootCmd.SetErr(bufOut) + initSelfCmd(rootCmd, "v0.0.0-test", func(string) error { return nil }, func(path string) error { + called = true + gotPath = path + + return nil + }) + + if err := rootCmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expected := filepath.Join(home, ".config", "lets", "config.yaml") + if !called { + t.Fatal("expected editor to be called") + } + + if gotPath != expected { + t.Fatalf("expected editor path %q, got %q", expected, gotPath) + } + + info, err := os.Stat(filepath.Dir(expected)) + if err != nil { + t.Fatalf("expected config directory to exist: %v", err) + } + + if perm := info.Mode().Perm(); perm != 0o700 { + t.Fatalf("expected config directory permissions 0700, got %o", perm) + } + }) + t.Run("should open documentation in browser", func(t *testing.T) { bufOut := new(bytes.Buffer) called := false @@ -252,7 +316,7 @@ func TestSelfCmd(t *testing.T) { rootCmd.SetArgs([]string{"self", "doc"}) rootCmd.SetOut(bufOut) rootCmd.SetErr(bufOut) - initSelfCmd(rootCmd, "v0.0.0-test", openURL) + initSelfCmd(rootCmd, "v0.0.0-test", openURL, func(string) error { return nil }) err := rootCmd.Execute() if err != nil { @@ -279,7 +343,7 @@ func TestSelfCmd(t *testing.T) { rootCmd.SetArgs([]string{"self", "doc"}) rootCmd.SetOut(bufOut) rootCmd.SetErr(bufOut) - initSelfCmd(rootCmd, "v0.0.0-test", openURL) + initSelfCmd(rootCmd, "v0.0.0-test", openURL, func(string) error { return nil }) err := rootCmd.Execute() if err == nil { diff --git a/internal/cmd/self.go b/internal/cmd/self.go index 9b0d2398..6bf4c1f9 100644 --- a/internal/cmd/self.go +++ b/internal/cmd/self.go @@ -7,10 +7,12 @@ import ( // InitSelfCmd intializes root 'self' subcommand. func InitSelfCmd(rootCmd *cobra.Command, version string) { - initSelfCmd(rootCmd, version, util.OpenURL) + initSelfCmd(rootCmd, version, util.OpenURL, util.OpenEditor) } -func initSelfCmd(rootCmd *cobra.Command, version string, openURL func(string) error) { +func initSelfCmd( + rootCmd *cobra.Command, version string, openURL func(string) error, openEditor func(string) error, +) { selfCmd := &cobra.Command{ Use: "self", Hidden: false, @@ -24,6 +26,7 @@ func initSelfCmd(rootCmd *cobra.Command, version string, openURL func(string) er rootCmd.AddCommand(selfCmd) + selfCmd.AddCommand(initConfigCommand(openEditor)) selfCmd.AddCommand(initDocCommand(openURL)) selfCmd.AddCommand(initLspCommand(version)) selfCmd.AddCommand(initSkillsCommand()) diff --git a/internal/cmd/self_config.go b/internal/cmd/self_config.go new file mode 100644 index 00000000..f3b011bc --- /dev/null +++ b/internal/cmd/self_config.go @@ -0,0 +1,73 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/lets-cli/lets/internal/util" + "github.com/spf13/cobra" +) + +func initConfigCommand(openEditor func(string) error) *cobra.Command { + configCmd := &cobra.Command{ + Use: "config ", + Short: "Manage lets user config", + Long: strings.TrimSpace(`Manage the per-user lets settings file. + +The user config is stored at ~/.config/lets/config.yaml and applies to all +projects on the machine.`), + Args: validateCommandArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } + + configCmd.AddCommand(initConfigPathCommand()) + configCmd.AddCommand(initConfigEditCommand(openEditor)) + + return configCmd +} + +func initConfigPathCommand() *cobra.Command { + return &cobra.Command{ + Use: "path", + Short: "Print lets user config path", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + path, err := util.LetsUserFile("config.yaml") + if err != nil { + return err + } + + _, err = fmt.Fprintln(cmd.OutOrStdout(), path) + + return err + }, + } +} + +func initConfigEditCommand(openEditor func(string) error) *cobra.Command { + return &cobra.Command{ + Use: "edit", + Short: "Open lets user config in EDITOR", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + path, err := util.LetsUserFile("config.yaml") + if err != nil { + return err + } + + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return fmt.Errorf("creating config directory: %w", err) + } + + if err := openEditor(path); err != nil { + return fmt.Errorf("can not open config: %w", err) + } + + return nil + }, + } +} diff --git a/internal/util/editor.go b/internal/util/editor.go new file mode 100644 index 00000000..11199d88 --- /dev/null +++ b/internal/util/editor.go @@ -0,0 +1,26 @@ +package util + +import ( + "errors" + "fmt" + "os" + "os/exec" +) + +func OpenEditor(path string) error { + editor := os.Getenv("EDITOR") + if editor == "" { + return errors.New("EDITOR is not set") + } + + cmd := exec.Command(editor, path) //nolint:gosec + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("run %s: %w", editor, err) + } + + return nil +}