v0.3.0 cf370a0

separators_test.go RawHistoryFolder

1package core
2
3import (
4	"reflect"
5	"testing"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10// tiles lays out rects edge to edge (no gap) with focus on index f.
11func tiles(f int, rs ...Rect) []Placement {
12	ps := make([]Placement, len(rs))
13	for i, r := range rs {
14		ps[i] = Placement{ID: WindowID(i + 1), Rect: r, Focused: i == f}
15	}
16	setVisibleNeighbors(ps, 0, bigOutput)
17	return ps
18}
19
20// bigOutput is an output wider than every test layout.
21var bigOutput = Rect{W: 1000, H: 1000}
22
23// colorAt is what the renderer shows at (x, y): the last separator over
24// it wins. "" is no line.
25func colorAt(seps []ports.Separator, x, y int) string {
26	got := ""
27	for _, s := range seps {
28		r := s.Rect
29		if x >= r.X && x < r.X+r.W && y >= r.Y && y < r.Y+r.H {
30			got = "gray"
31			if s.Active {
32				got = "lit"
33			}
34		}
35	}
36	return got
37}
38
39type px struct {
40	x, y int
41	want string
42}
43
44func TestSeparators(t *testing.T) {
45	cols2 := []Rect{{X: 0, Y: 0, W: 16, H: 16}, {X: 16, Y: 0, W: 16, H: 16}}
46	cols3 := []Rect{{X: 0, Y: 0, W: 16, H: 16}, {X: 16, Y: 0, W: 16, H: 16}, {X: 32, Y: 0, W: 16, H: 16}}
47	// A full-height column and a column split in two.
48	grid := []Rect{{X: 0, Y: 0, W: 16, H: 32}, {X: 16, Y: 0, W: 16, H: 16}, {X: 16, Y: 16, W: 16, H: 16}}
49	for _, tc := range []struct {
50		name  string
51		ps    []Placement
52		lit   bool
53		check []px
54	}{
55		// The left window owns the line (x 14-15); the client of the right
56		// window is not covered.
57		{"two columns, left focused: top half", tiles(0, cols2...), true, []px{{14, 3, "lit"}, {15, 7, "lit"}, {14, 8, "gray"}, {14, 15, "gray"}, {16, 3, ""}, {13, 3, ""}}},
58		{"two columns, right focused: bottom half", tiles(1, cols2...), true, []px{{14, 3, "gray"}, {14, 12, "lit"}}},
59		{"unfocused output: all gray", tiles(0, cols2...), false, []px{{14, 3, "gray"}, {14, 12, "gray"}}},
60		{"three columns, middle: both lines whole", tiles(1, cols3...), true, []px{{14, 3, "lit"}, {14, 12, "lit"}, {30, 3, "lit"}, {30, 12, "lit"}}},
61		{"three columns, first: its line only", tiles(0, cols3...), true, []px{{14, 3, "lit"}, {14, 12, "lit"}, {30, 3, "gray"}, {30, 12, "gray"}}},
62		{"no line at the screen edge", tiles(2, cols3...), true, []px{{47, 3, ""}, {0, 3, ""}}},
63		// The left and top lines meet: the corner square is lit.
64		{"bottom-right tile: corner closed", tiles(2, grid...), true, []px{{14, 30, "lit"}, {14, 20, "lit"}, {14, 14, "lit"}, {14, 15, "lit"}, {14, 5, "gray"}, {20, 14, "lit"}, {31, 15, "lit"}}},
65		{"top-right tile", tiles(1, grid...), true, []px{{14, 3, "lit"}, {14, 15, "lit"}, {14, 20, "gray"}, {20, 14, "lit"}}},
66		{"left column: whole line, split column untouched", tiles(0, grid...), true, []px{{14, 3, "lit"}, {14, 30, "lit"}, {20, 14, "gray"}}},
67	} {
68		t.Run(tc.name, func(t *testing.T) {
69			seps := separators(tc.ps, 2, 0, bigOutput, tc.lit)
70			for _, c := range tc.check {
71				if got := colorAt(seps, c.x, c.y); got != c.want {
72					t.Errorf("(%d,%d) = %q, want %q; %v", c.x, c.y, got, c.want, seps)
73				}
74			}
75		})
76	}
77}
78
79func TestSeparatorsSkip(t *testing.T) {
80	if s := separators(tiles(0, Rect{W: 32, H: 16}), 2, 0, bigOutput, true); len(s) != 0 {
81		t.Fatal("lone window:", s)
82	}
83	if s := separators(tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16}), 0, 0, bigOutput, true); len(s) != 0 {
84		t.Fatal("width 0:", s)
85	}
86	full := tiles(0, Rect{W: 16, H: 16}, Rect{X: 16, W: 16, H: 16})
87	full[0].Fullscreen, full[1].Hidden = true, true
88	if s := separators(full, 2, 0, bigOutput, true); len(s) != 0 {
89		t.Fatal("fullscreen:", s)
90	}
91}
92
93func TestSeparatorsFloat(t *testing.T) {
94	float := Placement{ID: 1, Rect: Rect{X: 10, Y: 10, W: 20, H: 20}, Floating: true, Focused: true, Inset: ports.SideAll}
95	seps := separators([]Placement{float}, 2, 0, bigOutput, true)
96	if seps[0].Window != 1 {
97		t.Fatal("float border not tied to its window:", seps)
98	}
99	for _, c := range [][2]int{{10, 10}, {29, 29}, {20, 11}} {
100		if got := colorAt(seps, c[0], c[1]); got != "lit" {
101			t.Errorf("(%d,%d) = %q", c[0], c[1], got)
102		}
103	}
104	if got := colorAt(seps, 20, 20); got != "" {
105		t.Error("inside:", got)
106	}
107}
108
109// Through the real layout: scrolling removes borders to off-output
110// neighbors and gives the client back the reserved space.
111func TestSeparatorsScroll(t *testing.T) {
112	m := monitor() // 100x80, 2 columns on screen
113	for id := WindowID(1); id <= 3; id++ {
114		m.AddWindow(id)
115	}
116	w := m.Current()
117	insets := func() map[WindowID]ports.Sides {
118		got := map[WindowID]ports.Sides{}
119		for _, p := range w.Layout() {
120			got[p.ID] = p.Inset
121		}
122		return got
123	}
124	before := insets()
125	if before[1] != 0 || before[3]&ports.SideRight != 0 {
126		t.Fatalf("scrolled tiles reserve off-output lines: %v", before)
127	}
128	if w.ViewX == 0 {
129		t.Fatal("expected a scrolled view")
130	}
131	m.Apply(ActionFocusColumnLeft)
132	m.Apply(ActionFocusColumnLeft)
133	if after := insets(); reflect.DeepEqual(before, after) || after[1]&ports.SideRight == 0 || after[3] != 0 {
134		t.Fatalf("expected visible borders to change with the view: %v then %v", before, after)
135	}
136	out := Rect{W: 100, H: 80}
137	seps := separators(w.Layout(), 2, 0, out, true)
138	if got := colorAt(seps, 99, 40); got != "" {
139		t.Fatalf("line at the output edge: %v", seps)
140	}
141	// Two columns on screen out of three: the half rule applies.
142	if got := colorAt(seps, 48, 60); got != "gray" {
143		t.Fatalf("(48,60) = %q; %v", got, seps)
144	}
145	if got := colorAt(seps, 48, 10); got != "lit" {
146		t.Fatalf("(48,10) = %q; %v", got, seps)
147	}
148}
149
150// With gaps each tile has its own line: the focused one lights it whole.
151func TestSeparatorsGaps(t *testing.T) {
152	ps := []Placement{{ID: 1, Rect: Rect{X: 0, W: 16, H: 16}, Focused: true}, {ID: 2, Rect: Rect{X: 20, W: 16, H: 16}}}
153	setVisibleNeighbors(ps, 4, bigOutput)
154	seps := separators(ps, 2, 4, bigOutput, true)
155	for _, c := range []px{{14, 2, "lit"}, {14, 13, "lit"}, {20, 8, "gray"}, {17, 8, ""}} {
156		if got := colorAt(seps, c.x, c.y); got != c.want {
157			t.Errorf("(%d,%d) = %q, want %q", c.x, c.y, got, c.want)
158		}
159	}
160}

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 25

Tags 10

↑↓ moveenter openesc close