From af5281eed296aca318626c80da69b3f61c8c9b21 Mon Sep 17 00:00:00 2001 From: Florian Walther Date: Mon, 5 Oct 2026 19:22:59 +0200 Subject: [PATCH] report the working directory to the terminal with OSC 7 Before each prompt gosh sends ESC ] 7 ; file://host/path BEL (host and path percent-encoded), so terminals like GNOME Terminal open new tabs and windows in the directory of the shell instead of the one gosh was started in. Nothing is sent if stdout is not a terminal or TERM=dumb. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 ++ main.go | 46 ++++++++++++++++++++++++++++++++++++++++++++ main_test.go | 25 ++++++++++++++++++++++++ packaging/man/gosh.1 | 7 +++++++ 4 files changed, 80 insertions(+) diff --git a/README.md b/README.md index ffdb53d..2ebc041 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ Features: GOSH_COLORS='user=1;33:path=36' (keys user, root, path, error; values are ANSI SGR codes, empty = no color); disabled with $NO_COLOR, TERM=dumb or when output is not a terminal + - Reports the working directory to the terminal (OSC 7) before each + prompt, so new tabs/windows open in the same directory - Variables: name=value sets a variable (like set), name=value command sets it only for that command; the value is not split or expanded as a pattern diff --git a/main.go b/main.go index f8c84a9..1f4bbf8 100644 --- a/main.go +++ b/main.go @@ -795,6 +795,49 @@ func useColor() bool { return readline.IsTerminal(int(os.Stdout.Fd())) } +// reportCwd tells the terminal the current working directory with the OSC 7 +// escape sequence, so that new tabs or windows open in the same directory +// (VTE terminals like GNOME Terminal, kitty, WezTerm, foot, iTerm2, ...). +// Terminals without support ignore it. Nothing is written if stdout is not a +// terminal or TERM is "dumb". +func reportCwd() { + if os.Getenv("TERM") == "dumb" || !readline.IsTerminal(int(os.Stdout.Fd())) { + return + } + wd, err := os.Getwd() + if err != nil { + return + } + hostname, _ := os.Hostname() + fmt.Fprint(os.Stdout, osc7Sequence(hostname, wd)) +} + +// osc7Sequence returns ESC ] 7 ; file://host/path BEL. Host and path are +// percent-encoded, so control characters in a directory name cannot end the +// sequence early or inject other escape sequences. +func osc7Sequence(hostname, dir string) string { + return "\x1b]7;file://" + percentEncode(hostname, false) + percentEncode(dir, true) + "\a" +} + +// percentEncode encodes every byte except unreserved URI characters (and '/' +// if keepSlash) as %XX. +func percentEncode(s string, keepSlash bool) string { + const hex = "0123456789ABCDEF" + var b strings.Builder + for i := 0; i < len(s); i++ { + c := s[i] + if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || + c == '-' || c == '.' || c == '_' || c == '~' || c == '/' && keepSlash { + b.WriteByte(c) + } else { + b.WriteByte('%') + b.WriteByte(hex[c>>4]) + b.WriteByte(hex[c&15]) + } + } + return b.String() +} + // shellCompleter implements readline.AutoCompleter for contextual completion type shellCompleter struct { commands []string @@ -1334,6 +1377,7 @@ func interactive() { // Report finished and stopped background jobs reapJobs(true) // Update prompt dynamically (in case directory changed) + reportCwd() l.SetPrompt(getPrompt()) } else { l.SetPrompt("> ") @@ -2137,6 +2181,8 @@ Features: GOSH_COLORS='user=1;33:path=36' (keys user, root, path, error; values are ANSI SGR codes, empty = no color); disabled with $NO_COLOR, TERM=dumb or when output is not a terminal + - Reports the working directory to the terminal (OSC 7) before each + prompt, so new tabs/windows open in the same directory - Variables: name=value sets a variable (like set), name=value command sets it only for that command; the value is not split or expanded as a pattern diff --git a/main_test.go b/main_test.go index 17ee64b..419240d 100644 --- a/main_test.go +++ b/main_test.go @@ -1133,6 +1133,31 @@ func TestBuildPrompt_ColorEscapesControlCharacters(t *testing.T) { } } +func TestOSC7Sequence(t *testing.T) { + tests := []struct{ host, dir, want string }{ + {"host", "/home/flow", "\x1b]7;file://host/home/flow\a"}, + {"host", "/tmp/a b/รค", "\x1b]7;file://host/tmp/a%20b/%C3%A4\a"}, + {"host", "/x%y#z?", "\x1b]7;file://host/x%25y%23z%3F\a"}, + {"", "/", "\x1b]7;file:///\a"}, + } + for _, tt := range tests { + if got := osc7Sequence(tt.host, tt.dir); got != tt.want { + t.Errorf("osc7Sequence(%q, %q) = %q, want %q", tt.host, tt.dir, got, tt.want) + } + } +} + +func TestOSC7Sequence_EscapesControlCharacters(t *testing.T) { + got := osc7Sequence("ho/st\x07", "/evil\x07\x1b]0;title\x1b\\") + body := strings.TrimSuffix(strings.TrimPrefix(got, "\x1b]7;"), "\a") + if strings.ContainsAny(body, "\x07\x1b") { + t.Errorf("sequence contains control characters: %q", got) + } + if !strings.HasPrefix(body, "file://ho%2Fst%07/evil%07%1B") { + t.Errorf("unexpected encoding: %q", got) + } +} + func TestUseColor_NoColor(t *testing.T) { t.Setenv("NO_COLOR", "1") if useColor() { diff --git a/packaging/man/gosh.1 b/packaging/man/gosh.1 index 08878ef..53d9003 100644 --- a/packaging/man/gosh.1 +++ b/packaging/man/gosh.1 @@ -739,6 +739,13 @@ If the last command failed, its exit status is shown as .BI [ N ] before the .BR $ . +.PP +Before each prompt, gosh reports the working directory to the terminal with +the OSC 7 escape sequence +.RB ( "ESC ] 7 ; file://" \fIhost\fP/\fIpath\fP " BEL" ), +so terminals like GNOME Terminal open new tabs and windows in the same +directory. Nothing is sent if the output is not a terminal or +.BR TERM=dumb . .SH ENVIRONMENT .TP .B GOSH_HISTFILE