add colored prompt
- user@host in bold green (bold red for root), directory in bold blue - colors are disabled with NO_COLOR, TERM=dumb or when stdout is not a terminal - parts are sanitized before adding the color codes - only replace the home directory with ~ on a path boundary Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
37d6802815
commit
7afcecd053
3 files changed
+78
-7
No files matched your search
@@ -22,6 +22,8 @@ Features:
|
||||
override with $GOSH_HISTFILE); lines starting with a space are not saved
|
||||
- Tab completion for commands and file paths
|
||||
- Tilde expansion (~, ~/path) at the start of unquoted words
|
||||
- Colored prompt (user@host green, red for root; directory blue),
|
||||
disabled with $NO_COLOR, TERM=dumb or when output is not a terminal
|
||||
- Environment variable expansion ($VAR, ${VAR}, ${VAR:-default});
|
||||
using an unset variable is an error, use ${VAR:-} for an empty default
|
||||
|
||||
|
||||
@@ -303,16 +303,47 @@ func getPrompt() string {
|
||||
hostname = "unknown"
|
||||
}
|
||||
|
||||
// Shorten the path for display
|
||||
// Use only the last component if in home directory
|
||||
// Replace the home directory with ~
|
||||
home := getHomeDir()
|
||||
if home != "" && strings.HasPrefix(wd, home) {
|
||||
// Replace home with ~
|
||||
displayPath := "~" + strings.TrimPrefix(wd, home)
|
||||
return sanitize(fmt.Sprintf("%s@%s:%s $ ", username, hostname, displayPath))
|
||||
if home != "" && (wd == home || strings.HasPrefix(wd, home+"/")) {
|
||||
wd = "~" + strings.TrimPrefix(wd, home)
|
||||
}
|
||||
|
||||
return sanitize(fmt.Sprintf("%s@%s:%s $ ", username, hostname, wd))
|
||||
return buildPrompt(username, hostname, wd, os.Geteuid() == 0, useColor())
|
||||
}
|
||||
|
||||
// ANSI escape sequences used for the prompt
|
||||
const (
|
||||
colorReset = "\x1b[0m"
|
||||
colorUser = "\x1b[1;32m" // bold green
|
||||
colorRoot = "\x1b[1;31m" // bold red
|
||||
colorPath = "\x1b[1;34m" // bold blue
|
||||
)
|
||||
|
||||
// buildPrompt formats the prompt as user@host:path $. The parts are sanitized
|
||||
// before the color codes are added, so only our own escape sequences end up
|
||||
// in the prompt.
|
||||
func buildPrompt(username, hostname, path string, root, color bool) string {
|
||||
userHost := sanitize(username + "@" + hostname)
|
||||
path = sanitize(path)
|
||||
if !color {
|
||||
return userHost + ":" + path + " $ "
|
||||
}
|
||||
userColor := colorUser
|
||||
if root {
|
||||
userColor = colorRoot
|
||||
}
|
||||
return userColor + userHost + colorReset + ":" + colorPath + path + colorReset + " $ "
|
||||
}
|
||||
|
||||
// useColor reports whether the prompt should be colored: stdout must be a
|
||||
// terminal, TERM must not be "dumb" and NO_COLOR (https://no-color.org) must
|
||||
// not be set.
|
||||
func useColor() bool {
|
||||
if os.Getenv("NO_COLOR") != "" || os.Getenv("TERM") == "dumb" {
|
||||
return false
|
||||
}
|
||||
return readline.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
// shellCompleter implements readline.AutoCompleter for contextual completion
|
||||
@@ -666,6 +697,8 @@ Features:
|
||||
override with $GOSH_HISTFILE); lines starting with a space are not saved
|
||||
- Tab completion for commands and file paths
|
||||
- Tilde expansion (~, ~/path) at the start of unquoted words
|
||||
- Colored prompt (user@host green, red for root; directory blue),
|
||||
disabled with $NO_COLOR, TERM=dumb or when output is not a terminal
|
||||
- Environment variable expansion ($VAR, ${VAR}, ${VAR:-default});
|
||||
using an unset variable is an error, use ${VAR:-} for an empty default`)
|
||||
return nil
|
||||
|
||||
@@ -1004,6 +1004,7 @@ func TestSanitize(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetPrompt_EscapesControlCharacters(t *testing.T) {
|
||||
t.Setenv("NO_COLOR", "1")
|
||||
origDir, _ := os.Getwd()
|
||||
defer os.Chdir(origDir)
|
||||
|
||||
@@ -1024,6 +1025,41 @@ 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 + " $ "
|
||||
if got != want {
|
||||
t.Errorf("buildPrompt() = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
got = buildPrompt("root", "host", "/", true, true)
|
||||
if !strings.HasPrefix(got, colorRoot+"root@host") {
|
||||
t.Errorf("expected red user for root, got %q", got)
|
||||
}
|
||||
|
||||
got = buildPrompt("flow", "host", "~/src", false, false)
|
||||
if got != "flow@host:~/src $ " {
|
||||
t.Errorf("buildPrompt() without color = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPrompt_ColorEscapesControlCharacters(t *testing.T) {
|
||||
got := buildPrompt("flow", "host", "evil\x1b[2J\x07", false, true)
|
||||
if strings.Contains(got, "\x1b[2J") || strings.Contains(got, "\x07") {
|
||||
t.Errorf("prompt contains control characters from path: %q", got)
|
||||
}
|
||||
if !strings.Contains(got, `evil\x1b[2J\x07`) {
|
||||
t.Errorf("expected escaped path in prompt, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseColor_NoColor(t *testing.T) {
|
||||
t.Setenv("NO_COLOR", "1")
|
||||
if useColor() {
|
||||
t.Error("expected no color when NO_COLOR is set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilePathCompleter_SkipsControlCharacters(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
for _, name := range []string{"good", "go\x1b[31mred"} {
|
||||
|
||||
Reference in new issue
Block a user