feat(ports): add hint scan, modifier override and hint config messages
- commit
- 689e994ef8fc44f11aafc324989a4066be2c9e28
- author
- brice · · 2026-10-19 16:03 UTC
- committer
- brice · · 2026-10-19 17:36 UTC
- parent
- 9793569
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +119 -3
M internal/ports/capture_messages.go +6 -0
| @@ -65,3 +65,9 @@ type CaptureDone struct { | |||
| 65 | 65 | Err error | |
| 66 | 66 | Time time.Time | |
| 67 | 67 | } | |
| 68 | + | ||
| 69 | + | // CaptureIDInternal is the bit set in the ID of captures requested by the | |
| 70 | + | // compositor itself (hint scanning) rather than by a Wayland client. Wayland | |
| 71 | + | // IDs start at 1 and increment, so they never set it. Replies carrying it are | |
| 72 | + | // demultiplexed away from the Wayland consumer. | |
| 73 | + | const CaptureIDInternal uint64 = 1 << 63 | |
| @@ -65,3 +65,9 @@ type CaptureDone struct { | |||||
| 65 | Err error | 65 | Err error | ||
| 66 | Time time.Time | 66 | Time time.Time | ||
| 67 | } | 67 | } | ||
| 68 | + | ||||
| 69 | + | // CaptureIDInternal is the bit set in the ID of captures requested by the | |||
| 70 | + | // compositor itself (hint scanning) rather than by a Wayland client. Wayland | |||
| 71 | + | // IDs start at 1 and increment, so they never set it. Replies carrying it are | |||
| 72 | + | // demultiplexed away from the Wayland consumer. | |||
| 73 | + | const CaptureIDInternal uint64 = 1 << 63 | |||
M internal/ports/client_commands.go +14 -0
| @@ -289,3 +289,17 @@ 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: State | |
| 297 | + | // is that state as core last saw it, which the seat adopts. | |
| 298 | + | type SetModifiers struct { | |
| 299 | + | ID WindowID | |
| 300 | + | Mods Mods | |
| 301 | + | Restore bool | |
| 302 | + | State ModState | |
| 303 | + | } | |
| 304 | + | ||
| 305 | + | func (SetModifiers) clientCommand() {} | |
| @@ -289,3 +289,17 @@ 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: State | |||
| 297 | + | // is that state as core last saw it, which the seat adopts. | |||
| 298 | + | type SetModifiers struct { | |||
| 299 | + | ID WindowID | |||
| 300 | + | Mods Mods | |||
| 301 | + | Restore bool | |||
| 302 | + | State ModState | |||
| 303 | + | } | |||
| 304 | + | ||||
| 305 | + | func (SetModifiers) clientCommand() {} | |||
M internal/ports/config.go +14 -0
| @@ -161,6 +161,20 @@ type Config struct { | |||
| 161 | 161 | FlashDuration time.Duration | |
| 162 | 162 | FlashColor string | |
| 163 | 163 | } | |
| 164 | + | // Hint configures keyboard hint mode (action hint). | |
| 165 | + | Hint struct { | |
| 166 | + | // Enable turns the experimental hint mode on (hint.enable). It is | |
| 167 | + | // read at startup: a reload keeps the startup value. | |
| 168 | + | Enable bool | |
| 169 | + | // LinksOnly limits the targets to links and URLs; false also | |
| 170 | + | // includes buttons (hint.targets = clickable). | |
| 171 | + | LinksOnly bool | |
| 172 | + | // TextClick are the modifiers held for a click on a link found by | |
| 173 | + | // OCR (hint.text-click); zero sends a plain click. | |
| 174 | + | TextClick Mods | |
| 175 | + | Size float64 // logical pixels; zero selects automatic density sizing | |
| 176 | + | Background, Foreground string // empty derives from Border.Active | |
| 177 | + | } | |
| 164 | 178 | // Workspaces are declared with workspace.<name>.* keys, in first-seen order. | |
| 165 | 179 | Workspaces []WorkspaceConfig | |
| 166 | 180 | // Rules are the window rules (rule.<name>.* keys), in first-seen order. | |
| @@ -161,6 +161,20 @@ type Config struct { | |||||
| 161 | FlashDuration time.Duration | 161 | FlashDuration time.Duration | ||
| 162 | FlashColor string | 162 | FlashColor string | ||
| 163 | } | 163 | } | ||
| 164 | + | // Hint configures keyboard hint mode (action hint). | |||
| 165 | + | Hint struct { | |||
| 166 | + | // Enable turns the experimental hint mode on (hint.enable). It is | |||
| 167 | + | // read at startup: a reload keeps the startup value. | |||
| 168 | + | Enable bool | |||
| 169 | + | // LinksOnly limits the targets to links and URLs; false also | |||
| 170 | + | // includes buttons (hint.targets = clickable). | |||
| 171 | + | LinksOnly bool | |||
| 172 | + | // TextClick are the modifiers held for a click on a link found by | |||
| 173 | + | // OCR (hint.text-click); zero sends a plain click. | |||
| 174 | + | TextClick Mods | |||
| 175 | + | Size float64 // logical pixels; zero selects automatic density sizing | |||
| 176 | + | Background, Foreground string // empty derives from Border.Active | |||
| 177 | + | } | |||
| 164 | // Workspaces are declared with workspace.<name>.* keys, in first-seen order. | 178 | // Workspaces are declared with workspace.<name>.* keys, in first-seen order. | ||
| 165 | Workspaces []WorkspaceConfig | 179 | Workspaces []WorkspaceConfig | ||
| 166 | // Rules are the window rules (rule.<name>.* keys), in first-seen order. | 180 | // Rules are the window rules (rule.<name>.* keys), in first-seen order. | ||
A internal/ports/hint_messages.go +67 -0
| @@ -0,0 +1,67 @@ | |||
| 1 | + | package ports | |
| 2 | + | ||
| 3 | + | // HintKind is the kind of a clickable hint target. | |
| 4 | + | type HintKind uint8 | |
| 5 | + | ||
| 6 | + | const ( | |
| 7 | + | // HintLink is a link or URL. | |
| 8 | + | HintLink HintKind = iota | |
| 9 | + | // HintButton is a button or control. | |
| 10 | + | HintButton | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // HintScanRequest asks the hint scanner to find clickable targets in the last | |
| 14 | + | // frame of Output. Core sends it and expects one HintScanResult echoing Seq. | |
| 15 | + | type HintScanRequest struct { | |
| 16 | + | Seq uint64 // core-chosen, echoed back | |
| 17 | + | Output string | |
| 18 | + | // Width and Height are the output buffer size in physical pixels | |
| 19 | + | // (OutputInfo.Width/Height). | |
| 20 | + | Width, Height int | |
| 21 | + | Scale float64 // screen scale | |
| 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 | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // HintTarget is a clickable region found in a frame. | |
| 39 | + | type HintTarget struct { | |
| 40 | + | Rect Rect // logical, output-local | |
| 41 | + | Kind HintKind | |
| 42 | + | // Text is set for targets found by OCR (read from pixels, not from an | |
| 43 | + | // accessibility tree). | |
| 44 | + | Text bool | |
| 45 | + | // Window is the source window of an accessibility target, 0 for OCR. | |
| 46 | + | Window WindowID | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | // HintScanResult answers a HintScanRequest. Core owns Targets. | |
| 50 | + | type HintScanResult struct { | |
| 51 | + | Seq uint64 | |
| 52 | + | Output string | |
| 53 | + | Targets []HintTarget | |
| 54 | + | Err error | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // HintLabel is a label drawn over a hint target. | |
| 58 | + | type HintLabel struct { | |
| 59 | + | // Rect is the label box in logical output-local coordinates; X and Y are | |
| 60 | + | // the target's top-left corner, which the renderer anchors the label | |
| 61 | + | // outside of. W and H may be 0: the renderer sizes the box from Text. | |
| 62 | + | Rect Rect | |
| 63 | + | // Text is the full label, lowercase a–z. | |
| 64 | + | Text string | |
| 65 | + | // Typed is the number of leading characters already typed (drawn dimmed). | |
| 66 | + | Typed int | |
| 67 | + | } | |
| @@ -0,0 +1,67 @@ | |||||
| 1 | + | package ports | |||
| 2 | + | ||||
| 3 | + | // HintKind is the kind of a clickable hint target. | |||
| 4 | + | type HintKind uint8 | |||
| 5 | + | ||||
| 6 | + | const ( | |||
| 7 | + | // HintLink is a link or URL. | |||
| 8 | + | HintLink HintKind = iota | |||
| 9 | + | // HintButton is a button or control. | |||
| 10 | + | HintButton | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // HintScanRequest asks the hint scanner to find clickable targets in the last | |||
| 14 | + | // frame of Output. Core sends it and expects one HintScanResult echoing Seq. | |||
| 15 | + | type HintScanRequest struct { | |||
| 16 | + | Seq uint64 // core-chosen, echoed back | |||
| 17 | + | Output string | |||
| 18 | + | // Width and Height are the output buffer size in physical pixels | |||
| 19 | + | // (OutputInfo.Width/Height). | |||
| 20 | + | Width, Height int | |||
| 21 | + | Scale float64 // screen scale | |||
| 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 | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // HintTarget is a clickable region found in a frame. | |||
| 39 | + | type HintTarget struct { | |||
| 40 | + | Rect Rect // logical, output-local | |||
| 41 | + | Kind HintKind | |||
| 42 | + | // Text is set for targets found by OCR (read from pixels, not from an | |||
| 43 | + | // accessibility tree). | |||
| 44 | + | Text bool | |||
| 45 | + | // Window is the source window of an accessibility target, 0 for OCR. | |||
| 46 | + | Window WindowID | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | // HintScanResult answers a HintScanRequest. Core owns Targets. | |||
| 50 | + | type HintScanResult struct { | |||
| 51 | + | Seq uint64 | |||
| 52 | + | Output string | |||
| 53 | + | Targets []HintTarget | |||
| 54 | + | Err error | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // HintLabel is a label drawn over a hint target. | |||
| 58 | + | type HintLabel struct { | |||
| 59 | + | // Rect is the label box in logical output-local coordinates; X and Y are | |||
| 60 | + | // the target's top-left corner, which the renderer anchors the label | |||
| 61 | + | // outside of. W and H may be 0: the renderer sizes the box from Text. | |||
| 62 | + | Rect Rect | |||
| 63 | + | // Text is the full label, lowercase a–z. | |||
| 64 | + | Text string | |||
| 65 | + | // Typed is the number of leading characters already typed (drawn dimmed). | |||
| 66 | + | Typed int | |||
| 67 | + | } | |||
M internal/ports/scene.go +14 -1
| @@ -69,6 +69,11 @@ type Scene struct { | |||
| 69 | 69 | // drawn over everything; nil when none. Captures never hold them: the | |
| 70 | 70 | // capture pipeline serves requests from a scene without them. | |
| 71 | 71 | CaptureIndicators []CaptureIndicator | |
| 72 | + | // HintLabels are the keyboard hint labels of this output, drawn over | |
| 73 | + | // everything, after CaptureIndicators; nil when hint mode is not shown | |
| 74 | + | // here. Captures never contain them. | |
| 75 | + | HintLabels []HintLabel | |
| 76 | + | HintStyle HintStyle | |
| 72 | 77 | // Flash is the focus flash drawn over everything of this output; nil | |
| 73 | 78 | // draws nothing. | |
| 74 | 79 | Flash *FocusFlash | |
| @@ -112,7 +117,8 @@ func (s Scene) SameAs(o Scene) bool { | |||
| 112 | 117 | } | |
| 113 | 118 | return slices.Equal(s.Windows, o.Windows) && slices.Equal(s.Separators, o.Separators) && | |
| 114 | 119 | slices.Equal(s.DropHints, o.DropHints) && slices.Equal(s.Layers, o.Layers) && | |
| 115 | - | slices.Equal(s.CaptureIndicators, o.CaptureIndicators) | |
| 120 | + | slices.Equal(s.CaptureIndicators, o.CaptureIndicators) && | |
| 121 | + | s.HintStyle == o.HintStyle && slices.Equal(s.HintLabels, o.HintLabels) | |
| 116 | 122 | } | |
| 117 | 123 | ||
| 118 | 124 | // Shows reports whether the scene draws the surface of id: only its | |
| @@ -233,3 +239,10 @@ type SceneWindow struct { | |||
| 233 | 239 | func (w SceneWindow) Tile() bool { | |
| 234 | 240 | return !w.Floating && !w.Fullscreen && !w.Popup && w.Preview == 0 | |
| 235 | 241 | } | |
| 242 | + | ||
| 243 | + | // HintStyle carries reloadable label settings and untransformed output density. | |
| 244 | + | type HintStyle struct { | |
| 245 | + | Size float64 | |
| 246 | + | Background, Foreground string | |
| 247 | + | Width, Height, PhysicalW, PhysicalH int | |
| 248 | + | } | |
| @@ -69,6 +69,11 @@ type Scene struct { | |||||
| 69 | // drawn over everything; nil when none. Captures never hold them: the | 69 | // drawn over everything; nil when none. Captures never hold them: the | ||
| 70 | // capture pipeline serves requests from a scene without them. | 70 | // capture pipeline serves requests from a scene without them. | ||
| 71 | CaptureIndicators []CaptureIndicator | 71 | CaptureIndicators []CaptureIndicator | ||
| 72 | + | // HintLabels are the keyboard hint labels of this output, drawn over | |||
| 73 | + | // everything, after CaptureIndicators; nil when hint mode is not shown | |||
| 74 | + | // here. Captures never contain them. | |||
| 75 | + | HintLabels []HintLabel | |||
| 76 | + | HintStyle HintStyle | |||
| 72 | // Flash is the focus flash drawn over everything of this output; nil | 77 | // Flash is the focus flash drawn over everything of this output; nil | ||
| 73 | // draws nothing. | 78 | // draws nothing. | ||
| 74 | Flash *FocusFlash | 79 | Flash *FocusFlash | ||
| @@ -112,7 +117,8 @@ func (s Scene) SameAs(o Scene) bool { | |||||
| 112 | } | 117 | } | ||
| 113 | return slices.Equal(s.Windows, o.Windows) && slices.Equal(s.Separators, o.Separators) && | 118 | return slices.Equal(s.Windows, o.Windows) && slices.Equal(s.Separators, o.Separators) && | ||
| 114 | slices.Equal(s.DropHints, o.DropHints) && slices.Equal(s.Layers, o.Layers) && | 119 | slices.Equal(s.DropHints, o.DropHints) && slices.Equal(s.Layers, o.Layers) && | ||
| 115 | - | slices.Equal(s.CaptureIndicators, o.CaptureIndicators) | 120 | + | slices.Equal(s.CaptureIndicators, o.CaptureIndicators) && |
| 121 | + | s.HintStyle == o.HintStyle && slices.Equal(s.HintLabels, o.HintLabels) | |||
| 116 | } | 122 | } | ||
| 117 | 123 | ||||
| 118 | // Shows reports whether the scene draws the surface of id: only its | 124 | // Shows reports whether the scene draws the surface of id: only its | ||
| @@ -233,3 +239,10 @@ type SceneWindow struct { | |||||
| 233 | func (w SceneWindow) Tile() bool { | 239 | func (w SceneWindow) Tile() bool { | ||
| 234 | return !w.Floating && !w.Fullscreen && !w.Popup && w.Preview == 0 | 240 | return !w.Floating && !w.Fullscreen && !w.Popup && w.Preview == 0 | ||
| 235 | } | 241 | } | ||
| 242 | + | ||||
| 243 | + | // HintStyle carries reloadable label settings and untransformed output density. | |||
| 244 | + | type HintStyle struct { | |||
| 245 | + | Size float64 | |||
| 246 | + | Background, Foreground string | |||
| 247 | + | Width, Height, PhysicalW, PhysicalH int | |||
| 248 | + | } | |||
M internal/ports/scene_same_test.go +4 -2
| @@ -25,6 +25,7 @@ func sameSceneBase() Scene { | |||
| 25 | 25 | ||
| 26 | 26 | Flash: &FocusFlash{Rect: Rect{W: 20, H: 10}, Width: 2, Color: "#ff0000", Alpha: 0.6}, | |
| 27 | 27 | CaptureIndicators: []CaptureIndicator{{Rect: Rect{W: 4, H: 4}}}, | |
| 28 | + | HintLabels: []HintLabel{{Rect: Rect{X: 3, Y: 4}, Text: "fj", Typed: 1}}, | |
| 28 | 29 | } | |
| 29 | 30 | } | |
| 30 | 31 | ||
| @@ -61,6 +62,7 @@ func TestSceneSameAs(t *testing.T) { | |||
| 61 | 62 | "drop hint": func(s *Scene) { s.DropHints[0].W++ }, | |
| 62 | 63 | "layer": func(s *Scene) { s.Layers[0].Rect.H++ }, | |
| 63 | 64 | "indicator": func(s *Scene) { s.CaptureIndicators[0].Pill = true }, | |
| 65 | + | "hint style": func(s *Scene) { s.HintStyle.Size = 24 }, | |
| 64 | 66 | "excluded": func(s *Scene) { s.Capture.Excluded[0] = 4 }, | |
| 65 | 67 | "transform": func(s *Scene) { s.Transform = 1 }, | |
| 66 | 68 | "capture shown": func(s *Scene) { s.Capture.Shown++ }, | |
| @@ -89,9 +91,9 @@ func TestSceneSameAs(t *testing.T) { | |||
| 89 | 91 | // TestSceneSameAsCoversEveryField fails when Scene gains or loses a field: | |
| 90 | 92 | // update Scene.SameAs, then this count. | |
| 91 | 93 | func TestSceneSameAsCoversEveryField(t *testing.T) { | |
| 92 | - | // 23 with Animating, which SameAs intentionally does not compare: it | |
| 94 | + | // 25 with Animating, which SameAs intentionally does not compare: it | |
| 93 | 95 | // only schedules the next frame, it draws nothing. | |
| 94 | - | const fields = 23 | |
| 96 | + | const fields = 25 | |
| 95 | 97 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(Scene{}))), | |
| 96 | 98 | "Scene fields changed: update Scene.SameAs (and TestSceneSameAs), then this count") | |
| 97 | 99 | } | |
| @@ -25,6 +25,7 @@ func sameSceneBase() Scene { | |||||
| 25 | 25 | ||||
| 26 | Flash: &FocusFlash{Rect: Rect{W: 20, H: 10}, Width: 2, Color: "#ff0000", Alpha: 0.6}, | 26 | Flash: &FocusFlash{Rect: Rect{W: 20, H: 10}, Width: 2, Color: "#ff0000", Alpha: 0.6}, | ||
| 27 | CaptureIndicators: []CaptureIndicator{{Rect: Rect{W: 4, H: 4}}}, | 27 | CaptureIndicators: []CaptureIndicator{{Rect: Rect{W: 4, H: 4}}}, | ||
| 28 | + | HintLabels: []HintLabel{{Rect: Rect{X: 3, Y: 4}, Text: "fj", Typed: 1}}, | |||
| 28 | } | 29 | } | ||
| 29 | } | 30 | } | ||
| 30 | 31 | ||||
| @@ -61,6 +62,7 @@ func TestSceneSameAs(t *testing.T) { | |||||
| 61 | "drop hint": func(s *Scene) { s.DropHints[0].W++ }, | 62 | "drop hint": func(s *Scene) { s.DropHints[0].W++ }, | ||
| 62 | "layer": func(s *Scene) { s.Layers[0].Rect.H++ }, | 63 | "layer": func(s *Scene) { s.Layers[0].Rect.H++ }, | ||
| 63 | "indicator": func(s *Scene) { s.CaptureIndicators[0].Pill = true }, | 64 | "indicator": func(s *Scene) { s.CaptureIndicators[0].Pill = true }, | ||
| 65 | + | "hint style": func(s *Scene) { s.HintStyle.Size = 24 }, | |||
| 64 | "excluded": func(s *Scene) { s.Capture.Excluded[0] = 4 }, | 66 | "excluded": func(s *Scene) { s.Capture.Excluded[0] = 4 }, | ||
| 65 | "transform": func(s *Scene) { s.Transform = 1 }, | 67 | "transform": func(s *Scene) { s.Transform = 1 }, | ||
| 66 | "capture shown": func(s *Scene) { s.Capture.Shown++ }, | 68 | "capture shown": func(s *Scene) { s.Capture.Shown++ }, | ||
| @@ -89,9 +91,9 @@ func TestSceneSameAs(t *testing.T) { | |||||
| 89 | // TestSceneSameAsCoversEveryField fails when Scene gains or loses a field: | 91 | // TestSceneSameAsCoversEveryField fails when Scene gains or loses a field: | ||
| 90 | // update Scene.SameAs, then this count. | 92 | // update Scene.SameAs, then this count. | ||
| 91 | func TestSceneSameAsCoversEveryField(t *testing.T) { | 93 | func TestSceneSameAsCoversEveryField(t *testing.T) { | ||
| 92 | - | // 23 with Animating, which SameAs intentionally does not compare: it | 94 | + | // 25 with Animating, which SameAs intentionally does not compare: it |
| 93 | // only schedules the next frame, it draws nothing. | 95 | // only schedules the next frame, it draws nothing. | ||
| 94 | - | const fields = 23 | 96 | + | const fields = 25 |
| 95 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(Scene{}))), | 97 | assert.Equal(t, fields, len(reflect.VisibleFields(reflect.TypeOf(Scene{}))), | ||
| 96 | "Scene fields changed: update Scene.SameAs (and TestSceneSameAs), then this count") | 98 | "Scene fields changed: update Scene.SameAs (and TestSceneSameAs), then this count") | ||
| 97 | } | 99 | } | ||