make prompt colors configurable via GOSH_COLORS

- GOSH_COLORS='user=..:root=..:path=..' with ANSI SGR codes,
  empty value disables the color for that part
- only digits and ';' are accepted, invalid entries are ignored
- read before every prompt, so 'set GOSH_COLORS=...' takes effect immediately
- document in help and README, including example color schemes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Florian WaltherandClaude Opus 5.5 committed 2026-10-03 17:47:49 +02:00
1 parent 7afcecd053
commit 2b8f06ea6d
3 files changed
+155 -20

No files matched your search

+33 -6
View File
@@ -1026,25 +1026,52 @@ func TestGetPrompt_EscapesControlCharacters(t *testing.T) {
}
func TestBuildPrompt_Color(t *testing.T) {
got := buildPrompt("flow", "host", "~/src", false, true)
want := colorUser + "flow@host" + colorReset + ":" + colorPath + "~/src" + colorReset + " $ "
got := buildPrompt("flow", "host", "~/src", false, &defaultColors)
want := "\x1b[1;32mflow@host\x1b[0m:\x1b[1;34m~/src\x1b[0m $ "
if got != want {
t.Errorf("buildPrompt() = %q, want %q", got, want)
}
got = buildPrompt("root", "host", "/", true, true)
if !strings.HasPrefix(got, colorRoot+"root@host") {
got = buildPrompt("root", "host", "/", true, &defaultColors)
if !strings.HasPrefix(got, "\x1b[1;31mroot@host") {
t.Errorf("expected red user for root, got %q", got)
}
got = buildPrompt("flow", "host", "~/src", false, false)
got = buildPrompt("flow", "host", "~/src", false, nil)
if got != "flow@host:~/src $ " {
t.Errorf("buildPrompt() without color = %q", got)
}
colors := promptColors{user: "38;5;208", path: ""}
got = buildPrompt("flow", "host", "~/src", false, &colors)
if got != "\x1b[38;5;208mflow@host\x1b[0m:~/src $ " {
t.Errorf("buildPrompt() with custom colors = %q", got)
}
}
func TestParseColors(t *testing.T) {
tests := []struct {
spec string
want promptColors
}{
{"", defaultColors},
{"user=1;33", promptColors{user: "1;33", root: "1;31", path: "1;34"}},
{"user=33:root=41;97:path=38;5;208", promptColors{user: "33", root: "41;97", path: "38;5;208"}},
{"path=", promptColors{user: "1;32", root: "1;31", path: ""}},
{"path=0m\x1b[2J", defaultColors}, // escape sequences are rejected
{"path=red", defaultColors}, // only numeric codes
{"foo=1:user", defaultColors}, // unknown key, missing value
{"user=35::path=36", promptColors{user: "35", root: "1;31", path: "36"}},
}
for _, tc := range tests {
if got := parseColors(tc.spec); got != tc.want {
t.Errorf("parseColors(%q) = %+v, want %+v", tc.spec, got, tc.want)
}
}
}
func TestBuildPrompt_ColorEscapesControlCharacters(t *testing.T) {
got := buildPrompt("flow", "host", "evil\x1b[2J\x07", false, true)
got := buildPrompt("flow", "host", "evil\x1b[2J\x07", false, &defaultColors)
if strings.Contains(got, "\x1b[2J") || strings.Contains(got, "\x07") {
t.Errorf("prompt contains control characters from path: %q", got)
}