detached b094c63

Merge 3d41bc5a00034b81cc0ddeffaafc23d02beddf14 into 74503b06728886efee45120ebb821810347ca353

commit
b094c632bae6bccc28b072f4058362f822a23473
author
ermite · · 2026-10-22 17:02 UTC
merge of
74503b0 + 3d41bc5 · diff against the first parent
signature
unsigned
7 files · +33 -15
M go.mod +1 -1
@@ -6,7 +6,7 @@ require (
66 github.com/bnema/go-wayland-bindings v0.1.0
77 github.com/bnema/kvconf v0.1.2
88 github.com/bnema/neferclient v0.3.0
9- github.com/bnema/nefergui v0.7.2
9+ github.com/bnema/nefergui v0.7.3
1010 github.com/bnema/purego-libwayland v0.8.0
1111 github.com/bnema/wlturbo v0.6.2
1212 github.com/bnema/zerowrap v1.4.1
@@ -6,7 +6,7 @@ require (
6 github.com/bnema/go-wayland-bindings v0.1.06 github.com/bnema/go-wayland-bindings v0.1.0
7 github.com/bnema/kvconf v0.1.27 github.com/bnema/kvconf v0.1.2
8 github.com/bnema/neferclient v0.3.08 github.com/bnema/neferclient v0.3.0
9- github.com/bnema/nefergui v0.7.29+ github.com/bnema/nefergui v0.7.3
10 github.com/bnema/purego-libwayland v0.8.010 github.com/bnema/purego-libwayland v0.8.0
11 github.com/bnema/wlturbo v0.6.211 github.com/bnema/wlturbo v0.6.2
12 github.com/bnema/zerowrap v1.4.112 github.com/bnema/zerowrap v1.4.1
M go.sum +2 -2
@@ -4,8 +4,8 @@ github.com/bnema/kvconf v0.1.2 h1:1hDKL9jkQSBdR9dSKbwTONhUUBfJgAdh5vu+yFHOb70=
44 github.com/bnema/kvconf v0.1.2/go.mod h1:+P2X9I/sLx+Lfir45s9a7WA0GnC22WgKbZWE9LK6lcU=
55 github.com/bnema/neferclient v0.3.0 h1:sEBphi11YpgMOPYHW1OcDtQYA8aD3WSaFILnb8fdp90=
66 github.com/bnema/neferclient v0.3.0/go.mod h1:CzZflJIvinLP8u0rYPjdmAnZjl4Ec2cvdnEGvqFO4Yw=
7-github.com/bnema/nefergui v0.7.2 h1:9gnYhVlMxpBp+cVE+9Uursc/HwPXe/Li16+m4HL/gkU=
8-github.com/bnema/nefergui v0.7.2/go.mod h1:G2iRRleA9bjSohU1RySwRqqxS0BbkPziK407HkuyEBc=
7+github.com/bnema/nefergui v0.7.3 h1:T6NamM17arkan3K38+YF1/flsQ/v4ROSytSNNMz+3M4=
8+github.com/bnema/nefergui v0.7.3/go.mod h1:G2iRRleA9bjSohU1RySwRqqxS0BbkPziK407HkuyEBc=
99 github.com/bnema/purego v0.13.0-bnema.1 h1:fKRdztnvTmRqAjANMoN+utEIH73dvPqVPJh2iZCCAO8=
1010 github.com/bnema/purego v0.13.0-bnema.1/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM=
1111 github.com/bnema/purego-libwayland v0.8.0 h1:e82G1nMre4HvnR1Q8TvdnVNITUStSc+hD3eMwkYjxQY=
@@ -4,8 +4,8 @@ github.com/bnema/kvconf v0.1.2 h1:1hDKL9jkQSBdR9dSKbwTONhUUBfJgAdh5vu+yFHOb70=
4 github.com/bnema/kvconf v0.1.2/go.mod h1:+P2X9I/sLx+Lfir45s9a7WA0GnC22WgKbZWE9LK6lcU=4 github.com/bnema/kvconf v0.1.2/go.mod h1:+P2X9I/sLx+Lfir45s9a7WA0GnC22WgKbZWE9LK6lcU=
5 github.com/bnema/neferclient v0.3.0 h1:sEBphi11YpgMOPYHW1OcDtQYA8aD3WSaFILnb8fdp90=5 github.com/bnema/neferclient v0.3.0 h1:sEBphi11YpgMOPYHW1OcDtQYA8aD3WSaFILnb8fdp90=
6 github.com/bnema/neferclient v0.3.0/go.mod h1:CzZflJIvinLP8u0rYPjdmAnZjl4Ec2cvdnEGvqFO4Yw=6 github.com/bnema/neferclient v0.3.0/go.mod h1:CzZflJIvinLP8u0rYPjdmAnZjl4Ec2cvdnEGvqFO4Yw=
7-github.com/bnema/nefergui v0.7.2 h1:9gnYhVlMxpBp+cVE+9Uursc/HwPXe/Li16+m4HL/gkU=7+github.com/bnema/nefergui v0.7.3 h1:T6NamM17arkan3K38+YF1/flsQ/v4ROSytSNNMz+3M4=
8-github.com/bnema/nefergui v0.7.2/go.mod h1:G2iRRleA9bjSohU1RySwRqqxS0BbkPziK407HkuyEBc=8+github.com/bnema/nefergui v0.7.3/go.mod h1:G2iRRleA9bjSohU1RySwRqqxS0BbkPziK407HkuyEBc=
9 github.com/bnema/purego v0.13.0-bnema.1 h1:fKRdztnvTmRqAjANMoN+utEIH73dvPqVPJh2iZCCAO8=9 github.com/bnema/purego v0.13.0-bnema.1 h1:fKRdztnvTmRqAjANMoN+utEIH73dvPqVPJh2iZCCAO8=
10 github.com/bnema/purego v0.13.0-bnema.1/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM=10 github.com/bnema/purego v0.13.0-bnema.1/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM=
11 github.com/bnema/purego-libwayland v0.8.0 h1:e82G1nMre4HvnR1Q8TvdnVNITUStSc+hD3eMwkYjxQY=11 github.com/bnema/purego-libwayland v0.8.0 h1:e82G1nMre4HvnR1Q8TvdnVNITUStSc+hD3eMwkYjxQY=
M internal/adapters/selection/backdrop_test.go +11 -2
@@ -31,13 +31,22 @@ func TestWithBackdrops(t *testing.T) {
3131
3232 huge := ports.Frame{Width: maxBackdrop + 1, Height: 1, Stride: (maxBackdrop + 1) * 4, Format: ports.XRGB8888}
3333 _, err = New(ports.Screenshot, ports.VideoSettings{}, nil, false).WithBackdrops(map[uint32]ports.Frame{9: huge})
34- assert.ErrorContains(t, err, "at most 4096", "the caller freezes only outputs CanBackdrop accepts")
34+ assert.ErrorContains(t, err, "at most 8192", "the caller freezes only outputs CanBackdrop accepts")
35+
36+ huge = ports.Frame{Width: 7680, Height: 4320, Stride: 7680 * 4, Format: ports.XRGB8888}
37+ _, err = New(ports.Screenshot, ports.VideoSettings{}, nil, false).WithBackdrops(map[uint32]ports.Frame{9: huge})
38+ assert.ErrorContains(t, err, "67108864 bytes")
3539 }
3640
3741 func TestCanBackdrop(t *testing.T) {
3842 assert.True(t, CanBackdrop(ports.Output{Width: 3840, Height: 2160}))
3943 assert.True(t, CanBackdrop(ports.Output{Width: 2160, Height: 4096}))
40- assert.False(t, CanBackdrop(ports.Output{Width: 5120, Height: 2880}))
44+ assert.True(t, CanBackdrop(ports.Output{Width: 5120, Height: 2160}))
45+ assert.True(t, CanBackdrop(ports.Output{Width: 8192, Height: 2048}), "exact byte budget")
46+ assert.False(t, CanBackdrop(ports.Output{Width: 8192, Height: 2049}))
47+ assert.False(t, CanBackdrop(ports.Output{Width: 7680, Height: 4320}))
48+ assert.False(t, CanBackdrop(ports.Output{Width: 8193, Height: 1}))
49+ assert.False(t, CanBackdrop(ports.Output{Width: 1, Height: 8193}))
4150 assert.False(t, CanBackdrop(ports.Output{}), "an unknown mode stays live")
4251 }
4352
@@ -31,13 +31,22 @@ func TestWithBackdrops(t *testing.T) {
31 31
32 huge := ports.Frame{Width: maxBackdrop + 1, Height: 1, Stride: (maxBackdrop + 1) * 4, Format: ports.XRGB8888}32 huge := ports.Frame{Width: maxBackdrop + 1, Height: 1, Stride: (maxBackdrop + 1) * 4, Format: ports.XRGB8888}
33 _, err = New(ports.Screenshot, ports.VideoSettings{}, nil, false).WithBackdrops(map[uint32]ports.Frame{9: huge})33 _, err = New(ports.Screenshot, ports.VideoSettings{}, nil, false).WithBackdrops(map[uint32]ports.Frame{9: huge})
34- assert.ErrorContains(t, err, "at most 4096", "the caller freezes only outputs CanBackdrop accepts")34+ assert.ErrorContains(t, err, "at most 8192", "the caller freezes only outputs CanBackdrop accepts")
35+
36+ huge = ports.Frame{Width: 7680, Height: 4320, Stride: 7680 * 4, Format: ports.XRGB8888}
37+ _, err = New(ports.Screenshot, ports.VideoSettings{}, nil, false).WithBackdrops(map[uint32]ports.Frame{9: huge})
38+ assert.ErrorContains(t, err, "67108864 bytes")
35 }39 }
36 40
37 func TestCanBackdrop(t *testing.T) {41 func TestCanBackdrop(t *testing.T) {
38 assert.True(t, CanBackdrop(ports.Output{Width: 3840, Height: 2160}))42 assert.True(t, CanBackdrop(ports.Output{Width: 3840, Height: 2160}))
39 assert.True(t, CanBackdrop(ports.Output{Width: 2160, Height: 4096}))43 assert.True(t, CanBackdrop(ports.Output{Width: 2160, Height: 4096}))
40- assert.False(t, CanBackdrop(ports.Output{Width: 5120, Height: 2880}))44+ assert.True(t, CanBackdrop(ports.Output{Width: 5120, Height: 2160}))
45+ assert.True(t, CanBackdrop(ports.Output{Width: 8192, Height: 2048}), "exact byte budget")
46+ assert.False(t, CanBackdrop(ports.Output{Width: 8192, Height: 2049}))
47+ assert.False(t, CanBackdrop(ports.Output{Width: 7680, Height: 4320}))
48+ assert.False(t, CanBackdrop(ports.Output{Width: 8193, Height: 1}))
49+ assert.False(t, CanBackdrop(ports.Output{Width: 1, Height: 8193}))
41 assert.False(t, CanBackdrop(ports.Output{}), "an unknown mode stays live")50 assert.False(t, CanBackdrop(ports.Output{}), "an unknown mode stays live")
42 }51 }
43 52
M internal/adapters/selection/selector.go +11 -7
@@ -87,13 +87,17 @@ func (s *Selector) WithCapabilities(c ports.Capabilities) *Selector {
8787 return s
8888 }
8989
90-// maxBackdrop is the largest side NeferGUI accepts for one image.
91-const maxBackdrop = 4096
90+// These bounds mirror NeferGUI's image dimension and cache byte limits.
91+const (
92+ maxBackdrop = 8192
93+ maxBackdropBytes = 64 << 20
94+)
9295
93-// CanBackdrop reports whether an output of this physical mode can show a
94-// frozen frame. A larger output must stay live: freeze only the others.
96+// CanBackdrop reports whether an output's physical mode fits NeferGUI's image
97+// limits. Outputs exceeding either the side or byte limit stay live.
9598 func CanBackdrop(o ports.Output) bool {
96- return o.Width > 0 && o.Height > 0 && max(o.Width, o.Height) <= maxBackdrop
99+ return o.Width > 0 && o.Height > 0 && max(o.Width, o.Height) <= maxBackdrop &&
100+ o.Width <= maxBackdropBytes/ports.BytesPerPixel/o.Height
97101 }
98102
99103 // WithBackdrops draws each output's frozen frame under its overlay instead of
@@ -103,8 +107,8 @@ func CanBackdrop(o ports.Output) bool {
103107 func (s *Selector) WithBackdrops(frames map[uint32]ports.Frame) (*Selector, error) {
104108 s.backdrops = make(map[uint32]image.Image, len(frames))
105109 for id, f := range frames {
106- if f.Width > maxBackdrop || f.Height > maxBackdrop {
107- return s, fmt.Errorf("selection: frozen frame of output %d is %dx%d, at most %d per side", id, f.Width, f.Height, maxBackdrop)
110+ if !CanBackdrop(ports.Output{Width: f.Width, Height: f.Height}) {
111+ return s, fmt.Errorf("selection: frozen frame of output %d is %dx%d, at most %d per side and %d bytes", id, f.Width, f.Height, maxBackdrop, maxBackdropBytes)
108112 }
109113 s.backdrops[id] = backdropImage(f)
110114 }
@@ -87,13 +87,17 @@ func (s *Selector) WithCapabilities(c ports.Capabilities) *Selector {
87 return s87 return s
88 }88 }
89 89
90-// maxBackdrop is the largest side NeferGUI accepts for one image.90+// These bounds mirror NeferGUI's image dimension and cache byte limits.
91-const maxBackdrop = 409691+const (
92+ maxBackdrop = 8192
93+ maxBackdropBytes = 64 << 20
94+)
92 95
93-// CanBackdrop reports whether an output of this physical mode can show a96+// CanBackdrop reports whether an output's physical mode fits NeferGUI's image
94-// frozen frame. A larger output must stay live: freeze only the others.97+// limits. Outputs exceeding either the side or byte limit stay live.
95 func CanBackdrop(o ports.Output) bool {98 func CanBackdrop(o ports.Output) bool {
96- return o.Width > 0 && o.Height > 0 && max(o.Width, o.Height) <= maxBackdrop99+ return o.Width > 0 && o.Height > 0 && max(o.Width, o.Height) <= maxBackdrop &&
100+ o.Width <= maxBackdropBytes/ports.BytesPerPixel/o.Height
97 }101 }
98 102
99 // WithBackdrops draws each output's frozen frame under its overlay instead of103 // WithBackdrops draws each output's frozen frame under its overlay instead of
@@ -103,8 +107,8 @@ func CanBackdrop(o ports.Output) bool {
103 func (s *Selector) WithBackdrops(frames map[uint32]ports.Frame) (*Selector, error) {107 func (s *Selector) WithBackdrops(frames map[uint32]ports.Frame) (*Selector, error) {
104 s.backdrops = make(map[uint32]image.Image, len(frames))108 s.backdrops = make(map[uint32]image.Image, len(frames))
105 for id, f := range frames {109 for id, f := range frames {
106- if f.Width > maxBackdrop || f.Height > maxBackdrop {110+ if !CanBackdrop(ports.Output{Width: f.Width, Height: f.Height}) {
107- return s, fmt.Errorf("selection: frozen frame of output %d is %dx%d, at most %d per side", id, f.Width, f.Height, maxBackdrop)111+ return s, fmt.Errorf("selection: frozen frame of output %d is %dx%d, at most %d per side and %d bytes", id, f.Width, f.Height, maxBackdrop, maxBackdropBytes)
108 }112 }
109 s.backdrops[id] = backdropImage(f)113 s.backdrops[id] = backdropImage(f)
110 }114 }
M internal/adapters/wayland/extensions_test.go +2 -0
@@ -259,6 +259,8 @@ func TestRefusedExclusionDoesNotPinStoppedSession(t *testing.T) {
259259 target := ports.Target{OutputID: firstOutput(t, s).ID}
260260 _, err := s.BeginExclusion(context.Background(), target)
261261 require.ErrorIs(t, err, ErrExclusionUnavailable)
262+ // BeginExclusion sends destroy but does not wait for the server to read it.
263+ require.NoError(t, s.roundtrip())
262264 require.Equal(t, 1, srv.Stats().DestroyedExclusions, "the failed exclusion object is destroyed")
263265 srv.StopSessions()
264266 _, err = s.Capture(context.Background(), target)
@@ -259,6 +259,8 @@ func TestRefusedExclusionDoesNotPinStoppedSession(t *testing.T) {
259 target := ports.Target{OutputID: firstOutput(t, s).ID}259 target := ports.Target{OutputID: firstOutput(t, s).ID}
260 _, err := s.BeginExclusion(context.Background(), target)260 _, err := s.BeginExclusion(context.Background(), target)
261 require.ErrorIs(t, err, ErrExclusionUnavailable)261 require.ErrorIs(t, err, ErrExclusionUnavailable)
262+ // BeginExclusion sends destroy but does not wait for the server to read it.
263+ require.NoError(t, s.roundtrip())
262 require.Equal(t, 1, srv.Stats().DestroyedExclusions, "the failed exclusion object is destroyed")264 require.Equal(t, 1, srv.Stats().DestroyedExclusions, "the failed exclusion object is destroyed")
263 srv.StopSessions()265 srv.StopSessions()
264 _, err = s.Capture(context.Background(), target)266 _, err = s.Capture(context.Background(), target)
M internal/adapters/wayland/source_test.go +4 -1
@@ -171,8 +171,11 @@ func TestFailedUnknownRepeatsLastFrame(t *testing.T) {
171171 require.NoError(t, err, "recording continues")
172172 require.NotEqual(t, want, fresh.Pixels, "with a new frame")
173173
174+ // Capture prefetched the next frame. Wait for the server to answer it
175+ // before arming failures, so it cannot consume the first injected failure.
176+ require.NoError(t, s.roundtrip())
174177 srv.Fail(maxFrameAttempts, 0)
175- // The capture started after the previous frame was answered before Fail.
178+ // Consume the successful prefetched frame before counting failures.
176179 _, err = s.Capture(context.Background(), target)
177180 require.NoError(t, err)
178181 for range maxFrameAttempts - 1 {
@@ -171,8 +171,11 @@ func TestFailedUnknownRepeatsLastFrame(t *testing.T) {
171 require.NoError(t, err, "recording continues")171 require.NoError(t, err, "recording continues")
172 require.NotEqual(t, want, fresh.Pixels, "with a new frame")172 require.NotEqual(t, want, fresh.Pixels, "with a new frame")
173 173
174+ // Capture prefetched the next frame. Wait for the server to answer it
175+ // before arming failures, so it cannot consume the first injected failure.
176+ require.NoError(t, s.roundtrip())
174 srv.Fail(maxFrameAttempts, 0)177 srv.Fail(maxFrameAttempts, 0)
175- // The capture started after the previous frame was answered before Fail.178+ // Consume the successful prefetched frame before counting failures.
176 _, err = s.Capture(context.Background(), target)179 _, err = s.Capture(context.Background(), target)
177 require.NoError(t, err)180 require.NoError(t, err)
178 for range maxFrameAttempts - 1 {181 for range maxFrameAttempts - 1 {
M internal/app/freeze_test.go +2 -2
@@ -32,6 +32,6 @@ func TestFreezes(t *testing.T) {
3232 // Outputs too large for a backdrop are not frozen, so their screenshot is
3333 // live like their selector.
3434 func TestFrozenOutputs(t *testing.T) {
35- outputs := []ports.Output{{ID: 7, Width: 3840, Height: 2160}, {ID: 8, Width: 5120, Height: 2880}, {ID: 9, Width: 1080, Height: 1920}, {ID: 10}}
36- assert.Equal(t, []uint32{7, 9}, frozenOutputs(outputs))
35+ outputs := []ports.Output{{ID: 7, Width: 3840, Height: 2160}, {ID: 8, Width: 5120, Height: 2160}, {ID: 9, Width: 1080, Height: 1920}, {ID: 10}, {ID: 11, Width: 7680, Height: 4320}}
36+ assert.Equal(t, []uint32{7, 8, 9}, frozenOutputs(outputs))
3737 }
@@ -32,6 +32,6 @@ func TestFreezes(t *testing.T) {
32 // Outputs too large for a backdrop are not frozen, so their screenshot is32 // Outputs too large for a backdrop are not frozen, so their screenshot is
33 // live like their selector.33 // live like their selector.
34 func TestFrozenOutputs(t *testing.T) {34 func TestFrozenOutputs(t *testing.T) {
35- outputs := []ports.Output{{ID: 7, Width: 3840, Height: 2160}, {ID: 8, Width: 5120, Height: 2880}, {ID: 9, Width: 1080, Height: 1920}, {ID: 10}}35+ outputs := []ports.Output{{ID: 7, Width: 3840, Height: 2160}, {ID: 8, Width: 5120, Height: 2160}, {ID: 9, Width: 1080, Height: 1920}, {ID: 10}, {ID: 11, Width: 7680, Height: 4320}}
36- assert.Equal(t, []uint32{7, 9}, frozenOutputs(outputs))36+ assert.Equal(t, []uint32{7, 8, 9}, frozenOutputs(outputs))
37 }37 }

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 2

Tags 9

↑↓ moveenter openesc close