detached da68309

fix(selection): freeze outputs wider than 4096 pixels

Mirror the NeferGUI 8192-pixel ceiling and 64 MiB image budget so ultrawide outputs can show frozen backdrops while oversized modes remain live. Requires NeferGUI v0.7.3; the dependency bump follows its release.
commit
da683098966021dd8addec7bf26cd3fba80db495
author
brice · · 2026-10-22 14:30 UTC
parent
74503b0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
checks
1/1
3 files · +24 -11
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/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