feat/data-dnd e05cb15

layers_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// laterClock moves fullscreenGrace forward on each Now: fullscreen
13// requests are past the grace.
14func laterClock(t *testing.T) *portsmocks.MockClock {
15	t.Helper()
16	now := time.Unix(0, 0)
17	clock := portsmocks.NewMockClock(t)
18	clock.EXPECT().Now().RunAndReturn(func() time.Time {
19		now = now.Add(fullscreenGrace)
20		return now
21	}).Maybe()
22	return clock
23}
24
25func TestArrangeLayers(t *testing.T) {
26	bar := ports.LayerSurface{ID: 1, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30}
27	tests := []struct {
28		name   string
29		layers []ports.LayerSurface
30		rects  []ports.Rect
31		usable ports.Rect
32	}{
33		{"top", []ports.LayerSurface{bar}, []ports.Rect{{W: 1920, H: 30}}, ports.Rect{Y: 30, W: 1920, H: 1050}},
34		{"bottom margin", []ports.LayerSurface{{Anchor: ports.AnchorBottom, Width: 100, Height: 20, ExclusiveZone: 20, Margin: [4]int32{0, 0, 5, 0}}}, []ports.Rect{{X: 910, Y: 1055, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1055}},
35		{"left dock", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorTop | ports.AnchorBottom, Width: 40, Height: 1080, ExclusiveZone: 40}}, []ports.Rect{{W: 40, H: 1080}}, ports.Rect{X: 40, W: 1880, H: 1080}},
36		{"center", []ports.LayerSurface{{Width: 100, Height: 40, Layer: ports.LayerOverlay}}, []ports.Rect{{X: 910, Y: 520, W: 100, H: 40}}, ports.Rect{W: 1920, H: 1080}},
37		{"opposing horizontal anchors with margins", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorRight, Width: 100, Height: 20, Margin: [4]int32{0, 30, 0, 10}}}, []ports.Rect{{X: 900, Y: 530, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1080}},
38		{"top only", []ports.LayerSurface{{Anchor: ports.AnchorTop, Width: 100, Height: 20, Margin: [4]int32{7, 0, 0, 0}}}, []ports.Rect{{X: 910, Y: 7, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1080}},
39		{"full span between margins", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorRight | ports.AnchorTop, Height: 20, Margin: [4]int32{7, 30, 0, 10}}}, []ports.Rect{{X: 10, Y: 7, W: 1880, H: 20}}, ports.Rect{W: 1920, H: 1080}},
40		{"opposing vertical anchors with margins", []ports.LayerSurface{{Anchor: ports.AnchorTop | ports.AnchorBottom, Width: 100, Height: 20, Margin: [4]int32{10, 0, 30, 0}}}, []ports.Rect{{X: 910, Y: 520, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1080}},
41		{"minus one", []ports.LayerSurface{bar, {Layer: ports.LayerTop, Anchor: ports.AnchorTop, Width: 100, Height: 20, ExclusiveZone: -1}}, []ports.Rect{{W: 1920, H: 30}, {X: 910, W: 100, H: 20}}, ports.Rect{Y: 30, W: 1920, H: 1050}},
42		{"stack", []ports.LayerSurface{bar, {Layer: ports.LayerTop, Anchor: bar.Anchor, Width: 1920, Height: 20, ExclusiveZone: 20}}, []ports.Rect{{W: 1920, H: 30}, {Y: 30, W: 1920, H: 20}}, ports.Rect{Y: 50, W: 1920, H: 1030}},
43	}
44	for _, tt := range tests {
45		t.Run(tt.name, func(t *testing.T) {
46			got, u := arrangeLayers(1920, 1080, tt.layers)
47			if u != tt.usable || len(got) != len(tt.rects) {
48				t.Fatalf("got %v %v", got, u)
49			}
50			for i, r := range tt.rects {
51				if got[i].Rect != r {
52					t.Fatalf("rect %d: %v want %v", i, got[i].Rect, r)
53				}
54			}
55		})
56	}
57	layers := []ports.LayerSurface{{ID: 1, Layer: ports.LayerOverlay}, {ID: 2, Layer: ports.LayerBottom}, {ID: 3, Layer: ports.LayerBackground}, {ID: 4, Layer: ports.LayerTop}, {ID: 5, Layer: ports.LayerBottom}}
58	got, _ := arrangeLayers(100, 100, layers)
59	for i, id := range []ports.WindowID{3, 2, 5, 4, 1} {
60		if got[i].ID != id {
61			t.Fatalf("order: %v", got)
62		}
63	}
64}
65
66func TestLayerChangedChannels(t *testing.T) {
67	client := make(chan ports.ClientEvent)
68	output := make(chan ports.OutputEvent)
69	commands := make(chan ports.ClientCommand, 16)
70	scenes := make(chan []ports.Scene, 1)
71	cfg := ports.Config{}
72	cfg.Keyboard.CmdKey = "super"
73	cfg.Layout.MaxColumns = 2
74	c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}, Options{})
75	if err != nil {
76		t.Fatal(err)
77	}
78	ctx, cancel := context.WithCancel(context.Background())
79	defer cancel()
80	done := make(chan struct{})
81	go func() { defer close(done); _ = c.Run(ctx) }()
82	recv := func() ports.Scene {
83		t.Helper()
84		select {
85		case s := <-scenes:
86			return s[0]
87		case <-time.After(time.Second):
88			t.Fatal("scene timeout")
89			return ports.Scene{}
90		}
91	}
92	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 1920, Height: 1080}}
93	recv()
94	client <- ports.WindowMapped{ID: 1}
95	before := recv()
96	for len(commands) > 0 {
97		<-commands
98	}
99	bar := ports.LayerSurface{ID: 2, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30}
100	client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
101	scene := recv()
102	if len(scene.Layers) != 1 || scene.Layers[0].Rect != (ports.Rect{W: 1920, H: 30}) || len(scene.Windows) != 1 || scene.Windows[0].Rect.Y < 30 {
103		t.Fatalf("scene: %v", scene)
104	}
105	if scene.Windows[0].Rect.H != before.Windows[0].Rect.H-30 {
106		t.Fatalf("height: %v vs %v", scene.Windows, before.Windows)
107	}
108	select {
109	case v := <-commands:
110		cfg, ok := v.(ports.ConfigureWindow)
111		if !ok || cfg.ID != 1 || cfg.Height != scene.Windows[0].Rect.H {
112			t.Fatalf("configure: %v", v)
113		}
114	case <-time.After(time.Second):
115		t.Fatal("configure timeout")
116	}
117	scene.Layers[0].Rect.Y = 99
118	client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
119	if next := recv(); next.Layers[0].Rect.Y != 0 {
120		t.Fatalf("scene alias: %v", next)
121	}
122	cancel()
123	<-done
124}
125
126// In scroll overflow, a fullscreen column the user scrolled off no longer
127// covers the output: the layers show again.
128func TestScrolledOffFullscreenShowsLayers(t *testing.T) {
129	m := newMonitor("", "")
130	m.SetOutput(100, 80)
131	m.AddWindow(1)
132	m.SetFullscreen(1, true)
133	m.AddWindow(2)
134	sc := &screen{mon: m}
135	bar := ports.SceneLayer{ID: 6, Layer: ports.LayerTop}
136	if shown(sc, bar) {
137		t.Fatal("bar shown over fullscreen")
138	}
139	m.Current().FocusID(2)
140	if !shown(sc, bar) {
141		t.Fatal("bar hidden with the fullscreen column scrolled off")
142	}
143}
144
145// In the overview a fullscreen window is a preview: the layers show.
146func TestOverviewOverFullscreenShowsLayers(t *testing.T) {
147	m := newMonitor("", "")
148	m.SetOutput(100, 80)
149	m.AddWindow(1)
150	m.SetFullscreen(1, true)
151	sc := &screen{mon: m}
152	bar := ports.SceneLayer{ID: 6, Layer: ports.LayerTop}
153	m.ToggleOverview()
154	if !shown(sc, bar) {
155		t.Fatal("bar hidden in the overview")
156	}
157	m.ToggleOverview()
158	if shown(sc, bar) {
159		t.Fatal("bar shown over fullscreen after the overview")
160	}
161}
162
163// A fullscreen window is exclusive: the scene carries only the background
164// and a layer taking the keyboard exclusively (a locker), so the output
165// can scan the window out.
166func TestFullscreenSceneLayers(t *testing.T) {
167	client := make(chan ports.ClientEvent)
168	output := make(chan ports.OutputEvent)
169	commands := make(chan ports.ClientCommand, 64)
170	scenes := make(chan []ports.Scene, 1)
171	cfg := ports.Config{}
172	cfg.Keyboard.CmdKey = "super"
173	cfg.Layout.MaxColumns = 2
174	c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}, Options{Clock: laterClock(t)})
175	if err != nil {
176		t.Fatal(err)
177	}
178	ctx, cancel := context.WithCancel(context.Background())
179	defer cancel()
180	done := make(chan struct{})
181	go func() { defer close(done); _ = c.Run(ctx) }()
182	go func() {
183		for range commands {
184		}
185	}()
186	recv := func() ports.Scene {
187		t.Helper()
188		select {
189		case s := <-scenes:
190			return s[0]
191		case <-time.After(time.Second):
192			t.Fatal("scene timeout")
193			return ports.Scene{}
194		}
195	}
196	layers := func(s ports.Scene) []ports.WindowID {
197		var ids []ports.WindowID
198		for _, l := range s.Layers {
199			ids = append(ids, l.ID)
200		}
201		return ids
202	}
203	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
204	recv()
205	client <- ports.WindowMapped{ID: 1}
206	recv()
207	all := []ports.LayerSurface{
208		{ID: 5, Layer: ports.LayerBackground, Anchor: 15},
209		{ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 10},
210		{ID: 7, Layer: ports.LayerOverlay, Width: 20, Height: 10},
211	}
212	client <- ports.LayerChanged{Layers: all}
213	if got := layers(recv()); len(got) != 3 {
214		t.Fatalf("layers %v", got)
215	}
216	client <- ports.WindowFullscreenRequest{ID: 1, Fullscreen: true}
217	if got := layers(recv()); len(got) != 1 || got[0] != 5 {
218		t.Fatalf("fullscreen layers %v, want the background", got)
219	}
220	locker := ports.LayerSurface{ID: 8, Layer: ports.LayerOverlay, Anchor: 15, Keyboard: 1}
221	client <- ports.LayerChanged{Layers: append(all, locker)}
222	if got := layers(recv()); len(got) != 2 || got[1] != 8 {
223		t.Fatalf("fullscreen layers %v, want the background and the locker", got)
224	}
225	cancel()
226	<-done
227	close(commands)
228}

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