detached ffe1c25

test(wayland): reuse the surface mapper and shm buffer in the damage log test

TestClientDamageLog built its own xdg surface and wl_shm pool. It now maps a window with surfaceMapper (new surfaceMapperAt binds wl_compositor at a given version, since damage_buffer needs 4) and takes a 16x16 buffer from shmBuffer's new sized variant. The LogDamage on/off loop is split into subtests. The mapping commit now logs one extra damage entry that the test skips.
commit
ffe1c25ef556c532a464751bd6d5533736fa6519
author
brice · · 2026-10-14 03:43 UTC
committer
brice · · 2026-10-14 04:34 UTC
parent
fa55dc9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +70 -87
M internal/adapters/wayland/damage_log_test.go +54 -82
@@ -1,105 +1,77 @@
11 package wayland
22
33 import (
4+ "fmt"
45 "strings"
56 "testing"
67 "time"
78
89 "github.com/bnema/go-wayland-bindings/server/wayland"
9- "github.com/bnema/go-wayland-bindings/server/xdgshell"
1010 "github.com/bnema/zerowrap"
11- "golang.org/x/sys/unix"
1211 )
1312
1413 // --debug=wayland-damage logs one "client damage" entry per fresh commit
1514 // with the damage in buffer pixels; without it nothing is logged.
1615 func TestClientDamageLog(t *testing.T) {
1716 for _, on := range []bool{true, false} {
18- var out lockedBuffer
19- log := zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &out}).WithField("component", "wayland")
20- s, events, _, contents, dir := contentServerWith(t, Options{LogDamage: on}, log)
21- c := protocolClient(t, s, dir)
22- comp := bindVersion(t, c, "wl_compositor", 4)
23- wm := bindProtocol(t, c, "xdg_wm_base")
24- shm := bindProtocol(t, c, "wl_shm")
25- registerProtocol(t, c, shm)
26- surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID()
27- requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
28- registerProtocol(t, c, surf)
29- requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)
30- serials := make(chan uint32, 8)
31- xp := &configureProxy{serial: serials}
32- xp.SetID(xdg)
33- registerWireProxy(c, xp)
34- registerProtocol(t, c, top)
35- requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
36- requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
37- if err := c.Roundtrip(); err != nil {
38- t.Fatal(err)
39- }
40- requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials)
41- fd, err := unix.MemfdCreate("damagelog", 0)
42- if err != nil {
43- t.Fatal(err)
44- }
45- defer unix.Close(fd)
46- if err := unix.Ftruncate(fd, 16*16*4); err != nil {
47- t.Fatal(err)
48- }
49- pool, buf := c.AllocateID(), c.AllocateID()
50- registerProtocol(t, c, pool)
51- if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(16*16*4)); err != nil {
52- t.Fatal(err)
53- }
54- registerProtocol(t, c, buf)
55- requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(16), int32(16), int32(64), uint32(0))
56- commit := func(rects ...[4]int32) {
57- t.Helper()
58- requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
59- for _, r := range rects {
60- requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, r[0], r[1], r[2], r[3])
17+ t.Run(fmt.Sprintf("LogDamage=%v", on), func(t *testing.T) {
18+ var out lockedBuffer
19+ log := zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &out}).WithField("component", "wayland")
20+ s, events, _, contents, dir := contentServerWith(t, Options{LogDamage: on}, log)
21+ c := protocolClient(t, s, dir)
22+ // A mapped window (its 1×1 first buffer is not under test), then
23+ // the 16×16 buffer the commits below attach.
24+ _, surf, _ := surfaceMapperAt(t, c, events, 4)()
25+ drainContents(contents)
26+ buf := shmBufferSized(t, c, 16, 16)
27+ commit := func(rects ...[4]int32) {
28+ t.Helper()
29+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
30+ for _, r := range rects {
31+ requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, r[0], r[1], r[2], r[3])
32+ }
33+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
34+ if err := c.Roundtrip(); err != nil {
35+ t.Fatal(err)
36+ }
37+ select {
38+ case <-contents:
39+ case <-time.After(2 * time.Second):
40+ t.Fatal("no content")
41+ }
6142 }
62- requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
63- if err := c.Roundtrip(); err != nil {
64- t.Fatal(err)
43+ // First buffer: a new size, full. Then a small rect, then one
44+ // covering the whole buffer.
45+ commit([4]int32{0, 0, 4, 4})
46+ commit([4]int32{2, 3, 4, 5})
47+ commit([4]int32{0, 0, 16, 16})
48+ var damage []string
49+ for _, l := range strings.Split(strings.TrimSpace(out.String()), "\n") {
50+ if strings.Contains(l, `"client damage"`) {
51+ damage = append(damage, l)
52+ }
6553 }
66- select {
67- case <-contents:
68- case <-time.After(2 * time.Second):
69- t.Fatal("no content")
54+ if !on {
55+ if len(damage) != 0 {
56+ t.Fatalf("logged with LogDamage off: %q", damage)
57+ }
58+ return
7059 }
71- }
72- // First buffer: a new size, full. Then a small rect, then one
73- // covering the whole buffer.
74- commit([4]int32{0, 0, 4, 4})
75- mapped(t, events, 2*time.Second)
76- commit([4]int32{2, 3, 4, 5})
77- commit([4]int32{0, 0, 16, 16})
78- lines := strings.Split(strings.TrimSpace(out.String()), "\n")
79- var damage []string
80- for _, l := range lines {
81- if strings.Contains(l, `"client damage"`) {
82- damage = append(damage, l)
60+ // The mapping commit logged one entry of its own.
61+ if len(damage) != 4 {
62+ t.Fatalf("%d client damage entries, want 4 (mapping commit and 3 commits): %q", len(damage), damage)
8363 }
84- }
85- if !on {
86- if len(damage) != 0 {
87- t.Fatalf("logged with LogDamage off: %q", damage)
64+ damage = damage[1:]
65+ for i, want := range []string{`"full_buffer":true`, `"full_buffer":false`, `"full_buffer":true`} {
66+ if !strings.Contains(damage[i], want) {
67+ t.Errorf("entry %d lacks %s: %s", i, want, damage[i])
68+ }
8869 }
89- continue
90- }
91- if len(damage) != 3 {
92- t.Fatalf("%d client damage entries, want 3: %q", len(damage), damage)
93- }
94- for i, want := range []string{`"full_buffer":true`, `"full_buffer":false`, `"full_buffer":true`} {
95- if !strings.Contains(damage[i], want) {
96- t.Errorf("entry %d lacks %s: %s", i, want, damage[i])
70+ for _, want := range []string{`"component":"wayland"`, `"bw":16`, `"bh":16`, `"rects":1`, `"window":`, `"first_rects":[{"x":2,"y":3,"w":4,"h":5}]`} {
71+ if !strings.Contains(damage[1], want) {
72+ t.Errorf("entry 1 lacks %s: %s", want, damage[1])
73+ }
9774 }
98- }
99- for _, want := range []string{`"component":"wayland"`, `"bw":16`, `"bh":16`, `"rects":1`, `"window":`, `"first_rects":[{"x":2,"y":3,"w":4,"h":5}]`} {
100- if !strings.Contains(damage[1], want) {
101- t.Errorf("entry 1 lacks %s: %s", want, damage[1])
102- }
103- }
75+ })
10476 }
10577 }
@@ -1,105 +1,77 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4+ "fmt"
4 "strings"5 "strings"
5 "testing"6 "testing"
6 "time"7 "time"
7 8
8 "github.com/bnema/go-wayland-bindings/server/wayland"9 "github.com/bnema/go-wayland-bindings/server/wayland"
9- "github.com/bnema/go-wayland-bindings/server/xdgshell"
10 "github.com/bnema/zerowrap"10 "github.com/bnema/zerowrap"
11- "golang.org/x/sys/unix"
12 )11 )
13 12
14 // --debug=wayland-damage logs one "client damage" entry per fresh commit13 // --debug=wayland-damage logs one "client damage" entry per fresh commit
15 // with the damage in buffer pixels; without it nothing is logged.14 // with the damage in buffer pixels; without it nothing is logged.
16 func TestClientDamageLog(t *testing.T) {15 func TestClientDamageLog(t *testing.T) {
17 for _, on := range []bool{true, false} {16 for _, on := range []bool{true, false} {
18- var out lockedBuffer17+ t.Run(fmt.Sprintf("LogDamage=%v", on), func(t *testing.T) {
19- log := zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &out}).WithField("component", "wayland")18+ var out lockedBuffer
20- s, events, _, contents, dir := contentServerWith(t, Options{LogDamage: on}, log)19+ log := zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: &out}).WithField("component", "wayland")
21- c := protocolClient(t, s, dir)20+ s, events, _, contents, dir := contentServerWith(t, Options{LogDamage: on}, log)
22- comp := bindVersion(t, c, "wl_compositor", 4)21+ c := protocolClient(t, s, dir)
23- wm := bindProtocol(t, c, "xdg_wm_base")22+ // A mapped window (its 1×1 first buffer is not under test), then
24- shm := bindProtocol(t, c, "wl_shm")23+ // the 16×16 buffer the commits below attach.
25- registerProtocol(t, c, shm)24+ _, surf, _ := surfaceMapperAt(t, c, events, 4)()
26- surf, xdg, top := c.AllocateID(), c.AllocateID(), c.AllocateID()25+ drainContents(contents)
27- requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)26+ buf := shmBufferSized(t, c, 16, 16)
28- registerProtocol(t, c, surf)27+ commit := func(rects ...[4]int32) {
29- requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surf)28+ t.Helper()
30- serials := make(chan uint32, 8)29+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
31- xp := &configureProxy{serial: serials}30+ for _, r := range rects {
32- xp.SetID(xdg)31+ requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, r[0], r[1], r[2], r[3])
33- registerWireProxy(c, xp)32+ }
34- registerProtocol(t, c, top)33+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
35- requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)34+ if err := c.Roundtrip(); err != nil {
36- requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)35+ t.Fatal(err)
37- if err := c.Roundtrip(); err != nil {36+ }
38- t.Fatal(err)37+ select {
39- }38+ case <-contents:
40- requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, <-serials)39+ case <-time.After(2 * time.Second):
41- fd, err := unix.MemfdCreate("damagelog", 0)40+ t.Fatal("no content")
42- if err != nil {41+ }
43- t.Fatal(err)
44- }
45- defer unix.Close(fd)
46- if err := unix.Ftruncate(fd, 16*16*4); err != nil {
47- t.Fatal(err)
48- }
49- pool, buf := c.AllocateID(), c.AllocateID()
50- registerProtocol(t, c, pool)
51- if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(16*16*4)); err != nil {
52- t.Fatal(err)
53- }
54- registerProtocol(t, c, buf)
55- requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(16), int32(16), int32(64), uint32(0))
56- commit := func(rects ...[4]int32) {
57- t.Helper()
58- requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
59- for _, r := range rects {
60- requestProtocol(t, c, surf, wayland.SurfaceRequestDamageBuffer, r[0], r[1], r[2], r[3])
61 }42 }
62- requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)43+ // First buffer: a new size, full. Then a small rect, then one
63- if err := c.Roundtrip(); err != nil {44+ // covering the whole buffer.
64- t.Fatal(err)45+ commit([4]int32{0, 0, 4, 4})
46+ commit([4]int32{2, 3, 4, 5})
47+ commit([4]int32{0, 0, 16, 16})
48+ var damage []string
49+ for _, l := range strings.Split(strings.TrimSpace(out.String()), "\n") {
50+ if strings.Contains(l, `"client damage"`) {
51+ damage = append(damage, l)
52+ }
65 }53 }
66- select {54+ if !on {
67- case <-contents:55+ if len(damage) != 0 {
68- case <-time.After(2 * time.Second):56+ t.Fatalf("logged with LogDamage off: %q", damage)
69- t.Fatal("no content")57+ }
58+ return
70 }59 }
71- }60+ // The mapping commit logged one entry of its own.
72- // First buffer: a new size, full. Then a small rect, then one61+ if len(damage) != 4 {
73- // covering the whole buffer.62+ t.Fatalf("%d client damage entries, want 4 (mapping commit and 3 commits): %q", len(damage), damage)
74- commit([4]int32{0, 0, 4, 4})
75- mapped(t, events, 2*time.Second)
76- commit([4]int32{2, 3, 4, 5})
77- commit([4]int32{0, 0, 16, 16})
78- lines := strings.Split(strings.TrimSpace(out.String()), "\n")
79- var damage []string
80- for _, l := range lines {
81- if strings.Contains(l, `"client damage"`) {
82- damage = append(damage, l)
83 }63 }
84- }64+ damage = damage[1:]
85- if !on {65+ for i, want := range []string{`"full_buffer":true`, `"full_buffer":false`, `"full_buffer":true`} {
86- if len(damage) != 0 {66+ if !strings.Contains(damage[i], want) {
87- t.Fatalf("logged with LogDamage off: %q", damage)67+ t.Errorf("entry %d lacks %s: %s", i, want, damage[i])
68+ }
88 }69 }
89- continue70+ for _, want := range []string{`"component":"wayland"`, `"bw":16`, `"bh":16`, `"rects":1`, `"window":`, `"first_rects":[{"x":2,"y":3,"w":4,"h":5}]`} {
90- }71+ if !strings.Contains(damage[1], want) {
91- if len(damage) != 3 {72+ t.Errorf("entry 1 lacks %s: %s", want, damage[1])
92- t.Fatalf("%d client damage entries, want 3: %q", len(damage), damage)73+ }
93- }
94- for i, want := range []string{`"full_buffer":true`, `"full_buffer":false`, `"full_buffer":true`} {
95- if !strings.Contains(damage[i], want) {
96- t.Errorf("entry %d lacks %s: %s", i, want, damage[i])
97 }74 }
98- }75+ })
99- for _, want := range []string{`"component":"wayland"`, `"bw":16`, `"bh":16`, `"rects":1`, `"window":`, `"first_rects":[{"x":2,"y":3,"w":4,"h":5}]`} {
100- if !strings.Contains(damage[1], want) {
101- t.Errorf("entry 1 lacks %s: %s", want, damage[1])
102- }
103- }
104 }76 }
105 }77 }
M internal/adapters/wayland/fifo_test.go +8 -4
@@ -29,7 +29,10 @@ func nextContent(t *testing.T, contents <-chan ports.SurfaceContent, id ports.Wi
2929 }
3030
3131 // shmBuffer creates a 1x1 wl_shm buffer.
32-func shmBuffer(t *testing.T, c *wlturbo.Display) uint32 {
32+func shmBuffer(t *testing.T, c *wlturbo.Display) uint32 { return shmBufferSized(t, c, 1, 1) }
33+
34+// shmBufferSized is a w×h ARGB8888 wl_shm buffer.
35+func shmBufferSized(t *testing.T, c *wlturbo.Display, w, h int32) uint32 {
3336 t.Helper()
3437 shm := bindProtocol(t, c, "wl_shm")
3538 registerProtocol(t, c, shm)
@@ -38,16 +41,17 @@ func shmBuffer(t *testing.T, c *wlturbo.Display) uint32 {
3841 t.Fatal(err)
3942 }
4043 defer unix.Close(fd)
41- if err := unix.Ftruncate(fd, 4); err != nil {
44+ size := w * h * 4
45+ if err := unix.Ftruncate(fd, int64(size)); err != nil {
4246 t.Fatal(err)
4347 }
4448 pool, buffer := c.AllocateID(), c.AllocateID()
4549 registerProtocol(t, c, pool)
4650 registerProtocol(t, c, buffer)
47- if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
51+ if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, size); err != nil {
4852 t.Fatal(err)
4953 }
50- requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
54+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), w, h, w*4, uint32(0))
5155 return buffer
5256 }
5357
@@ -29,7 +29,10 @@ func nextContent(t *testing.T, contents <-chan ports.SurfaceContent, id ports.Wi
29 }29 }
30 30
31 // shmBuffer creates a 1x1 wl_shm buffer.31 // shmBuffer creates a 1x1 wl_shm buffer.
32-func shmBuffer(t *testing.T, c *wlturbo.Display) uint32 {32+func shmBuffer(t *testing.T, c *wlturbo.Display) uint32 { return shmBufferSized(t, c, 1, 1) }
33+
34+// shmBufferSized is a w×h ARGB8888 wl_shm buffer.
35+func shmBufferSized(t *testing.T, c *wlturbo.Display, w, h int32) uint32 {
33 t.Helper()36 t.Helper()
34 shm := bindProtocol(t, c, "wl_shm")37 shm := bindProtocol(t, c, "wl_shm")
35 registerProtocol(t, c, shm)38 registerProtocol(t, c, shm)
@@ -38,16 +41,17 @@ func shmBuffer(t *testing.T, c *wlturbo.Display) uint32 {
38 t.Fatal(err)41 t.Fatal(err)
39 }42 }
40 defer unix.Close(fd)43 defer unix.Close(fd)
41- if err := unix.Ftruncate(fd, 4); err != nil {44+ size := w * h * 4
45+ if err := unix.Ftruncate(fd, int64(size)); err != nil {
42 t.Fatal(err)46 t.Fatal(err)
43 }47 }
44 pool, buffer := c.AllocateID(), c.AllocateID()48 pool, buffer := c.AllocateID(), c.AllocateID()
45 registerProtocol(t, c, pool)49 registerProtocol(t, c, pool)
46 registerProtocol(t, c, buffer)50 registerProtocol(t, c, buffer)
47- if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {51+ if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, size); err != nil {
48 t.Fatal(err)52 t.Fatal(err)
49 }53 }
50- requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))54+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), w, h, w*4, uint32(0))
51 return buffer55 return buffer
52 }56 }
53 57
M internal/adapters/wayland/protocol_test.go +8 -1
@@ -627,7 +627,14 @@ func toplevelMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.Client
627627 // xdg_surface IDs.
628628 func surfaceMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent) func() (ports.WindowMapped, uint32, uint32) {
629629 t.Helper()
630- comp := bindProtocol(t, c, "wl_compositor")
630+ return surfaceMapperAt(t, c, events, 1)
631+}
632+
633+// surfaceMapperAt is surfaceMapper with wl_compositor bound at compositorVersion
634+// (damage_buffer needs 4).
635+func surfaceMapperAt(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent, compositorVersion uint32) func() (ports.WindowMapped, uint32, uint32) {
636+ t.Helper()
637+ comp := bindVersion(t, c, "wl_compositor", compositorVersion)
631638 wm := bindProtocol(t, c, "xdg_wm_base")
632639 shm := bindProtocol(t, c, "wl_shm")
633640 registerProtocol(t, c, shm)
@@ -627,7 +627,14 @@ func toplevelMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.Client
627 // xdg_surface IDs.627 // xdg_surface IDs.
628 func surfaceMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent) func() (ports.WindowMapped, uint32, uint32) {628 func surfaceMapper(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent) func() (ports.WindowMapped, uint32, uint32) {
629 t.Helper()629 t.Helper()
630- comp := bindProtocol(t, c, "wl_compositor")630+ return surfaceMapperAt(t, c, events, 1)
631+}
632+
633+// surfaceMapperAt is surfaceMapper with wl_compositor bound at compositorVersion
634+// (damage_buffer needs 4).
635+func surfaceMapperAt(t *testing.T, c *wlturbo.Display, events <-chan ports.ClientEvent, compositorVersion uint32) func() (ports.WindowMapped, uint32, uint32) {
636+ t.Helper()
637+ comp := bindVersion(t, c, "wl_compositor", compositorVersion)
631 wm := bindProtocol(t, c, "xdg_wm_base")638 wm := bindProtocol(t, c, "xdg_wm_base")
632 shm := bindProtocol(t, c, "wl_shm")639 shm := bindProtocol(t, c, "wl_shm")
633 registerProtocol(t, c, shm)640 registerProtocol(t, c, shm)

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