detached 88edc52

fix(core): send hint clicks without held modifiers

commit
88edc5229625bae3b7b1d82ec853635e9be4418d
author
brice · · 2026-10-21 13:35 UTC
committer
brice · · 2026-10-22 20:27 UTC
parent
492a5a9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +34 -5
M internal/core/hint.go +11 -1
@@ -287,7 +287,7 @@ func (c *Core) hintKey(ctx context.Context, key ports.KeyEvent) (bool, error) {
287287 // still be down) from turning it into a window drag, without touching c.mods.
288288 //
289289 // The window gets the keyboard first.
290-func (c *Core) hintClick(ctx context.Context, t hintTarget) error {
290+func (c *Core) hintClick(ctx context.Context, t hintTarget) (err error) {
291291 c.hintCancel()
292292 if err := c.publish(ctx); err != nil {
293293 return err
@@ -298,6 +298,16 @@ func (c *Core) hintClick(ctx context.Context, t hintTarget) error {
298298 if err := c.clickFocus(ctx, t.window); err != nil {
299299 return err
300300 }
301+ // The physical modifiers (bind's Super, Ctrl, Shift) must not reach the
302+ // client: state none, and always send the real state back afterwards.
303+ if err := c.command(ctx, ports.SetModifiers{ID: t.window, Mods: 0}); err != nil {
304+ return err
305+ }
306+ defer func() {
307+ if e := c.command(ctx, ports.SetModifiers{ID: t.window, Restore: true, State: c.keyState}); err == nil {
308+ err = e
309+ }
310+ }()
301311 c.synthetic = true
302312 defer func() { c.synthetic = false }()
303313 if err := c.pointerMotion(ctx, ports.PointerMotion{X: t.x, Y: t.y, Time: c.motionTime}); err != nil {
@@ -287,7 +287,7 @@ func (c *Core) hintKey(ctx context.Context, key ports.KeyEvent) (bool, error) {
287 // still be down) from turning it into a window drag, without touching c.mods.287 // still be down) from turning it into a window drag, without touching c.mods.
288 //288 //
289 // The window gets the keyboard first.289 // The window gets the keyboard first.
290-func (c *Core) hintClick(ctx context.Context, t hintTarget) error {290+func (c *Core) hintClick(ctx context.Context, t hintTarget) (err error) {
291 c.hintCancel()291 c.hintCancel()
292 if err := c.publish(ctx); err != nil {292 if err := c.publish(ctx); err != nil {
293 return err293 return err
@@ -298,6 +298,16 @@ func (c *Core) hintClick(ctx context.Context, t hintTarget) error {
298 if err := c.clickFocus(ctx, t.window); err != nil {298 if err := c.clickFocus(ctx, t.window); err != nil {
299 return err299 return err
300 }300 }
301+ // The physical modifiers (bind's Super, Ctrl, Shift) must not reach the
302+ // client: state none, and always send the real state back afterwards.
303+ if err := c.command(ctx, ports.SetModifiers{ID: t.window, Mods: 0}); err != nil {
304+ return err
305+ }
306+ defer func() {
307+ if e := c.command(ctx, ports.SetModifiers{ID: t.window, Restore: true, State: c.keyState}); err == nil {
308+ err = e
309+ }
310+ }()
301 c.synthetic = true311 c.synthetic = true
302 defer func() { c.synthetic = false }()312 defer func() { c.synthetic = false }()
303 if err := c.pointerMotion(ctx, ports.PointerMotion{X: t.x, Y: t.y, Time: c.motionTime}); err != nil {313 if err := c.pointerMotion(ctx, ports.PointerMotion{X: t.x, Y: t.y, Time: c.motionTime}); err != nil {
M internal/core/hint_test.go +23 -4
@@ -462,7 +462,20 @@ func TestHintClickWithBindModifierHeld(t *testing.T) {
462462 h.drain()
463463 h.press("f", ports.ModSuper) // Super is still down
464464 h.labelled(0, 0)
465- presses, releases := h.buttonsSent()
465+ cmds := h.barrier()
466+ if got, want := commandKinds(cmds), []string{"mods:0", "motion", "press", "release", "mods:restore"}; !slices.Equal(got, want) {
467+ t.Fatalf("commands %v, want %v", got, want)
468+ }
469+ presses, releases := 0, 0
470+ for _, c := range cmds {
471+ if b, ok := c.(ports.PointerButtonTo); ok {
472+ if b.Pressed {
473+ presses++
474+ } else {
475+ releases++
476+ }
477+ }
478+ }
466479 if presses != 1 || releases != 1 {
467480 t.Fatalf("client got %d presses, %d releases", presses, releases)
468481 }
@@ -588,7 +601,7 @@ func (h *hintHarness) commandsSent() []ports.ClientCommand {
588601 var out []ports.ClientCommand
589602 for _, cmd := range h.barrier() {
590603 switch cmd.(type) {
591- case ports.FocusWindow, ports.PointerMotionTo, ports.PointerButtonTo:
604+ case ports.FocusWindow, ports.SetModifiers, ports.PointerMotionTo, ports.PointerButtonTo:
592605 out = append(out, cmd)
593606 }
594607 }
@@ -601,6 +614,12 @@ func commandKinds(cmds []ports.ClientCommand) []string {
601614 switch v := c.(type) {
602615 case ports.FocusWindow:
603616 out = append(out, fmt.Sprintf("focus:%d", v.ID))
617+ case ports.SetModifiers:
618+ if v.Restore {
619+ out = append(out, "mods:restore")
620+ } else {
621+ out = append(out, fmt.Sprintf("mods:%d", v.Mods))
622+ }
604623 case ports.PointerMotionTo:
605624 out = append(out, "motion")
606625 case ports.PointerButtonTo:
@@ -622,7 +641,7 @@ func TestHintClickSequence(t *testing.T) {
622641 h.drain()
623642 h.press("f", 0)
624643 got := commandKinds(h.commandsSent())
625- want := []string{"motion", "press", "release"}
644+ want := []string{"mods:0", "motion", "press", "release", "mods:restore"}
626645 if !slices.Equal(got, want) {
627646 t.Fatalf("commands %v, want %v", got, want)
628647 }
@@ -697,7 +716,7 @@ func TestHintClickFocusesWindowFirst(t *testing.T) {
697716 h.drain()
698717 h.press("f", 0)
699718 got := commandKinds(h.commandsSent())
700- want := []string{"focus:1", "motion", "press", "release"}
719+ want := []string{"focus:1", "mods:0", "motion", "press", "release", "mods:restore"}
701720 if !slices.Equal(got, want) {
702721 t.Fatalf("commands %v, want %v", got, want)
703722 }
@@ -462,7 +462,20 @@ func TestHintClickWithBindModifierHeld(t *testing.T) {
462 h.drain()462 h.drain()
463 h.press("f", ports.ModSuper) // Super is still down463 h.press("f", ports.ModSuper) // Super is still down
464 h.labelled(0, 0)464 h.labelled(0, 0)
465- presses, releases := h.buttonsSent()465+ cmds := h.barrier()
466+ if got, want := commandKinds(cmds), []string{"mods:0", "motion", "press", "release", "mods:restore"}; !slices.Equal(got, want) {
467+ t.Fatalf("commands %v, want %v", got, want)
468+ }
469+ presses, releases := 0, 0
470+ for _, c := range cmds {
471+ if b, ok := c.(ports.PointerButtonTo); ok {
472+ if b.Pressed {
473+ presses++
474+ } else {
475+ releases++
476+ }
477+ }
478+ }
466 if presses != 1 || releases != 1 {479 if presses != 1 || releases != 1 {
467 t.Fatalf("client got %d presses, %d releases", presses, releases)480 t.Fatalf("client got %d presses, %d releases", presses, releases)
468 }481 }
@@ -588,7 +601,7 @@ func (h *hintHarness) commandsSent() []ports.ClientCommand {
588 var out []ports.ClientCommand601 var out []ports.ClientCommand
589 for _, cmd := range h.barrier() {602 for _, cmd := range h.barrier() {
590 switch cmd.(type) {603 switch cmd.(type) {
591- case ports.FocusWindow, ports.PointerMotionTo, ports.PointerButtonTo:604+ case ports.FocusWindow, ports.SetModifiers, ports.PointerMotionTo, ports.PointerButtonTo:
592 out = append(out, cmd)605 out = append(out, cmd)
593 }606 }
594 }607 }
@@ -601,6 +614,12 @@ func commandKinds(cmds []ports.ClientCommand) []string {
601 switch v := c.(type) {614 switch v := c.(type) {
602 case ports.FocusWindow:615 case ports.FocusWindow:
603 out = append(out, fmt.Sprintf("focus:%d", v.ID))616 out = append(out, fmt.Sprintf("focus:%d", v.ID))
617+ case ports.SetModifiers:
618+ if v.Restore {
619+ out = append(out, "mods:restore")
620+ } else {
621+ out = append(out, fmt.Sprintf("mods:%d", v.Mods))
622+ }
604 case ports.PointerMotionTo:623 case ports.PointerMotionTo:
605 out = append(out, "motion")624 out = append(out, "motion")
606 case ports.PointerButtonTo:625 case ports.PointerButtonTo:
@@ -622,7 +641,7 @@ func TestHintClickSequence(t *testing.T) {
622 h.drain()641 h.drain()
623 h.press("f", 0)642 h.press("f", 0)
624 got := commandKinds(h.commandsSent())643 got := commandKinds(h.commandsSent())
625- want := []string{"motion", "press", "release"}644+ want := []string{"mods:0", "motion", "press", "release", "mods:restore"}
626 if !slices.Equal(got, want) {645 if !slices.Equal(got, want) {
627 t.Fatalf("commands %v, want %v", got, want)646 t.Fatalf("commands %v, want %v", got, want)
628 }647 }
@@ -697,7 +716,7 @@ func TestHintClickFocusesWindowFirst(t *testing.T) {
697 h.drain()716 h.drain()
698 h.press("f", 0)717 h.press("f", 0)
699 got := commandKinds(h.commandsSent())718 got := commandKinds(h.commandsSent())
700- want := []string{"focus:1", "motion", "press", "release"}719+ want := []string{"focus:1", "mods:0", "motion", "press", "release", "mods:restore"}
701 if !slices.Equal(got, want) {720 if !slices.Equal(got, want) {
702 t.Fatalf("commands %v, want %v", got, want)721 t.Fatalf("commands %v, want %v", got, want)
703 }722 }

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 26

Tags 10

↑↓ moveenter openesc close