detached 0c04aee

fix(focus): flash the focused window on click focus

commit
0c04aee60aa73b5c3ac719f4fb2075628f2ca2a3
author
brice · · 2026-10-21 14:33 UTC
committer
brice · · 2026-10-21 15:07 UTC
parent
ec21f32
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +46 -4
M internal/core/flash_test.go +42 -4
@@ -389,11 +389,8 @@ func TestFlashNoImplicitFocusTriggers(t *testing.T) {
389389 if err := c.pointerMotion(ctx, ports.PointerMotion{X: 50, Y: 50}); err != nil {
390390 t.Fatal(err)
391391 }
392- if err := c.pointerButton(ctx, ports.PointerButton{Button: btnLeft, Pressed: true}); err != nil {
393- t.Fatal(err)
394- }
395392 if c.flash.pending != nil {
396- t.Fatal("pointer focus flashed")
393+ t.Fatal("hover focus flashed")
397394 }
398395 }
399396
@@ -774,3 +771,44 @@ func TestFlashFollowsInnerBorderAndRadius(t *testing.T) {
774771 })
775772 }
776773 }
774+
775+func TestFlashClickFocus(t *testing.T) {
776+ for _, test := range []struct {
777+ name string
778+ target WindowID
779+ enabled bool
780+ want bool
781+ }{
782+ {"different window", 1, true, true},
783+ {"already focused", 3, true, false},
784+ {"disabled", 1, false, false},
785+ } {
786+ t.Run(test.name, func(t *testing.T) {
787+ c, clock := flashCore(t)
788+ c.cfg.Focus.Flash = test.enabled
789+ indicatorScene(t, c)
790+ if c.keyboardFocus() != 3 {
791+ t.Fatal("setup: expected window 3 focused")
792+ }
793+ c.pointer = test.target
794+ if err := c.pointerButton(context.Background(), ports.PointerButton{Button: btnLeft, Pressed: true}); err != nil {
795+ t.Fatal(err)
796+ }
797+ if c.keyboardFocus() != test.target {
798+ t.Fatalf("focus = %d, want %d", c.keyboardFocus(), test.target)
799+ }
800+ if (c.flash.pending != nil) != test.want {
801+ t.Fatalf("flash request = %+v, want flash %v", c.flash.pending, test.want)
802+ }
803+ // Let the click's viewport transition settle before the flash.
804+ clock.now = clock.now.Add(time.Second)
805+ stepScenes(t, c, c.cur())
806+ if c.flashTick() != test.want {
807+ t.Fatalf("flash start: want %v", test.want)
808+ }
809+ if test.want && (c.flash.running.window != test.target || c.flash.running.level != flashWindow || c.flashFor(c.cur()) == nil) {
810+ t.Fatalf("wrong running flash: %+v", c.flash.running)
811+ }
812+ })
813+ }
814+}
@@ -389,11 +389,8 @@ func TestFlashNoImplicitFocusTriggers(t *testing.T) {
389 if err := c.pointerMotion(ctx, ports.PointerMotion{X: 50, Y: 50}); err != nil {389 if err := c.pointerMotion(ctx, ports.PointerMotion{X: 50, Y: 50}); err != nil {
390 t.Fatal(err)390 t.Fatal(err)
391 }391 }
392- if err := c.pointerButton(ctx, ports.PointerButton{Button: btnLeft, Pressed: true}); err != nil {
393- t.Fatal(err)
394- }
395 if c.flash.pending != nil {392 if c.flash.pending != nil {
396- t.Fatal("pointer focus flashed")393+ t.Fatal("hover focus flashed")
397 }394 }
398 }395 }
399 396
@@ -774,3 +771,44 @@ func TestFlashFollowsInnerBorderAndRadius(t *testing.T) {
774 })771 })
775 }772 }
776 }773 }
774+
775+func TestFlashClickFocus(t *testing.T) {
776+ for _, test := range []struct {
777+ name string
778+ target WindowID
779+ enabled bool
780+ want bool
781+ }{
782+ {"different window", 1, true, true},
783+ {"already focused", 3, true, false},
784+ {"disabled", 1, false, false},
785+ } {
786+ t.Run(test.name, func(t *testing.T) {
787+ c, clock := flashCore(t)
788+ c.cfg.Focus.Flash = test.enabled
789+ indicatorScene(t, c)
790+ if c.keyboardFocus() != 3 {
791+ t.Fatal("setup: expected window 3 focused")
792+ }
793+ c.pointer = test.target
794+ if err := c.pointerButton(context.Background(), ports.PointerButton{Button: btnLeft, Pressed: true}); err != nil {
795+ t.Fatal(err)
796+ }
797+ if c.keyboardFocus() != test.target {
798+ t.Fatalf("focus = %d, want %d", c.keyboardFocus(), test.target)
799+ }
800+ if (c.flash.pending != nil) != test.want {
801+ t.Fatalf("flash request = %+v, want flash %v", c.flash.pending, test.want)
802+ }
803+ // Let the click's viewport transition settle before the flash.
804+ clock.now = clock.now.Add(time.Second)
805+ stepScenes(t, c, c.cur())
806+ if c.flashTick() != test.want {
807+ t.Fatalf("flash start: want %v", test.want)
808+ }
809+ if test.want && (c.flash.running.window != test.target || c.flash.running.level != flashWindow || c.flashFor(c.cur()) == nil) {
810+ t.Fatalf("wrong running flash: %+v", c.flash.running)
811+ }
812+ })
813+ }
814+}
M internal/core/input.go +4 -0
@@ -134,11 +134,15 @@ func (c *Core) clickFocus(ctx context.Context, id WindowID) error {
134134 if s == nil || w != s.mon.Current() || (c.keyboard.sent == id && s == c.cur()) {
135135 return nil
136136 }
137+ focused, _ := c.cur().mon.Focused()
137138 now := c.now()
138139 before := c.snapshot(now)
139140 w.Click(id)
140141 c.focusScreen = c.screenIndex(s.name())
141142 c.transition(before, now)
143+ if focused != id {
144+ c.requestFlash(flashWindow, id)
145+ }
142146 return c.publish(ctx)
143147 }
144148
@@ -134,11 +134,15 @@ func (c *Core) clickFocus(ctx context.Context, id WindowID) error {
134 if s == nil || w != s.mon.Current() || (c.keyboard.sent == id && s == c.cur()) {134 if s == nil || w != s.mon.Current() || (c.keyboard.sent == id && s == c.cur()) {
135 return nil135 return nil
136 }136 }
137+ focused, _ := c.cur().mon.Focused()
137 now := c.now()138 now := c.now()
138 before := c.snapshot(now)139 before := c.snapshot(now)
139 w.Click(id)140 w.Click(id)
140 c.focusScreen = c.screenIndex(s.name())141 c.focusScreen = c.screenIndex(s.name())
141 c.transition(before, now)142 c.transition(before, now)
143+ if focused != id {
144+ c.requestFlash(flashWindow, id)
145+ }
142 return c.publish(ctx)146 return c.publish(ctx)
143 }147 }
144 148

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