detached 6a15f35

fix(flash): bound the workspace lookup by the flash time budget

commit
6a15f351a146c8672c46a8801323ffb5eeb2d6d8
author
brice · · 2026-10-15 09:55 UTC
parent
6f1b9dd
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +13 -6
M internal/adapters/flash/flash.go +3 -3
@@ -27,9 +27,9 @@ var styleSheet string
2727 const (
2828 namespace = "nefercap-flash"
2929 step = 30 * time.Millisecond
30- // limit bounds the whole effect, setup included, so a slow compositor or
30+ // Limit bounds the whole effect, setup included, so a slow compositor or
3131 // GPU never keeps the process alive.
32- limit = 2 * time.Second
32+ Limit = 2 * time.Second
3333 )
3434
3535 // stepClasses are the opacity steps of style.css, from opaque to faint.
@@ -56,7 +56,7 @@ func show(ctx context.Context, output ports.Output, area ports.Region) error {
5656 }
5757 defer remove()
5858
59- child, cancel := context.WithTimeout(ctx, limit)
59+ child, cancel := context.WithTimeout(ctx, Limit)
6060 wake := make(chan struct{}, 1)
6161 var wg sync.WaitGroup
6262 wg.Go(func() { ticker(child, wake) })
@@ -27,9 +27,9 @@ var styleSheet string
27 const (27 const (
28 namespace = "nefercap-flash"28 namespace = "nefercap-flash"
29 step = 30 * time.Millisecond29 step = 30 * time.Millisecond
30- // limit bounds the whole effect, setup included, so a slow compositor or30+ // Limit bounds the whole effect, setup included, so a slow compositor or
31 // GPU never keeps the process alive.31 // GPU never keeps the process alive.
32- limit = 2 * time.Second32+ Limit = 2 * time.Second
33 )33 )
34 34
35 // stepClasses are the opacity steps of style.css, from opaque to faint.35 // stepClasses are the opacity steps of style.css, from opaque to faint.
@@ -56,7 +56,7 @@ func show(ctx context.Context, output ports.Output, area ports.Region) error {
56 }56 }
57 defer remove()57 defer remove()
58 58
59- child, cancel := context.WithTimeout(ctx, limit)59+ child, cancel := context.WithTimeout(ctx, Limit)
60 wake := make(chan struct{}, 1)60 wake := make(chan struct{}, 1)
61 var wg sync.WaitGroup61 var wg sync.WaitGroup
62 wg.Go(func() { ticker(child, wake) })62 wg.Go(func() { ticker(child, wake) })
M internal/adapters/flash/flash_test.go +1 -1
@@ -69,5 +69,5 @@ func TestSmokeShow(t *testing.T) {
6969 }
7070 began := time.Now()
7171 require.NoError(t, Show(context.Background(), ports.Output{ID: 1, Name: name}, ports.Region{X: 40, Y: 40, Width: 320, Height: 200}))
72- assert.Less(t, time.Since(began), limit+time.Second)
72+ assert.Less(t, time.Since(began), Limit+time.Second)
7373 }
@@ -69,5 +69,5 @@ func TestSmokeShow(t *testing.T) {
69 }69 }
70 began := time.Now()70 began := time.Now()
71 require.NoError(t, Show(context.Background(), ports.Output{ID: 1, Name: name}, ports.Region{X: 40, Y: 40, Width: 320, Height: 200}))71 require.NoError(t, Show(context.Background(), ports.Output{ID: 1, Name: name}, ports.Region{X: 40, Y: 40, Width: 320, Height: 200}))
72- assert.Less(t, time.Since(began), limit+time.Second)72+ assert.Less(t, time.Since(began), Limit+time.Second)
73 }73 }
M internal/app/app.go +8 -1
@@ -112,8 +112,12 @@ func Run(ctx context.Context, options cli.Options, output io.Writer) (err error)
112112 }
113113
114114 // confirmScreenshot flashes the captured area once the screenshot is done. The
115-// screenshot already succeeded, so a failed flash is only logged.
115+// screenshot already succeeded, so a failed flash is only logged. One budget
116+// bounds the workspace lookup and the flash, so a silent compositor cannot
117+// keep the process alive.
116118 func confirmScreenshot(ctx context.Context, source *wayland.Source, outputs []ports.Output, target ports.Target) {
119+ ctx, cancel := context.WithTimeout(ctx, flash.Limit)
120+ defer cancel()
117121 for _, o := range outputs {
118122 if o.ID != target.OutputID {
119123 continue
@@ -121,6 +125,9 @@ func confirmScreenshot(ctx context.Context, source *wayland.Source, outputs []po
121125 area := target.Region
122126 if target.WorkspaceID != 0 {
123127 area = recordedFrame(ctx, source, target)
128+ if ctx.Err() != nil {
129+ return // the budget went on the lookup
130+ }
124131 }
125132 if err := flash.Show(ctx, o, area); err != nil {
126133 log := logging.For(ctx, "app")
@@ -112,8 +112,12 @@ func Run(ctx context.Context, options cli.Options, output io.Writer) (err error)
112 }112 }
113 113
114 // confirmScreenshot flashes the captured area once the screenshot is done. The114 // confirmScreenshot flashes the captured area once the screenshot is done. The
115-// screenshot already succeeded, so a failed flash is only logged.115+// screenshot already succeeded, so a failed flash is only logged. One budget
116+// bounds the workspace lookup and the flash, so a silent compositor cannot
117+// keep the process alive.
116 func confirmScreenshot(ctx context.Context, source *wayland.Source, outputs []ports.Output, target ports.Target) {118 func confirmScreenshot(ctx context.Context, source *wayland.Source, outputs []ports.Output, target ports.Target) {
119+ ctx, cancel := context.WithTimeout(ctx, flash.Limit)
120+ defer cancel()
117 for _, o := range outputs {121 for _, o := range outputs {
118 if o.ID != target.OutputID {122 if o.ID != target.OutputID {
119 continue123 continue
@@ -121,6 +125,9 @@ func confirmScreenshot(ctx context.Context, source *wayland.Source, outputs []po
121 area := target.Region125 area := target.Region
122 if target.WorkspaceID != 0 {126 if target.WorkspaceID != 0 {
123 area = recordedFrame(ctx, source, target)127 area = recordedFrame(ctx, source, target)
128+ if ctx.Err() != nil {
129+ return // the budget went on the lookup
130+ }
124 }131 }
125 if err := flash.Show(ctx, o, area); err != nil {132 if err := flash.Show(ctx, o, area); err != nil {
126 log := logging.For(ctx, "app")133 log := logging.For(ctx, "app")
M internal/app/record_interactive.go +1 -1
@@ -188,7 +188,7 @@ func recordedFrame(ctx context.Context, source *wayland.Source, t ports.Target)
188188 return ports.Region{}
189189 }
190190 log := logging.For(ctx, "app")
191- log.Warn().Err(err).Msg("workspace frame unavailable: HUD on the whole output")
191+ log.Warn().Err(err).Msg("workspace frame unavailable: using the whole output")
192192 return ports.Region{}
193193 }
194194 for _, w := range list {
@@ -188,7 +188,7 @@ func recordedFrame(ctx context.Context, source *wayland.Source, t ports.Target)
188 return ports.Region{}188 return ports.Region{}
189 }189 }
190 log := logging.For(ctx, "app")190 log := logging.For(ctx, "app")
191- log.Warn().Err(err).Msg("workspace frame unavailable: HUD on the whole output")191+ log.Warn().Err(err).Msg("workspace frame unavailable: using the whole output")
192 return ports.Region{}192 return ports.Region{}
193 }193 }
194 for _, w := range list {194 for _, w := range list {

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