feat(dirs): add some styling to the dirs command

Kieran Klukas created

Change summary

internal/cmd/dirs.go      | 129 +++++++++++++++++-----------------------
internal/cmd/dirs_test.go |  48 ---------------
2 files changed, 55 insertions(+), 122 deletions(-)

Detailed changes

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"
+	}
 }

internal/cmd/dirs_test.go 🔗

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