main 7a3b92c

css.go RawHistoryFolder

1package popup
2
3import (
4	"fmt"
5	"os"
6	"path/filepath"
7	"strconv"
8	"strings"
9	"unicode"
10)
11
12// Inset is the space the popup stylesheet puts between the surface edge and
13// the content: 4px of padding and a 1px border.
14const Inset = 5
15
16// Style is what the popup stylesheet takes from the bar.
17type Style struct {
18	Font           string
19	Size           float64 // font size in logical pixels
20	FG, BG, Accent [3]uint8
21}
22
23func hex(c [3]uint8) string { return fmt.Sprintf("#%02x%02x%02x", c[0], c[1], c[2]) }
24
25// cssString makes a font name safe inside a quoted CSS string: it drops the
26// characters that could end the string or the declaration, and everything that
27// does not print (control characters, bidi marks, unassigned runes).
28func cssString(s string) string {
29	return strings.Map(func(r rune) rune {
30		switch {
31		case r == '"' || r == '\\' || r == '{' || r == '}' || r == ';' || !unicode.IsPrint(r):
32			return -1
33		}
34		return r
35	}, s)
36}
37
38// CSS is the stylesheet of every popup.
39func CSS(s Style) string {
40	fg, bg, ac := hex(s.FG), hex(s.BG), hex(s.Accent)
41	size := strconv.FormatFloat(s.Size, 'f', -1, 64)
42	var b strings.Builder
43	fmt.Fprintf(&b, "app { background-color: %s; color: %s; font-family: \"%s\"; font-size: %spx; padding: 4px; border: 1px solid %s; }\n",
44		bg, fg, cssString(s.Font), size, ac)
45	b.WriteString(".title { font-weight: bold; }\n")
46	b.WriteString(".body { opacity: 0.85; }\n")
47	b.WriteString(".table { column-gap: 1.5em; }\n")
48	b.WriteString(".key { opacity: 0.7; }\n")
49	b.WriteString("button.item, checkbox.item { text-align: left; background-color: transparent; padding: 2px 12px; }\n")
50	// nefergui paints the 1em check box centered in the left padding: leave
51	// room on both sides of it so the label does not touch the box.
52	b.WriteString("checkbox.item { padding-left: 2em; }\n")
53	fmt.Fprintf(&b, "button.item:hover, checkbox.item:hover, button.item:focus-visible, checkbox.item:focus-visible { background-color: %s; color: %s; }\n", ac, bg)
54	b.WriteString("button.item:disabled, checkbox.item:disabled { opacity: 0.5; }\n")
55	fmt.Fprintf(&b, "button.item:disabled:hover, checkbox.item:disabled:hover { background-color: transparent; color: %s; }\n", fg)
56	fmt.Fprintf(&b, "separator { background-color: %s; opacity: 0.3; margin: 4px 0; }\n", fg)
57	return b.String()
58}
59
60// CSSPath is the stylesheet of this process, popup-<pid>.css in
61// $XDG_RUNTIME_DIR/neferbar: two bars never overwrite each other's file.
62func CSSPath() (string, error) {
63	rt := os.Getenv("XDG_RUNTIME_DIR")
64	if rt == "" {
65		return "", fmt.Errorf("popup: XDG_RUNTIME_DIR is not set")
66	}
67	return filepath.Join(rt, "neferbar", "popup-"+strconv.Itoa(os.Getpid())+".css"), nil
68}
69
70// RemoveCSS deletes this process's stylesheet, if it was written.
71func RemoveCSS() {
72	if p, err := CSSPath(); err == nil {
73		_ = os.Remove(p)
74	}
75}
76
77// WriteCSS stores css as CSSPath (directory 0700, file 0600, replaced
78// atomically) and returns the path. nefergui reads its stylesheet from a path.
79// Call RemoveCSS when done.
80func WriteCSS(css string) (string, error) {
81	path, err := CSSPath()
82	if err != nil {
83		return "", err
84	}
85	dir := filepath.Dir(path)
86	if err := os.MkdirAll(dir, 0o700); err != nil {
87		return "", fmt.Errorf("popup: %w", err)
88	}
89	if err := os.Chmod(dir, 0o700); err != nil {
90		return "", fmt.Errorf("popup: %w", err)
91	}
92	f, err := os.CreateTemp(dir, "popup-*.css")
93	if err != nil {
94		return "", fmt.Errorf("popup: %w", err)
95	}
96	tmp := f.Name()
97	_, err = f.WriteString(css)
98	if cerr := f.Close(); err == nil {
99		err = cerr
100	}
101	if err == nil {
102		err = os.Chmod(tmp, 0o600)
103	}
104	if err == nil {
105		err = os.Rename(tmp, path)
106	}
107	if err != nil {
108		_ = os.Remove(tmp)
109		return "", fmt.Errorf("popup: %w", err)
110	}
111	return path, nil
112}

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 5

Tags 4

↑↓ moveenter openesc close