feat(core): send visible windows with hint scan requests
- commit
- 74fc2f3285035abc6a9f2c2ff7fb2d34db0ffb24
- author
- brice · · 2026-10-19 12:36 UTC
- parent
- be629c4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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() { | |||
| 59 | 59 | if sc.name() == "" || sc.transform != 0 || sc.mon.ov.open || sc.info.Width <= 0 || sc.info.Height <= 0 { | |
| 60 | 60 | 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 | 63 | select { | |
| 64 | 64 | case c.ch.HintScans <- req: | |
| 65 | 65 | default: | |
| @@ -68,6 +68,27 @@ func (c *Core) hintRequest() { | |||
| 68 | 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 | 92 | // hintPointerBusy reports whether the pointer is in use by something a | |
| 72 | 93 | // synthetic click would disturb: a pointer constraint, a grab, a held | |
| 73 | 94 | // 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 | return | 60 | 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 a | 92 | // hintPointerBusy reports whether the pointer is in use by something a | ||
| 72 | // synthetic click would disturb: a pointer constraint, a grab, a held | 93 | // synthetic click would disturb: a pointer constraint, a grab, a held | ||
| 73 | // button (swallowed ones included: they stay in c.buttons), a window or data | 94 | // 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 | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | + | "slices" | |
| 6 | 7 | "sync/atomic" | |
| 7 | 8 | "testing" | |
| 8 | 9 | "time" | |
| @@ -55,7 +56,7 @@ func newHint(t *testing.T, opts core.Options) *hintHarness { | |||
| 55 | 56 | t.Cleanup(func() { cancel(); receive(t, done) }) | |
| 56 | 57 | h.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 1000, Height: 800}} | |
| 57 | 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 | 60 | s := scene(t, h.scenes) | |
| 60 | 61 | h.win = s.Windows[0].Rect | |
| 61 | 62 | h.drain() | |
| @@ -133,6 +134,10 @@ func TestHintRequestOnBind(t *testing.T) { | |||
| 133 | 134 | if req.Output != "OUT-1" || req.Width != 1000 || req.Height != 800 || req.Scale != 1 || req.LinksOnly || req.Seq == 0 { | |
| 134 | 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 | 141 | // A second bind while pending does not ask again; the pending phase | |
| 137 | 142 | // cancels on any key. | |
| 138 | 143 | 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].Rect | 61 | 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 phase | 141 | // 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 { | |||
| 20 | 20 | Width, Height int | |
| 21 | 21 | Scale float64 // screen scale | |
| 22 | 22 | 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 | 38 | // HintTarget is a clickable region found in a frame. | |
| @@ -20,6 +20,19 @@ type HintScanRequest struct { | |||||
| 20 | Width, Height int | 20 | Width, Height int | ||
| 21 | Scale float64 // screen scale | 21 | Scale float64 // screen scale | ||
| 22 | LinksOnly bool | 22 | 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. | ||