animations_interrupt_internal_test.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"testing"
6	"time"
7
8	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12// movingRects makes two windows on sc swap places and returns with their
13// rect motions running, one frame in (the clock moved 16 ms and stepped).
14func movingRects(t *testing.T, c *Core, ic *indicatorClock, sc *screen) {
15	t.Helper()
16	c.cfg.Animations.On = true
17	sc.mon.AddWindow(1)
18	sc.mon.AddWindow(2)
19	settleShown(t, c)
20	c.focusScreen = c.screenIndex(sc.name())
21	before := c.snapshot(ic.now)
22	c.applyAction(ActionMoveColumnLeft)
23	c.transition(before, ic.now)
24	if len(sc.rects) != 2 {
25		t.Fatalf("%d rect motions after the move, want 2", len(sc.rects))
26	}
27	indicatorScene(t, c)
28	ic.now = ic.now.Add(16 * time.Millisecond)
29	if err := c.step(context.Background(), nil); err != nil {
30		t.Fatal(err)
31	}
32	<-c.ch.Scenes
33	if rm := sc.rects[1]; rm.dx == 0 || !rm.on() {
34		t.Fatalf("window 1 offset %v (running %t) one frame in: not mid-motion", rm.dx, rm.on())
35	}
36}
37
38// A session lock in the middle of a rect motion stops every motion and the
39// frame timer, drops the screens' last scene and shown layouts, and the
40// scene after the unlock is the settled one with a Seq nothing used before.
41func TestSessionLockMidRectMotion(t *testing.T) {
42	c, ic := indicatorCore(t)
43	state := ports.SecurityState{}
44	gate := portsmocks.NewMockSessionSecurity(t)
45	gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state })
46	c.opts.Security = gate
47	sc := c.cur()
48	movingRects(t, c, ic, sc)
49	if !c.animating() || c.frameC == nil || sc.last.Seq == 0 {
50		t.Fatalf("nothing to interrupt: animating %t, timer %t, last seq %d", c.animating(), c.frameC != nil, sc.last.Seq)
51	}
52	seq := sc.last.Seq
53
54	state = ports.SecurityState{Generation: 1, Protected: true}
55	if err := c.publish(context.Background()); err != nil {
56		t.Fatal(err)
57	}
58	locked := (<-c.ch.Scenes)[0]
59	if len(locked.Windows) != 0 || len(locked.Separators) != 0 || locked.Seq <= seq {
60		t.Fatalf("locked scene shows the desktop or reuses a Seq (%d <= %d): %+v", locked.Seq, seq, locked)
61	}
62	if len(sc.rects) != 0 || sc.rectsWS != nil || sc.springing() || c.animating() || c.frameC != nil {
63		t.Fatalf("motion survived the lock: rects %d, springing %t, animating %t, timer %t", len(sc.rects), sc.springing(), c.animating(), c.frameC != nil)
64	}
65	if sc.last.Seq != 0 || sc.shown != nil || sc.settledLayout != nil {
66		t.Fatalf("lock kept the desktop scene: last seq %d, shown %v, settled %v", sc.last.Seq, sc.shown != nil, sc.settledLayout != nil)
67	}
68
69	// Unlocked: the windows are where the move left them, nothing runs.
70	state = ports.SecurityState{Generation: 2}
71	s := indicatorScene(t, c)
72	if s.Seq <= locked.Seq {
73		t.Fatalf("unlocked scene Seq %d reuses the locked one %d", s.Seq, locked.Seq)
74	}
75	var got1, got2 Rect
76	for _, w := range s.Windows {
77		switch w.ID {
78		case 1:
79			got1 = w.Rect
80		case 2:
81			got2 = w.Rect
82		}
83	}
84	// 300x200 output, two columns of 150: the move put 2 first.
85	if got2.X != 0 || got1.X != 150 {
86		t.Fatalf("after the unlock 1 at %+v and 2 at %+v, want the settled 150 and 0", got1, got2)
87	}
88	if c.animating() || len(sc.rects) != 0 || c.frameC != nil {
89		t.Fatalf("a motion runs after the unlock: animating %t, rects %d, timer %t", c.animating(), len(sc.rects), c.frameC != nil)
90	}
91}
92
93// An output unplugged in the middle of its windows' rect motion: no panic,
94// its motions go with it, a late page flip for it does nothing, and the
95// remaining output keeps no motion of the gone one.
96func TestOutputUnplugMidRectMotion(t *testing.T) {
97	c, ic := indicatorCore(t)
98	a := c.cur()
99	c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200})
100	b := c.screens[1]
101	movingRects(t, c, ic, b)
102	c.removeScreen("B")
103	if c.screenIndex("B") >= 0 || len(c.screens) != 1 {
104		t.Fatalf("B still plugged: %d screens", len(c.screens))
105	}
106	if len(a.rects) != 0 || a.springing() {
107		t.Fatalf("A inherited the motions of B: rects %d, springing %t", len(a.rects), a.springing())
108	}
109	if c.sliding("B") {
110		t.Fatal("the gone output still slides")
111	}
112	// B's windows are guests of A now: the next publish and a stray flip are fine.
113	if err := c.publish(context.Background()); err != nil {
114		t.Fatal(err)
115	}
116	<-c.ch.Scenes
117	ic.now = ic.now.Add(5 * time.Second)
118	if err := c.step(context.Background(), nil); err != nil {
119		t.Fatal(err)
120	}
121	<-c.ch.Scenes
122	if c.animating() || c.frameC != nil {
123		t.Fatalf("still animating after the unplug and a settle: animating %t, timer %t", c.animating(), c.frameC != nil)
124	}
125}
126
127// The last output going away keeps its screen as a placeholder: none of its
128// springs (rect motions, a workspace switch) may outlive the output, or the
129// fallback timer would wake core for a screen nothing draws. When an output
130// comes back, a new move animates and lands from a fresh Seq.
131func TestLastOutputUnplugMidRectMotion(t *testing.T) {
132	c, ic := indicatorCore(t)
133	sc := c.cur()
134	movingRects(t, c, ic, sc)
135	// A workspace switch spring runs too.
136	m := sc.mon
137	m.switchView.off, m.switchView.motion = 0.5, c.spring(workspaceSpring(0.5, 0), ic.now)
138	if !m.switchView.motion.on || !sc.springing() {
139		t.Fatal("no workspace switch to interrupt")
140	}
141	c.removeScreen("A")
142	if sc.mon.springing() || len(sc.rects) != 0 || sc.rectsWS != nil || sc.mon.switchView.motion.on || sc.mon.switchView.off != 0 {
143		t.Fatalf("the placeholder keeps springs: monitor %t, rects %d, switch %t (%v)", sc.mon.springing(), len(sc.rects), sc.mon.switchView.motion.on, sc.mon.switchView.off)
144	}
145	if err := c.publish(context.Background()); err != nil {
146		t.Fatal(err)
147	}
148	gone := (<-c.ch.Scenes)[0]
149	if c.animating() || c.frameC != nil {
150		t.Fatalf("the placeholder keeps asking for frames: animating %t, timer %t", c.animating(), c.frameC != nil)
151	}
152
153	// The output comes back: its windows are where the move left them.
154	c.addScreen(ports.OutputInfo{Name: "A", Width: 300, Height: 200})
155	settled := indicatorScene(t, c)
156	if settled.Seq <= gone.Seq {
157		t.Fatalf("scene after the replug reuses Seq %d (placeholder %d)", settled.Seq, gone.Seq)
158	}
159	x := func(s ports.Scene, id WindowID) int {
160		for _, w := range s.Windows {
161			if w.ID == id {
162				return w.Rect.X
163			}
164		}
165		t.Fatalf("window %d not in the scene", id)
166		return 0
167	}
168	if x(settled, 2) != 0 || x(settled, 1) != 150 {
169		t.Fatalf("after the replug 1 at %d and 2 at %d, want the settled 150 and 0", x(settled, 1), x(settled, 2))
170	}
171	if c.animating() || len(c.cur().rects) != 0 {
172		t.Fatal("a motion runs after the replug")
173	}
174
175	// A new move animates from the settled rects and lands.
176	before := c.snapshot(ic.now)
177	c.applyAction(ActionMoveColumnRight)
178	c.transition(before, ic.now)
179	start := indicatorScene(t, c)
180	if x(start, 1) != 150 || x(start, 2) != 0 {
181		t.Fatalf("right after the move 1 at %d and 2 at %d, want the old 150 and 0", x(start, 1), x(start, 2))
182	}
183	ic.now = ic.now.Add(16 * time.Millisecond)
184	if err := c.step(context.Background(), nil); err != nil {
185		t.Fatal(err)
186	}
187	mid := (<-c.ch.Scenes)[0]
188	if got := x(mid, 1); got >= 150 || got <= 0 {
189		t.Fatalf("window 1 at %d on the first frame, want between 0 and 150", got)
190	}
191	if mid.Seq == start.Seq {
192		t.Fatalf("an animating frame reuses Seq %d", mid.Seq)
193	}
194	ic.now = ic.now.Add(5 * time.Second)
195	if err := c.step(context.Background(), nil); err != nil {
196		t.Fatal(err)
197	}
198	end := (<-c.ch.Scenes)[0]
199	if x(end, 1) != 0 || x(end, 2) != 150 || end.Seq == mid.Seq {
200		t.Fatalf("landed 1 at %d and 2 at %d (Seq %d after %d), want 0 and 150 and a fresh Seq", x(end, 1), x(end, 2), end.Seq, mid.Seq)
201	}
202	if c.animating() || c.frameC != nil {
203		t.Fatal("still animating after the landing")
204	}
205}

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