1package visual
2
3import (
4 "fmt"
5 "strings"
6 "time"
7
8 "github.com/bnema/nefergui"
9)
10
11// Lock look: black screen, one monospace box with a grey border. The masked
12// input sits at the exact screen center: the date line above the box and an
13// equal-height spacer below it balance each other, and an empty spacer inside
14// the box balances the status line under the input. The view sees only the
15// typed-character count, never the secret.
16//
17// NeferGUI's flex layout does not center a fixed-width child across the
18// column here, so each field gets an explicit left margin derived from the
19// geometry below.
20const (
21 frameW = 360 // shared width: the date line ends at the box edge
22 boxBorder = 2
23 boxPadding = 24
24 fieldPadX = 8
25 fieldEdge = 2 // relief width (bottom and right)
26 passwordW = 280
27 pinW = 160
28
29 // boxContentW is the width available to the field inside the box.
30 boxContentW = frameW - 2*boxBorder - 2*boxPadding
31
32 // Monospace advance at 18px is about 10.8px: 28 characters fit the
33 // box content, 24 bullets plus the cursor fit the password field, and
34 // 7 dots at 24px plus spacing fit the PIN field.
35 maxHintRunes = 28
36 maxPasswordMask = 24
37 maxPINMask = 7
38
39 cssScreen = "display:flex;flex-direction:column;align-items:center;justify-content:center;" +
40 "width:100%;height:100%;background-color:#000000;color:#ffffff;font-family:monospace;font-size:18px"
41 cssClock = "color:#ffffff;text-align:right;height:24px;margin-bottom:8px"
42 // cssBalance mirrors cssClock below the box.
43 cssBalance = "height:24px;margin-top:8px"
44 // cssHint and cssHintBalance have the same outer height.
45 cssHint = "color:#ffffff;margin-top:12px;height:22px;text-align:center"
46 cssHintBalance = "margin-bottom:12px;height:22px"
47
48 clockLayout = "Mon 02 Jan 2006 15:04"
49)
50
51var (
52 // cssRelief is the inverted relief: bottom and right edges at 50% white.
53 cssRelief = fmt.Sprintf("border-bottom:%[1]dpx solid rgba(255,255,255,0.5);"+
54 "border-right:%[1]dpx solid rgba(255,255,255,0.5)", fieldEdge)
55 cssFrame = fmt.Sprintf("display:flex;flex-direction:column;align-items:stretch;width:%dpx", frameW)
56 cssBox = fmt.Sprintf("display:flex;flex-direction:column;align-items:stretch;"+
57 "border:%dpx solid #bfbfbf;padding:%dpx;background-color:#000000", boxBorder, boxPadding)
58 cssMask = fmt.Sprintf("color:#ffffff;height:32px;line-height:32px;width:%dpx;"+
59 "padding:4px %dpx;margin-left:%dpx;%s", passwordW, fieldPadX, fieldMargin(passwordW), cssRelief)
60 cssMaskPIN = fmt.Sprintf("color:#ffffff;height:32px;line-height:32px;width:%dpx;"+
61 "padding:4px %dpx;margin-left:%dpx;font-size:24px;letter-spacing:6px;text-align:center;%s",
62 pinW, fieldPadX, fieldMargin(pinW), cssRelief)
63)
64
65// fieldMargin centers a field of content width w inside the box content.
66func fieldMargin(w int) int {
67 return (boxContentW - (w + 2*fieldPadX + fieldEdge)) / 2
68}
69
70// hintFor is the line under the input: the attempt status first, then a note
71// for prompts the bare field cannot convey. It never carries typed input.
72func hintFor(s nefergui.LockStatus, p promptKind) string {
73 switch s {
74 case nefergui.LockBusy:
75 return "..."
76 case nefergui.LockFailed:
77 return "Denied"
78 }
79 switch p {
80 case promptFallback:
81 return "PIN off, use password"
82 case promptUnavailable:
83 return "Authentication unavailable"
84 case promptMore:
85 return "Enter again"
86 }
87 return " "
88}
89
90// maskFor is the field content and style: large dots for a PIN, bullets
91// plus a cursor for a password. The count is capped to the field width.
92func maskFor(s nefergui.LockState, p promptKind) (string, string) {
93 if p == promptPIN {
94 return strings.Repeat("●", min(s.Mask, maxPINMask)), cssMaskPIN
95 }
96 return strings.Repeat("•", min(s.Mask, maxPasswordMask)) + "_", cssMask
97}
98
99// UnlockBox draws the date line, the box and the balancing spacer.
100func UnlockBox(root nefergui.Node, s nefergui.LockState, p promptKind, now time.Time) {
101 frame := root.Column(nefergui.Class("frame"), nefergui.Inline(cssFrame))
102 frame.Text(now.Format(clockLayout), nefergui.Key("clock"), nefergui.Inline(cssClock))
103 box := frame.Column(nefergui.Class("box"), nefergui.Key("box"), nefergui.Inline(cssBox))
104 box.Spacer(nefergui.Key("hint-balance"), nefergui.Inline(cssHintBalance))
105 mask, css := maskFor(s, p)
106 box.Text(mask, nefergui.Key("mask"), nefergui.Inline(css))
107 box.Text(hintFor(s.Status, p), nefergui.Key("hint"), nefergui.Inline(cssHint))
108 frame.Spacer(nefergui.Key("balance"), nefergui.Inline(cssBalance))
109}
110
111// lockView builds the whole lock screen.
112func lockView(f *nefergui.Frame, s nefergui.LockState, p promptKind, now time.Time) {
113 UnlockBox(f.Root(nefergui.Class("screen"), nefergui.Inline(cssScreen)), s, p, now)
114}
115
116// clockPoll is short because the monotonic timer stops during suspend: a
117// lock held across suspend must show the current minute soon after resume.
118const clockPoll = 5 * time.Second
119
120// clockWake requests a redraw whenever the wall-clock minute changes, until
121// done closes. Sends never block: a pending request is enough.
122func clockWake(done <-chan struct{}, wake chan<- struct{}) {
123 t := time.NewTicker(clockPoll)
124 defer t.Stop()
125 shown := time.Now().Truncate(time.Minute)
126 for {
127 select {
128 case <-done:
129 return
130 case <-t.C:
131 if m := time.Now().Truncate(time.Minute); !m.Equal(shown) {
132 shown = m
133 select {
134 case wake <- struct{}{}:
135 default:
136 }
137 }
138 }
139 }
140}