Detailed changes
@@ -16,6 +16,7 @@ import (
"github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/env"
"github.com/charmbracelet/crush/internal/fsext"
+ "github.com/charmbracelet/crush/internal/home"
"github.com/charmbracelet/crush/internal/log"
)
@@ -584,7 +585,7 @@ func globalConfig() string {
return filepath.Join(localAppData, appName, fmt.Sprintf("%s.json", appName))
}
- return filepath.Join(os.Getenv("HOME"), ".config", appName, fmt.Sprintf("%s.json", appName))
+ return filepath.Join(home.Dir(), ".config", appName, fmt.Sprintf("%s.json", appName))
}
// GlobalConfigData returns the path to the main data directory for the application.
@@ -606,5 +607,5 @@ func GlobalConfigData() string {
return filepath.Join(localAppData, appName, fmt.Sprintf("%s.json", appName))
}
- return filepath.Join(os.Getenv("HOME"), ".local", "share", appName, fmt.Sprintf("%s.json", appName))
+ return filepath.Join(home.Dir(), ".local", "share", appName, fmt.Sprintf("%s.json", appName))
}
@@ -12,6 +12,7 @@ import (
"time"
"github.com/charmbracelet/catwalk/pkg/catwalk"
+ "github.com/charmbracelet/crush/internal/home"
)
type ProviderClient interface {
@@ -41,7 +42,7 @@ func providerCacheFileData() string {
return filepath.Join(localAppData, appName, "providers.json")
}
- return filepath.Join(os.Getenv("HOME"), ".local", "share", appName, "providers.json")
+ return filepath.Join(home.Dir(), ".local", "share", appName, "providers.json")
}
func saveProvidersInCache(path string, providers []catwalk.Provider) error {
@@ -10,6 +10,7 @@ import (
"github.com/bmatcuk/doublestar/v4"
"github.com/charlievieth/fastwalk"
+ "github.com/charmbracelet/crush/internal/home"
ignore "github.com/sabhiram/go-gitignore"
)
@@ -182,12 +183,7 @@ func GlobWithDoubleStar(pattern, searchPath string, limit int) ([]string, bool,
}
func PrettyPath(path string) string {
- // replace home directory with ~
- homeDir, err := os.UserHomeDir()
- if err == nil {
- path = strings.ReplaceAll(path, homeDir, "~")
- }
- return path
+ return home.Short(path)
}
func DirTrim(pwd string, lim int) string {
@@ -1,20 +0,0 @@
-package fsext
-
-import (
- "cmp"
- "os"
- "os/user"
- "sync"
-)
-
-var HomeDir = sync.OnceValue(func() string {
- u, err := user.Current()
- if err == nil {
- return u.HomeDir
- }
- return cmp.Or(
- os.Getenv("HOME"),
- os.Getenv("USERPROFILE"),
- os.Getenv("HOMEPATH"),
- )
-})
@@ -9,6 +9,7 @@ import (
"github.com/charlievieth/fastwalk"
"github.com/charmbracelet/crush/internal/csync"
+ "github.com/charmbracelet/crush/internal/home"
ignore "github.com/sabhiram/go-gitignore"
)
@@ -73,7 +74,7 @@ var commonIgnorePatterns = sync.OnceValue(func() ignore.IgnoreParser {
})
var homeIgnore = sync.OnceValue(func() ignore.IgnoreParser {
- home := HomeDir()
+ home := home.Dir()
var lines []string
for _, name := range []string{
filepath.Join(home, ".gitignore"),
@@ -4,6 +4,8 @@ import (
"errors"
"os"
"path/filepath"
+
+ "github.com/charmbracelet/crush/internal/home"
)
// SearchParent searches for a target file or directory starting from dir
@@ -33,7 +35,7 @@ func SearchParent(dir, target string) (string, bool) {
for {
parent := filepath.Dir(previousParent)
- if parent == previousParent || parent == HomeDir() {
+ if parent == previousParent || parent == home.Dir() {
return "", false
}
@@ -0,0 +1,42 @@
+package home
+
+import (
+ "log/slog"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+)
+
+// Dir returns the users home directory, or if it fails, tries to create a new
+// temporary directory and use that instead.
+var Dir = sync.OnceValue(func() string {
+ home, err := os.UserHomeDir()
+ if err == nil {
+ slog.Debug("user home directory", "home", home)
+ return home
+ }
+ tmp, err := os.MkdirTemp("crush", "")
+ if err != nil {
+ slog.Error("could not find the user home directory")
+ return ""
+ }
+ slog.Warn("could not find the user home directory, using a temporary one", "home", tmp)
+ return tmp
+})
+
+// Short replaces the actual home path from [Dir] with `~`.
+func Short(p string) string {
+ if !strings.HasPrefix(p, Dir()) || Dir() == "" {
+ return p
+ }
+ return filepath.Join("~", strings.TrimPrefix(p, Dir()))
+}
+
+// Long replaces the `~` with actual home path from [Dir].
+func Long(p string) string {
+ if !strings.HasPrefix(p, "~") || Dir() == "" {
+ return p
+ }
+ return strings.Replace(p, "~", Dir(), 1)
+}
@@ -0,0 +1,26 @@
+package home
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestDir(t *testing.T) {
+ require.NotEmpty(t, Dir())
+}
+
+func TestShort(t *testing.T) {
+ d := filepath.Join(Dir(), "documents", "file.txt")
+ require.Equal(t, filepath.FromSlash("~/documents/file.txt"), Short(d))
+ ad := filepath.FromSlash("/absolute/path/file.txt")
+ require.Equal(t, ad, Short(ad))
+}
+
+func TestLong(t *testing.T) {
+ d := filepath.FromSlash("~/documents/file.txt")
+ require.Equal(t, filepath.Join(Dir(), "documents", "file.txt"), Long(d))
+ ad := filepath.FromSlash("/absolute/path/file.txt")
+ require.Equal(t, ad, Long(ad))
+}
@@ -9,6 +9,7 @@ import (
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/csync"
"github.com/charmbracelet/crush/internal/env"
+ "github.com/charmbracelet/crush/internal/home"
)
type PromptID string
@@ -44,18 +45,7 @@ func getContextFromPaths(workingDir string, contextPaths []string) string {
// expandPath expands ~ and environment variables in file paths
func expandPath(path string) string {
- // Handle tilde expansion
- if strings.HasPrefix(path, "~/") {
- homeDir, err := os.UserHomeDir()
- if err == nil {
- path = filepath.Join(homeDir, path[2:])
- }
- } else if path == "~" {
- homeDir, err := os.UserHomeDir()
- if err == nil {
- path = homeDir
- }
- }
+ path = home.Long(path)
// Handle environment variable expansion using the same pattern as config
if strings.HasPrefix(path, "$") {
@@ -2,10 +2,10 @@ package prompt
import (
"os"
- "path/filepath"
- "runtime"
"strings"
"testing"
+
+ "github.com/charmbracelet/crush/internal/home"
)
func TestExpandPath(t *testing.T) {
@@ -25,16 +25,14 @@ func TestExpandPath(t *testing.T) {
name: "tilde expansion",
input: "~/documents",
expected: func() string {
- home, _ := os.UserHomeDir()
- return filepath.Join(home, "documents")
+ return home.Dir() + "/documents"
},
},
{
name: "tilde only",
input: "~",
expected: func() string {
- home, _ := os.UserHomeDir()
- return home
+ return home.Dir()
},
},
{
@@ -69,55 +67,3 @@ func TestExpandPath(t *testing.T) {
})
}
}
-
-func TestProcessContextPaths(t *testing.T) {
- // Create a temporary directory and file for testing
- tmpDir := t.TempDir()
- testFile := filepath.Join(tmpDir, "test.txt")
- testContent := "test content"
-
- err := os.WriteFile(testFile, []byte(testContent), 0o644)
- if err != nil {
- t.Fatalf("Failed to create test file: %v", err)
- }
-
- // Test with absolute path to file
- result := processContextPaths("", []string{testFile})
- expected := "# From:" + testFile + "\n" + testContent
-
- if result != expected {
- t.Errorf("processContextPaths with absolute path failed.\nGot: %q\nWant: %q", result, expected)
- }
-
- // Test with directory path (should process all files in directory)
- result = processContextPaths("", []string{tmpDir})
- if !strings.Contains(result, testContent) {
- t.Errorf("processContextPaths with directory path failed to include file content")
- }
-
- // Test with tilde expansion (if we can create a file in home directory)
- tmpDir = t.TempDir()
- setHomeEnv(t, tmpDir)
- homeTestFile := filepath.Join(tmpDir, "crush_test_file.txt")
- err = os.WriteFile(homeTestFile, []byte(testContent), 0o644)
- if err == nil {
- defer os.Remove(homeTestFile) // Clean up
-
- tildeFile := "~/crush_test_file.txt"
- result = processContextPaths("", []string{tildeFile})
- expected = "# From:" + homeTestFile + "\n" + testContent
-
- if result != expected {
- t.Errorf("processContextPaths with tilde expansion failed.\nGot: %q\nWant: %q", result, expected)
- }
- }
-}
-
-func setHomeEnv(tb testing.TB, path string) {
- tb.Helper()
- key := "HOME"
- if runtime.GOOS == "windows" {
- key = "USERPROFILE"
- }
- tb.Setenv(key, path)
-}
@@ -3,7 +3,6 @@ package sidebar
import (
"context"
"fmt"
- "os"
"slices"
"strings"
@@ -14,6 +13,7 @@ import (
"github.com/charmbracelet/crush/internal/diff"
"github.com/charmbracelet/crush/internal/fsext"
"github.com/charmbracelet/crush/internal/history"
+ "github.com/charmbracelet/crush/internal/home"
"github.com/charmbracelet/crush/internal/lsp"
"github.com/charmbracelet/crush/internal/pubsub"
"github.com/charmbracelet/crush/internal/session"
@@ -609,11 +609,5 @@ func (m *sidebarCmp) SetCompactMode(compact bool) {
func cwd() string {
cwd := config.Get().WorkingDir()
t := styles.CurrentTheme()
- // Replace home directory with ~, unless we're at the top level of the
- // home directory).
- homeDir, err := os.UserHomeDir()
- if err == nil && cwd != homeDir {
- cwd = strings.ReplaceAll(cwd, homeDir, "~")
- }
- return t.S().Muted.Render(cwd)
+ return t.S().Muted.Render(home.Short(cwd))
}
@@ -2,7 +2,6 @@ package splash
import (
"fmt"
- "os"
"strings"
"time"
@@ -11,6 +10,7 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/catwalk/pkg/catwalk"
"github.com/charmbracelet/crush/internal/config"
+ "github.com/charmbracelet/crush/internal/home"
"github.com/charmbracelet/crush/internal/llm/prompt"
"github.com/charmbracelet/crush/internal/tui/components/chat"
"github.com/charmbracelet/crush/internal/tui/components/core"
@@ -648,12 +648,7 @@ func (s *splashCmp) cwdPart() string {
}
func (s *splashCmp) cwd() string {
- cwd := config.Get().WorkingDir()
- homeDir, err := os.UserHomeDir()
- if err == nil && cwd != homeDir {
- cwd = strings.ReplaceAll(cwd, homeDir, "~")
- }
- return cwd
+ return home.Short(config.Get().WorkingDir())
}
func LSPList(maxWidth int) []string {
@@ -10,6 +10,7 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/crush/internal/config"
+ "github.com/charmbracelet/crush/internal/home"
"github.com/charmbracelet/crush/internal/tui/util"
)
@@ -54,7 +55,7 @@ func buildCommandSources(cfg *config.Config) []commandSource {
}
// Home directory
- if home, err := os.UserHomeDir(); err == nil {
+ if home := home.Dir(); home != "" {
sources = append(sources, commandSource{
path: filepath.Join(home, ".crush", "commands"),
prefix: UserCommandPrefix,
@@ -73,7 +74,7 @@ func buildCommandSources(cfg *config.Config) []commandSource {
func getXDGCommandsDir() string {
xdgHome := os.Getenv("XDG_CONFIG_HOME")
if xdgHome == "" {
- if home, err := os.UserHomeDir(); err == nil {
+ if home := home.Dir(); home != "" {
xdgHome = filepath.Join(home, ".config")
}
}
@@ -11,6 +11,7 @@ import (
"github.com/charmbracelet/bubbles/v2/help"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
+ "github.com/charmbracelet/crush/internal/home"
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/tui/components/core"
"github.com/charmbracelet/crush/internal/tui/components/dialogs"
@@ -60,7 +61,7 @@ func NewFilePickerCmp(workingDir string) FilePicker {
if cwd, err := os.Getwd(); err == nil {
fp.CurrentDirectory = cwd
} else {
- fp.CurrentDirectory, _ = os.UserHomeDir()
+ fp.CurrentDirectory = home.Dir()
}
}
@@ -106,8 +107,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
if key.Matches(msg, m.filePicker.KeyMap.Back) {
// make sure we don't go back if we are at the home directory
- homeDir, _ := os.UserHomeDir()
- if m.filePicker.CurrentDirectory == homeDir {
+ if m.filePicker.CurrentDirectory == home.Dir() {
return m, nil
}
}
@@ -2,13 +2,12 @@ package models
import (
"fmt"
- "strings"
"github.com/charmbracelet/bubbles/v2/spinner"
"github.com/charmbracelet/bubbles/v2/textinput"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/crush/internal/config"
- "github.com/charmbracelet/crush/internal/fsext"
+ "github.com/charmbracelet/crush/internal/home"
"github.com/charmbracelet/crush/internal/tui/styles"
"github.com/charmbracelet/lipgloss/v2"
)
@@ -145,7 +144,7 @@ func (a *APIKeyInput) View() string {
inputView := a.input.View()
dataPath := config.GlobalConfigData()
- dataPath = strings.Replace(dataPath, fsext.HomeDir(), "~", 1)
+ dataPath = home.Short(dataPath)
helpText := styles.CurrentTheme().S().Muted.
Render(fmt.Sprintf("This will be written to the global configuration: %s", dataPath))