1package indicator
2
3import (
4 "strconv"
5 "time"
6 "unicode"
7 "unicode/utf8"
8)
9
10// prefix starts the recording label; the elapsed time follows it.
11const prefix = "● REC "
12
13// maxLabelRunes bounds the optional target label so the line fits beside the
14// Stop button: 228 px of 13 px monospace is about 29 cells, and "● REC h:mm:ss · "
15// takes 16.
16const maxLabelRunes = 12
17
18// suffixFor returns " · label" with the label cut to maxLabelRunes (ending in
19// an ellipsis when cut), or "" for an empty label. Anything that is not a
20// printable character is dropped, so a workspace name cannot break the single
21// line: controls, unassigned and private-use code points, the line and
22// paragraph separators U+2028 and U+2029, and the bidirectional controls
23// (Unicode Bidi_Control) that could reorder the timer beside it.
24func suffixFor(label string) string {
25 var r [maxLabelRunes + 1]rune
26 n := 0
27 for _, c := range label {
28 if keepRune(c) {
29 r[n] = c
30 n++
31 if n > maxLabelRunes {
32 r[maxLabelRunes-1] = '…'
33 n = maxLabelRunes
34 break
35 }
36 }
37 }
38 if n == 0 {
39 return ""
40 }
41 // Encode prefix and label together: a long rune-to-string conversion
42 // followed by concatenation would allocate an intermediate string too.
43 var buf [len(" · ") + maxLabelRunes*utf8.UTFMax]byte
44 b := buf[:copy(buf[:], " · ")]
45 for _, c := range r[:n] {
46 b = utf8.AppendRune(b, c)
47 }
48 return string(b)
49}
50
51// keepRune reports whether c may appear in the label.
52func keepRune(c rune) bool {
53 if c == 0x2028 || c == 0x2029 || unicode.Is(unicode.Bidi_Control, c) {
54 return false
55 }
56 // IsPrint admits only letters, marks, numbers, punctuation, symbols and the
57 // ASCII space; other spaces, controls and format characters are dropped.
58 return unicode.IsPrint(c)
59}
60
61// hud is the indicator model. The view and every write run on the layerui
62// owner loop, so nothing is locked. The elapsed time is computed from the
63// monotonic clock reading the view passes to refresh, never from a counter
64// another goroutine advances. start carries a monotonic reading (time.Now), so
65// wall-clock steps do not move the timer.
66type hud struct {
67 start time.Time
68 stop chan<- struct{}
69
70 suffix string // fixed for the session, built once
71 secs int64 // seconds shown in text; -1 before the first view
72 text string
73 buf []byte
74}
75
76func newHUD(start time.Time, stop chan<- struct{}, label string) *hud {
77 return &hud{start: start, stop: stop, suffix: suffixFor(label), secs: -1}
78}
79
80// refresh returns the label for the reading now, rebuilding it only when the
81// displayed second changed. A reading before start shows zero.
82func (h *hud) refresh(now time.Time) string {
83 s := int64(now.Sub(h.start) / time.Second)
84 if s < 0 {
85 s = 0
86 }
87 if s == h.secs {
88 return h.text
89 }
90 h.secs = s
91 h.buf = append(appendElapsed(append(h.buf[:0], prefix...), s), h.suffix...)
92 h.text = string(h.buf)
93 return h.text
94}
95
96// requestStop notifies the owner without blocking. A full channel already
97// holds a pending stop request.
98func (h *hud) requestStop() {
99 select {
100 case h.stop <- struct{}{}:
101 default:
102 }
103}
104
105// appendElapsed appends mm:ss, or h:mm:ss from one hour.
106func appendElapsed(b []byte, secs int64) []byte {
107 h, m, s := secs/3600, secs/60%60, secs%60
108 if h > 0 {
109 b = strconv.AppendInt(b, h, 10)
110 b = append(b, ':')
111 }
112 b = appendTwo(b, m)
113 b = append(b, ':')
114 return appendTwo(b, s)
115}
116
117func appendTwo(b []byte, n int64) []byte {
118 return append(b, byte('0'+n/10), byte('0'+n%10))
119}