feat(ports): add temporary modifier override for clients
- commit
- 6c0dbb620bad9980e11b56b922eaf85f8a7c0e1b
- author
- brice · · 2026-10-19 13:46 UTC
- parent
- 853fbd9
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +121 -8
M internal/adapters/hints/ocr.go +1 -1
| @@ -237,7 +237,7 @@ func textLinks(words []OCRWord, scale float64) []ports.HintTarget { | |||
| 237 | 237 | } | |
| 238 | 238 | x, y := int(math.Floor(float64(rect.Min.X)/scale)), int(math.Floor(float64(rect.Min.Y)/scale)) | |
| 239 | 239 | right, bottom := int(math.Ceil(float64(rect.Max.X)/scale)), int(math.Ceil(float64(rect.Max.Y)/scale)) | |
| 240 | - | out = append(out, ports.HintTarget{Kind: ports.HintLink, Rect: ports.Rect{X: x, Y: y, W: right - x, H: bottom - y}}) | |
| 240 | + | out = append(out, ports.HintTarget{Kind: ports.HintLink, Text: true, Rect: ports.Rect{X: x, Y: y, W: right - x, H: bottom - y}}) | |
| 241 | 241 | } | |
| 242 | 242 | return out | |
| 243 | 243 | } | |
| @@ -237,7 +237,7 @@ func textLinks(words []OCRWord, scale float64) []ports.HintTarget { | |||||
| 237 | } | 237 | } | ||
| 238 | x, y := int(math.Floor(float64(rect.Min.X)/scale)), int(math.Floor(float64(rect.Min.Y)/scale)) | 238 | x, y := int(math.Floor(float64(rect.Min.X)/scale)), int(math.Floor(float64(rect.Min.Y)/scale)) | ||
| 239 | right, bottom := int(math.Ceil(float64(rect.Max.X)/scale)), int(math.Ceil(float64(rect.Max.Y)/scale)) | 239 | right, bottom := int(math.Ceil(float64(rect.Max.X)/scale)), int(math.Ceil(float64(rect.Max.Y)/scale)) | ||
| 240 | - | out = append(out, ports.HintTarget{Kind: ports.HintLink, Rect: ports.Rect{X: x, Y: y, W: right - x, H: bottom - y}}) | 240 | + | out = append(out, ports.HintTarget{Kind: ports.HintLink, Text: true, Rect: ports.Rect{X: x, Y: y, W: right - x, H: bottom - y}}) |
| 241 | } | 241 | } | ||
| 242 | return out | 242 | return out | ||
| 243 | } | 243 | } | ||
M internal/adapters/wayland/keyboard_test.go +45 -1
| @@ -130,13 +130,14 @@ type eventsProxy struct { | |||
| 130 | 130 | opcodes []uint16 | |
| 131 | 131 | keymaps []string | |
| 132 | 132 | locked []uint32 // locked modifiers of each modifiers event | |
| 133 | + | depress []uint32 // depressed modifiers of each modifiers event | |
| 133 | 134 | } | |
| 134 | 135 | ||
| 135 | 136 | func (p *eventsProxy) Dispatch(e *wlturbo.Event) { | |
| 136 | 137 | p.opcodes = append(p.opcodes, e.Opcode) | |
| 137 | 138 | if e.Opcode == uint16(wayland.KeyboardEventModifiers) { | |
| 138 | 139 | e.Uint32() // serial | |
| 139 | - | e.Uint32() // depressed | |
| 140 | + | p.depress = append(p.depress, e.Uint32()) | |
| 140 | 141 | e.Uint32() // latched | |
| 141 | 142 | p.locked = append(p.locked, e.Uint32()) | |
| 142 | 143 | return | |
| @@ -254,3 +255,46 @@ func TestFootKeyboard(t *testing.T) { | |||
| 254 | 255 | } | |
| 255 | 256 | } | |
| 256 | 257 | } | |
| 258 | + | ||
| 259 | + | // SetModifiers sends the requested depressed mask with the real latched and | |
| 260 | + | // locked state, and Restore sends the real state back; the seat's own state | |
| 261 | + | // is untouched. A window that is not focused is ignored. | |
| 262 | + | func TestSetModifiersOverrideAndRestore(t *testing.T) { | |
| 263 | + | s, events, commands, dir := keyboardServer(t) | |
| 264 | + | target, proxy := focusedTarget(t, s, events, commands, dir) | |
| 265 | + | const capsLocked = 2 | |
| 266 | + | var id ports.WindowID | |
| 267 | + | set := make(chan struct{}) | |
| 268 | + | s.display.Do(func() { s.seat.modState = ports.ModState{Locked: capsLocked}; id = s.seat.focused; close(set) }) | |
| 269 | + | <-set | |
| 270 | + | wait := func() { | |
| 271 | + | t.Helper() | |
| 272 | + | deadline := time.Now().Add(time.Second) | |
| 273 | + | for len(commands) > 0 && time.Now().Before(deadline) { | |
| 274 | + | time.Sleep(5 * time.Millisecond) | |
| 275 | + | } | |
| 276 | + | time.Sleep(20 * time.Millisecond) | |
| 277 | + | roundtrip(t, target) | |
| 278 | + | } | |
| 279 | + | proxy.depress, proxy.locked = nil, nil | |
| 280 | + | commands <- ports.SetModifiers{ID: id + 100, Mods: ports.ModCtrl} | |
| 281 | + | wait() | |
| 282 | + | if len(proxy.depress) != 0 { | |
| 283 | + | t.Fatalf("unfocused window got modifiers %v", proxy.depress) | |
| 284 | + | } | |
| 285 | + | commands <- ports.SetModifiers{ID: id, Mods: ports.ModCtrl | ports.ModShift} | |
| 286 | + | wait() | |
| 287 | + | if !slices.Equal(proxy.depress, []uint32{xkbControl | xkbShift}) || !slices.Equal(proxy.locked, []uint32{capsLocked}) { | |
| 288 | + | t.Fatalf("override depressed %v locked %v", proxy.depress, proxy.locked) | |
| 289 | + | } | |
| 290 | + | commands <- ports.SetModifiers{ID: id, Restore: true} | |
| 291 | + | wait() | |
| 292 | + | if !slices.Equal(proxy.depress, []uint32{xkbControl | xkbShift, 0}) || !slices.Equal(proxy.locked, []uint32{capsLocked, capsLocked}) { | |
| 293 | + | t.Fatalf("restore depressed %v locked %v", proxy.depress, proxy.locked) | |
| 294 | + | } | |
| 295 | + | done := make(chan ports.ModState, 1) | |
| 296 | + | s.display.Do(func() { done <- s.seat.modState }) | |
| 297 | + | if m := <-done; m != (ports.ModState{Locked: capsLocked}) { | |
| 298 | + | t.Fatalf("seat state changed: %+v", m) | |
| 299 | + | } | |
| 300 | + | } | |
| @@ -130,13 +130,14 @@ type eventsProxy struct { | |||||
| 130 | opcodes []uint16 | 130 | opcodes []uint16 | ||
| 131 | keymaps []string | 131 | keymaps []string | ||
| 132 | locked []uint32 // locked modifiers of each modifiers event | 132 | locked []uint32 // locked modifiers of each modifiers event | ||
| 133 | + | depress []uint32 // depressed modifiers of each modifiers event | |||
| 133 | } | 134 | } | ||
| 134 | 135 | ||||
| 135 | func (p *eventsProxy) Dispatch(e *wlturbo.Event) { | 136 | func (p *eventsProxy) Dispatch(e *wlturbo.Event) { | ||
| 136 | p.opcodes = append(p.opcodes, e.Opcode) | 137 | p.opcodes = append(p.opcodes, e.Opcode) | ||
| 137 | if e.Opcode == uint16(wayland.KeyboardEventModifiers) { | 138 | if e.Opcode == uint16(wayland.KeyboardEventModifiers) { | ||
| 138 | e.Uint32() // serial | 139 | e.Uint32() // serial | ||
| 139 | - | e.Uint32() // depressed | 140 | + | p.depress = append(p.depress, e.Uint32()) |
| 140 | e.Uint32() // latched | 141 | e.Uint32() // latched | ||
| 141 | p.locked = append(p.locked, e.Uint32()) | 142 | p.locked = append(p.locked, e.Uint32()) | ||
| 142 | return | 143 | return | ||
| @@ -254,3 +255,46 @@ func TestFootKeyboard(t *testing.T) { | |||||
| 254 | } | 255 | } | ||
| 255 | } | 256 | } | ||
| 256 | } | 257 | } | ||
| 258 | + | ||||
| 259 | + | // SetModifiers sends the requested depressed mask with the real latched and | |||
| 260 | + | // locked state, and Restore sends the real state back; the seat's own state | |||
| 261 | + | // is untouched. A window that is not focused is ignored. | |||
| 262 | + | func TestSetModifiersOverrideAndRestore(t *testing.T) { | |||
| 263 | + | s, events, commands, dir := keyboardServer(t) | |||
| 264 | + | target, proxy := focusedTarget(t, s, events, commands, dir) | |||
| 265 | + | const capsLocked = 2 | |||
| 266 | + | var id ports.WindowID | |||
| 267 | + | set := make(chan struct{}) | |||
| 268 | + | s.display.Do(func() { s.seat.modState = ports.ModState{Locked: capsLocked}; id = s.seat.focused; close(set) }) | |||
| 269 | + | <-set | |||
| 270 | + | wait := func() { | |||
| 271 | + | t.Helper() | |||
| 272 | + | deadline := time.Now().Add(time.Second) | |||
| 273 | + | for len(commands) > 0 && time.Now().Before(deadline) { | |||
| 274 | + | time.Sleep(5 * time.Millisecond) | |||
| 275 | + | } | |||
| 276 | + | time.Sleep(20 * time.Millisecond) | |||
| 277 | + | roundtrip(t, target) | |||
| 278 | + | } | |||
| 279 | + | proxy.depress, proxy.locked = nil, nil | |||
| 280 | + | commands <- ports.SetModifiers{ID: id + 100, Mods: ports.ModCtrl} | |||
| 281 | + | wait() | |||
| 282 | + | if len(proxy.depress) != 0 { | |||
| 283 | + | t.Fatalf("unfocused window got modifiers %v", proxy.depress) | |||
| 284 | + | } | |||
| 285 | + | commands <- ports.SetModifiers{ID: id, Mods: ports.ModCtrl | ports.ModShift} | |||
| 286 | + | wait() | |||
| 287 | + | if !slices.Equal(proxy.depress, []uint32{xkbControl | xkbShift}) || !slices.Equal(proxy.locked, []uint32{capsLocked}) { | |||
| 288 | + | t.Fatalf("override depressed %v locked %v", proxy.depress, proxy.locked) | |||
| 289 | + | } | |||
| 290 | + | commands <- ports.SetModifiers{ID: id, Restore: true} | |||
| 291 | + | wait() | |||
| 292 | + | if !slices.Equal(proxy.depress, []uint32{xkbControl | xkbShift, 0}) || !slices.Equal(proxy.locked, []uint32{capsLocked, capsLocked}) { | |||
| 293 | + | t.Fatalf("restore depressed %v locked %v", proxy.depress, proxy.locked) | |||
| 294 | + | } | |||
| 295 | + | done := make(chan ports.ModState, 1) | |||
| 296 | + | s.display.Do(func() { done <- s.seat.modState }) | |||
| 297 | + | if m := <-done; m != (ports.ModState{Locked: capsLocked}) { | |||
| 298 | + | t.Fatalf("seat state changed: %+v", m) | |||
| 299 | + | } | |||
| 300 | + | } | |||
M internal/adapters/wayland/seat.go +51 -0
| @@ -83,6 +83,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||
| 83 | 83 | target = c.ID | |
| 84 | 84 | case ports.ForwardKey: | |
| 85 | 85 | target = c.ID | |
| 86 | + | case ports.SetModifiers: | |
| 87 | + | target = c.ID | |
| 86 | 88 | } | |
| 87 | 89 | if target != 0 && !s.lockInputTarget(target) { | |
| 88 | 90 | return | |
| @@ -177,6 +179,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||
| 177 | 179 | if s.seat.focused != c.ID { | |
| 178 | 180 | s.changeFocus(c.ID) | |
| 179 | 181 | } | |
| 182 | + | case ports.SetModifiers: | |
| 183 | + | s.setModifiers(c) | |
| 180 | 184 | case ports.ForwardKey: | |
| 181 | 185 | if !(s.security != nil && s.security.Snapshot().Protected) && s.grabKey(c) { | |
| 182 | 186 | return | |
| @@ -219,6 +223,53 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||
| 219 | 223 | } | |
| 220 | 224 | } | |
| 221 | 225 | ||
| 226 | + | // xkb modifier masks of the default keymaps: Shift, Control, Mod1 (Alt) and | |
| 227 | + | // Mod4 (Super). The server does not parse the keymap for its modifier indices. | |
| 228 | + | const ( | |
| 229 | + | xkbShift = 1 << 0 | |
| 230 | + | xkbControl = 1 << 2 | |
| 231 | + | xkbMod1 = 1 << 3 | |
| 232 | + | xkbMod4 = 1 << 6 | |
| 233 | + | ) | |
| 234 | + | ||
| 235 | + | // depressedMask maps core modifier flags to xkb depressed masks. | |
| 236 | + | func depressedMask(m ports.Mods) uint32 { | |
| 237 | + | var d uint32 | |
| 238 | + | for _, f := range []struct { | |
| 239 | + | flag ports.Mods | |
| 240 | + | mask uint32 | |
| 241 | + | }{{ports.ModShift, xkbShift}, {ports.ModCtrl, xkbControl}, {ports.ModAlt, xkbMod1}, {ports.ModSuper, xkbMod4}} { | |
| 242 | + | if m&f.flag != 0 { | |
| 243 | + | d |= f.mask | |
| 244 | + | } | |
| 245 | + | } | |
| 246 | + | return d | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | // setModifiers overrides, then restores, the modifiers the focused window's | |
| 250 | + | // keyboards (or the input method grab) see. The override replaces the | |
| 251 | + | // depressed modifiers and keeps latched, locked and group; the seat's real | |
| 252 | + | // modState is never changed by it. | |
| 253 | + | func (s *Server) setModifiers(c ports.SetModifiers) { | |
| 254 | + | _, keyboards := s.focusTarget(c.ID) | |
| 255 | + | if c.ID != s.seat.focused || len(keyboards) == 0 { | |
| 256 | + | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored set modifiers") | |
| 257 | + | return | |
| 258 | + | } | |
| 259 | + | s.useKeymap(nil) | |
| 260 | + | s.serial++ | |
| 261 | + | m := s.seat.modState | |
| 262 | + | if !c.Restore { | |
| 263 | + | m.Depressed = depressedMask(c.Mods) | |
| 264 | + | } | |
| 265 | + | for _, k := range keyboards { | |
| 266 | + | k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group) | |
| 267 | + | } | |
| 268 | + | if g := s.grab(); g != nil { | |
| 269 | + | g.res.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group) | |
| 270 | + | } | |
| 271 | + | } | |
| 272 | + | ||
| 222 | 273 | func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | |
| 223 | 274 | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | |
| 224 | 275 | return nil | |
| @@ -83,6 +83,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||||
| 83 | target = c.ID | 83 | target = c.ID | ||
| 84 | case ports.ForwardKey: | 84 | case ports.ForwardKey: | ||
| 85 | target = c.ID | 85 | target = c.ID | ||
| 86 | + | case ports.SetModifiers: | |||
| 87 | + | target = c.ID | |||
| 86 | } | 88 | } | ||
| 87 | if target != 0 && !s.lockInputTarget(target) { | 89 | if target != 0 && !s.lockInputTarget(target) { | ||
| 88 | return | 90 | return | ||
| @@ -177,6 +179,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||||
| 177 | if s.seat.focused != c.ID { | 179 | if s.seat.focused != c.ID { | ||
| 178 | s.changeFocus(c.ID) | 180 | s.changeFocus(c.ID) | ||
| 179 | } | 181 | } | ||
| 182 | + | case ports.SetModifiers: | |||
| 183 | + | s.setModifiers(c) | |||
| 180 | case ports.ForwardKey: | 184 | case ports.ForwardKey: | ||
| 181 | if !(s.security != nil && s.security.Snapshot().Protected) && s.grabKey(c) { | 185 | if !(s.security != nil && s.security.Snapshot().Protected) && s.grabKey(c) { | ||
| 182 | return | 186 | return | ||
| @@ -219,6 +223,53 @@ func (s *Server) applyInput(cmd ports.ClientCommand) { | |||||
| 219 | } | 223 | } | ||
| 220 | } | 224 | } | ||
| 221 | 225 | ||||
| 226 | + | // xkb modifier masks of the default keymaps: Shift, Control, Mod1 (Alt) and | |||
| 227 | + | // Mod4 (Super). The server does not parse the keymap for its modifier indices. | |||
| 228 | + | const ( | |||
| 229 | + | xkbShift = 1 << 0 | |||
| 230 | + | xkbControl = 1 << 2 | |||
| 231 | + | xkbMod1 = 1 << 3 | |||
| 232 | + | xkbMod4 = 1 << 6 | |||
| 233 | + | ) | |||
| 234 | + | ||||
| 235 | + | // depressedMask maps core modifier flags to xkb depressed masks. | |||
| 236 | + | func depressedMask(m ports.Mods) uint32 { | |||
| 237 | + | var d uint32 | |||
| 238 | + | for _, f := range []struct { | |||
| 239 | + | flag ports.Mods | |||
| 240 | + | mask uint32 | |||
| 241 | + | }{{ports.ModShift, xkbShift}, {ports.ModCtrl, xkbControl}, {ports.ModAlt, xkbMod1}, {ports.ModSuper, xkbMod4}} { | |||
| 242 | + | if m&f.flag != 0 { | |||
| 243 | + | d |= f.mask | |||
| 244 | + | } | |||
| 245 | + | } | |||
| 246 | + | return d | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | // setModifiers overrides, then restores, the modifiers the focused window's | |||
| 250 | + | // keyboards (or the input method grab) see. The override replaces the | |||
| 251 | + | // depressed modifiers and keeps latched, locked and group; the seat's real | |||
| 252 | + | // modState is never changed by it. | |||
| 253 | + | func (s *Server) setModifiers(c ports.SetModifiers) { | |||
| 254 | + | _, keyboards := s.focusTarget(c.ID) | |||
| 255 | + | if c.ID != s.seat.focused || len(keyboards) == 0 { | |||
| 256 | + | s.log.Debug().Uint64("id", uint64(c.ID)).Msg("ignored set modifiers") | |||
| 257 | + | return | |||
| 258 | + | } | |||
| 259 | + | s.useKeymap(nil) | |||
| 260 | + | s.serial++ | |||
| 261 | + | m := s.seat.modState | |||
| 262 | + | if !c.Restore { | |||
| 263 | + | m.Depressed = depressedMask(c.Mods) | |||
| 264 | + | } | |||
| 265 | + | for _, k := range keyboards { | |||
| 266 | + | k.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group) | |||
| 267 | + | } | |||
| 268 | + | if g := s.grab(); g != nil { | |||
| 269 | + | g.res.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group) | |||
| 270 | + | } | |||
| 271 | + | } | |||
| 272 | + | ||||
| 222 | func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | 273 | func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard { | ||
| 223 | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | 274 | if w == nil || !w.mapped || !w.xdg.resource.Resource.Alive() { | ||
| 224 | return nil | 275 | return nil | ||
M internal/adapters/wayland/server.go +2 -2
| @@ -752,13 +752,13 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||
| 752 | 752 | // Only input/focus commands require an owner envelope. Config relay | |
| 753 | 753 | // also produces SetKeymap and other epoch-independent commands. | |
| 754 | 754 | switch cmd.(type) { | |
| 755 | - | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey, | |
| 755 | + | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey, ports.SetModifiers, | |
| 756 | 756 | ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel: | |
| 757 | 757 | return | |
| 758 | 758 | } | |
| 759 | 759 | } | |
| 760 | 760 | switch c := cmd.(type) { | |
| 761 | - | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey: | |
| 761 | + | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey, ports.SetModifiers: | |
| 762 | 762 | s.applyInput(c) | |
| 763 | 763 | case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel: | |
| 764 | 764 | s.applyDrag(c) | |
| @@ -752,13 +752,13 @@ func (s *Server) apply(cmd ports.ClientCommand) { | |||||
| 752 | // Only input/focus commands require an owner envelope. Config relay | 752 | // Only input/focus commands require an owner envelope. Config relay | ||
| 753 | // also produces SetKeymap and other epoch-independent commands. | 753 | // also produces SetKeymap and other epoch-independent commands. | ||
| 754 | switch cmd.(type) { | 754 | switch cmd.(type) { | ||
| 755 | - | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey, | 755 | + | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey, ports.SetModifiers, |
| 756 | ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel: | 756 | ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel: | ||
| 757 | return | 757 | return | ||
| 758 | } | 758 | } | ||
| 759 | } | 759 | } | ||
| 760 | switch c := cmd.(type) { | 760 | switch c := cmd.(type) { | ||
| 761 | - | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey: | 761 | + | case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey, ports.SetModifiers: |
| 762 | s.applyInput(c) | 762 | s.applyInput(c) | ||
| 763 | case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel: | 763 | case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel: | ||
| 764 | s.applyDrag(c) | 764 | s.applyDrag(c) | ||
M internal/ports/client_commands.go +12 -0
| @@ -289,3 +289,15 @@ type SpawnRequest struct { | |||
| 289 | 289 | // Env adds KEY=value entries to the child environment. | |
| 290 | 290 | Env []string | |
| 291 | 291 | } | |
| 292 | + | ||
| 293 | + | // SetModifiers carries core → wayland a temporary modifier override for the | |
| 294 | + | // focused window's keyboards: Mods are sent as depressed modifiers (mapped | |
| 295 | + | // to the current keymap's masks), latched/locked/group kept from the real | |
| 296 | + | // state. Mods 0 with Restore true sends the real modifier state again. | |
| 297 | + | type SetModifiers struct { | |
| 298 | + | ID WindowID | |
| 299 | + | Mods Mods | |
| 300 | + | Restore bool | |
| 301 | + | } | |
| 302 | + | ||
| 303 | + | func (SetModifiers) clientCommand() {} | |
| @@ -289,3 +289,15 @@ type SpawnRequest struct { | |||||
| 289 | // Env adds KEY=value entries to the child environment. | 289 | // Env adds KEY=value entries to the child environment. | ||
| 290 | Env []string | 290 | Env []string | ||
| 291 | } | 291 | } | ||
| 292 | + | ||||
| 293 | + | // SetModifiers carries core → wayland a temporary modifier override for the | |||
| 294 | + | // focused window's keyboards: Mods are sent as depressed modifiers (mapped | |||
| 295 | + | // to the current keymap's masks), latched/locked/group kept from the real | |||
| 296 | + | // state. Mods 0 with Restore true sends the real modifier state again. | |||
| 297 | + | type SetModifiers struct { | |||
| 298 | + | ID WindowID | |||
| 299 | + | Mods Mods | |||
| 300 | + | Restore bool | |||
| 301 | + | } | |||
| 302 | + | ||||
| 303 | + | func (SetModifiers) clientCommand() {} | |||
M internal/ports/config.go +4 -1
| @@ -165,7 +165,10 @@ type Config struct { | |||
| 165 | 165 | Hint struct { | |
| 166 | 166 | // LinksOnly limits the targets to links and URLs; false also | |
| 167 | 167 | // includes buttons (hint.targets = clickable). | |
| 168 | - | LinksOnly bool | |
| 168 | + | LinksOnly bool | |
| 169 | + | // TextClick are the modifiers held for a click on a link found by | |
| 170 | + | // OCR (hint.text-click); zero sends a plain click. | |
| 171 | + | TextClick Mods | |
| 169 | 172 | Size float64 // logical pixels; zero selects automatic density sizing | |
| 170 | 173 | Background, Foreground string // empty derives from Border.Active | |
| 171 | 174 | } | |
| @@ -165,7 +165,10 @@ type Config struct { | |||||
| 165 | Hint struct { | 165 | Hint struct { | ||
| 166 | // LinksOnly limits the targets to links and URLs; false also | 166 | // LinksOnly limits the targets to links and URLs; false also | ||
| 167 | // includes buttons (hint.targets = clickable). | 167 | // includes buttons (hint.targets = clickable). | ||
| 168 | - | LinksOnly bool | 168 | + | LinksOnly bool |
| 169 | + | // TextClick are the modifiers held for a click on a link found by | |||
| 170 | + | // OCR (hint.text-click); zero sends a plain click. | |||
| 171 | + | TextClick Mods | |||
| 169 | Size float64 // logical pixels; zero selects automatic density sizing | 172 | Size float64 // logical pixels; zero selects automatic density sizing | ||
| 170 | Background, Foreground string // empty derives from Border.Active | 173 | Background, Foreground string // empty derives from Border.Active | ||
| 171 | } | 174 | } | ||
M internal/ports/hint_messages.go +6 -3
| @@ -39,6 +39,9 @@ type HintWindow struct { | |||
| 39 | 39 | type HintTarget struct { | |
| 40 | 40 | Rect Rect // logical, output-local | |
| 41 | 41 | Kind HintKind | |
| 42 | + | // Text is set for targets found by OCR (read from pixels, not from an | |
| 43 | + | // accessibility tree). | |
| 44 | + | Text bool | |
| 42 | 45 | } | |
| 43 | 46 | ||
| 44 | 47 | // HintScanResult answers a HintScanRequest. Core owns Targets. | |
| @@ -51,9 +54,9 @@ type HintScanResult struct { | |||
| 51 | 54 | ||
| 52 | 55 | // HintLabel is a label drawn over a hint target. | |
| 53 | 56 | type HintLabel struct { | |
| 54 | - | // Rect is the label box in logical output-local coordinates, placed at | |
| 55 | - | // the target's top-left. W and H may be 0: the renderer sizes the box | |
| 56 | - | // from Text. | |
| 57 | + | // Rect is the label box in logical output-local coordinates; X and Y are | |
| 58 | + | // the target's top-left corner, which the renderer anchors the label | |
| 59 | + | // outside of. W and H may be 0: the renderer sizes the box from Text. | |
| 57 | 60 | Rect Rect | |
| 58 | 61 | // Text is the full label, lowercase a–z. | |
| 59 | 62 | Text string | |
| @@ -39,6 +39,9 @@ type HintWindow struct { | |||||
| 39 | type HintTarget struct { | 39 | type HintTarget struct { | ||
| 40 | Rect Rect // logical, output-local | 40 | Rect Rect // logical, output-local | ||
| 41 | Kind HintKind | 41 | Kind HintKind | ||
| 42 | + | // Text is set for targets found by OCR (read from pixels, not from an | |||
| 43 | + | // accessibility tree). | |||
| 44 | + | Text bool | |||
| 42 | } | 45 | } | ||
| 43 | 46 | ||||
| 44 | // HintScanResult answers a HintScanRequest. Core owns Targets. | 47 | // HintScanResult answers a HintScanRequest. Core owns Targets. | ||
| @@ -51,9 +54,9 @@ type HintScanResult struct { | |||||
| 51 | 54 | ||||
| 52 | // HintLabel is a label drawn over a hint target. | 55 | // HintLabel is a label drawn over a hint target. | ||
| 53 | type HintLabel struct { | 56 | type HintLabel struct { | ||
| 54 | - | // Rect is the label box in logical output-local coordinates, placed at | 57 | + | // Rect is the label box in logical output-local coordinates; X and Y are |
| 55 | - | // the target's top-left. W and H may be 0: the renderer sizes the box | 58 | + | // the target's top-left corner, which the renderer anchors the label |
| 56 | - | // from Text. | 59 | + | // outside of. W and H may be 0: the renderer sizes the box from Text. |
| 57 | Rect Rect | 60 | Rect Rect | ||
| 58 | // Text is the full label, lowercase a–z. | 61 | // Text is the full label, lowercase a–z. | ||
| 59 | Text string | 62 | Text string | ||