From c602b145a4a1f3d11965b4cc042fbf03bd6270cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=20=E2=80=98TK=E2=80=99=20Taylor?= Date: Mon, 15 Dec 2025 13:59:46 +0000 Subject: [PATCH 1/3] feat: add PromptColor setting for customizable shell prompt color Add a new Set PromptColor setting that allows users to customize the shell prompt color via a hex value (e.g., Set PromptColor "#f6821f"). Changes: - Add PROMPT_COLOR token to token/token.go - Refactor shell.go to support configurable prompt colors via ShellConfig() - Add hexToRGB() helper function for color conversion - Update tty.go to pass prompt color to shell configuration - Add PromptColor field to Options struct with default #5B56E0 - Add ExecuteSetPromptColor function in command.go - Update evaluator.go to handle PromptColor before VHS starts The prompt color is applied to all supported shells (bash, zsh, fish, nushell, osh, xonsh, powershell, pwsh). The cmdexe shell doesn't support color customization. --- command.go | 7 ++++ evaluator.go | 4 +- shell.go | 111 +++++++++++++++++++++++++++++++------------------ token/token.go | 5 ++- tty.go | 9 ++-- vhs.go | 4 +- 6 files changed, 92 insertions(+), 48 deletions(-) diff --git a/command.go b/command.go index 987df2f7..f63bb9fa 100644 --- a/command.go +++ b/command.go @@ -431,6 +431,7 @@ var Settings = map[string]CommandFunc{ "WaitPattern": ExecuteSetWaitPattern, "WaitTimeout": ExecuteSetWaitTimeout, "CursorBlink": ExecuteSetCursorBlink, + "PromptColor": ExecuteSetPromptColor, } // ExecuteSet applies the settings on the running vhs specified by the @@ -700,6 +701,12 @@ func ExecuteSetCursorBlink(c parser.Command, v *VHS) error { return nil } +// ExecuteSetPromptColor sets the prompt color. +func ExecuteSetPromptColor(c parser.Command, v *VHS) error { + v.Options.PromptColor = c.Args + return nil +} + // ExecuteScreenshot is a CommandFunc that indicates a new screenshot must be taken. func ExecuteScreenshot(c parser.Command, v *VHS) error { v.ScreenshotNextFrame(c.Args) diff --git a/evaluator.go b/evaluator.go index 077f2281..d2636b4a 100644 --- a/evaluator.go +++ b/evaluator.go @@ -30,7 +30,7 @@ func Evaluate(ctx context.Context, tape string, out io.Writer, opts ...Evaluator v := New() for _, cmd := range cmds { - if cmd.Type == token.SET && cmd.Options == "Shell" || cmd.Type == token.ENV { + if cmd.Type == token.SET && (cmd.Options == "Shell" || cmd.Options == "PromptColor") || cmd.Type == token.ENV { err := Execute(cmd, &v) if err != nil { return []error{err} @@ -56,7 +56,7 @@ func Evaluate(ctx context.Context, tape string, out io.Writer, opts ...Evaluator for i, cmd := range cmds { if cmd.Type == token.SET || cmd.Type == token.OUTPUT || cmd.Type == token.REQUIRE { _, _ = fmt.Fprintln(out, Highlight(cmd, false)) - if cmd.Options != "Shell" { + if cmd.Options != "Shell" && cmd.Options != "PromptColor" { err := Execute(cmd, &v) if err != nil { return []error{err} diff --git a/shell.go b/shell.go index fb836f3a..37b26845 100644 --- a/shell.go +++ b/shell.go @@ -1,5 +1,10 @@ package main +import ( + "fmt" + "strings" +) + // Supported shells of VH. const ( bash = "bash" @@ -13,64 +18,90 @@ const ( zsh = "zsh" ) +// DefaultPromptColor is the default color for the shell prompt. +const DefaultPromptColor = "#5B56E0" + // Shell is a type that contains a prompt and the command to set up the shell. type Shell struct { - Command []string - Env []string + Name string } -// Shells contains a mapping from shell names to their Shell struct. -var Shells = map[string]Shell{ - bash: { - Env: []string{"PS1=\\[\\e[38;2;90;86;224m\\]> \\[\\e[0m\\]", "BASH_SILENCE_DEPRECATION_WARNING=1"}, - Command: []string{"bash", "--noprofile", "--norc", "--login", "+o", "history"}, - }, - zsh: { - Env: []string{`PROMPT=%F{#5B56E0}> %F{reset_color}`}, - Command: []string{"zsh", "--histnostore", "--no-rcs"}, - }, - fish: { - Command: []string{ +// ShellConfig returns the shell configuration with the given prompt color. +func ShellConfig(name, promptColor string) (env []string, command []string) { + // Parse hex color to RGB components + r, g, b := hexToRGB(promptColor) + hexNoHash := strings.TrimPrefix(promptColor, "#") + + switch name { + case bash: + return []string{ + fmt.Sprintf("PS1=\\[\\e[38;2;%d;%d;%dm\\]> \\[\\e[0m\\]", r, g, b), + "BASH_SILENCE_DEPRECATION_WARNING=1", + }, + []string{"bash", "--noprofile", "--norc", "--login", "+o", "history"} + case zsh: + return []string{fmt.Sprintf(`PROMPT=%%F{#%s}> %%F{reset_color}`, hexNoHash)}, + []string{"zsh", "--histnostore", "--no-rcs"} + case fish: + return nil, []string{ "fish", "--login", "--no-config", "--private", "-C", "function fish_greeting; end", - "-C", `function fish_prompt; set_color 5B56E0; echo -n "> "; set_color normal; end`, - }, - }, - powershell: { - Command: []string{ + "-C", fmt.Sprintf(`function fish_prompt; set_color %s; echo -n "> "; set_color normal; end`, hexNoHash), + } + case powershell: + return nil, []string{ "powershell", "-NoLogo", "-NoExit", "-NoProfile", "-Command", - `Set-PSReadLineOption -HistorySaveStyle SaveNothing; function prompt { Write-Host '>' -NoNewLine -ForegroundColor Blue; return ' ' }`, - }, - }, - pwsh: { - Command: []string{ + fmt.Sprintf(`Set-PSReadLineOption -HistorySaveStyle SaveNothing; function prompt { Write-Host '>' -NoNewLine -ForegroundColor ([System.Drawing.Color]::FromArgb(%d,%d,%d)); return ' ' }`, r, g, b), + } + case pwsh: + return nil, []string{ "pwsh", "-Login", "-NoLogo", "-NoExit", "-NoProfile", "-Command", - `Set-PSReadLineOption -HistorySaveStyle SaveNothing; Function prompt { Write-Host -ForegroundColor Blue -NoNewLine '>'; return ' ' }`, - }, - }, - cmdexe: { - Command: []string{"cmd.exe", "/k", "prompt=^> "}, - }, - nushell: { - Command: []string{"nu", "--execute", "$env.PROMPT_COMMAND = {'\033[;38;2;91;86;224m>\033[m '}; $env.PROMPT_COMMAND_RIGHT = {''}"}, - }, - osh: { - Env: []string{"PS1=\\[\\e[38;2;90;86;224m\\]> \\[\\e[0m\\]"}, - Command: []string{"osh", "--norc"}, - }, - xonsh: { - Command: []string{"xonsh", "--no-rc", "-D", "PROMPT=\033[;38;2;91;86;224m>\033[m "}, - }, + fmt.Sprintf(`Set-PSReadLineOption -HistorySaveStyle SaveNothing; Function prompt { Write-Host -ForegroundColor ([System.Drawing.Color]::FromArgb(%d,%d,%d)) -NoNewLine '>'; return ' ' }`, r, g, b), + } + case cmdexe: + return nil, []string{"cmd.exe", "/k", "prompt=^> "} + case nushell: + return nil, []string{"nu", "--execute", fmt.Sprintf("$env.PROMPT_COMMAND = {'\033[;38;2;%d;%d;%dm>\033[m '}; $env.PROMPT_COMMAND_RIGHT = {''}", r, g, b)} + case osh: + return []string{fmt.Sprintf("PS1=\\[\\e[38;2;%d;%d;%dm\\]> \\[\\e[0m\\]", r, g, b)}, + []string{"osh", "--norc"} + case xonsh: + return nil, []string{"xonsh", "--no-rc", "-D", fmt.Sprintf("PROMPT=\033[;38;2;%d;%d;%dm>\033[m ", r, g, b)} + default: + return nil, nil + } +} + +// hexToRGB converts a hex color string to RGB components. +func hexToRGB(hex string) (r, g, b int) { + hex = strings.TrimPrefix(hex, "#") + if len(hex) == 6 { + fmt.Sscanf(hex, "%02x%02x%02x", &r, &g, &b) + } + return +} + +// Shells contains a mapping from shell names to their Shell struct. +var Shells = map[string]Shell{ + bash: {Name: bash}, + zsh: {Name: zsh}, + fish: {Name: fish}, + powershell: {Name: powershell}, + pwsh: {Name: pwsh}, + cmdexe: {Name: cmdexe}, + nushell: {Name: nushell}, + osh: {Name: osh}, + xonsh: {Name: xonsh}, } diff --git a/token/token.go b/token/token.go index 2389db52..c2f64942 100644 --- a/token/token.go +++ b/token/token.go @@ -103,6 +103,7 @@ const ( WAIT_TIMEOUT = "WAIT_TIMEOUT" //nolint:revive WAIT_PATTERN = "WAIT_PATTERN" //nolint:revive CURSOR_BLINK = "CURSOR_BLINK" //nolint:revive + PROMPT_COLOR = "PROMPT_COLOR" //nolint:revive ) // Keywords maps keyword strings to tokens. @@ -159,6 +160,7 @@ var Keywords = map[string]Type{ "Wait": WAIT, "Source": SOURCE, "CursorBlink": CURSOR_BLINK, + "PromptColor": PROMPT_COLOR, "true": BOOLEAN, "false": BOOLEAN, "Screenshot": SCREENSHOT, @@ -173,7 +175,8 @@ func IsSetting(t Type) bool { case SHELL, FONT_FAMILY, FONT_SIZE, LETTER_SPACING, LINE_HEIGHT, FRAMERATE, TYPING_SPEED, THEME, PLAYBACK_SPEED, HEIGHT, WIDTH, PADDING, LOOP_OFFSET, MARGIN_FILL, MARGIN, WINDOW_BAR, - WINDOW_BAR_SIZE, BORDER_RADIUS, CURSOR_BLINK, WAIT_TIMEOUT, WAIT_PATTERN: + WINDOW_BAR_SIZE, BORDER_RADIUS, CURSOR_BLINK, WAIT_TIMEOUT, WAIT_PATTERN, + PROMPT_COLOR: return true default: return false diff --git a/tty.go b/tty.go index b32d19ff..a1ca3183 100644 --- a/tty.go +++ b/tty.go @@ -24,7 +24,7 @@ func randomPort() int { } // buildTtyCmd builds the ttyd exec.Command on the given port. -func buildTtyCmd(port int, shell Shell) *exec.Cmd { +func buildTtyCmd(port int, shell Shell, promptColor string) *exec.Cmd { args := []string{ fmt.Sprintf("--port=%d", port), "--interface", "127.0.0.1", @@ -36,11 +36,12 @@ func buildTtyCmd(port int, shell Shell) *exec.Cmd { "--writable", } - args = append(args, shell.Command...) + env, command := ShellConfig(shell.Name, promptColor) + args = append(args, command...) cmd := exec.Command("ttyd", args...) - if shell.Env != nil { - cmd.Env = append(shell.Env, os.Environ()...) + if env != nil { + cmd.Env = append(env, os.Environ()...) } return cmd } diff --git a/vhs.go b/vhs.go index 383b0cfc..fe64acd5 100644 --- a/vhs.go +++ b/vhs.go @@ -52,6 +52,7 @@ type Options struct { CursorBlink bool Screenshot ScreenshotOptions Style StyleOptions + PromptColor string } const ( @@ -107,6 +108,7 @@ func DefaultVHSOptions() Options { Screenshot: screenshot, WaitTimeout: defaultWaitTimeout, WaitPattern: defaultWaitPattern, + PromptColor: DefaultPromptColor, } } @@ -131,7 +133,7 @@ func (vhs *VHS) Start() error { } port := randomPort() - vhs.tty = buildTtyCmd(port, vhs.Options.Shell) + vhs.tty = buildTtyCmd(port, vhs.Options.Shell, vhs.Options.PromptColor) if err := vhs.tty.Start(); err != nil { return fmt.Errorf("could not start tty: %w", err) } From de78b5e87317b037bbf4c5dfc61ccd070708dfb8 Mon Sep 17 00:00:00 2001 From: Matt 'TK' Taylor Date: Fri, 20 Feb 2026 15:07:31 +0800 Subject: [PATCH 2/3] test: add ShellConfig and hexToRGB tests Cover hex parsing edge cases (with/without #, invalid length, empty), ShellConfig output for all 9 shells, custom colour embedding in bash and zsh, default colour round-trip, and unknown shell handling. Co-Authored-By: Claude Opus 4.6 --- shell_test.go | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 shell_test.go diff --git a/shell_test.go b/shell_test.go new file mode 100644 index 00000000..cf7b9a6d --- /dev/null +++ b/shell_test.go @@ -0,0 +1,87 @@ +package main + +import ( + "strings" + "testing" +) + +func TestHexToRGB(t *testing.T) { + tests := []struct { + hex string + r, g, b int + }{ + {"#5B56E0", 91, 86, 224}, + {"5B56E0", 91, 86, 224}, + {"#F6821F", 246, 130, 31}, + {"#000000", 0, 0, 0}, + {"#FFFFFF", 255, 255, 255}, + {"", 0, 0, 0}, + {"#FFF", 0, 0, 0}, // invalid length + } + + for _, tc := range tests { + r, g, b := hexToRGB(tc.hex) + if r != tc.r || g != tc.g || b != tc.b { + t.Errorf("hexToRGB(%q) = (%d,%d,%d), want (%d,%d,%d)", + tc.hex, r, g, b, tc.r, tc.g, tc.b) + } + } +} + +func TestShellConfigReturnsNonNil(t *testing.T) { + // Every known shell should return a non-nil command from ShellConfig. + shellNames := []string{ + bash, zsh, fish, powershell, pwsh, cmdexe, nushell, osh, xonsh, + } + + for _, name := range shellNames { + t.Run(name, func(t *testing.T) { + _, command := ShellConfig(name, DefaultPromptColor) + if len(command) == 0 { + t.Errorf("ShellConfig(%q, %q) returned empty command", name, DefaultPromptColor) + } + }) + } +} + +func TestShellConfigCustomColor(t *testing.T) { + // Shells that use RGB values should embed the custom colour. + // bash uses PS1 with ANSI 38;2;R;G;B escape, so changing the colour + // should produce different RGB values in the env string. + env, _ := ShellConfig(bash, "#FF8000") + if len(env) == 0 { + t.Fatal("expected env for bash") + } + // #FF8000 = 255,128,0 + if !strings.Contains(env[0], "255;128;0") { + t.Errorf("bash PS1 does not contain expected RGB values for #FF8000: %s", env[0]) + } + + // zsh uses hex in the PROMPT string + env, _ = ShellConfig(zsh, "#FF8000") + if len(env) == 0 { + t.Fatal("expected env for zsh") + } + if !strings.Contains(env[0], "FF8000") { + t.Errorf("zsh PROMPT does not contain hex colour FF8000: %s", env[0]) + } +} + +func TestShellConfigDefaultColor(t *testing.T) { + // With the default colour, bash should produce the original RGB values. + // #5B56E0 = 91,86,224 + env, _ := ShellConfig(bash, DefaultPromptColor) + if len(env) == 0 { + t.Fatal("expected env for bash") + } + if !strings.Contains(env[0], "91;86;224") { + t.Errorf("bash PS1 with default colour does not contain expected RGB: %s", env[0]) + } +} + +func TestShellConfigUnknownShell(t *testing.T) { + env, command := ShellConfig("unknown-shell", DefaultPromptColor) + if env != nil || command != nil { + t.Errorf("expected nil for unknown shell, got env=%v command=%v", env, command) + } +} From 02289f54c39326f4e4715e29fb5bdd1b7300272f Mon Sep 17 00:00:00 2001 From: Matt 'TK' Taylor Date: Fri, 20 Feb 2026 15:27:19 +0800 Subject: [PATCH 3/3] feat: add Set Prompt setting for customizable prompt symbol Extends ShellConfig to accept a prompt parameter, allowing users to change the prompt symbol from the default ">" to any character or string. Example usage: Set Prompt "$" # bash-style Set Prompt "%" # zsh-style Set Prompt ">>>" # Python REPL-style Set Prompt "~" Co-Authored-By: Claude Opus 4.6 --- command.go | 7 +++++++ evaluator.go | 4 ++-- shell.go | 25 ++++++++++++++----------- shell_test.go | 27 ++++++++++++++++++++++----- token/token.go | 4 +++- tty.go | 4 ++-- vhs.go | 4 +++- 7 files changed, 53 insertions(+), 22 deletions(-) diff --git a/command.go b/command.go index f63bb9fa..57a6554c 100644 --- a/command.go +++ b/command.go @@ -432,6 +432,7 @@ var Settings = map[string]CommandFunc{ "WaitTimeout": ExecuteSetWaitTimeout, "CursorBlink": ExecuteSetCursorBlink, "PromptColor": ExecuteSetPromptColor, + "Prompt": ExecuteSetPrompt, } // ExecuteSet applies the settings on the running vhs specified by the @@ -707,6 +708,12 @@ func ExecuteSetPromptColor(c parser.Command, v *VHS) error { return nil } +// ExecuteSetPrompt sets the prompt symbol. +func ExecuteSetPrompt(c parser.Command, v *VHS) error { + v.Options.Prompt = c.Args + return nil +} + // ExecuteScreenshot is a CommandFunc that indicates a new screenshot must be taken. func ExecuteScreenshot(c parser.Command, v *VHS) error { v.ScreenshotNextFrame(c.Args) diff --git a/evaluator.go b/evaluator.go index d2636b4a..b2bb6927 100644 --- a/evaluator.go +++ b/evaluator.go @@ -30,7 +30,7 @@ func Evaluate(ctx context.Context, tape string, out io.Writer, opts ...Evaluator v := New() for _, cmd := range cmds { - if cmd.Type == token.SET && (cmd.Options == "Shell" || cmd.Options == "PromptColor") || cmd.Type == token.ENV { + if cmd.Type == token.SET && (cmd.Options == "Shell" || cmd.Options == "PromptColor" || cmd.Options == "Prompt") || cmd.Type == token.ENV { err := Execute(cmd, &v) if err != nil { return []error{err} @@ -56,7 +56,7 @@ func Evaluate(ctx context.Context, tape string, out io.Writer, opts ...Evaluator for i, cmd := range cmds { if cmd.Type == token.SET || cmd.Type == token.OUTPUT || cmd.Type == token.REQUIRE { _, _ = fmt.Fprintln(out, Highlight(cmd, false)) - if cmd.Options != "Shell" && cmd.Options != "PromptColor" { + if cmd.Options != "Shell" && cmd.Options != "PromptColor" && cmd.Options != "Prompt" { err := Execute(cmd, &v) if err != nil { return []error{err} diff --git a/shell.go b/shell.go index 37b26845..dc0878ce 100644 --- a/shell.go +++ b/shell.go @@ -21,13 +21,16 @@ const ( // DefaultPromptColor is the default color for the shell prompt. const DefaultPromptColor = "#5B56E0" +// DefaultPrompt is the default prompt symbol. +const DefaultPrompt = ">" + // Shell is a type that contains a prompt and the command to set up the shell. type Shell struct { Name string } -// ShellConfig returns the shell configuration with the given prompt color. -func ShellConfig(name, promptColor string) (env []string, command []string) { +// ShellConfig returns the shell configuration with the given prompt and color. +func ShellConfig(name, promptColor, prompt string) (env []string, command []string) { // Parse hex color to RGB components r, g, b := hexToRGB(promptColor) hexNoHash := strings.TrimPrefix(promptColor, "#") @@ -35,12 +38,12 @@ func ShellConfig(name, promptColor string) (env []string, command []string) { switch name { case bash: return []string{ - fmt.Sprintf("PS1=\\[\\e[38;2;%d;%d;%dm\\]> \\[\\e[0m\\]", r, g, b), + fmt.Sprintf("PS1=\\[\\e[38;2;%d;%d;%dm\\]%s \\[\\e[0m\\]", r, g, b, prompt), "BASH_SILENCE_DEPRECATION_WARNING=1", }, []string{"bash", "--noprofile", "--norc", "--login", "+o", "history"} case zsh: - return []string{fmt.Sprintf(`PROMPT=%%F{#%s}> %%F{reset_color}`, hexNoHash)}, + return []string{fmt.Sprintf(`PROMPT=%%F{#%s}%s %%F{reset_color}`, hexNoHash, prompt)}, []string{"zsh", "--histnostore", "--no-rcs"} case fish: return nil, []string{ @@ -49,7 +52,7 @@ func ShellConfig(name, promptColor string) (env []string, command []string) { "--no-config", "--private", "-C", "function fish_greeting; end", - "-C", fmt.Sprintf(`function fish_prompt; set_color %s; echo -n "> "; set_color normal; end`, hexNoHash), + "-C", fmt.Sprintf(`function fish_prompt; set_color %s; echo -n "%s "; set_color normal; end`, hexNoHash, prompt), } case powershell: return nil, []string{ @@ -58,7 +61,7 @@ func ShellConfig(name, promptColor string) (env []string, command []string) { "-NoExit", "-NoProfile", "-Command", - fmt.Sprintf(`Set-PSReadLineOption -HistorySaveStyle SaveNothing; function prompt { Write-Host '>' -NoNewLine -ForegroundColor ([System.Drawing.Color]::FromArgb(%d,%d,%d)); return ' ' }`, r, g, b), + fmt.Sprintf(`Set-PSReadLineOption -HistorySaveStyle SaveNothing; function prompt { Write-Host '%s' -NoNewLine -ForegroundColor ([System.Drawing.Color]::FromArgb(%d,%d,%d)); return ' ' }`, prompt, r, g, b), } case pwsh: return nil, []string{ @@ -68,17 +71,17 @@ func ShellConfig(name, promptColor string) (env []string, command []string) { "-NoExit", "-NoProfile", "-Command", - fmt.Sprintf(`Set-PSReadLineOption -HistorySaveStyle SaveNothing; Function prompt { Write-Host -ForegroundColor ([System.Drawing.Color]::FromArgb(%d,%d,%d)) -NoNewLine '>'; return ' ' }`, r, g, b), + fmt.Sprintf(`Set-PSReadLineOption -HistorySaveStyle SaveNothing; Function prompt { Write-Host -ForegroundColor ([System.Drawing.Color]::FromArgb(%d,%d,%d)) -NoNewLine '%s'; return ' ' }`, r, g, b, prompt), } case cmdexe: - return nil, []string{"cmd.exe", "/k", "prompt=^> "} + return nil, []string{"cmd.exe", "/k", fmt.Sprintf("prompt=%s ", prompt)} case nushell: - return nil, []string{"nu", "--execute", fmt.Sprintf("$env.PROMPT_COMMAND = {'\033[;38;2;%d;%d;%dm>\033[m '}; $env.PROMPT_COMMAND_RIGHT = {''}", r, g, b)} + return nil, []string{"nu", "--execute", fmt.Sprintf("$env.PROMPT_COMMAND = {'\033[;38;2;%d;%d;%dm%s\033[m '}; $env.PROMPT_COMMAND_RIGHT = {''}", r, g, b, prompt)} case osh: - return []string{fmt.Sprintf("PS1=\\[\\e[38;2;%d;%d;%dm\\]> \\[\\e[0m\\]", r, g, b)}, + return []string{fmt.Sprintf("PS1=\\[\\e[38;2;%d;%d;%dm\\]%s \\[\\e[0m\\]", r, g, b, prompt)}, []string{"osh", "--norc"} case xonsh: - return nil, []string{"xonsh", "--no-rc", "-D", fmt.Sprintf("PROMPT=\033[;38;2;%d;%d;%dm>\033[m ", r, g, b)} + return nil, []string{"xonsh", "--no-rc", "-D", fmt.Sprintf("PROMPT=\033[;38;2;%d;%d;%dm%s\033[m ", r, g, b, prompt)} default: return nil, nil } diff --git a/shell_test.go b/shell_test.go index cf7b9a6d..ed4e3ae5 100644 --- a/shell_test.go +++ b/shell_test.go @@ -36,7 +36,7 @@ func TestShellConfigReturnsNonNil(t *testing.T) { for _, name := range shellNames { t.Run(name, func(t *testing.T) { - _, command := ShellConfig(name, DefaultPromptColor) + _, command := ShellConfig(name, DefaultPromptColor, DefaultPrompt) if len(command) == 0 { t.Errorf("ShellConfig(%q, %q) returned empty command", name, DefaultPromptColor) } @@ -48,7 +48,7 @@ func TestShellConfigCustomColor(t *testing.T) { // Shells that use RGB values should embed the custom colour. // bash uses PS1 with ANSI 38;2;R;G;B escape, so changing the colour // should produce different RGB values in the env string. - env, _ := ShellConfig(bash, "#FF8000") + env, _ := ShellConfig(bash, "#FF8000", DefaultPrompt) if len(env) == 0 { t.Fatal("expected env for bash") } @@ -58,7 +58,7 @@ func TestShellConfigCustomColor(t *testing.T) { } // zsh uses hex in the PROMPT string - env, _ = ShellConfig(zsh, "#FF8000") + env, _ = ShellConfig(zsh, "#FF8000", DefaultPrompt) if len(env) == 0 { t.Fatal("expected env for zsh") } @@ -70,7 +70,7 @@ func TestShellConfigCustomColor(t *testing.T) { func TestShellConfigDefaultColor(t *testing.T) { // With the default colour, bash should produce the original RGB values. // #5B56E0 = 91,86,224 - env, _ := ShellConfig(bash, DefaultPromptColor) + env, _ := ShellConfig(bash, DefaultPromptColor, DefaultPrompt) if len(env) == 0 { t.Fatal("expected env for bash") } @@ -79,8 +79,25 @@ func TestShellConfigDefaultColor(t *testing.T) { } } +func TestShellConfigCustomPrompt(t *testing.T) { + // Every shell should embed the custom prompt symbol in its config. + shellNames := []string{ + bash, zsh, fish, powershell, pwsh, cmdexe, nushell, osh, xonsh, + } + + for _, name := range shellNames { + t.Run(name, func(t *testing.T) { + env, command := ShellConfig(name, DefaultPromptColor, "λ") + combined := strings.Join(env, " ") + " " + strings.Join(command, " ") + if !strings.Contains(combined, "λ") { + t.Errorf("ShellConfig(%q) with prompt λ does not contain the symbol: env=%v command=%v", name, env, command) + } + }) + } +} + func TestShellConfigUnknownShell(t *testing.T) { - env, command := ShellConfig("unknown-shell", DefaultPromptColor) + env, command := ShellConfig("unknown-shell", DefaultPromptColor, DefaultPrompt) if env != nil || command != nil { t.Errorf("expected nil for unknown shell, got env=%v command=%v", env, command) } diff --git a/token/token.go b/token/token.go index c2f64942..3df3bef2 100644 --- a/token/token.go +++ b/token/token.go @@ -104,6 +104,7 @@ const ( WAIT_PATTERN = "WAIT_PATTERN" //nolint:revive CURSOR_BLINK = "CURSOR_BLINK" //nolint:revive PROMPT_COLOR = "PROMPT_COLOR" //nolint:revive + PROMPT = "PROMPT" //nolint:revive ) // Keywords maps keyword strings to tokens. @@ -161,6 +162,7 @@ var Keywords = map[string]Type{ "Source": SOURCE, "CursorBlink": CURSOR_BLINK, "PromptColor": PROMPT_COLOR, + "Prompt": PROMPT, "true": BOOLEAN, "false": BOOLEAN, "Screenshot": SCREENSHOT, @@ -176,7 +178,7 @@ func IsSetting(t Type) bool { FRAMERATE, TYPING_SPEED, THEME, PLAYBACK_SPEED, HEIGHT, WIDTH, PADDING, LOOP_OFFSET, MARGIN_FILL, MARGIN, WINDOW_BAR, WINDOW_BAR_SIZE, BORDER_RADIUS, CURSOR_BLINK, WAIT_TIMEOUT, WAIT_PATTERN, - PROMPT_COLOR: + PROMPT_COLOR, PROMPT: return true default: return false diff --git a/tty.go b/tty.go index a1ca3183..362d4cb5 100644 --- a/tty.go +++ b/tty.go @@ -24,7 +24,7 @@ func randomPort() int { } // buildTtyCmd builds the ttyd exec.Command on the given port. -func buildTtyCmd(port int, shell Shell, promptColor string) *exec.Cmd { +func buildTtyCmd(port int, shell Shell, promptColor, prompt string) *exec.Cmd { args := []string{ fmt.Sprintf("--port=%d", port), "--interface", "127.0.0.1", @@ -36,7 +36,7 @@ func buildTtyCmd(port int, shell Shell, promptColor string) *exec.Cmd { "--writable", } - env, command := ShellConfig(shell.Name, promptColor) + env, command := ShellConfig(shell.Name, promptColor, prompt) args = append(args, command...) cmd := exec.Command("ttyd", args...) diff --git a/vhs.go b/vhs.go index fe64acd5..ec1c49f3 100644 --- a/vhs.go +++ b/vhs.go @@ -53,6 +53,7 @@ type Options struct { Screenshot ScreenshotOptions Style StyleOptions PromptColor string + Prompt string } const ( @@ -109,6 +110,7 @@ func DefaultVHSOptions() Options { WaitTimeout: defaultWaitTimeout, WaitPattern: defaultWaitPattern, PromptColor: DefaultPromptColor, + Prompt: DefaultPrompt, } } @@ -133,7 +135,7 @@ func (vhs *VHS) Start() error { } port := randomPort() - vhs.tty = buildTtyCmd(port, vhs.Options.Shell, vhs.Options.PromptColor) + vhs.tty = buildTtyCmd(port, vhs.Options.Shell, vhs.Options.PromptColor, vhs.Options.Prompt) if err := vhs.tty.Start(); err != nil { return fmt.Errorf("could not start tty: %w", err) }