From ce08b2204512203b05cf0451fe64151ae5dc8fdc Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Thu, 14 May 2026 13:24:15 -0400 Subject: [PATCH] feat(dirs): add some styling to the dirs command --- internal/cmd/dirs.go | 129 ++++++++++++++++---------------------- internal/cmd/dirs_test.go | 48 -------------- 2 files changed, 55 insertions(+), 122 deletions(-) delete mode 100644 internal/cmd/dirs_test.go diff --git a/internal/cmd/dirs.go b/internal/cmd/dirs.go index 42fa20b3f931493664b7d9d26d7496e0fb28f10e..7c04b942384ef172dd72d9b1cf42d1fab38dbf92 100644 --- a/internal/cmd/dirs.go +++ b/internal/cmd/dirs.go @@ -1,110 +1,91 @@ package cmd import ( - "fmt" "os" "path/filepath" "strings" "charm.land/lipgloss/v2" - "charm.land/lipgloss/v2/table" "github.com/charmbracelet/crush/internal/config" + "github.com/charmbracelet/x/exp/charmtone" "github.com/charmbracelet/x/term" "github.com/spf13/cobra" ) var dirsCmd = &cobra.Command{ Use: "dirs", - Short: "Print directories used by Crush", - Long: `Print the directories where Crush stores its configuration and data files. -This includes the global configuration directory and data directory.`, + Short: "Show config and data directories", + Long: `Show where Crush stores its configuration and data, +including any project-level config files discovered +from the current directory up to the project root.`, Example: ` -# Print all global directories +# Show all directories crush dirs - -# Print data directory and all project specific config directories -crush dirs -p - -# Print only global config directory -crush dirs config - -# Print only project specific config directories -crush dirs -p config - -# Print only the data directory -crush dirs data `, - RunE: func(cmd *cobra.Command, args []string) error { - dirs, err := configDirs(cmd) - if err != nil { - return fmt.Errorf("cannot collect config directories: %w", err) - } - + Run: func(cmd *cobra.Command, args []string) { + entries := collectDirs(cmd) if term.IsTerminal(os.Stdout.Fd()) { - // We're in a TTY: make it fancy. - t := table.New(). - Border(lipgloss.RoundedBorder()). - StyleFunc(func(row, col int) lipgloss.Style { - return lipgloss.NewStyle().Padding(0, 2) - }). - Row("Config", dirs). - Row("Data", filepath.Dir(config.GlobalConfigData())) - lipgloss.Println(t) - return nil + printDirs(cmd, entries) + return } - // Not a TTY. - cmd.Println(dirs) - cmd.Println(filepath.Dir(config.GlobalConfigData())) - - return nil - }, -} - -var configDirCmd = &cobra.Command{ - Use: "config", - Short: "Print the configuration directory used by Crush", - RunE: func(cmd *cobra.Command, args []string) error { - dirs, err := configDirs(cmd) - if err != nil { - return fmt.Errorf("cannot collect config directories: %w", err) + for _, e := range entries { + cmd.Println(e) } - cmd.Println(dirs) - return nil }, } -var dataDirCmd = &cobra.Command{ - Use: "data", - Short: "Print the datauration directory used by Crush", - Run: func(cmd *cobra.Command, args []string) { - cmd.Println(filepath.Dir(config.GlobalConfigData())) - }, -} +func collectDirs(cmd *cobra.Command) []string { + var dirs []string -// configDirs returns formatted string with one or more project config paths -func configDirs(cmd *cobra.Command) (string, error) { - configDir := filepath.Dir(config.GlobalConfig()) - if ok, _ := cmd.Flags().GetBool("project"); !ok { - return configDir, nil - } + dirs = append(dirs, filepath.Dir(config.GlobalConfig())) + dirs = append(dirs, filepath.Dir(config.GlobalConfigData())) cwd, err := ResolveCwd(cmd) if err != nil { - return "", fmt.Errorf("cannot resolve current working directory: %w", err) + return dirs } - var sb strings.Builder - for i, path := range config.ProjectConfigs(cwd) { - if i > 0 { - sb.WriteByte('\n') + for _, p := range config.ProjectConfigs(cwd) { + d := filepath.Dir(p) + // Skip global paths, already shown. + if d == filepath.Dir(config.GlobalConfig()) || d == filepath.Dir(config.GlobalConfigData()) { + continue } - sb.WriteString(filepath.Dir(path)) + dirs = append(dirs, d) } - return sb.String(), nil + + return dirs } -func init() { - dirsCmd.PersistentFlags().BoolP("project", "p", false, "Print project specific configs") +func printDirs(cmd *cobra.Command, dirs []string) { + labelStyle := lipgloss.NewStyle().Bold(true).Foreground(charmtone.Charple) + + labels := make([]string, len(dirs)) + longest := 0 + for i := range dirs { + l := dirLabel(i) + labels[i] = l + ":" + if len(labels[i]) > longest { + longest = len(labels[i]) + } + } + + for i, d := range dirs { + lipgloss.Println(labelStyle.Render(labels[i]) + + strings.Repeat(" ", longest-len(labels[i])) + + " " + d) + } + + lipgloss.Println(lipgloss.NewStyle().Foreground(charmtone.Squid).Render("Configs merge from top to bottom")) +} - dirsCmd.AddCommand(configDirCmd, dataDirCmd) +func dirLabel(i int) string { + switch i { + case 0: + return "Config" + case 1: + return "Data" + default: + return "Project" + } } diff --git a/internal/cmd/dirs_test.go b/internal/cmd/dirs_test.go deleted file mode 100644 index 222e833f87b88fb859f54b7f5c4953b58423afaa..0000000000000000000000000000000000000000 --- a/internal/cmd/dirs_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package cmd - -import ( - "bytes" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" -) - -func init() { - os.Setenv("XDG_CONFIG_HOME", "/tmp/fakeconfig") - os.Setenv("XDG_DATA_HOME", "/tmp/fakedata") - os.Unsetenv("CRUSH_GLOBAL_CONFIG") - os.Unsetenv("CRUSH_GLOBAL_DATA") -} - -func TestDirs(t *testing.T) { - var b bytes.Buffer - dirsCmd.SetOut(&b) - dirsCmd.SetErr(&b) - dirsCmd.SetIn(bytes.NewReader(nil)) - dirsCmd.Run(dirsCmd, nil) - expected := filepath.FromSlash("/tmp/fakeconfig/crush") + "\n" + - filepath.FromSlash("/tmp/fakedata/crush") + "\n" - require.Equal(t, expected, b.String()) -} - -func TestConfigDir(t *testing.T) { - var b bytes.Buffer - configDirCmd.SetOut(&b) - configDirCmd.SetErr(&b) - configDirCmd.SetIn(bytes.NewReader(nil)) - configDirCmd.Run(configDirCmd, nil) - expected := filepath.FromSlash("/tmp/fakeconfig/crush") + "\n" - require.Equal(t, expected, b.String()) -} - -func TestDataDir(t *testing.T) { - var b bytes.Buffer - dataDirCmd.SetOut(&b) - dataDirCmd.SetErr(&b) - dataDirCmd.SetIn(bytes.NewReader(nil)) - dataDirCmd.Run(dataDirCmd, nil) - expected := filepath.FromSlash("/tmp/fakedata/crush") + "\n" - require.Equal(t, expected, b.String()) -}