detached 74fc2f3

feat(core): send visible windows with hint scan requests

commit
74fc2f3285035abc6a9f2c2ff7fb2d34db0ffb24
author
brice · · 2026-10-19 12:36 UTC
parent
be629c4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +41 -2
M internal/core/hint.go +22 -1
@@ -59,7 +59,7 @@ func (c *Core) hintRequest() {
5959 if sc.name() == "" || sc.transform != 0 || sc.mon.ov.open || sc.info.Width <= 0 || sc.info.Height <= 0 {
6060 return
6161 }
62- req := ports.HintScanRequest{Seq: c.hint.seq + 1, Output: sc.name(), Width: sc.info.Width, Height: sc.info.Height, Scale: sc.scale, LinksOnly: c.cfg.Hint.LinksOnly}
62+ req := ports.HintScanRequest{Seq: c.hint.seq + 1, Output: sc.name(), Width: sc.info.Width, Height: sc.info.Height, Scale: sc.scale, LinksOnly: c.cfg.Hint.LinksOnly, Windows: c.hintWindows(sc)}
6363 select {
6464 case c.ch.HintScans <- req:
6565 default:
@@ -68,6 +68,27 @@ func (c *Core) hintRequest() {
6868 c.hint = hintState{phase: hintPending, seq: req.Seq, output: sc.name(), ws: sc.mon.Current()}
6969 }
7070
71+// hintWindows lists the windows drawn on sc, bottom to top, with their client
72+// area (placements are output-local).
73+func (c *Core) hintWindows(sc *screen) []ports.HintWindow {
74+ view := sc.mon.viewport()
75+ var out []ports.HintWindow
76+ for _, p := range sc.shownLayout() {
77+ if p.Hidden || p.Preview > 0 || !view.shows(p) {
78+ continue
79+ }
80+ r := c.clientRect(p)
81+ if r.W <= 0 || r.H <= 0 {
82+ continue
83+ }
84+ w := ports.HintWindow{ID: p.ID, Rect: r, Focused: p.Focused}
85+ rec := c.windows.lookup(p.ID)
86+ w.PID, w.AppID = rec.client.PID, rec.client.AppID
87+ out = append(out, w)
88+ }
89+ return out
90+}
91+
7192 // hintPointerBusy reports whether the pointer is in use by something a
7293 // synthetic click would disturb: a pointer constraint, a grab, a held
7394 // button (swallowed ones included: they stay in c.buttons), a window or data
@@ -59,7 +59,7 @@ func (c *Core) hintRequest() {
59 if sc.name() == "" || sc.transform != 0 || sc.mon.ov.open || sc.info.Width <= 0 || sc.info.Height <= 0 {59 if sc.name() == "" || sc.transform != 0 || sc.mon.ov.open || sc.info.Width <= 0 || sc.info.Height <= 0 {
60 return60 return
61 }61 }
62- req := ports.HintScanRequest{Seq: c.hint.seq + 1, Output: sc.name(), Width: sc.info.Width, Height: sc.info.Height, Scale: sc.scale, LinksOnly: c.cfg.Hint.LinksOnly}62+ req := ports.HintScanRequest{Seq: c.hint.seq + 1, Output: sc.name(), Width: sc.info.Width, Height: sc.info.Height, Scale: sc.scale, LinksOnly: c.cfg.Hint.LinksOnly, Windows: c.hintWindows(sc)}
63 select {63 select {
64 case c.ch.HintScans <- req:64 case c.ch.HintScans <- req:
65 default:65 default:
@@ -68,6 +68,27 @@ func (c *Core) hintRequest() {
68 c.hint = hintState{phase: hintPending, seq: req.Seq, output: sc.name(), ws: sc.mon.Current()}68 c.hint = hintState{phase: hintPending, seq: req.Seq, output: sc.name(), ws: sc.mon.Current()}
69 }69 }
70 70
71+// hintWindows lists the windows drawn on sc, bottom to top, with their client
72+// area (placements are output-local).
73+func (c *Core) hintWindows(sc *screen) []ports.HintWindow {
74+ view := sc.mon.viewport()
75+ var out []ports.HintWindow
76+ for _, p := range sc.shownLayout() {
77+ if p.Hidden || p.Preview > 0 || !view.shows(p) {
78+ continue
79+ }
80+ r := c.clientRect(p)
81+ if r.W <= 0 || r.H <= 0 {
82+ continue
83+ }
84+ w := ports.HintWindow{ID: p.ID, Rect: r, Focused: p.Focused}
85+ rec := c.windows.lookup(p.ID)
86+ w.PID, w.AppID = rec.client.PID, rec.client.AppID
87+ out = append(out, w)
88+ }
89+ return out
90+}
91+
71 // hintPointerBusy reports whether the pointer is in use by something a92 // hintPointerBusy reports whether the pointer is in use by something a
72 // synthetic click would disturb: a pointer constraint, a grab, a held93 // synthetic click would disturb: a pointer constraint, a grab, a held
73 // button (swallowed ones included: they stay in c.buttons), a window or data94 // button (swallowed ones included: they stay in c.buttons), a window or data
M internal/core/hint_test.go +6 -1
@@ -3,6 +3,7 @@ package core_test
33 import (
44 "context"
55 "fmt"
6+ "slices"
67 "sync/atomic"
78 "testing"
89 "time"
@@ -55,7 +56,7 @@ func newHint(t *testing.T, opts core.Options) *hintHarness {
5556 t.Cleanup(func() { cancel(); receive(t, done) })
5657 h.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 1000, Height: 800}}
5758 scene(t, h.scenes)
58- h.client <- ports.WindowMapped{ID: 1}
59+ h.client <- ports.WindowMapped{ID: 1, AppID: "org.test.app", PID: 4242}
5960 s := scene(t, h.scenes)
6061 h.win = s.Windows[0].Rect
6162 h.drain()
@@ -133,6 +134,10 @@ func TestHintRequestOnBind(t *testing.T) {
133134 if req.Output != "OUT-1" || req.Width != 1000 || req.Height != 800 || req.Scale != 1 || req.LinksOnly || req.Seq == 0 {
134135 t.Fatalf("request %+v", req)
135136 }
137+ want := []ports.HintWindow{{ID: 1, PID: 4242, AppID: "org.test.app", Rect: h.win, Focused: true}}
138+ if !slices.Equal(req.Windows, want) {
139+ t.Fatalf("windows %+v, want %+v", req.Windows, want)
140+ }
136141 // A second bind while pending does not ask again; the pending phase
137142 // cancels on any key.
138143 h.press("g", ports.ModSuper)
@@ -3,6 +3,7 @@ package core_test
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6+ "slices"
6 "sync/atomic"7 "sync/atomic"
7 "testing"8 "testing"
8 "time"9 "time"
@@ -55,7 +56,7 @@ func newHint(t *testing.T, opts core.Options) *hintHarness {
55 t.Cleanup(func() { cancel(); receive(t, done) })56 t.Cleanup(func() { cancel(); receive(t, done) })
56 h.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 1000, Height: 800}}57 h.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 1000, Height: 800}}
57 scene(t, h.scenes)58 scene(t, h.scenes)
58- h.client <- ports.WindowMapped{ID: 1}59+ h.client <- ports.WindowMapped{ID: 1, AppID: "org.test.app", PID: 4242}
59 s := scene(t, h.scenes)60 s := scene(t, h.scenes)
60 h.win = s.Windows[0].Rect61 h.win = s.Windows[0].Rect
61 h.drain()62 h.drain()
@@ -133,6 +134,10 @@ func TestHintRequestOnBind(t *testing.T) {
133 if req.Output != "OUT-1" || req.Width != 1000 || req.Height != 800 || req.Scale != 1 || req.LinksOnly || req.Seq == 0 {134 if req.Output != "OUT-1" || req.Width != 1000 || req.Height != 800 || req.Scale != 1 || req.LinksOnly || req.Seq == 0 {
134 t.Fatalf("request %+v", req)135 t.Fatalf("request %+v", req)
135 }136 }
137+ want := []ports.HintWindow{{ID: 1, PID: 4242, AppID: "org.test.app", Rect: h.win, Focused: true}}
138+ if !slices.Equal(req.Windows, want) {
139+ t.Fatalf("windows %+v, want %+v", req.Windows, want)
140+ }
136 // A second bind while pending does not ask again; the pending phase141 // A second bind while pending does not ask again; the pending phase
137 // cancels on any key.142 // cancels on any key.
138 h.press("g", ports.ModSuper)143 h.press("g", ports.ModSuper)
M internal/ports/hint_messages.go +13 -0
@@ -20,6 +20,19 @@ type HintScanRequest struct {
2020 Width, Height int
2121 Scale float64 // screen scale
2222 LinksOnly bool
23+ // Windows are the windows visible on Output, for sources that read a
24+ // toolkit's accessibility tree. Core owns the slice per request.
25+ Windows []HintWindow
26+}
27+
28+// HintWindow is a window visible on the hint output.
29+type HintWindow struct {
30+ ID WindowID
31+ PID int
32+ AppID string
33+ // Rect is the client area, logical output-local.
34+ Rect Rect
35+ Focused bool
2336 }
2437
2538 // HintTarget is a clickable region found in a frame.
@@ -20,6 +20,19 @@ type HintScanRequest struct {
20 Width, Height int20 Width, Height int
21 Scale float64 // screen scale21 Scale float64 // screen scale
22 LinksOnly bool22 LinksOnly bool
23+ // Windows are the windows visible on Output, for sources that read a
24+ // toolkit's accessibility tree. Core owns the slice per request.
25+ Windows []HintWindow
26+}
27+
28+// HintWindow is a window visible on the hint output.
29+type HintWindow struct {
30+ ID WindowID
31+ PID int
32+ AppID string
33+ // Rect is the client area, logical output-local.
34+ Rect Rect
35+ Focused bool
23 }36 }
24 37
25 // HintTarget is a clickable region found in a frame.38 // HintTarget is a clickable region found in a frame.

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