v0.3.0 99dc266

bar_script.go RawHistoryFolder

1package daemon
2
3import (
4	"bytes"
5	"context"
6	"errors"
7	"fmt"
8	"strconv"
9	"strings"
10	"time"
11	"unicode/utf8"
12
13	"github.com/bnema/vev/internal/ports"
14)
15
16const (
17	barScriptOutputLimit  = 1024
18	barScriptStderrLimit  = 512
19	barScriptDisplayLimit = 256
20	barScriptTimeout      = time.Second
21)
22
23type barScriptContext struct {
24	Anchor  string
25	Session string
26	Tab     string
27	Pane    string
28	PaneCWD string
29	Cols    int
30}
31
32func (c barScriptContext) env(base []string) []string {
33	vars := map[string]string{
34		"VEV_ANCHOR":   c.Anchor,
35		"VEV_SESSION":  c.Session,
36		"VEV_TAB":      c.Tab,
37		"VEV_PANE":     c.Pane,
38		"VEV_PANE_CWD": c.PaneCWD,
39		"VEV_COLS":     strconv.Itoa(c.Cols),
40	}
41	out := make([]string, 0, len(base)+len(vars))
42	for _, entry := range base {
43		key, _, ok := strings.Cut(entry, "=")
44		if ok {
45			if _, replace := vars[key]; replace {
46				continue
47			}
48		}
49		out = append(out, entry)
50	}
51	for _, key := range []string{"VEV_ANCHOR", "VEV_SESSION", "VEV_TAB", "VEV_PANE", "VEV_PANE_CWD", "VEV_COLS"} {
52		out = append(out, key+"="+vars[key])
53	}
54	return out
55}
56
57type barScriptRunner struct {
58	runner  ports.ShellCommandRunner
59	timeout time.Duration
60}
61
62func (r barScriptRunner) run(ctx context.Context, command string, env []string, scriptCtx barScriptContext) (string, error) {
63	if strings.TrimSpace(command) == "" {
64		return "", nil
65	}
66	if r.runner == nil {
67		return "", errors.New("bar script command runner is nil")
68	}
69	timeout := r.timeout
70	if timeout <= 0 {
71		timeout = barScriptTimeout
72	}
73	ctx, cancel := context.WithTimeout(ctx, timeout)
74	defer cancel()
75
76	res, err := r.runner.Run(ctx, ports.CommandSpec{
77		Command:     command,
78		Env:         scriptCtx.env(env),
79		Timeout:     timeout,
80		StdoutLimit: barScriptOutputLimit,
81		StderrLimit: barScriptStderrLimit,
82	})
83	if errors.Is(ctx.Err(), context.DeadlineExceeded) {
84		return "", context.DeadlineExceeded
85	}
86	text := sanitizeBarScriptOutput(res.Stdout, barScriptOutputLimit)
87	if err != nil {
88		return text, &barScriptError{
89			exitCode: res.ExitCode,
90			stderr:   sanitizeBarScriptOutput(res.Stderr, barScriptStderrLimit),
91			err:      err,
92		}
93	}
94	return text, nil
95}
96
97func sanitizeBarScriptOutput(raw []byte, limit int) string {
98	if limit <= 0 || limit > len(raw) {
99		limit = len(raw)
100	}
101	raw = raw[:limit]
102	if i := bytes.IndexAny(raw, "\r\n"); i >= 0 {
103		raw = raw[:i]
104	}
105	text := strings.ToValidUTF8(string(raw), "")
106	text = stripANSIAndControls(text)
107	if len(text) <= barScriptDisplayLimit {
108		return text
109	}
110	return trimUTF8Bytes(text, barScriptDisplayLimit)
111}
112
113func stripANSIAndControls(s string) string {
114	var out strings.Builder
115	for i := 0; i < len(s); {
116		r, size := utf8.DecodeRuneInString(s[i:])
117		switch {
118		case r == '\x1b':
119			i = skipEscapeSequence(s, i+size)
120			continue
121		case r == 0x9b:
122			i = skipCSI(s, i+size)
123			continue
124		case r == 0x90 || r == 0x98 || r == 0x9d || r == 0x9e || r == 0x9f:
125			i = skipStringControl(s, i+size)
126			continue
127		case r < 0x20 || r == 0x7f || (r >= 0x80 && r <= 0x9f):
128			i += size
129			continue
130		}
131		out.WriteRune(r)
132		i += size
133	}
134	return out.String()
135}
136
137func skipEscapeSequence(s string, i int) int {
138	if i >= len(s) {
139		return i
140	}
141	switch s[i] {
142	case '[':
143		return skipCSI(s, i+1)
144	case ']', 'P', '^', '_', 'X':
145		return skipStringControl(s, i+1)
146	}
147	for i < len(s) && s[i] >= 0x20 && s[i] <= 0x2f {
148		i++
149	}
150	if i < len(s) && s[i] >= 0x30 && s[i] <= 0x7e {
151		return i + 1
152	}
153	return i
154}
155
156func skipCSI(s string, i int) int {
157	for i < len(s) {
158		c := s[i]
159		i++
160		if c >= 0x40 && c <= 0x7e {
161			break
162		}
163	}
164	return i
165}
166
167func skipStringControl(s string, i int) int {
168	for i < len(s) {
169		if s[i] == '\a' {
170			return i + 1
171		}
172		if s[i] == '\x1b' && i+1 < len(s) && s[i+1] == '\\' {
173			return i + 2
174		}
175		_, size := utf8.DecodeRuneInString(s[i:])
176		i += size
177	}
178	return i
179}
180
181func trimUTF8Bytes(s string, limit int) string {
182	if len(s) <= limit {
183		return s
184	}
185	for limit > 0 && !utf8.ValidString(s[:limit]) {
186		limit--
187	}
188	return s[:limit]
189}
190
191// barScriptError carries the diagnostic detail needed to explain why a bar
192// script failed: the process exit code and whatever it wrote to stderr.
193type barScriptError struct {
194	exitCode int
195	stderr   string
196	err      error
197}
198
199func (e *barScriptError) Error() string {
200	if e.stderr != "" {
201		return fmt.Sprintf("exit %d: %s", e.exitCode, e.stderr)
202	}
203	return fmt.Sprintf("exit %d", e.exitCode)
204}
205
206func (e *barScriptError) Unwrap() error { return e.err }
207
208// pathFromEnv returns the PATH entry's value, or "" when unset.
209func pathFromEnv(env []string) string {
210	for _, entry := range env {
211		if name, value, ok := environmentEntry(entry); ok && name == "PATH" {
212			return value
213		}
214	}
215	return ""
216}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close