fix: ZDOTDIR and XDG_CONFIG_HOME config detecting (#51)

From this discussion:

https://www.reddit.com/r/zsh/comments/1v8rq7s/comment/p09b05o/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button

---

Problem: Iris hardcodes config paths to `~/.zshrc` and
`~/.config/fish/config.fish`. This ignores users' custom `ZDOTDIR` or
`XDG_CONFIG_HOME` setups, polluting their `$HOME` directory during init
and failing to scan their actual aliases. Also, ZDOTDIR is often set in
`~/.zshenv` without export, making it invisible to Go's `os.Getenv`

Solution:
- Bash: default `$HOME/.bashrc` so dont mind about it
- For Zsh, Iris now probes zsh directly (`zsh -c "echo $ZDOTDIR"`) with
a timeout to accurately resolve custom paths even if they aint exported
- For Fish, Iris now natively respects the `XDG_CONFIG_HOME` environment
variable
- Shared this robust resolution logic across init, uninstall, and alias
scanning
- Fixed alias scanning to process files in the correct zsh startup order
(`.zshenv` -> `.zprofile` -> `.zshrc`) and correctly merge them
This commit is contained in:
VERSE
2026-07-28 21:46:34 +07:00
committed by GitHub
parent 6f488c02cd
commit ebb0bead7d
3 changed files with 57 additions and 6 deletions
+49 -2
View File
@@ -1,11 +1,14 @@
package shell package shell
import ( import (
"context"
"fmt" "fmt"
"maps" "maps"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
) )
// Adapter defines the behavior for different shell environments // Adapter defines the behavior for different shell environments
@@ -59,7 +62,43 @@ func (z *ZshAdapter) PrepareSelectSequence(selected string) []byte {
return append([]byte{0x15}, []byte(selected)...) return append([]byte{0x15}, []byte(selected)...)
} }
func (z *ZshAdapter) ScanAliases() map[string]string { func (z *ZshAdapter) ScanAliases() map[string]string {
return ScanPosixAliases([]string{".zshrc", ".zshenv", ".zprofile"}) envSet := os.Getenv("ZDOTDIR") != ""
zdotdir := GetZshConfigDir()
home, _ := os.UserHomeDir()
var files []string
if !envSet && zdotdir != home {
files = append(files, filepath.Join(home, ".zshenv"))
}
files = append(files,
filepath.Join(zdotdir, ".zshenv"),
filepath.Join(zdotdir, ".zprofile"),
filepath.Join(zdotdir, ".zshrc"),
)
return ScanPosixAliases(files)
}
func GetZshConfigDir() string {
if zdotdir := os.Getenv("ZDOTDIR"); zdotdir != "" {
return zdotdir
}
// Fallback: ask zsh directly in case ZDOTDIR is set in ~/.zshenv without export
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
cmd := exec.CommandContext(ctx, "zsh", "-c", "echo $ZDOTDIR")
out, err := cmd.Output()
if err == nil {
zdotdir := strings.TrimSpace(string(out))
if zdotdir != "" {
return zdotdir
}
}
home, _ := os.UserHomeDir()
return home
} }
// FishAdapter implementation // FishAdapter implementation
@@ -75,7 +114,15 @@ func (f *FishAdapter) PrepareSelectSequence(selected string) []byte {
} }
func (f *FishAdapter) ScanAliases() map[string]string { func (f *FishAdapter) ScanAliases() map[string]string {
// fish uses 'alias' command in config.fish or separate function files // fish uses 'alias' command in config.fish or separate function files
return ScanPosixAliases([]string{filepath.Join(".config", "fish", "config.fish")}) return ScanPosixAliases([]string{filepath.Join(GetFishConfigDir(), "config.fish")})
}
func GetFishConfigDir() string {
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
return filepath.Join(xdg, "fish")
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config", "fish")
} }
func ScanPosixAliases(files []string) map[string]string { func ScanPosixAliases(files []string) map[string]string {
+3 -2
View File
@@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/versenilvis/iris/integration/shell"
"github.com/versenilvis/iris/internal/config" "github.com/versenilvis/iris/internal/config"
) )
@@ -135,13 +136,13 @@ var setupCmd = &cobra.Command{
switch shellName { switch shellName {
case "zsh": case "zsh":
configFile = filepath.Join(home, ".zshrc") configFile = filepath.Join(shell.GetZshConfigDir(), ".zshrc")
evalCmd = `eval "$(iris init zsh)"` evalCmd = `eval "$(iris init zsh)"`
case "bash": case "bash":
configFile = filepath.Join(home, ".bashrc") configFile = filepath.Join(home, ".bashrc")
evalCmd = `eval "$(iris init bash)"` evalCmd = `eval "$(iris init bash)"`
case "fish": case "fish":
configFile = filepath.Join(home, ".config", "fish", "config.fish") configFile = filepath.Join(shell.GetFishConfigDir(), "config.fish")
evalCmd = `iris init fish | source` evalCmd = `iris init fish | source`
default: default:
fmt.Printf("Unsupported shell: %s. Please add iris init manually.\n", shellName) fmt.Printf("Unsupported shell: %s. Please add iris init manually.\n", shellName)
+5 -2
View File
@@ -8,6 +8,7 @@ import (
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/versenilvis/iris/integration/shell"
"github.com/versenilvis/iris/internal/config" "github.com/versenilvis/iris/internal/config"
) )
@@ -26,10 +27,12 @@ var uninstallCmd = &cobra.Command{
return return
} }
zshrcPath := filepath.Join(shell.GetZshConfigDir(), ".zshrc")
configFiles := []string{ configFiles := []string{
filepath.Join(home, ".zshrc"), zshrcPath,
filepath.Join(home, ".bashrc"), filepath.Join(home, ".bashrc"),
filepath.Join(home, ".config", "fish", "config.fish"), filepath.Join(shell.GetFishConfigDir(), "config.fish"),
} }
for _, file := range configFiles { for _, file := range configFiles {