@desklink/host 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -9
- package/bin/desklink-host.mjs +2 -1
- package/dist/bridge.d.ts +3 -4
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +18 -7
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +0 -1
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +4 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/docs/BOUNDARY.md +8 -8
- package/docs/PROTOCOL.md +72 -45
- package/engine/Cargo.lock +1 -1
- package/engine/Cargo.toml +1 -1
- package/engine/build.rs +1 -0
- package/engine/native/agent_indicator.h +57 -0
- package/engine/native/agent_overlay_wayland.c +47 -26
- package/engine/native/agent_overlay_x11.c +116 -57
- package/engine/src/capture.rs +4 -2
- package/engine/src/clipboard.rs +174 -0
- package/engine/src/indicator.rs +175 -10
- package/engine/src/main.rs +2 -2
- package/engine/src/peer.rs +10 -4
- package/engine/src/protocol.rs +7 -10
- package/engine/src/session.rs +305 -209
- package/engine/src/x11.rs +29 -29
- package/engine/src/x11_mac.rs +4 -0
- package/examples/reference-client.html +3 -5
- package/package.json +3 -3
package/docs/PROTOCOL.md
CHANGED
|
@@ -10,8 +10,7 @@ macOS engine uses the same v3 stdio/WebRTC session path, with native display
|
|
|
10
10
|
capture, input and clipboard adapters. A Windows build with static libvpx
|
|
11
11
|
(`DESKLINK_VPX_STATIC_DIR`) also serves consumer-fed H.264 encoded sources over
|
|
12
12
|
the shared session path. Native desktop capture, input and clipboard are not
|
|
13
|
-
implemented on Windows
|
|
14
|
-
`indicator-unavailable`. Without static libvpx, the Windows compile seam
|
|
13
|
+
implemented on Windows; optional agent feedback is unavailable there. Without static libvpx, the Windows compile seam
|
|
15
14
|
reports encoding unavailable and refuses `serve`. The protocol contains no
|
|
16
15
|
application concepts: no accounts, no chat, no machine ids, no pane ids.
|
|
17
16
|
|
|
@@ -96,16 +95,16 @@ dimensions, origin and scale), the current keyboard layout, console state, and
|
|
|
96
95
|
Screen Recording/Accessibility grant states. A `display` source accepts an
|
|
97
96
|
optional `display_id` from `capture.displays`; an absent ID selects the main
|
|
98
97
|
display. Screen Recording consent is requested by `session.open` when capture
|
|
99
|
-
is needed.
|
|
100
|
-
|
|
101
|
-
launching app (for example Terminal, iTerm, or the Node.js host), not to a
|
|
98
|
+
is needed. Input may request Accessibility consent; if it remains unavailable,
|
|
99
|
+
the session still opens and input actions are refused with `input-unavailable`.
|
|
100
|
+
TCC permissions attach to the responsible launching app (for example Terminal, iTerm, or the Node.js host), not to a
|
|
102
101
|
separate DesklinkHost.app bundle. Enable that app in **System Settings → Privacy
|
|
103
102
|
& Security → Screen & System Audio Recording** and **Accessibility**, then
|
|
104
103
|
reconnect. No TCC state is pre-seeded or bypassed.
|
|
105
104
|
|
|
106
|
-
macOS clipboard read/write is plain text and
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
macOS clipboard read/write is plain text and available to paired consumers.
|
|
106
|
+
`restore_token` is not supported by this native backend; a new session may
|
|
107
|
+
require fresh consent. The macOS engine requires a VP9-enabled build
|
|
109
108
|
(`DESKLINK_VPX_STATIC_DIR`) for `serve`.
|
|
110
109
|
|
|
111
110
|
## Capabilities
|
|
@@ -186,12 +185,17 @@ desktop is visible here rather than silent. `capture.backends` lists what this
|
|
|
186
185
|
build has, not what this machine can necessarily use.
|
|
187
186
|
|
|
188
187
|
`input.unavailable_reason` and `input.grant` describe the portal/uinput path:
|
|
189
|
-
without kernel input access a portal session
|
|
190
|
-
|
|
191
|
-
not require that grant. The
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
188
|
+
without kernel input access a portal session opens and captures, but input
|
|
189
|
+
messages are refused with `input-unavailable`. X display capture uses XTest
|
|
190
|
+
instead and does not require that grant. The engine probes XTest on that named
|
|
191
|
+
display; if it is unavailable, capture and clipboard still open, and input
|
|
192
|
+
messages are refused with `input-unavailable`. X11 clipboard uses CLIPBOARD on the
|
|
193
|
+
session's named display; the WebSocket bridge reports it independently of the
|
|
194
|
+
engine's ambient Wayland clipboard probe. The engine's Linux `clipboard.read`
|
|
195
|
+
reports the Wayland session environment; `clipboard.write` additionally requires
|
|
196
|
+
`wl-copy`. A portal bridge preserves those flags even when Xwayland is reachable.
|
|
197
|
+
On Linux the engine never asks for
|
|
198
|
+
privileges on its own; macOS may request Screen Recording or Accessibility consent at `session.open`.
|
|
195
199
|
|
|
196
200
|
`capabilities` is safe to call before any consent has been given and must not
|
|
197
201
|
trigger a capture request.
|
|
@@ -203,7 +207,6 @@ trigger a capture request.
|
|
|
203
207
|
"source": {"kind":"portal"}, // or {"kind":"x11","display":":99"}
|
|
204
208
|
// macOS: {"kind":"display","display_id":123}
|
|
205
209
|
// or {"kind":"encoded","codec":"h264","width":486,"height":1080}
|
|
206
|
-
"permissions": ["view","control","clipboard"],
|
|
207
210
|
"max_width": 3840, "max_height": 2160, // encode box (default); never upscales
|
|
208
211
|
"bitrate_kbps": 0, // 0 (default): sized to the encoded surface
|
|
209
212
|
"max_fps": 60,
|
|
@@ -223,7 +226,7 @@ Result:
|
|
|
223
226
|
"source":{"kind":"monitor","width":2560,"height":1440,"origin":{"x":0,"y":0}},
|
|
224
227
|
"geometry":{"source":{"width":2560,"height":1440},
|
|
225
228
|
"encoded":{"width":2560,"height":1440},
|
|
226
|
-
"origin":{"x":0,"y":0}}}
|
|
229
|
+
"origin":{"x":0,"y":0}},"agentIndicator":false}
|
|
227
230
|
```
|
|
228
231
|
|
|
229
232
|
`source` selects the desktop. `portal` (or absent) asks the compositor for a
|
|
@@ -234,11 +237,12 @@ main display when omitted); `encoded` carries video the consumer encoded itself.
|
|
|
234
237
|
The choice is normally the *consumer's* decision, because a client is not in a
|
|
235
238
|
position to know which backend a machine can offer.
|
|
236
239
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
`
|
|
240
|
+
There are no per-session permissions: whoever holds the channel is paired and
|
|
241
|
+
gets all available view, input and clipboard access. The `permissions` field
|
|
242
|
+
from 0.3 clients is accepted and ignored, including unknown scope names.
|
|
243
|
+
Missing input or clipboard backends fail per action, not at open. The
|
|
244
|
+
`permission` refusal code is reserved for 0.3 peers and is no longer emitted by
|
|
245
|
+
this engine.
|
|
242
246
|
|
|
243
247
|
`max_fps` defaults to 60 and is bounded to 1–60. It caps the encoded frame
|
|
244
248
|
rate and X11 capture loop; portal capture offers it as the preferred PipeWire
|
|
@@ -246,14 +250,29 @@ frame rate, and macOS sets its ScreenCaptureKit stream minimum frame interval.
|
|
|
246
250
|
A source may still supply fewer frames.
|
|
247
251
|
|
|
248
252
|
`agent_indicator` is opt-in local feedback for an agent controller; desklink-axi
|
|
249
|
-
sets it for
|
|
250
|
-
adds an eased
|
|
251
|
-
|
|
253
|
+
sets it for sessions it drives (except Windows); human phone control omits it. On
|
|
254
|
+
Linux it adds an eased arrow inside a halo that brightens on clicks and keys,
|
|
255
|
+
animating in and out, with light and dark keylines so it reads on any content.
|
|
256
|
+
On Wayland it uses a click-through layer-shell surface with an edge glow, drawn
|
|
257
|
+
at the highest announced output integer scale, capped at 4; its bounded area is
|
|
252
258
|
masked before capture/diff (pixels behind the effect can remain briefly stale).
|
|
253
|
-
On bare X11 it
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
259
|
+
On bare X11 it is an override-redirect, click-through window that follows the
|
|
260
|
+
pointer, so it stays
|
|
261
|
+
visible over windows that set their own cursor and never replaces the cursor.
|
|
262
|
+
Its size follows `Xft.dpi` from 96–384 DPI, defaulting to 1x otherwise;
|
|
263
|
+
the shaped cue reveals outward from the tip and retracts inward, while a
|
|
264
|
+
compositing manager with alpha enables fading instead. The helper announces each
|
|
265
|
+
state before drawing it, and X11 capture restores just
|
|
266
|
+
the pixels the cue covers from the previous frame, so the halo's interior stays
|
|
267
|
+
live; changes under the arrow and ring show once the cue moves away. Click and
|
|
268
|
+
key feedback only recolour the cue, so a still cue never changes what is
|
|
269
|
+
restored. There is no edge glow on that path.
|
|
270
|
+
macOS adds an eased arrow and halo, click ripple, and typing pulse in a
|
|
271
|
+
nonactivating, click-through window excluded by the ScreenCaptureKit content
|
|
272
|
+
filter. Closing the session animates out and removes the indicator.
|
|
273
|
+
Indicator startup is best-effort on every platform; a missing helper or overlay
|
|
274
|
+
backend does not refuse the session. The open result reports `agentIndicator:
|
|
275
|
+
true` only when the helper started.
|
|
257
276
|
|
|
258
277
|
`local_frames` keeps the latest pre-encode frame and its tile hashes for
|
|
259
278
|
`session.frame` and `session.frame.changed` (see "Latest frame over the local
|
|
@@ -514,14 +533,14 @@ an explicit `cursor:"hidden"`. macOS and Windows refuse hidden capture with
|
|
|
514
533
|
the engine cannot remove pixels the consumer already encoded. Unknown cursor
|
|
515
534
|
options are `malformed`.
|
|
516
535
|
|
|
517
|
-
`@desklink/host` exposes `openSession({
|
|
536
|
+
`@desklink/host` exposes `openSession({cursor:'hidden'})`
|
|
518
537
|
and typed `session.cursor` events through `onEvent`/`drainEvents`. The client
|
|
519
538
|
capture request also accepts `cursor`, and AXI accepts `start --cursor hidden`.
|
|
520
539
|
|
|
521
540
|
## Point on the host desktop
|
|
522
541
|
|
|
523
542
|
A local consumer can guide the person without taking control of their pointer
|
|
524
|
-
or keyboard. `session.point` requires
|
|
543
|
+
or keyboard. `session.point` requires an open session and is independent
|
|
525
544
|
of `agent_indicator`. It draws one blue ring centered at `x,y`, with an optional
|
|
526
545
|
label. A new point replaces the previous one and restarts its timer.
|
|
527
546
|
|
|
@@ -560,7 +579,7 @@ output, no origin is needed. Points map from the selected source to that output.
|
|
|
560
579
|
An ambiguous or missing selected output returns `indicator-unavailable`.
|
|
561
580
|
Compositors without layer-shell return `point_unsupported` with reason
|
|
562
581
|
`layer_shell_unavailable` (including GNOME Mutter). Consumer-fed encoded sources return `operation`
|
|
563
|
-
because they have no host desktop overlay target. Other refusals are `
|
|
582
|
+
because they have no host desktop overlay target. Other refusals are `session`,
|
|
564
583
|
`coordinates`, `malformed`, and `indicator-unavailable` (including unavailable
|
|
565
584
|
X Shape support). Unknown fields are refused. No control-channel or remote
|
|
566
585
|
bridge forwarding is added; the consumer chooses when to request a local cue.
|
|
@@ -568,13 +587,13 @@ bridge forwarding is added; the consumer chooses when to request a local cue.
|
|
|
568
587
|
`@desklink/host` exposes `EngineClient.point(sessionId, {x, y, label?, timeoutMs?})`
|
|
569
588
|
and `EngineClient.point(sessionId, {clear: true})`. `@desklink/axi` exposes
|
|
570
589
|
`desklink-axi point 400,300 --label "Here" --timeout 3000` and
|
|
571
|
-
`desklink-axi point --clear
|
|
590
|
+
`desklink-axi point --clear`.
|
|
572
591
|
|
|
573
592
|
## Input and clipboard: the session's control channel
|
|
574
593
|
|
|
575
594
|
Input does **not** travel the local protocol. It rides the WebRTC data channel
|
|
576
595
|
the engine creates for the session, which is inside the DTLS/SRTP session that
|
|
577
|
-
was opened
|
|
596
|
+
was opened by a paired consumer: it inherits that session's identity and dies
|
|
578
597
|
with it, so a client that kept a socket open cannot keep driving a session that
|
|
579
598
|
was revoked, and the consumer's request path never carries a pointer move.
|
|
580
599
|
|
|
@@ -611,12 +630,12 @@ The engine answers on the same channel, and sends two things unprompted:
|
|
|
611
630
|
{"kind":"revoked","reason":"…","code":"closed"} // closed, or a session.revoked code
|
|
612
631
|
```
|
|
613
632
|
|
|
614
|
-
Refusal codes: `
|
|
615
|
-
`text-
|
|
633
|
+
Refusal codes: `session`, `input-replay`, `coordinates`, `text-too-large`,
|
|
634
|
+
`text-unsupported`, `input-unavailable`, `clipboard-unsupported`, `operation`.
|
|
616
635
|
|
|
617
636
|
`seq` is monotonic per session. A repeat or a lower value is refused with
|
|
618
637
|
`input-replay` and **no side effect**, so a replayed message cannot move the
|
|
619
|
-
pointer twice. Validation
|
|
638
|
+
pointer twice. Validation and ordering checks all run before any
|
|
620
639
|
physical effect.
|
|
621
640
|
|
|
622
641
|
The engine holds its own pressed-key/button state. `close`, `release_all`,
|
|
@@ -647,8 +666,7 @@ WebRTC, congestion and teardown logic that is already proven on this path.
|
|
|
647
666
|
it is the only party that knows them (`1..7680` by `1..4320`). Nothing is scaled:
|
|
648
667
|
`geometry.source` and `geometry.encoded` are both that size, the client aims at
|
|
649
668
|
those pixels, and the consumer maps them to its device. `max_width`/
|
|
650
|
-
`max_height`/`bitrate_kbps` are ignored for this source
|
|
651
|
-
as it does anywhere else — with one difference: `control` needs no local input
|
|
669
|
+
`max_height`/`bitrate_kbps` are ignored for this source. Input needs no local
|
|
652
670
|
backend, because nothing is applied locally.
|
|
653
671
|
|
|
654
672
|
`session.open` returns before any offer exists. The offer must name the profile
|
|
@@ -719,7 +737,7 @@ pixels, exactly as a client sends them.
|
|
|
719
737
|
```
|
|
720
738
|
|
|
721
739
|
The event carries the control message as it arrived. The engine still validates
|
|
722
|
-
|
|
740
|
+
ordering and replay before forwarding, so the client keeps the same
|
|
723
741
|
`ack`/`rejected` behaviour and a replayed gesture still cannot be delivered
|
|
724
742
|
twice. `ack` means "accepted for this session", not "applied to a desktop" —
|
|
725
743
|
`session.metrics.input_forwarded` counts what was handed on, and `input_applied`
|
|
@@ -738,7 +756,7 @@ because packetization is the only stage the engine itself runs.
|
|
|
738
756
|
|
|
739
757
|
## Latest frame over the local protocol
|
|
740
758
|
|
|
741
|
-
`session.frame`
|
|
759
|
+
`session.frame` writes the latest pre-encode frame as
|
|
742
760
|
packed BGRX bytes to the caller's path, with optional `[x,y,w,h]` `region`.
|
|
743
761
|
The directory must already exist and be private to the caller; pixels never
|
|
744
762
|
travel through JSON. `seq` advances only when capture changes, `still_ms` is
|
|
@@ -775,9 +793,17 @@ instead; both reach the same clipboard:
|
|
|
775
793
|
{"id":10,"result":{"written":true}}
|
|
776
794
|
```
|
|
777
795
|
|
|
778
|
-
Refused with `error.code = "clipboard"` when the
|
|
779
|
-
|
|
780
|
-
|
|
796
|
+
Refused with `error.code = "clipboard"` when the desktop has no text or a
|
|
797
|
+
clipboard operation fails. Encoded sources have no desktop clipboard; their
|
|
798
|
+
control channel answers with an empty clipboard reply and an explanatory error.
|
|
799
|
+
X11 uses a fresh connection to the session's selected display for each operation.
|
|
800
|
+
Writes verify CLIPBOARD ownership before succeeding and serve TARGETS,
|
|
801
|
+
UTF8_STRING and TEXT; reads request UTF8_STRING. Writes are limited to 256 KiB
|
|
802
|
+
and the server's single request limit; the text lives until another client takes ownership, the engine
|
|
803
|
+
exits, or the display dies. Reads wait at most 2.5 seconds for the owner.
|
|
804
|
+
INCR transfers are refused explicitly as unsupported, not reported as success;
|
|
805
|
+
STRING, PRIMARY and persistence after engine exit are not implemented.
|
|
806
|
+
A read is bounded by `capabilities.clipboard.maxBytes`; when the desktop's text is longer, `truncated`
|
|
781
807
|
is true and the text is cut on a character boundary, so the returned text is
|
|
782
808
|
still valid.
|
|
783
809
|
|
|
@@ -853,8 +879,9 @@ anything to show.
|
|
|
853
879
|
|
|
854
880
|
## What the engine deliberately does not do
|
|
855
881
|
|
|
856
|
-
- It does not decide *who* the user is.
|
|
857
|
-
|
|
882
|
+
- It does not decide *who* the user is. Whoever holds stdio (or an authenticated
|
|
883
|
+
bridge socket) is paired; pairing, identity and revocation are the consumer's.
|
|
884
|
+
Revocation must end both live sessions and future access. There is no second
|
|
858
885
|
account, token or pairing ceremony. For consumer integration, see the
|
|
859
886
|
[BYOKit guidance](../../../README.md#why-desklink-exists).
|
|
860
887
|
- It does not open ports, register a service, install udev rules, join groups or
|
package/engine/Cargo.lock
CHANGED
package/engine/Cargo.toml
CHANGED
package/engine/build.rs
CHANGED
|
@@ -38,6 +38,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
38
38
|
println!("cargo:rerun-if-changed=native/mac_stream.mm");
|
|
39
39
|
println!("cargo:rerun-if-changed=native/agent_overlay_mac.m");
|
|
40
40
|
println!("cargo:rerun-if-changed=native/agent_overlay_x11.c");
|
|
41
|
+
println!("cargo:rerun-if-changed=native/agent_indicator.h");
|
|
41
42
|
println!("cargo:rerun-if-changed=native/agent_overlay_wayland.c");
|
|
42
43
|
println!("cargo:rerun-if-changed=Info.plist");
|
|
43
44
|
println!("cargo:rerun-if-changed=vendor/inputtino/src/uinput/include/inputtino/keyboard.hpp");
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Shared Linux agent cue, in logical pixels. The arrow follows the Mac outline;
|
|
2
|
+
// light and dark keylines keep it visible on both bright and dark desktops.
|
|
3
|
+
#ifndef DESKLINK_AGENT_INDICATOR_H
|
|
4
|
+
#define DESKLINK_AGENT_INDICATOR_H
|
|
5
|
+
#include <math.h>
|
|
6
|
+
#include <stdint.h>
|
|
7
|
+
|
|
8
|
+
static double indicator_segment(double x, double y, double ax, double ay, double bx, double by) {
|
|
9
|
+
double dx=bx-ax, dy=by-ay;
|
|
10
|
+
double t=fmax(0,fmin(1,((x-ax)*dx+(y-ay)*dy)/(dx*dx+dy*dy)));
|
|
11
|
+
return hypot(x-ax-t*dx,y-ay-t*dy);
|
|
12
|
+
}
|
|
13
|
+
static uint32_t indicator_over(uint32_t dst, double coverage, unsigned rgb) {
|
|
14
|
+
unsigned a=(unsigned)(255*fmax(0,fmin(1,coverage))), inverse=255-a;
|
|
15
|
+
unsigned da=dst>>24, r=(dst>>16)&255, g=(dst>>8)&255, b=dst&255;
|
|
16
|
+
return ((a+da*inverse/255)<<24) |
|
|
17
|
+
((((rgb>>16)&255)*a/255+r*inverse/255)<<16) |
|
|
18
|
+
((((rgb>>8)&255)*a/255+g*inverse/255)<<8) |
|
|
19
|
+
((rgb&255)*a/255+b*inverse/255);
|
|
20
|
+
}
|
|
21
|
+
// Recolour without changing coverage, so feedback on a still cue never
|
|
22
|
+
// changes which pixels capture has to restore.
|
|
23
|
+
static uint32_t indicator_tint(uint32_t c, double amount, unsigned rgb) {
|
|
24
|
+
double keep=1-fmax(0,fmin(1,amount)), a=(c>>24)/255.0;
|
|
25
|
+
return (c&0xff000000) |
|
|
26
|
+
((unsigned)(((c>>16)&255)*keep+((rgb>>16)&255)*a*(1-keep))<<16) |
|
|
27
|
+
((unsigned)(((c>>8)&255)*keep+((rgb>>8)&255)*a*(1-keep))<<8) |
|
|
28
|
+
(unsigned)((c&255)*keep+(rgb&255)*a*(1-keep));
|
|
29
|
+
}
|
|
30
|
+
static uint32_t indicator_pixel(double x, double y, double opacity, double click, double typed) {
|
|
31
|
+
double radius=hypot(x,y);
|
|
32
|
+
uint32_t color=0;
|
|
33
|
+
// 62 logical px halo: a blue band, wide white keyline and dark rim, so the
|
|
34
|
+
// ring keeps >=2px of contrast after a 2:1 downscale. Inside the band only
|
|
35
|
+
// the arrow is opaque, so capture can keep the click area live.
|
|
36
|
+
color=indicator_over(color,fmax(0,fmin(1,2.5-fabs(radius-28.5)))*opacity,0x123047);
|
|
37
|
+
color=indicator_over(color,fmax(0,fmin(1,2.5-fabs(radius-24)))*opacity,0xffffff);
|
|
38
|
+
color=indicator_over(color,fmax(0,fmin(1,1.5-fabs(radius-20)))*0.95*opacity,0x2999ff);
|
|
39
|
+
static const double arrow[][2]={{0,0},{2,24},{8,18},{15,31},{21,28},{14,15},{24,14}};
|
|
40
|
+
double distance=1000; int inside=0;
|
|
41
|
+
for(int i=0,j=6;i<7;j=i++) {
|
|
42
|
+
double ax=arrow[j][0],ay=arrow[j][1],bx=arrow[i][0],by=arrow[i][1];
|
|
43
|
+
distance=fmin(distance,indicator_segment(x,y,ax,ay,bx,by));
|
|
44
|
+
if((ay>y)!=(by>y) && x<(bx-ax)*(y-ay)/(by-ay)+ax)inside=!inside;
|
|
45
|
+
}
|
|
46
|
+
double signed_distance=inside?-distance:distance;
|
|
47
|
+
color=indicator_over(color,fmax(0,fmin(1,3-signed_distance))*opacity,0x123047);
|
|
48
|
+
color=indicator_over(color,fmax(0,fmin(1,2-signed_distance))*opacity,0xffffff);
|
|
49
|
+
color=indicator_over(color,fmax(0,fmin(1,-signed_distance))*opacity,0x2999ff);
|
|
50
|
+
// A click sweeps a bright wave out through the cue; a key flashes it.
|
|
51
|
+
if(click>=0 && click<1)
|
|
52
|
+
color=indicator_tint(color,fmax(0,1-fabs(radius-(4+32*click))/5)*0.9*(1-click),0x9fdcff);
|
|
53
|
+
if(typed>=0 && typed<1)
|
|
54
|
+
color=indicator_tint(color,0.6*(1-typed),0xc8ecff);
|
|
55
|
+
return color;
|
|
56
|
+
}
|
|
57
|
+
#endif
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Minimal wlroots layer-shell surface: transparent, unfocusable and input-empty.
|
|
2
|
-
//
|
|
2
|
+
// Portal capture masking is implemented by capture.rs::IndicatorMask.
|
|
3
3
|
// Protocol interface names/signatures from wlr-layer-shell-unstable-v1:
|
|
4
4
|
// Copyright (c) 2017 Drew DeVault. Permission is hereby granted to use, copy,
|
|
5
5
|
// modify and distribute this protocol for any purpose without fee, provided
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
#include <unistd.h>
|
|
15
15
|
#include <fcntl.h>
|
|
16
16
|
#include <math.h>
|
|
17
|
+
#include "agent_indicator.h"
|
|
17
18
|
#include <stdio.h>
|
|
18
19
|
#include <stdlib.h>
|
|
19
20
|
#include <string.h>
|
|
@@ -63,7 +64,13 @@ static const struct wl_interface xdg_manager_interface={"zxdg_output_manager_v1"
|
|
|
63
64
|
static const struct wl_message output_requests[]={{"destroy","",NULL}};
|
|
64
65
|
static const struct wl_message output_events[]={{"logical_position","ii",NULL},{"logical_size","ii",NULL},{"done","",NULL},{"name","2s",NULL},{"description","2s",NULL}};
|
|
65
66
|
static const struct wl_interface xdg_output_interface={"zxdg_output_v1",2,1,output_requests,5,output_events};
|
|
66
|
-
struct Output { struct wl_output *handle; struct wl_proxy *logical; int x,y,w,h,valid; };
|
|
67
|
+
struct Output { struct wl_output *handle; struct wl_proxy *logical; int x,y,w,h,valid,scale; };
|
|
68
|
+
static void output_geometry(void *data,struct wl_output *p,int32_t x,int32_t y,int32_t pw,int32_t ph,int32_t sub,const char *make,const char *model,int32_t transform)
|
|
69
|
+
{(void)data;(void)p;(void)x;(void)y;(void)pw;(void)ph;(void)sub;(void)make;(void)model;(void)transform;}
|
|
70
|
+
static void output_mode(void *data,struct wl_output *p,uint32_t flags,int32_t w,int32_t h,int32_t refresh){(void)data;(void)p;(void)flags;(void)w;(void)h;(void)refresh;}
|
|
71
|
+
static void output_done(void *data,struct wl_output *p){(void)data;(void)p;}
|
|
72
|
+
static void output_scale(void *data,struct wl_output *p,int32_t scale){(void)p;((struct Output *)data)->scale=scale;}
|
|
73
|
+
static const struct wl_output_listener output_listener={.geometry=output_geometry,.mode=output_mode,.done=output_done,.scale=output_scale};
|
|
67
74
|
static void logical_position(void *data, struct wl_proxy *p,int32_t x,int32_t y){(void)p;struct Output *o=data;o->x=x;o->y=y;o->valid=1;}
|
|
68
75
|
static void logical_size(void *data,struct wl_proxy *p,int32_t w,int32_t h){(void)p;struct Output *o=data;o->w=w;o->h=h;}
|
|
69
76
|
static void logical_done(void *data,struct wl_proxy *p){(void)data;(void)p;}
|
|
@@ -89,14 +96,16 @@ struct Overlay {
|
|
|
89
96
|
struct Output outputs[32]; int output_count;
|
|
90
97
|
struct wl_surface *surface;
|
|
91
98
|
struct Buffer buffers[2];
|
|
92
|
-
int width, height, source_w, source_h, configured, closed;
|
|
99
|
+
int width, height, scale, source_w, source_h, configured, closed;
|
|
93
100
|
};
|
|
94
101
|
static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec+t.tv_nsec/1e9; }
|
|
95
102
|
static void registry_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
|
|
96
103
|
struct Overlay *o=data;
|
|
97
104
|
if (!strcmp(interface,"wl_output") && o->output_count<32) {
|
|
98
105
|
struct Output *out=&o->outputs[o->output_count++];
|
|
99
|
-
out->handle=wl_registry_bind(registry,name,&wl_output_interface,
|
|
106
|
+
out->handle=wl_registry_bind(registry,name,&wl_output_interface,version<2?version:2);
|
|
107
|
+
out->scale=1;
|
|
108
|
+
wl_output_add_listener(out->handle,&output_listener,out);
|
|
100
109
|
}
|
|
101
110
|
if (!strcmp(interface,"zxdg_output_manager_v1")) o->output_manager=wl_registry_bind(registry,name,&xdg_manager_interface,version<2?version:2);
|
|
102
111
|
if (!strcmp(interface,"wl_compositor")) o->compositor=wl_registry_bind(registry,name,&wl_compositor_interface,version<4?version:4);
|
|
@@ -119,13 +128,14 @@ static void buffer_release(void *data, struct wl_buffer *buffer) { (void)buffer;
|
|
|
119
128
|
static const struct wl_buffer_listener buffer_listener={buffer_release};
|
|
120
129
|
|
|
121
130
|
static int create_buffer(struct Overlay *o, struct Buffer *b) {
|
|
122
|
-
|
|
131
|
+
int w=o->width*o->scale,h=o->height*o->scale;
|
|
132
|
+
b->length=(size_t)w*h*4;
|
|
123
133
|
int fd=memfd_create("desklink-agent-overlay",MFD_CLOEXEC);
|
|
124
134
|
if (fd<0 || ftruncate(fd,b->length)) { if(fd>=0)close(fd); return -1; }
|
|
125
135
|
b->pixels=mmap(NULL,b->length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
|
|
126
136
|
if (b->pixels==MAP_FAILED) {close(fd);return -1;}
|
|
127
137
|
struct wl_shm_pool *pool=wl_shm_create_pool(o->shm,fd,(int)b->length);
|
|
128
|
-
b->handle=wl_shm_pool_create_buffer(pool,0,
|
|
138
|
+
b->handle=wl_shm_pool_create_buffer(pool,0,w,h,w*4,WL_SHM_FORMAT_ARGB8888);
|
|
129
139
|
wl_shm_pool_destroy(pool); close(fd);
|
|
130
140
|
wl_buffer_add_listener(b->handle,&buffer_listener,b);
|
|
131
141
|
return 0;
|
|
@@ -134,12 +144,15 @@ static void blend(uint32_t *pixel, unsigned alpha, unsigned rgb) {
|
|
|
134
144
|
// premultiplied ARGB8888: the compositor blends this over the desktop.
|
|
135
145
|
*pixel=(alpha<<24)|(((rgb>>16)&255)*alpha/255<<16)|(((rgb>>8)&255)*alpha/255<<8)|((rgb&255)*alpha/255);
|
|
136
146
|
}
|
|
137
|
-
static void draw(struct Overlay *o, struct Buffer *b, double x, double y, double click, double typed, double ending,
|
|
138
|
-
double t=now(), opacity=ending?fmax(0,1-(t-ending)/0.6):1;
|
|
147
|
+
static void draw(struct Overlay *o, struct Buffer *b, double x, double y, double click, double typed, double ending, double started, double cue_started) {
|
|
148
|
+
double t=now(), opacity=ending?fmax(0,1-(t-ending)/0.6):fmin(1,(t-started)/0.18);
|
|
149
|
+
// The cue fades in from its first position, not from the session's start.
|
|
150
|
+
double cue=ending?opacity:fmin(1,(t-cue_started)/0.18);
|
|
139
151
|
memset(b->pixels,0,b->length);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
// Draw in buffer pixels; the cue itself is specified in logical pixels.
|
|
153
|
+
int s=o->scale,w=o->width*s,h=o->height*s;x*=s;y*=s;
|
|
154
|
+
for (int edge=0;edge<7*s;edge++) {
|
|
155
|
+
unsigned alpha=(unsigned)((7-(double)edge/s)*1.8*opacity);
|
|
143
156
|
for(int px=edge;px<w-edge;px++) {
|
|
144
157
|
blend(&b->pixels[(size_t)edge*w+px],alpha,0x4c9ed0);
|
|
145
158
|
blend(&b->pixels[(size_t)(h-1-edge)*w+px],alpha,0x4c9ed0);
|
|
@@ -149,16 +162,17 @@ static void draw(struct Overlay *o, struct Buffer *b, double x, double y, double
|
|
|
149
162
|
blend(&b->pixels[(size_t)py*w+w-1-edge],alpha,0x4c9ed0);
|
|
150
163
|
}
|
|
151
164
|
}
|
|
152
|
-
int left=fmax(0,x-
|
|
153
|
-
if(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
unsigned
|
|
161
|
-
if(
|
|
165
|
+
int left=fmax(0,x-40*s),right=fmin(w,x+40*s),top=fmax(0,y-40*s),bottom=fmin(h,y+40*s);
|
|
166
|
+
if(cue_started)for(int py=top;py<bottom;py++)for(int px=left;px<right;px++) {
|
|
167
|
+
// Capture keeps the pointer's own pixels live (IndicatorMask), so the
|
|
168
|
+
// tip stays clear; the real cursor sits there anyway.
|
|
169
|
+
if(hypot((px-x)/s,(py-y)/s)<6)continue;
|
|
170
|
+
uint32_t src=indicator_pixel((px-x)/s,(py-y)/s,cue,
|
|
171
|
+
click?(t-click)/0.55:-1,typed?(t-typed)/0.4:-1), *dst=&b->pixels[(size_t)py*w+px];
|
|
172
|
+
// Premultiplied source-over, keeping the edge glow under the cue.
|
|
173
|
+
unsigned inverse=255-(src>>24);
|
|
174
|
+
if(src)*dst=src+((((*dst>>24)&255)*inverse/255)<<24)+((((*dst>>16)&255)*inverse/255)<<16)
|
|
175
|
+
+((((*dst>>8)&255)*inverse/255)<<8)+((*dst&255)*inverse/255);
|
|
162
176
|
}
|
|
163
177
|
}
|
|
164
178
|
|
|
@@ -202,7 +216,7 @@ static void draw_point(struct Overlay *o,struct Buffer *b,double x,double y,cons
|
|
|
202
216
|
|
|
203
217
|
static int overlay_main(int source_w,int source_h,int point_mode,int origin_known,int origin_x,int origin_y) {
|
|
204
218
|
|
|
205
|
-
struct Overlay o={.source_w=source_w,.source_h=source_h};
|
|
219
|
+
struct Overlay o={.source_w=source_w,.source_h=source_h,.scale=1};
|
|
206
220
|
o.display=wl_display_connect(NULL);
|
|
207
221
|
if(!o.display)return 2;
|
|
208
222
|
struct wl_registry *registry=wl_display_get_registry(o.display);
|
|
@@ -228,6 +242,11 @@ static int overlay_main(int source_w,int source_h,int point_mode,int origin_know
|
|
|
228
242
|
if(!selected && o.output_count==1 && (!origin_known || !o.output_manager))selected=o.outputs[0].handle;
|
|
229
243
|
if(!selected){puts("selected_output_unavailable");fflush(stdout);wl_display_disconnect(o.display);return 2;}
|
|
230
244
|
}
|
|
245
|
+
if(!point_mode && wl_proxy_get_version((struct wl_proxy *)o.compositor)>=3) {
|
|
246
|
+
// Render the cue at the densest output's integer scale so it stays crisp on HiDPI.
|
|
247
|
+
if(wl_display_roundtrip(o.display)<0)return 2;
|
|
248
|
+
for(int i=0;i<o.output_count;i++)if(o.outputs[i].scale>o.scale)o.scale=o.outputs[i].scale<4?o.outputs[i].scale:4;
|
|
249
|
+
}
|
|
231
250
|
FT_Library library=NULL;FT_Face font=point_mode?point_font(&library):NULL;
|
|
232
251
|
if(point_mode&&!font){wl_display_disconnect(o.display);return 2;}
|
|
233
252
|
o.surface=wl_compositor_create_surface(o.compositor);
|
|
@@ -244,13 +263,14 @@ static int overlay_main(int source_w,int source_h,int point_mode,int origin_know
|
|
|
244
263
|
wl_surface_set_input_region(o.surface,empty);
|
|
245
264
|
wl_region_destroy(empty);
|
|
246
265
|
}
|
|
266
|
+
if(o.scale>1)wl_surface_set_buffer_scale(o.surface,o.scale);
|
|
247
267
|
wl_surface_commit(o.surface);
|
|
248
268
|
while(!o.configured&&!o.closed&&wl_display_dispatch(o.display)>=0) {}
|
|
249
269
|
if(o.closed||o.width<=0||o.height<=0){wl_display_disconnect(o.display);return 2;}
|
|
250
270
|
if(create_buffer(&o,&o.buffers[0])||create_buffer(&o,&o.buffers[1])){wl_display_disconnect(o.display);return 2;}
|
|
251
271
|
int flags=fcntl(STDIN_FILENO,F_GETFL);fcntl(STDIN_FILENO,F_SETFL,flags|O_NONBLOCK);
|
|
252
272
|
puts("READY");fflush(stdout);
|
|
253
|
-
double tx=0,ty=0,x=0,y=0,click=0,typed=0,ending=0;
|
|
273
|
+
double tx=0,ty=0,x=0,y=0,click=0,typed=0,ending=0,started=0,cue_started=0;
|
|
254
274
|
int active=0,positioned=0;
|
|
255
275
|
double deadline=0;char label[97]={0};int dirty=0,visible=0;
|
|
256
276
|
char pending[512];size_t used=0;
|
|
@@ -277,8 +297,9 @@ static int overlay_main(int source_w,int source_h,int point_mode,int origin_know
|
|
|
277
297
|
else if(kind=='M'||kind=='C'||kind=='T'||kind=='A') {
|
|
278
298
|
if(px>=0&&py>=0) {
|
|
279
299
|
if(desklink_wayland_geometry(o.source_w,o.source_h,o.width,o.height,px,py,&tx,&ty)
|
|
280
|
-
&& !positioned){x=tx;y=ty;positioned=1;}
|
|
300
|
+
&& !positioned){x=tx;y=ty;positioned=1;cue_started=now();}
|
|
281
301
|
}
|
|
302
|
+
if(!active)started=now();
|
|
282
303
|
active=1;
|
|
283
304
|
if(kind=='C')click=now();
|
|
284
305
|
if(kind=='T')typed=now();
|
|
@@ -292,11 +313,11 @@ static int overlay_main(int source_w,int source_h,int point_mode,int origin_know
|
|
|
292
313
|
if(!point_mode){x+=(tx-x)*0.24;y+=(ty-y)*0.24;}
|
|
293
314
|
for(int i=0;i<2;i++)if(!o.buffers[i].busy) {
|
|
294
315
|
if(point_mode)draw_point(&o,&o.buffers[i],x,y,label,font,visible);
|
|
295
|
-
else draw(&o,&o.buffers[i],x,y,click,typed,ending,
|
|
316
|
+
else draw(&o,&o.buffers[i],x,y,click,typed,ending,started,cue_started);
|
|
296
317
|
dirty=0;
|
|
297
318
|
o.buffers[i].busy=1;
|
|
298
319
|
wl_surface_attach(o.surface,o.buffers[i].handle,0,0);
|
|
299
|
-
wl_surface_damage_buffer(o.surface,0,0,o.width,o.height);
|
|
320
|
+
wl_surface_damage_buffer(o.surface,0,0,o.width*o.scale,o.height*o.scale);
|
|
300
321
|
wl_surface_commit(o.surface);
|
|
301
322
|
break;
|
|
302
323
|
}
|