detached 4bfcb44

feat(domain): add unified UI anchors

commit
4bfcb44f2496b1ddde80fb8a0e73e61283ab9b7d
author
brice · · 2026-07-11 06:18 UTC
parent
8fe83c3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +139 -0
M internal/domain/types.go +74 -0
@@ -1,5 +1,79 @@
11 package domain
22
3+import "strings"
4+
5+// Anchor specifies a position in a 3x3 grid.
6+type Anchor uint8
7+
8+const (
9+ AnchorCenter Anchor = iota
10+ AnchorTopLeft
11+ AnchorTop
12+ AnchorTopRight
13+ AnchorLeft
14+ AnchorRight
15+ AnchorBottomLeft
16+ AnchorBottom
17+ AnchorBottomRight
18+)
19+
20+// Valid reports whether a is a defined anchor.
21+func (a Anchor) Valid() bool {
22+ return a <= AnchorBottomRight
23+}
24+
25+// String returns the canonical text representation of a.
26+func (a Anchor) String() string {
27+ switch a {
28+ case AnchorCenter:
29+ return "center"
30+ case AnchorTopLeft:
31+ return "top-left"
32+ case AnchorTop:
33+ return "top"
34+ case AnchorTopRight:
35+ return "top-right"
36+ case AnchorLeft:
37+ return "left"
38+ case AnchorRight:
39+ return "right"
40+ case AnchorBottomLeft:
41+ return "bottom-left"
42+ case AnchorBottom:
43+ return "bottom"
44+ case AnchorBottomRight:
45+ return "bottom-right"
46+ default:
47+ return "unknown"
48+ }
49+}
50+
51+// ParseAnchor parses a case-insensitive anchor name.
52+func ParseAnchor(s string) (Anchor, bool) {
53+ switch strings.ToLower(strings.TrimSpace(s)) {
54+ case "center":
55+ return AnchorCenter, true
56+ case "top-left":
57+ return AnchorTopLeft, true
58+ case "top":
59+ return AnchorTop, true
60+ case "top-right":
61+ return AnchorTopRight, true
62+ case "left":
63+ return AnchorLeft, true
64+ case "right":
65+ return AnchorRight, true
66+ case "bottom-left":
67+ return AnchorBottomLeft, true
68+ case "bottom":
69+ return AnchorBottom, true
70+ case "bottom-right":
71+ return AnchorBottomRight, true
72+ default:
73+ return AnchorCenter, false
74+ }
75+}
76+
377 // Size is a terminal dimension in columns and rows.
478 type Size struct {
579 Cols, Rows int
@@ -1,5 +1,79 @@
1 package domain1 package domain
2 2
3+import "strings"
4+
5+// Anchor specifies a position in a 3x3 grid.
6+type Anchor uint8
7+
8+const (
9+ AnchorCenter Anchor = iota
10+ AnchorTopLeft
11+ AnchorTop
12+ AnchorTopRight
13+ AnchorLeft
14+ AnchorRight
15+ AnchorBottomLeft
16+ AnchorBottom
17+ AnchorBottomRight
18+)
19+
20+// Valid reports whether a is a defined anchor.
21+func (a Anchor) Valid() bool {
22+ return a <= AnchorBottomRight
23+}
24+
25+// String returns the canonical text representation of a.
26+func (a Anchor) String() string {
27+ switch a {
28+ case AnchorCenter:
29+ return "center"
30+ case AnchorTopLeft:
31+ return "top-left"
32+ case AnchorTop:
33+ return "top"
34+ case AnchorTopRight:
35+ return "top-right"
36+ case AnchorLeft:
37+ return "left"
38+ case AnchorRight:
39+ return "right"
40+ case AnchorBottomLeft:
41+ return "bottom-left"
42+ case AnchorBottom:
43+ return "bottom"
44+ case AnchorBottomRight:
45+ return "bottom-right"
46+ default:
47+ return "unknown"
48+ }
49+}
50+
51+// ParseAnchor parses a case-insensitive anchor name.
52+func ParseAnchor(s string) (Anchor, bool) {
53+ switch strings.ToLower(strings.TrimSpace(s)) {
54+ case "center":
55+ return AnchorCenter, true
56+ case "top-left":
57+ return AnchorTopLeft, true
58+ case "top":
59+ return AnchorTop, true
60+ case "top-right":
61+ return AnchorTopRight, true
62+ case "left":
63+ return AnchorLeft, true
64+ case "right":
65+ return AnchorRight, true
66+ case "bottom-left":
67+ return AnchorBottomLeft, true
68+ case "bottom":
69+ return AnchorBottom, true
70+ case "bottom-right":
71+ return AnchorBottomRight, true
72+ default:
73+ return AnchorCenter, false
74+ }
75+}
76+
3 // Size is a terminal dimension in columns and rows.77 // Size is a terminal dimension in columns and rows.
4 type Size struct {78 type Size struct {
5 Cols, Rows int79 Cols, Rows int
M internal/domain/types_test.go +65 -0
@@ -2,6 +2,71 @@ package domain
22
33 import "testing"
44
5+func TestAnchor(t *testing.T) {
6+ tests := []struct {
7+ name string
8+ anchor Anchor
9+ valid bool
10+ text string
11+ }{
12+ {"center", AnchorCenter, true, "center"},
13+ {"top left", AnchorTopLeft, true, "top-left"},
14+ {"top", AnchorTop, true, "top"},
15+ {"top right", AnchorTopRight, true, "top-right"},
16+ {"left", AnchorLeft, true, "left"},
17+ {"right", AnchorRight, true, "right"},
18+ {"bottom left", AnchorBottomLeft, true, "bottom-left"},
19+ {"bottom", AnchorBottom, true, "bottom"},
20+ {"bottom right", AnchorBottomRight, true, "bottom-right"},
21+ {"invalid", Anchor(255), false, "unknown"},
22+ }
23+
24+ if AnchorCenter != 0 {
25+ t.Errorf("AnchorCenter = %d, want 0", AnchorCenter)
26+ }
27+
28+ for _, tt := range tests {
29+ t.Run(tt.name, func(t *testing.T) {
30+ if got := tt.anchor.Valid(); got != tt.valid {
31+ t.Errorf("Anchor(%d).Valid() = %v, want %v", tt.anchor, got, tt.valid)
32+ }
33+ if got := tt.anchor.String(); got != tt.text {
34+ t.Errorf("Anchor(%d).String() = %q, want %q", tt.anchor, got, tt.text)
35+ }
36+ })
37+ }
38+}
39+
40+func TestParseAnchor(t *testing.T) {
41+ tests := []struct {
42+ input string
43+ want Anchor
44+ ok bool
45+ }{
46+ {"center", AnchorCenter, true},
47+ {" TOP-LEFT ", AnchorTopLeft, true},
48+ {"Top", AnchorTop, true},
49+ {"top-right", AnchorTopRight, true},
50+ {"LEFT", AnchorLeft, true},
51+ {"right", AnchorRight, true},
52+ {"bottom-left", AnchorBottomLeft, true},
53+ {"Bottom", AnchorBottom, true},
54+ {"bottom-right", AnchorBottomRight, true},
55+ {"auto", AnchorCenter, false},
56+ {"", AnchorCenter, false},
57+ {"diagonal", AnchorCenter, false},
58+ }
59+
60+ for _, tt := range tests {
61+ t.Run(tt.input, func(t *testing.T) {
62+ got, ok := ParseAnchor(tt.input)
63+ if got != tt.want || ok != tt.ok {
64+ t.Errorf("ParseAnchor(%q) = (%q, %v), want (%q, %v)", tt.input, got, ok, tt.want, tt.ok)
65+ }
66+ })
67+ }
68+}
69+
570 func TestSizeValid(t *testing.T) {
671 tests := []struct {
772 name string
@@ -2,6 +2,71 @@ package domain
2 2
3 import "testing"3 import "testing"
4 4
5+func TestAnchor(t *testing.T) {
6+ tests := []struct {
7+ name string
8+ anchor Anchor
9+ valid bool
10+ text string
11+ }{
12+ {"center", AnchorCenter, true, "center"},
13+ {"top left", AnchorTopLeft, true, "top-left"},
14+ {"top", AnchorTop, true, "top"},
15+ {"top right", AnchorTopRight, true, "top-right"},
16+ {"left", AnchorLeft, true, "left"},
17+ {"right", AnchorRight, true, "right"},
18+ {"bottom left", AnchorBottomLeft, true, "bottom-left"},
19+ {"bottom", AnchorBottom, true, "bottom"},
20+ {"bottom right", AnchorBottomRight, true, "bottom-right"},
21+ {"invalid", Anchor(255), false, "unknown"},
22+ }
23+
24+ if AnchorCenter != 0 {
25+ t.Errorf("AnchorCenter = %d, want 0", AnchorCenter)
26+ }
27+
28+ for _, tt := range tests {
29+ t.Run(tt.name, func(t *testing.T) {
30+ if got := tt.anchor.Valid(); got != tt.valid {
31+ t.Errorf("Anchor(%d).Valid() = %v, want %v", tt.anchor, got, tt.valid)
32+ }
33+ if got := tt.anchor.String(); got != tt.text {
34+ t.Errorf("Anchor(%d).String() = %q, want %q", tt.anchor, got, tt.text)
35+ }
36+ })
37+ }
38+}
39+
40+func TestParseAnchor(t *testing.T) {
41+ tests := []struct {
42+ input string
43+ want Anchor
44+ ok bool
45+ }{
46+ {"center", AnchorCenter, true},
47+ {" TOP-LEFT ", AnchorTopLeft, true},
48+ {"Top", AnchorTop, true},
49+ {"top-right", AnchorTopRight, true},
50+ {"LEFT", AnchorLeft, true},
51+ {"right", AnchorRight, true},
52+ {"bottom-left", AnchorBottomLeft, true},
53+ {"Bottom", AnchorBottom, true},
54+ {"bottom-right", AnchorBottomRight, true},
55+ {"auto", AnchorCenter, false},
56+ {"", AnchorCenter, false},
57+ {"diagonal", AnchorCenter, false},
58+ }
59+
60+ for _, tt := range tests {
61+ t.Run(tt.input, func(t *testing.T) {
62+ got, ok := ParseAnchor(tt.input)
63+ if got != tt.want || ok != tt.ok {
64+ t.Errorf("ParseAnchor(%q) = (%q, %v), want (%q, %v)", tt.input, got, ok, tt.want, tt.ok)
65+ }
66+ })
67+ }
68+}
69+
5 func TestSizeValid(t *testing.T) {70 func TestSizeValid(t *testing.T) {
6 tests := []struct {71 tests := []struct {
7 name string72 name string

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 12

Tags 11

↑↓ moveenter openesc close