refactor/kvconf 3421f16

view.go RawHistoryFolder

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 lockStatus, p promptKind) string {
73	switch s {
74	case lockBusy:
75		return "..."
76	case 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 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 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 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}

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 2

Tags 9

↑↓ moveenter openesc close