on main help sign in
feat(dirs): add some styling to the dirs command
commitce08b2204512203b05cf0451fe64151ae5dc8fdc authorKieran Klukas <kieran@dunkirk.sh> dateThu 14 May 2026, 13:24 (5 months ago) parent5331fe2
2 files changed+55−122
internal/cmd/dirs.go+55−74 internal/cmd/dirs_test.go−48
internal/cmd/dirs.go+55−74
@@ -1,110 +1,91 @@
11package cmd
22
33import (
4− "fmt"
54 "os"
65 "path/filepath"
76 "strings"
87
98 "charm.land/lipgloss/v2"
10− "charm.land/lipgloss/v2/table"
119 "github.com/charmbracelet/crush/internal/config"
10+ "github.com/charmbracelet/x/exp/charmtone"
1211 "github.com/charmbracelet/x/term"
1312 "github.com/spf13/cobra"
1413)
1514
1615var dirsCmd = &cobra.Command{
1716 Use: "dirs",
18− Short: "Print directories used by Crush",
19− Long: `Print the directories where Crush stores its configuration and data files.
20−This includes the global configuration directory and data directory.`,
17+ Short: "Show config and data directories",
18+ Long: `Show where Crush stores its configuration and data,
19+including any project-level config files discovered
20+from the current directory up to the project root.`,
2121 Example: `
22−# Print all global directories
22+# Show all directories
2323crush dirs
24−
25−# Print data directory and all project specific config directories
26−crush dirs -p
27−
28−# Print only global config directory
29−crush dirs config
30−
31−# Print only project specific config directories
32−crush dirs -p config
33−
34−# Print only the data directory
35−crush dirs data
3624 `,
37− RunE: func(cmd *cobra.Command, args []string) error {
38− dirs, err := configDirs(cmd)
39− if err != nil {
40− return fmt.Errorf("cannot collect config directories: %w", err)
41− }
42−
25+ Run: func(cmd *cobra.Command, args []string) {
26+ entries := collectDirs(cmd)
4327 if term.IsTerminal(os.Stdout.Fd()) {
44− // We're in a TTY: make it fancy.
45− t := table.New().
46− Border(lipgloss.RoundedBorder()).
47− StyleFunc(func(row, col int) lipgloss.Style {
48− return lipgloss.NewStyle().Padding(0, 2)
49− }).
50− Row("Config", dirs).
51− Row("Data", filepath.Dir(config.GlobalConfigData()))
52− lipgloss.Println(t)
53− return nil
28+ printDirs(cmd, entries)
29+ return
5430 }
55− // Not a TTY.
56− cmd.Println(dirs)
57− cmd.Println(filepath.Dir(config.GlobalConfigData()))
58−
59− return nil
60− },
61−}
62−
63−var configDirCmd = &cobra.Command{
64− Use: "config",
65− Short: "Print the configuration directory used by Crush",
66− RunE: func(cmd *cobra.Command, args []string) error {
67− dirs, err := configDirs(cmd)
68− if err != nil {
69− return fmt.Errorf("cannot collect config directories: %w", err)
31+ for _, e := range entries {
32+ cmd.Println(e)
7033 }
71− cmd.Println(dirs)
72− return nil
7334 },
7435}
7536
76−var dataDirCmd = &cobra.Command{
77− Use: "data",
78− Short: "Print the datauration directory used by Crush",
79− Run: func(cmd *cobra.Command, args []string) {
80− cmd.Println(filepath.Dir(config.GlobalConfigData()))
81− },
82−}
37+func collectDirs(cmd *cobra.Command) []string {
38+ var dirs []string
8339
84−// configDirs returns formatted string with one or more project config paths
85−func configDirs(cmd *cobra.Command) (string, error) {
86− configDir := filepath.Dir(config.GlobalConfig())
87− if ok, _ := cmd.Flags().GetBool("project"); !ok {
88− return configDir, nil
89− }
40+ dirs = append(dirs, filepath.Dir(config.GlobalConfig()))
41+ dirs = append(dirs, filepath.Dir(config.GlobalConfigData()))
9042
9143 cwd, err := ResolveCwd(cmd)
9244 if err != nil {
93− return "", fmt.Errorf("cannot resolve current working directory: %w", err)
45+ return dirs
9446 }
9547
96− var sb strings.Builder
97− for i, path := range config.ProjectConfigs(cwd) {
98− if i > 0 {
99− sb.WriteByte('\n')
48+ for _, p := range config.ProjectConfigs(cwd) {
49+ d := filepath.Dir(p)
50+ // Skip global paths, already shown.
51+ if d == filepath.Dir(config.GlobalConfig()) || d == filepath.Dir(config.GlobalConfigData()) {
52+ continue
10053 }
101− sb.WriteString(filepath.Dir(path))
54+ dirs = append(dirs, d)
10255 }
103− return sb.String(), nil
56+
57+ return dirs
10458}
10559
106−func init() {
107− dirsCmd.PersistentFlags().BoolP("project", "p", false, "Print project specific configs")
60+func printDirs(cmd *cobra.Command, dirs []string) {
61+ labelStyle := lipgloss.NewStyle().Bold(true).Foreground(charmtone.Charple)
62+
63+ labels := make([]string, len(dirs))
64+ longest := 0
65+ for i := range dirs {
66+ l := dirLabel(i)
67+ labels[i] = l + ":"
68+ if len(labels[i]) > longest {
69+ longest = len(labels[i])
70+ }
71+ }
72+
73+ for i, d := range dirs {
74+ lipgloss.Println(labelStyle.Render(labels[i]) +
75+ strings.Repeat(" ", longest-len(labels[i])) +
76+ " " + d)
77+ }
78+
79+ lipgloss.Println(lipgloss.NewStyle().Foreground(charmtone.Squid).Render("Configs merge from top to bottom"))
80+}
10881
109− dirsCmd.AddCommand(configDirCmd, dataDirCmd)
82+func dirLabel(i int) string {
83+ switch i {
84+ case 0:
85+ return "Config"
86+ case 1:
87+ return "Data"
88+ default:
89+ return "Project"
90+ }
11091}
internal/cmd/dirs_test.godeleted−48
@@ -1,48 +0,0 @@
1−package cmd
2−
3−import (
4− "bytes"
5− "os"
6− "path/filepath"
7− "testing"
8−
9− "github.com/stretchr/testify/require"
10−)
11−
12−func init() {
13− os.Setenv("XDG_CONFIG_HOME", "/tmp/fakeconfig")
14− os.Setenv("XDG_DATA_HOME", "/tmp/fakedata")
15− os.Unsetenv("CRUSH_GLOBAL_CONFIG")
16− os.Unsetenv("CRUSH_GLOBAL_DATA")
17−}
18−
19−func TestDirs(t *testing.T) {
20− var b bytes.Buffer
21− dirsCmd.SetOut(&b)
22− dirsCmd.SetErr(&b)
23− dirsCmd.SetIn(bytes.NewReader(nil))
24− dirsCmd.Run(dirsCmd, nil)
25− expected := filepath.FromSlash("/tmp/fakeconfig/crush") + "\n" +
26− filepath.FromSlash("/tmp/fakedata/crush") + "\n"
27− require.Equal(t, expected, b.String())
28−}
29−
30−func TestConfigDir(t *testing.T) {
31− var b bytes.Buffer
32− configDirCmd.SetOut(&b)
33− configDirCmd.SetErr(&b)
34− configDirCmd.SetIn(bytes.NewReader(nil))
35− configDirCmd.Run(configDirCmd, nil)
36− expected := filepath.FromSlash("/tmp/fakeconfig/crush") + "\n"
37− require.Equal(t, expected, b.String())
38−}
39−
40−func TestDataDir(t *testing.T) {
41− var b bytes.Buffer
42− dataDirCmd.SetOut(&b)
43− dataDirCmd.SetErr(&b)
44− dataDirCmd.SetIn(bytes.NewReader(nil))
45− dataDirCmd.Run(dataDirCmd, nil)
46− expected := filepath.FromSlash("/tmp/fakedata/crush") + "\n"
47− require.Equal(t, expected, b.String())
48−}