v0.2.0 13c6a90

capture_workspace_test.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"testing"
6	"time"
7
8	"github.com/bnema/neferwl/internal/ports"
9)
10
11// hiddenCapture is a 300x200 output whose first workspace (windows 1 and 2,
12// a 100x100 viewport) is off screen, captured.
13func hiddenCapture(t *testing.T) (*Core, *Workspace) {
14	t.Helper()
15	c, ws, _ := hiddenCaptureCommands(t)
16	return c, ws
17}
18
19// hiddenCaptureCommands is hiddenCapture with the channel core sends its commands on.
20func hiddenCaptureCommands(t *testing.T) (*Core, *Workspace, chan ports.ClientCommand) {
21	t.Helper()
22	commands := make(chan ports.ClientCommand, 64)
23	var cfg ports.Config
24	cfg.Keyboard.CmdKey = "super"
25	cfg.Layout.MaxColumns = 2
26	cfg.Border.Width = 2
27	cfg.Background.Color = "#102030"
28	c, err := New(cfg, Channels{
29		Scenes: make(chan []ports.Scene, 1), Commands: commands,
30	})
31	if err != nil {
32		t.Fatal(err)
33	}
34	c.addScreen(ports.OutputInfo{Name: "A", Width: 300, Height: 200})
35	m := c.screens[0].mon
36	ws := m.Current()
37	ws.SetSize(100, 100)
38	m.AddWindow(1)
39	m.AddWindow(2)
40	m.Focus(1)
41	if m.Current() == ws {
42		t.Fatal("workspace still on screen")
43	}
44	c.configures.cw.load(c.screens[0], ws)
45	return c, ws, commands
46}
47
48func TestCaptureConfigureSizesHiddenWindowWithoutShowingIt(t *testing.T) {
49	c, _ := hiddenCapture(t)
50	sc := c.screens[0]
51	phys := Placement{ID: 1, Hidden: true}
52	t0 := configureTarget{output: "A", area: sc.mon.Frame()}
53	cp, tt := c.captureConfigure(sc, phys, t0)
54	if cp == nil || cp.Hidden {
55		t.Fatalf("no real placement: %+v", cp)
56	}
57	v, send := c.configures.nextWithCapture(phys, tt, cp)
58	want := c.clientRect(*cp)
59	if !send || v.Width != want.W || v.Height != want.H || want.W == 0 {
60		t.Fatalf("configure %+v send %v want size %+v", v, send, want)
61	}
62	if v.Visible || v.Activated || !v.Captured || v.Output != "A" {
63		t.Fatalf("captured hidden window must be sized, unfocused, physically invisible: %+v", v)
64	}
65	c.configures.mark(v)
66	if _, send := c.configures.nextWithCapture(phys, tt, cp); send {
67		t.Fatal("unchanged captured configure sent again")
68	}
69	// The session stops: the window is only hidden again, keeps its size and drops Captured.
70	c.configures.cw.reset()
71	cp, tt = c.captureConfigure(sc, phys, t0)
72	if cp != nil {
73		t.Fatal("placement kept after the session")
74	}
75	v2, send := c.configures.nextWithCapture(phys, tt, cp)
76	if !send || v2.Captured || v2.Visible || v2.Width != v.Width || v2.Height != v.Height {
77		t.Fatalf("released window %+v send %v after %+v", v2, send, v)
78	}
79}
80
81func TestCaptureConfigureLeavesOtherWindowsAlone(t *testing.T) {
82	c, _ := hiddenCapture(t)
83	sc := c.screens[0]
84	phys := Placement{ID: 99, Hidden: true}
85	t0 := configureTarget{output: "A", area: sc.mon.Frame()}
86	if cp, tt := c.captureConfigure(sc, phys, t0); cp != nil || tt != t0 {
87		t.Fatalf("window of another workspace captured: %+v", cp)
88	}
89	v, _ := c.configures.nextWithCapture(phys, t0, nil)
90	if v.Captured || v.Visible || v.Width != 0 {
91		t.Fatalf("plain hidden configure %+v", v)
92	}
93}
94
95func TestCaptureSceneRebasesOnViewport(t *testing.T) {
96	c, ws := hiddenCapture(t)
97	f := ws.Output
98	if f != (Rect{X: 100, Y: 50, W: 100, H: 100}) {
99		t.Fatalf("viewport %+v", f)
100	}
101	c.popups[10] = &popupState{id: 10, parent: 1, rect: Rect{X: 3, Y: 4, W: 10, H: 8}, mapped: true}
102	c.popupOrder = append(c.popupOrder, 10)
103	s := c.captureScene(7)
104	if s == nil || s.OutputWidth != 100 || s.OutputHeight != 100 || s.WorkspaceClip != (Rect{W: 100, H: 100}) || s.Seq != 7 || s.Output != "A" {
105		t.Fatalf("scene %+v", s)
106	}
107	if s.Capture != nil || s.CaptureScene != nil || len(s.Layers) != 0 || s.Background != "#102030" || s.Border.Width != 2 {
108		t.Fatalf("capture scene must be flat, layerless and use the normal config: %+v", s)
109	}
110	byID := map[WindowID]ports.SceneWindow{}
111	for _, w := range s.Windows {
112		byID[w.ID] = w
113	}
114	for _, p := range ws.Layout() {
115		w := byID[p.ID]
116		if want := (Rect{X: p.Rect.X - f.X, Y: p.Rect.Y - f.Y, W: p.Rect.W, H: p.Rect.H}); w.Rect != want || w.Focused {
117			t.Fatalf("window %d rect %+v want %+v focused %v", p.ID, w.Rect, want, w.Focused)
118		}
119		if w.Rect.X < 0 || w.Rect.Y < 0 || w.Rect.X+w.Rect.W > 100 || w.Rect.Y+w.Rect.H > 100 {
120			t.Fatalf("window %d outside its viewport: %+v", p.ID, w.Rect)
121		}
122	}
123	client := c.clientRect(ws.Layout()[0])
124	pop := byID[10]
125	if !pop.Popup || pop.Rect != (Rect{X: client.X - f.X + 3, Y: client.Y - f.Y + 4, W: 10, H: 8}) {
126		t.Fatalf("popup %+v client %+v", pop, client)
127	}
128	for _, sep := range s.Separators {
129		if sep.Rect.X < 0 || sep.Rect.Y < 0 || sep.Rect.X+sep.Rect.W > 100 || sep.Rect.Y+sep.Rect.H > 100 {
130			t.Fatalf("separator outside the viewport: %+v", sep)
131		}
132	}
133	c.configures.cw.reset()
134	if c.captureScene(8) != nil {
135		t.Fatal("scene without a captured workspace")
136	}
137}
138
139func TestCapturedPopupsSurviveButHiddenWindowsTakeNoInput(t *testing.T) {
140	c, _ := hiddenCapture(t)
141	c.popups[10] = &popupState{id: 10, parent: 1, rect: Rect{X: 3, Y: 4, W: 10, H: 8}, mapped: true}
142	c.popupOrder = append(c.popupOrder, 10)
143	if _, _, ok := c.windowRect(10); ok {
144		t.Fatal("captured popup is physically on screen")
145	}
146	if _, _, ok := c.windowRect(1); ok || c.visible(1) {
147		t.Fatal("captured window is physically on screen")
148	}
149	if id, _, _ := c.hit(150, 100); id != 0 {
150		t.Fatalf("pointer hit hidden window %d", id)
151	}
152	if sc := c.screens[0]; len(c.scenePopups(sc)) != 0 {
153		t.Fatal("captured popup drawn on the physical scene")
154	}
155	if err := c.closeHiddenPopups(context.Background()); err != nil {
156		t.Fatal(err)
157	}
158	if c.popups[10] == nil {
159		t.Fatal("popup of a captured window dismissed")
160	}
161	c.configures.cw.reset()
162	if err := c.closeHiddenPopups(context.Background()); err != nil {
163		t.Fatal(err)
164	}
165	if c.popups[10] != nil {
166		t.Fatal("popup of a hidden window kept after the session")
167	}
168}
169
170func TestCaptureWorkspaceReusesStorage(t *testing.T) {
171	c, ws := hiddenCapture(t)
172	sc := c.screens[0]
173	cw := &c.configures.cw
174	layoutAllocs := testing.AllocsPerRun(50, func() { _ = ws.Layout() })
175	loadAllocs := testing.AllocsPerRun(50, func() { cw.load(sc, ws) })
176	if loadAllocs > layoutAllocs {
177		t.Fatalf("load allocates %v, more than the layout it wraps (%v)", loadAllocs, layoutAllocs)
178	}
179	if got := testing.AllocsPerRun(50, func() {
180		cw.reset()
181		cw.load(sc, ws)
182	}); got > layoutAllocs {
183		t.Fatalf("reset+load allocates %v, layout %v", got, layoutAllocs)
184	}
185}
186
187// While the captured workspace itself slides, the live (Active) session keeps
188// tracking it in its own viewport: its windows stay captured and its popups
189// stay, and both are forgotten when the session ends.
190func TestCapturedWorkspaceStaysCapturedWhileSliding(t *testing.T) {
191	c, ws, _ := hiddenCaptureCommands(t)
192	c.configures.cw.reset()
193	c.capt.sessions = []*capSession{{open: ports.CaptureSessionOpen{ID: 1, Workspace: ws.ID}}}
194	c.popups[10] = &popupState{id: 10, parent: 1, rect: Rect{X: 3, Y: 4, W: 10, H: 8}, mapped: true}
195	c.popupOrder = append(c.popupOrder, 10)
196	ws.motion = newMotion(viewSpring(10, 0), time.Time{})
197	ctx := context.Background()
198	if err := c.publish(ctx); err != nil {
199		t.Fatal(err)
200	}
201	if v := c.capt.sessions[0].last; !v.Hidden || !v.Active || v.Reason.Terminal() {
202		t.Fatalf("session state %+v", v)
203	}
204	if !c.configures.cw.active() || c.popups[10] == nil {
205		t.Fatal("sliding session lost its captured workspace or popup")
206	}
207	if v, ok := c.configures.sent[1]; !ok || !v.Captured || v.Visible || v.Activated || v.Width <= 0 {
208		t.Fatalf("window during the slide %+v (sent %v)", v, ok)
209	}
210	ws.motion = nil
211	if err := c.publish(ctx); err != nil {
212		t.Fatal(err)
213	}
214	if !c.configures.cw.active() {
215		t.Fatalf("captured workspace lost after the slide: %+v", c.capt.sessions[0].last)
216	}
217	// The session ends: the captured workspace is forgotten and its popup goes.
218	c.captureClose(1)
219	if err := c.publish(ctx); err != nil {
220		t.Fatal(err)
221	}
222	if c.configures.cw.active() || c.popups[10] != nil {
223		t.Fatal("captured workspace or popup kept after the session")
224	}
225	if v, ok := c.configures.sent[1]; !ok || v.Captured || v.Visible || v.Width <= 0 {
226		t.Fatalf("window after the session %+v (sent %v)", v, ok)
227	}
228}
229
230// In the overview the captured workspace's windows keep their real size and
231// their physical state; nothing is sized as a thumbnail.
232func TestCapturedWorkspaceInOverviewKeepsRealSize(t *testing.T) {
233	c, ws, _ := hiddenCaptureCommands(t)
234	c.configures.cw.reset()
235	m := c.screens[0].mon
236	m.Focus(0)
237	if m.Current() != ws {
238		t.Fatal("workspace not current")
239	}
240	ctx := context.Background()
241	if err := c.publish(ctx); err != nil {
242		t.Fatal(err)
243	}
244	before := map[WindowID]ports.ConfigureWindow{}
245	for _, p := range ws.Layout() {
246		v, ok := c.configures.sent[p.ID]
247		if !ok || v.Width <= 0 || !v.Visible || v.Captured {
248			t.Fatalf("window %d before the overview %+v (sent %v)", p.ID, v, ok)
249		}
250		before[p.ID] = v
251	}
252	m.ToggleOverview()
253	c.capt.sessions = []*capSession{{open: ports.CaptureSessionOpen{ID: 1, Workspace: ws.ID}}}
254	if err := c.publish(ctx); err != nil {
255		t.Fatal(err)
256	}
257	if !c.configures.cw.active() {
258		t.Fatal("overview did not track the captured workspace")
259	}
260	for _, p := range ws.Layout() {
261		v, ok := c.configures.sent[p.ID]
262		if !ok {
263			t.Fatalf("window %d has no configure in the overview", p.ID)
264		}
265		// A preview keeps the client's real size and its physical state; it
266		// is on screen, so it is not Captured.
267		if v.Width != before[p.ID].Width || v.Height != before[p.ID].Height || v.Width != c.clientRect(p).W || v.Height != c.clientRect(p).H {
268			t.Fatalf("window %d configured %dx%d in the overview, real %dx%d", p.ID, v.Width, v.Height, c.clientRect(p).W, c.clientRect(p).H)
269		}
270		if !v.Visible || v.Captured || v.Activated != before[p.ID].Activated {
271			t.Fatalf("window %d shown as a preview but %+v, was %+v", p.ID, v, before[p.ID])
272		}
273	}
274}

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