Repository navigation
Expand file tree
/
Copy pathshellinit.go
More file actions
154 lines (143 loc) · 5.2 KB
/
Copy pathshellinit.go
File metadata and controls
154 lines (143 loc) · 5.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package main
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
// `termdock shell-init` prints the shell snippet that makes semantic
// prompt marks (OSC 133) work — the thing that lets termdock tell a
// prompt from a command from its output, which in turn is what makes
// "jump to the previous command" and "copy that command's output"
// possible at all. See internal/vt10x/marks.go.
//
// It is printed rather than installed. The snippet goes in a file the
// user owns and has opinions about, and a program that edits your shell
// rc behind your back is a program you stop trusting; `eval "$(termdock
// shell-init bash)"` is one line they can read first and put where they
// want.
// shellSnippets is the emitted code, one per shell.
//
// bash and zsh differ in more than syntax here. zsh has real hooks
// (precmd/preexec) that fire in exactly the right places, so the marks
// land where they should with no games. bash has only PROMPT_COMMAND and
// the DEBUG trap, and the DEBUG trap fires for *every* command in a
// pipeline as well as for things the prompt itself runs — hence the
// guard that emits the "command started" mark once per prompt cycle.
// Getting that wrong is what makes hand-rolled versions of this mark the
// prompt's own subshells as commands.
var shellSnippets = map[string]string{
"bash": `# termdock semantic prompt marks (OSC 133)
__termdock_preexec() {
# The DEBUG trap fires for every command in a pipeline, and for the
# commands PROMPT_COMMAND itself runs. Only the first one after a
# prompt is the command the user actually typed.
[[ -n "$__termdock_in_cmd" ]] && return
[[ "$BASH_COMMAND" == "$PROMPT_COMMAND" ]] && return
__termdock_in_cmd=1
printf '\033]133;C\007'
}
__termdock_precmd() {
local exit=$?
# Close the previous command only if one was actually started, so a
# bare Enter at the prompt doesn't report a command that never ran.
if [[ -n "$__termdock_in_cmd" ]]; then
printf '\033]133;D;%s\007' "$exit"
unset __termdock_in_cmd
fi
printf '\033]133;A\007'
return $exit
}
trap '__termdock_preexec' DEBUG
PROMPT_COMMAND="__termdock_precmd${PROMPT_COMMAND:+; $PROMPT_COMMAND}"
# B marks the end of the prompt; appending it to PS1 is the only place
# that is true regardless of what the prompt itself contains.
PS1="${PS1}\[\033]133;B\007\]"
`,
"zsh": `# termdock semantic prompt marks (OSC 133)
autoload -Uz add-zsh-hook
__termdock_precmd() {
local exit=$?
if [[ -n "$__termdock_in_cmd" ]]; then
print -n "\e]133;D;$exit\a"
unset __termdock_in_cmd
fi
print -n "\e]133;A\a"
}
__termdock_preexec() {
__termdock_in_cmd=1
print -n "\e]133;C\a"
}
add-zsh-hook precmd __termdock_precmd
add-zsh-hook preexec __termdock_preexec
PS1="${PS1}%{$(print -n "\e]133;B\a")%}"
`,
"fish": `# termdock semantic prompt marks (OSC 133)
function __termdock_precmd --on-event fish_prompt
printf '\033]133;A\007'
end
function __termdock_preexec --on-event fish_preexec
printf '\033]133;C\007'
end
function __termdock_postexec --on-event fish_postexec
printf '\033]133;D;%s\007' $status
end
`,
}
// cmdShellInit prints the snippet for the named shell, defaulting to
// whatever $SHELL says when none is given — the shell they are in is
// nearly always the one they mean.
func cmdShellInit(args []string) {
name := shellInitName(args)
snippet, ok := shellSnippets[name]
if !ok {
known := make([]string, 0, len(shellSnippets))
for k := range shellSnippets {
known = append(known, k)
}
sort.Strings(known)
fatal(fmt.Sprintf("no shell integration for %q; supported: %s",
name, strings.Join(known, ", ")))
}
fmt.Print(snippet)
// The note is for someone who ran this by hand to see what it does.
// Putting it on stderr is not enough to keep it out of the way:
// `eval "$(termdock shell-init)"` captures stdout and leaves stderr
// pointed at the terminal, so the note printed on every single new
// shell — which is exactly how it was reported. Whether stdout is a
// terminal is the thing that actually separates the two cases.
if isTerminal(os.Stdout) {
fmt.Fprintf(os.Stderr, "\n# Add to your %s startup file:\n# eval \"$(termdock shell-init %s)\"\n",
name, name)
}
}
// shellInitName is which shell to emit for: the first non-flag argument,
// or the basename of $SHELL when none was given.
//
// $SHELL is the login shell, which is not necessarily the shell whose
// startup file you are editing. It is right for nearly everybody and
// wrong for anyone who logs in with one shell and also uses another, so
// naming the shell explicitly stays supported and is what the docs
// recommend for that case.
func shellInitName(args []string) string {
name := ""
for _, a := range args {
if !strings.HasPrefix(a, "-") {
name = a
break
}
}
if name == "" {
name = filepath.Base(os.Getenv("SHELL"))
}
return strings.ToLower(strings.TrimSpace(name))
}
// isTerminal reports whether f is a terminal rather than a pipe or a
// file. Command substitution gives a pipe, which is the case worth
// telling apart; a redirect to a file is not a terminal either, and a
// caller redirecting the snippet to a file does not need the note.
func isTerminal(f *os.File) bool {
fi, err := f.Stat()
return err == nil && fi.Mode()&os.ModeCharDevice != 0
}