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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 287 | 287 | // still be down) from turning it into a window drag, without touching c.mods. | |
| 288 | 288 | // | |
| 289 | 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 | 291 | c.hintCancel() | |
| 292 | 292 | if err := c.publish(ctx); err != nil { | |
| 293 | 293 | return err | |
| @@ -298,6 +298,16 @@ func (c *Core) hintClick(ctx context.Context, t hintTarget) error { | |||
| 298 | 298 | if err := c.clickFocus(ctx, t.window); err != nil { | |
| 299 | 299 | 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 | 311 | c.synthetic = true | |
| 302 | 312 | defer func() { c.synthetic = false }() | |
| 303 | 313 | 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 err | 293 | 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 err | 299 | 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 = true | 311 | 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) { | |||
| 462 | 462 | h.drain() | |
| 463 | 463 | h.press("f", ports.ModSuper) // Super is still down | |
| 464 | 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 | 479 | if presses != 1 || releases != 1 { | |
| 467 | 480 | t.Fatalf("client got %d presses, %d releases", presses, releases) | |
| 468 | 481 | } | |
| @@ -588,7 +601,7 @@ func (h *hintHarness) commandsSent() []ports.ClientCommand { | |||
| 588 | 601 | var out []ports.ClientCommand | |
| 589 | 602 | for _, cmd := range h.barrier() { | |
| 590 | 603 | switch cmd.(type) { | |
| 591 | - | case ports.FocusWindow, ports.PointerMotionTo, ports.PointerButtonTo: | |
| 604 | + | case ports.FocusWindow, ports.SetModifiers, ports.PointerMotionTo, ports.PointerButtonTo: | |
| 592 | 605 | out = append(out, cmd) | |
| 593 | 606 | } | |
| 594 | 607 | } | |
| @@ -601,6 +614,12 @@ func commandKinds(cmds []ports.ClientCommand) []string { | |||
| 601 | 614 | switch v := c.(type) { | |
| 602 | 615 | case ports.FocusWindow: | |
| 603 | 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 | 623 | case ports.PointerMotionTo: | |
| 605 | 624 | out = append(out, "motion") | |
| 606 | 625 | case ports.PointerButtonTo: | |
| @@ -622,7 +641,7 @@ func TestHintClickSequence(t *testing.T) { | |||
| 622 | 641 | h.drain() | |
| 623 | 642 | h.press("f", 0) | |
| 624 | 643 | got := commandKinds(h.commandsSent()) | |
| 625 | - | want := []string{"motion", "press", "release"} | |
| 644 | + | want := []string{"mods:0", "motion", "press", "release", "mods:restore"} | |
| 626 | 645 | if !slices.Equal(got, want) { | |
| 627 | 646 | t.Fatalf("commands %v, want %v", got, want) | |
| 628 | 647 | } | |
| @@ -697,7 +716,7 @@ func TestHintClickFocusesWindowFirst(t *testing.T) { | |||
| 697 | 716 | h.drain() | |
| 698 | 717 | h.press("f", 0) | |
| 699 | 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 | 720 | if !slices.Equal(got, want) { | |
| 702 | 721 | t.Fatalf("commands %v, want %v", got, want) | |
| 703 | 722 | } | |
| @@ -462,7 +462,20 @@ func TestHintClickWithBindModifierHeld(t *testing.T) { | |||||
| 462 | h.drain() | 462 | h.drain() | ||
| 463 | h.press("f", ports.ModSuper) // Super is still down | 463 | 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.ClientCommand | 601 | 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 | } | ||