border_invariants_test.go RawHistoryFolder

1package core_test
2
3import (
4	"math/rand"
5	"testing"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10// Published scenes, not just layouts: exercise the real owner loop, binds,
11// monitor transfers, unmaps, and both kinds of overflow with a fixed seed.
12func TestBorderSceneInvariants(t *testing.T) {
13	for _, overflow := range []string{"fixed", "scroll"} {
14		for _, gap := range []int{0, 6} {
15			t.Run(overflow+map[int]string{0: "-flush", 6: "-gaps"}[gap], func(t *testing.T) {
16				rng := rand.New(rand.NewSource(20260928))
17				r := startMulti(t, func(c *ports.Config) {
18					c.Border.Width = 2
19					c.Layout.MaxColumns = 3
20					c.Layout.Overflow = overflow
21					c.Layout.Gaps = gap
22				}, left, right)
23				var ids []ports.WindowID
24				next := ports.WindowID(1)
25				// A single keypress may publish a scene even when the action is
26				// inapplicable; check every resulting scene independently.
27				keys := []struct {
28					sym  string
29					mods ports.Mods
30				}{
31					{"Left", ports.ModAlt | ports.ModShift}, {"Right", ports.ModAlt | ports.ModShift},
32					{"Left", ports.ModAlt}, {"Right", ports.ModAlt},
33					{"Left", ports.ModAlt | ports.ModCtrl}, {"Right", ports.ModAlt | ports.ModCtrl},
34					{"Left", ports.ModAlt | ports.ModCtrl | ports.ModShift}, {"Right", ports.ModAlt | ports.ModCtrl | ports.ModShift},
35					{"bracketleft", ports.ModAlt}, {"bracketright", ports.ModAlt},
36					{"f", ports.ModAlt}, {"f", ports.ModAlt | ports.ModShift},
37					{"s", ports.ModAlt}, {"S", ports.ModAlt | ports.ModShift}, {"Up", ports.ModAlt}, {"Down", ports.ModAlt},
38				}
39				for step := range 180 {
40					var scenes []ports.Scene
41					switch {
42					case len(ids) < 2 || rng.Intn(5) == 0 && len(ids) < 12:
43						scenes = r.mapWindow(t, next)
44						ids = append(ids, next)
45						next++
46					case rng.Intn(9) == 0:
47						i := rng.Intn(len(ids))
48						r.client <- ports.WindowUnmapped{ID: ids[i]}
49						scenes = receive(t, r.scenes)
50						ids = append(ids[:i], ids[i+1:]...)
51					default:
52						k := keys[rng.Intn(len(keys))]
53						scenes = r.key(t, k.sym, k.mods)
54					}
55					checkBorderScenes(t, step, gap, scenes)
56				}
57			})
58		}
59	}
60}
61
62func checkBorderScenes(t *testing.T, step, gap int, scenes []ports.Scene) {
63	t.Helper()
64	focused := 0
65	for _, s := range scenes {
66		checkBorderScene(t, step, gap, s)
67		for _, w := range s.Windows {
68			if w.Focused && !w.Hidden && !w.Popup && w.Rect.Overlaps(ports.Rect{W: s.OutputWidth, H: s.OutputHeight}) {
69				focused++
70			}
71		}
72	}
73	if focused > 1 {
74		t.Fatalf("step %d: focused windows on multiple outputs: %+v", step, scenes)
75	}
76}
77
78func checkBorderScene(t *testing.T, step, gap int, s ports.Scene) {
79	t.Helper()
80	o := ports.Rect{W: s.OutputWidth, H: s.OutputHeight}
81	var focus *ports.SceneWindow
82	tiles := 0
83	for i := range s.Windows {
84		w := &s.Windows[i]
85		if w.Popup || w.Hidden || !w.Rect.Overlaps(o) {
86			continue
87		}
88		if w.Focused {
89			if focus != nil {
90				t.Fatalf("step %d %s: two focused windows: %+v", step, s.Output, s.Windows)
91			}
92			focus = w
93		}
94		if !w.Floating && !w.Fullscreen {
95			tiles++
96		}
97		if w.Fullscreen && w.Inset != 0 {
98			t.Fatalf("step %d %s: fullscreen inset: %+v", step, s.Output, w)
99		}
100		if !w.Floating && !w.Fullscreen {
101			if got, want := w.Inset, wantInset(s, *w, gap); got != want {
102				t.Fatalf("step %d %s: tile %d inset %v, want %v; scene %+v", step, s.Output, w.ID, got, want, s)
103			}
104		}
105	}
106	if tiles <= 1 {
107		for _, w := range s.Windows {
108			if !w.Floating && !w.Popup && !w.Hidden && w.Rect.Overlaps(o) && w.Inset != 0 {
109				t.Fatalf("step %d %s: lone tile reserves a border: %+v; scene %+v", step, s.Output, w, s)
110			}
111		}
112	}
113	for _, sep := range s.Separators {
114		r := sep.Rect
115		if r.W <= 0 || r.H <= 0 || r.X < 0 || r.Y < 0 || r.X+r.W > o.W || r.Y+r.H > o.H {
116			t.Fatalf("step %d %s: line outside output: %+v; scene %+v", step, s.Output, sep, s)
117		}
118		if sep.Window != 0 {
119			found := false
120			for _, w := range s.Windows {
121				if w.ID == sep.Window && w.Floating && !w.Fullscreen && !w.Hidden && w.Rect.Overlaps(r) {
122					found = true
123				}
124			}
125			if !found {
126				t.Fatalf("step %d %s: line belongs to invisible float: %+v; scene %+v", step, s.Output, sep, s)
127			}
128		} else if tiles < 2 {
129			t.Fatalf("step %d %s: lone tile has shared line: %+v; scene %+v", step, s.Output, sep, s)
130		}
131		if sep.Active && (focus == nil || focus.Fullscreen || sep.Window != 0 && sep.Window != focus.ID || sep.Window == 0 && focus.Floating) {
132			t.Fatalf("step %d %s: lit line without matching focused tile/float: %+v; scene %+v", step, s.Output, sep, s)
133		}
134	}
135	// A focused tile with a visible neighbor lights at least one line
136	// touching it.
137	if focus == nil || focus.Floating || focus.Fullscreen || neighborSides(s, *focus, gap) == 0 {
138		return
139	}
140	near := ports.Rect{X: focus.Rect.X - 2, Y: focus.Rect.Y - 2, W: focus.Rect.W + 4, H: focus.Rect.H + 4}
141	for _, sep := range s.Separators {
142		if sep.Active && sep.Window == 0 && sep.Rect.Overlaps(near) {
143			return
144		}
145	}
146	t.Fatalf("step %d %s: focused tile %d has neighbors but no lit line; scene %+v", step, s.Output, focus.ID, s)
147}
148
149// neighborSides are the sides of tile w facing another visible tile across
150// gap, with the shared edge strictly inside the output.
151func neighborSides(s ports.Scene, w ports.SceneWindow, gap int) ports.Sides {
152	o := ports.Rect{W: s.OutputWidth, H: s.OutputHeight}
153	var sides ports.Sides
154	for _, other := range s.Windows {
155		if other.ID == w.ID || other.Popup || other.Floating || other.Fullscreen || other.Hidden || !other.Rect.Overlaps(o) {
156			continue
157		}
158		a, b := w.Rect, other.Rect
159		rows := a.Y < b.Y+b.H && b.Y < a.Y+a.H
160		cols := a.X < b.X+b.W && b.X < a.X+a.W
161		switch {
162		case rows && a.X+a.W+gap == b.X && a.X+a.W > 0 && a.X+a.W < o.W:
163			sides |= ports.SideRight
164		case rows && b.X+b.W+gap == a.X && a.X > 0 && a.X < o.W:
165			sides |= ports.SideLeft
166		case cols && a.Y+a.H+gap == b.Y && a.Y+a.H > 0 && a.Y+a.H < o.H:
167			sides |= ports.SideBottom
168		case cols && b.Y+b.H+gap == a.Y && a.Y > 0 && a.Y < o.H:
169			sides |= ports.SideTop
170		}
171	}
172	return sides
173}
174
175// wantInset is the exact inset of a visible tile: every neighbor side with
176// gaps, only the right and bottom (shared-line owner) sides without.
177func wantInset(s ports.Scene, w ports.SceneWindow, gap int) ports.Sides {
178	sides := neighborSides(s, w, gap)
179	if gap == 0 {
180		sides &= ports.SideRight | ports.SideBottom
181	}
182	return sides
183}
184
185// Regression for the original two-monitor transfer: when the last tile
186// comes back, neither the old output nor the new one has a shared line.
187func TestBorderMoveBackAlone(t *testing.T) {
188	r := startMulti(t, func(c *ports.Config) {
189		c.Border.Width = 2
190		c.Layout.MaxColumns = 3
191		c.Layout.Overflow = "fixed"
192	}, left, right)
193	r.key(t, "Right", ports.ModAlt|ports.ModCtrl)
194	r.mapWindow(t, 1)
195	for _, sym := range []string{"Left", "Right"} {
196		scenes := r.key(t, sym, ports.ModAlt|ports.ModShift)
197		checkBorderScenes(t, 0, 0, scenes)
198		if got := shown(scenes); len(got["DP-1"]) != map[string]int{"Left": 1, "Right": 0}[sym] || len(got["DP-2"]) != map[string]int{"Left": 0, "Right": 1}[sym] {
199			t.Fatalf("%s: window did not move: %v", sym, got)
200		}
201	}
202}

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 26

Tags 10

↑↓ moveenter openesc close