detached 9fd9758

fix(core): drop releases of keys a bind consumed before shortcuts inhibit began

commit
9fd9758f2e139003b874a02d1e5214a4c288aef8
author
brice · · 2026-09-26 12:35 UTC
parent
bb97d3a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +55 -6
M internal/core/core.go +16 -4
@@ -852,6 +852,12 @@ func (c *Core) Run(ctx context.Context) error {
852852 // no bind runs (emergency quit and VT switch are handled by
853853 // input before core).
854854 if c.inhibiting != 0 && c.inhibiting == c.keyboardFocus() {
855+ if held := heldKey(key); !key.Pressed && c.pressed[held] {
856+ // Its press ran a bind before inhibiting began: the
857+ // window never saw it.
858+ delete(c.pressed, held)
859+ continue
860+ }
855861 if err := c.command(ctx, ports.ForwardKey{ID: c.inhibiting, Key: key}); err != nil {
856862 return nil
857863 }
@@ -868,10 +874,7 @@ func (c *Core) Run(ctx context.Context) error {
868874 action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]
869875 }
870876 // Track presses by physical key: Shift may be released before the key.
871- held := name
872- if key.Keycode != 0 {
873- held = "#" + strconv.FormatUint(uint64(key.Keycode), 10)
874- }
877+ held := heldKey(key)
875878 if !key.Pressed {
876879 consumed := c.pressed[held]
877880 delete(c.pressed, held)
@@ -962,3 +965,12 @@ func (c *Core) activate(ctx context.Context, id WindowID) error {
962965 c.focusScreen = c.screenIndex(s.name())
963966 return c.afterShow(ctx, before)
964967 }
968+
969+// heldKey names a key for press tracking: by physical key, since Shift
970+// may be released before the key.
971+func heldKey(key ports.KeyEvent) string {
972+ if key.Keycode != 0 {
973+ return "#" + strconv.FormatUint(uint64(key.Keycode), 10)
974+ }
975+ return keyName(key.Keysym)
976+}
@@ -852,6 +852,12 @@ func (c *Core) Run(ctx context.Context) error {
852 // no bind runs (emergency quit and VT switch are handled by852 // no bind runs (emergency quit and VT switch are handled by
853 // input before core).853 // input before core).
854 if c.inhibiting != 0 && c.inhibiting == c.keyboardFocus() {854 if c.inhibiting != 0 && c.inhibiting == c.keyboardFocus() {
855+ if held := heldKey(key); !key.Pressed && c.pressed[held] {
856+ // Its press ran a bind before inhibiting began: the
857+ // window never saw it.
858+ delete(c.pressed, held)
859+ continue
860+ }
855 if err := c.command(ctx, ports.ForwardKey{ID: c.inhibiting, Key: key}); err != nil {861 if err := c.command(ctx, ports.ForwardKey{ID: c.inhibiting, Key: key}); err != nil {
856 return nil862 return nil
857 }863 }
@@ -868,10 +874,7 @@ func (c *Core) Run(ctx context.Context) error {
868 action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]874 action, bound = c.binds[binding{key: "code:" + strconv.FormatUint(uint64(key.Keycode), 10), mods: key.Mods}]
869 }875 }
870 // Track presses by physical key: Shift may be released before the key.876 // Track presses by physical key: Shift may be released before the key.
871- held := name877+ held := heldKey(key)
872- if key.Keycode != 0 {
873- held = "#" + strconv.FormatUint(uint64(key.Keycode), 10)
874- }
875 if !key.Pressed {878 if !key.Pressed {
876 consumed := c.pressed[held]879 consumed := c.pressed[held]
877 delete(c.pressed, held)880 delete(c.pressed, held)
@@ -962,3 +965,12 @@ func (c *Core) activate(ctx context.Context, id WindowID) error {
962 c.focusScreen = c.screenIndex(s.name())965 c.focusScreen = c.screenIndex(s.name())
963 return c.afterShow(ctx, before)966 return c.afterShow(ctx, before)
964 }967 }
968+
969+// heldKey names a key for press tracking: by physical key, since Shift
970+// may be released before the key.
971+func heldKey(key ports.KeyEvent) string {
972+ if key.Keycode != 0 {
973+ return "#" + strconv.FormatUint(uint64(key.Keycode), 10)
974+ }
975+ return keyName(key.Keysym)
976+}
M internal/core/inhibit_test.go +37 -0
@@ -88,3 +88,40 @@ func TestShortcutsInhibit(t *testing.T) {
8888 }
8989 }
9090 }
91+
92+// A key whose press ran a bind before inhibiting began is not released to
93+// the window: it never saw the press.
94+func TestShortcutsInhibitDropsBoundRelease(t *testing.T) {
95+ cfg := config.Defaults()
96+ input := make(chan ports.InputEvent, 4)
97+ client := make(chan ports.ClientEvent, 4)
98+ commands := make(chan ports.ClientCommand, 64)
99+ scenes := make(chan []ports.Scene, 1)
100+ spawn := make(chan ports.SpawnRequest, 4)
101+ output := make(chan ports.OutputEvent, 1)
102+ c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn})
103+ if err != nil {
104+ t.Fatal(err)
105+ }
106+ ctx, cancel := context.WithCancel(context.Background())
107+ done := make(chan error, 1)
108+ go func() { done <- c.Run(ctx) }()
109+ defer func() { cancel(); receive(t, done) }()
110+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
111+ scene(t, scenes)
112+ client <- ports.WindowMapped{ID: 1}
113+ scene(t, scenes)
114+ press := ports.KeyEvent{Keysym: "Return", Keycode: 36, Mods: ports.ModSuper, Pressed: true}
115+ input <- press
116+ receive(t, spawn)
117+ client <- ports.ShortcutsInhibit{Window: 1, Active: true}
118+ commandOf[ports.ShortcutsInhibitState](t, commands)
119+ release := press
120+ release.Pressed = false
121+ input <- release
122+ other := ports.KeyEvent{Keysym: "a", Keycode: 38, Pressed: true}
123+ input <- other
124+ if v := commandOf[ports.ForwardKey](t, commands); v.Key != other {
125+ t.Fatalf("forwarded %+v before %+v", v.Key, other)
126+ }
127+}
@@ -88,3 +88,40 @@ func TestShortcutsInhibit(t *testing.T) {
88 }88 }
89 }89 }
90 }90 }
91+
92+// A key whose press ran a bind before inhibiting began is not released to
93+// the window: it never saw the press.
94+func TestShortcutsInhibitDropsBoundRelease(t *testing.T) {
95+ cfg := config.Defaults()
96+ input := make(chan ports.InputEvent, 4)
97+ client := make(chan ports.ClientEvent, 4)
98+ commands := make(chan ports.ClientCommand, 64)
99+ scenes := make(chan []ports.Scene, 1)
100+ spawn := make(chan ports.SpawnRequest, 4)
101+ output := make(chan ports.OutputEvent, 1)
102+ c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn})
103+ if err != nil {
104+ t.Fatal(err)
105+ }
106+ ctx, cancel := context.WithCancel(context.Background())
107+ done := make(chan error, 1)
108+ go func() { done <- c.Run(ctx) }()
109+ defer func() { cancel(); receive(t, done) }()
110+ output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
111+ scene(t, scenes)
112+ client <- ports.WindowMapped{ID: 1}
113+ scene(t, scenes)
114+ press := ports.KeyEvent{Keysym: "Return", Keycode: 36, Mods: ports.ModSuper, Pressed: true}
115+ input <- press
116+ receive(t, spawn)
117+ client <- ports.ShortcutsInhibit{Window: 1, Active: true}
118+ commandOf[ports.ShortcutsInhibitState](t, commands)
119+ release := press
120+ release.Pressed = false
121+ input <- release
122+ other := ports.KeyEvent{Keysym: "a", Keycode: 38, Pressed: true}
123+ input <- other
124+ if v := commandOf[ports.ForwardKey](t, commands); v.Key != other {
125+ t.Fatalf("forwarded %+v before %+v", v.Key, other)
126+ }
127+}
M internal/ports/messages.go +2 -2
@@ -388,8 +388,6 @@ type ConfigChanged struct{ Config Config }
388388 // ClientCommand carries core → wayland commands.
389389 type ClientCommand interface{ clientCommand() }
390390
391-// ConfigureWindow carries core → wayland geometry and state. Output is the
392-// connector showing the window, empty while it is hidden.
393391 // ShortcutsInhibitState carries core → wayland whether a window's
394392 // shortcuts inhibitor is in effect (it has keyboard focus).
395393 type ShortcutsInhibitState struct {
@@ -399,6 +397,8 @@ type ShortcutsInhibitState struct {
399397
400398 func (ShortcutsInhibitState) clientCommand() {}
401399
400+// ConfigureWindow carries core → wayland geometry and state. Output is the
401+// connector showing the window, empty while it is hidden.
402402 type ConfigureWindow struct {
403403 ID WindowID
404404 Width, Height int
@@ -388,8 +388,6 @@ type ConfigChanged struct{ Config Config }
388 // ClientCommand carries core → wayland commands.388 // ClientCommand carries core → wayland commands.
389 type ClientCommand interface{ clientCommand() }389 type ClientCommand interface{ clientCommand() }
390 390
391-// ConfigureWindow carries core → wayland geometry and state. Output is the
392-// connector showing the window, empty while it is hidden.
393 // ShortcutsInhibitState carries core → wayland whether a window's391 // ShortcutsInhibitState carries core → wayland whether a window's
394 // shortcuts inhibitor is in effect (it has keyboard focus).392 // shortcuts inhibitor is in effect (it has keyboard focus).
395 type ShortcutsInhibitState struct {393 type ShortcutsInhibitState struct {
@@ -399,6 +397,8 @@ type ShortcutsInhibitState struct {
399 397
400 func (ShortcutsInhibitState) clientCommand() {}398 func (ShortcutsInhibitState) clientCommand() {}
401 399
400+// ConfigureWindow carries core → wayland geometry and state. Output is the
401+// connector showing the window, empty while it is hidden.
402 type ConfigureWindow struct {402 type ConfigureWindow struct {
403 ID WindowID403 ID WindowID
404 Width, Height int404 Width, Height int

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