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}