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 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f507f6562
commit
5b53c30235
4 files changed
+80
No files matched your search
@@ -59,6 +59,8 @@ Features:
|
|||||||
GOSH_COLORS='user=1;33:path=36' (keys user, root, path, error; values
|
GOSH_COLORS='user=1;33:path=36' (keys user, root, path, error; values
|
||||||
are ANSI SGR codes, empty = no color); disabled with $NO_COLOR,
|
are ANSI SGR codes, empty = no color); disabled with $NO_COLOR,
|
||||||
TERM=dumb or when output is not a terminal
|
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
|
- 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
|
sets it only for that command; the value is not split or expanded as
|
||||||
a pattern
|
a pattern
|
||||||
|
|||||||
@@ -795,6 +795,49 @@ func useColor() bool {
|
|||||||
return readline.IsTerminal(int(os.Stdout.Fd()))
|
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
|
// shellCompleter implements readline.AutoCompleter for contextual completion
|
||||||
type shellCompleter struct {
|
type shellCompleter struct {
|
||||||
commands []string
|
commands []string
|
||||||
@@ -1334,6 +1377,7 @@ func interactive() {
|
|||||||
// Report finished and stopped background jobs
|
// Report finished and stopped background jobs
|
||||||
reapJobs(true)
|
reapJobs(true)
|
||||||
// Update prompt dynamically (in case directory changed)
|
// Update prompt dynamically (in case directory changed)
|
||||||
|
reportCwd()
|
||||||
l.SetPrompt(getPrompt())
|
l.SetPrompt(getPrompt())
|
||||||
} else {
|
} else {
|
||||||
l.SetPrompt("> ")
|
l.SetPrompt("> ")
|
||||||
@@ -2137,6 +2181,8 @@ Features:
|
|||||||
GOSH_COLORS='user=1;33:path=36' (keys user, root, path, error; values
|
GOSH_COLORS='user=1;33:path=36' (keys user, root, path, error; values
|
||||||
are ANSI SGR codes, empty = no color); disabled with $NO_COLOR,
|
are ANSI SGR codes, empty = no color); disabled with $NO_COLOR,
|
||||||
TERM=dumb or when output is not a terminal
|
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
|
- 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
|
sets it only for that command; the value is not split or expanded as
|
||||||
a pattern
|
a pattern
|
||||||
|
|||||||
@@ -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) {
|
func TestUseColor_NoColor(t *testing.T) {
|
||||||
t.Setenv("NO_COLOR", "1")
|
t.Setenv("NO_COLOR", "1")
|
||||||
if useColor() {
|
if useColor() {
|
||||||
|
|||||||
@@ -739,6 +739,13 @@ If the last command failed, its exit status is shown as
|
|||||||
.BI [ N ]
|
.BI [ N ]
|
||||||
before the
|
before the
|
||||||
.BR $ .
|
.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
|
.SH ENVIRONMENT
|
||||||
.TP
|
.TP
|
||||||
.B GOSH_HISTFILE
|
.B GOSH_HISTFILE
|
||||||
|
|||||||
Reference in new issue
Block a user