dsh-plugin-computer-use 2.4.1 → 2.5.1

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.
@@ -0,0 +1,1053 @@
1
+ import Gio from 'gi://Gio';
2
+ import GLib from 'gi://GLib';
3
+ import Shell from 'gi://Shell';
4
+ import St from 'gi://St';
5
+ import Clutter from 'gi://Clutter';
6
+ import Cairo from 'cairo';
7
+ import * as Main from 'resource:///org/gnome/shell/ui/main.js';
8
+ import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
9
+
10
+ Gio._promisify(Shell.Screenshot.prototype, 'screenshot');
11
+
12
+ const IFACE = `<node><interface name="org.cua.WinRects">
13
+ <method name="GetVersion"><arg type="u" direction="out" name="version"/></method>
14
+ <method name="GetRects"><arg type="s" direction="out" name="json"/></method>
15
+ <method name="Capture"><arg type="s" direction="out" name="png_base64"/></method>
16
+ <method name="Activate"><arg type="u" direction="in" name="id"/><arg type="b" direction="out" name="activated"/></method>
17
+ <method name="MoveCursor"><arg type="i" direction="in" name="x"/><arg type="i" direction="in" name="y"/></method>
18
+ <method name="ClickPulse"><arg type="i" direction="in" name="x"/><arg type="i" direction="in" name="y"/></method>
19
+ <method name="SetCursorColor"><arg type="s" direction="in" name="fill_color"/></method>
20
+ <method name="SetCursorState"><arg type="s" direction="in" name="action"/><arg type="s" direction="in" name="delivery"/><arg type="s" direction="in" name="target"/><arg type="b" direction="in" name="active"/></method>
21
+ <method name="SetSessionLabel"><arg type="s" direction="in" name="label"/></method>
22
+ <method name="HideCursor"></method>
23
+ </interface></node>`;
24
+
25
+ // GNOME cannot host the Rust renderer directly, so this Shell actor mirrors
26
+ // the embedded cua.default vector theme and semantic state vocabulary.
27
+ const CANVAS_SIZE = 128;
28
+ const DISPLAY_SIZE = 42;
29
+ const ACTOR_SIZE = 112;
30
+ const ACTOR_CENTER = ACTOR_SIZE / 2;
31
+ const SCALE = DISPLAY_SIZE / CANVAS_SIZE;
32
+ const FLOAT_DURATION = 4.0;
33
+ const GLOW_SURFACE_SCALE = 3;
34
+ const GLOW_PADDING = 24;
35
+ const BADGE_Y_OFFSET = 29;
36
+ const BADGE_HOLD_SECONDS = 2.0;
37
+ const BADGE_FADE_SECONDS = 0.4;
38
+ const BADGE_CHIP_SIZE = 18;
39
+ const ACTIONS = new Set([
40
+ 'idle',
41
+ 'observe',
42
+ 'click',
43
+ 'drag',
44
+ 'scroll',
45
+ 'text',
46
+ 'key',
47
+ 'navigate',
48
+ 'app',
49
+ 'transfer',
50
+ 'record',
51
+ 'system',
52
+ ]);
53
+ const ONE_SHOT_ACTIONS = new Set(['click', 'key', 'navigate', 'app', 'system']);
54
+ const ACTION_DURATIONS = {
55
+ idle: 4.0,
56
+ click: 0.67,
57
+ observe: 1.6,
58
+ drag: 1.6,
59
+ scroll: 1.6,
60
+ text: 1.6,
61
+ key: 1.6,
62
+ navigate: 1.6,
63
+ app: 1.6,
64
+ transfer: 1.6,
65
+ record: 1.6,
66
+ system: 1.6,
67
+ };
68
+
69
+ function nowSeconds() {
70
+ return GLib.get_monotonic_time() / 1_000_000;
71
+ }
72
+
73
+ function mixChannel(base, accent, weight) {
74
+ return Math.round(base * (1 - weight) + accent * weight);
75
+ }
76
+
77
+ function badgeStyle(fillColor) {
78
+ const start = fillColor.map((channel, index) => mixChannel([94, 151, 178][index], channel, 0.66));
79
+ const end = fillColor.map((channel, index) => mixChannel([13, 27, 38][index], channel, 0.26));
80
+ // The rim carries session identity now that the orb is gone, matching
81
+ // paint_session_badge in the Rust renderer.
82
+ const rim = fillColor.map((channel) => mixChannel(255, channel, 0.55));
83
+ return [
84
+ 'spacing: 7px',
85
+ 'padding: 6px 10px',
86
+ `background-gradient-start: rgba(${start[0]}, ${start[1]}, ${start[2]}, 0.93)`,
87
+ `background-gradient-end: rgba(${end[0]}, ${end[1]}, ${end[2]}, 0.96)`,
88
+ 'background-gradient-direction: horizontal',
89
+ `border: 1px solid rgba(${rim[0]}, ${rim[1]}, ${rim[2]}, 0.75)`,
90
+ 'border-radius: 14px',
91
+ 'box-shadow: 0 2px 9px rgba(0, 0, 0, 0.30)',
92
+ ].join(';');
93
+ }
94
+
95
+ function easeInOut(value) {
96
+ const t = Math.max(0, Math.min(1, value));
97
+ return t * t * (3 - 2 * t);
98
+ }
99
+
100
+ function triangleWave(value) {
101
+ const t = ((value % 1) + 1) % 1;
102
+ return t < 0.5 ? t * 2 : (1 - t) * 2;
103
+ }
104
+
105
+ function setInk(cr, alpha = 1) {
106
+ cr.setSourceRGBA(1, 1, 1, alpha);
107
+ }
108
+
109
+ function setPaper(cr, alpha = 1) {
110
+ cr.setSourceRGBA(1, 1, 1, alpha);
111
+ }
112
+
113
+ function setFill(cr, color, alpha = 1) {
114
+ cr.setSourceRGBA(color[0] / 255, color[1] / 255, color[2] / 255, alpha);
115
+ }
116
+
117
+ function glowPath(cr, width, alpha, glowColor) {
118
+ if (!glowColor) return;
119
+ const layers = 12;
120
+ const outerExpansion = 13;
121
+ const innerExpansion = 2.5;
122
+ const maxOpacity = 0.17;
123
+ let accumulatedOpacity = 0;
124
+ for (let layer = 0; layer < layers; layer++) {
125
+ const progress = (layer + 1) / layers;
126
+ const expansion = outerExpansion + (innerExpansion - outerExpansion) * progress;
127
+ const targetOpacity = maxOpacity * Math.max(0, Math.min(1, alpha)) * Math.pow(progress, 1.6);
128
+ const layerOpacity =
129
+ (targetOpacity - accumulatedOpacity) / Math.max(0.001, 1 - accumulatedOpacity);
130
+ accumulatedOpacity = targetOpacity;
131
+ cr.setLineWidth(width + expansion);
132
+ cr.setLineCap(Cairo.LineCap.ROUND);
133
+ cr.setLineJoin(Cairo.LineJoin.ROUND);
134
+ setFill(cr, glowColor, layerOpacity);
135
+ cr.strokePreserve();
136
+ }
137
+ }
138
+
139
+ function strokePath(cr, width, alpha = 1, glowColor = null) {
140
+ glowPath(cr, width, alpha, glowColor);
141
+ cr.setLineWidth(glowColor ? width + 1.5 : width);
142
+ cr.setLineCap(Cairo.LineCap.ROUND);
143
+ cr.setLineJoin(Cairo.LineJoin.ROUND);
144
+ setInk(cr, alpha);
145
+ if (!glowColor) {
146
+ cr.stroke();
147
+ return;
148
+ }
149
+ cr.strokePreserve();
150
+ cr.setLineWidth(Math.max(1.5, width - 1));
151
+ setFill(cr, glowColor, alpha);
152
+ cr.stroke();
153
+ }
154
+
155
+ function fillPath(cr, glowWidth, alpha = 1, glowColor = null) {
156
+ glowPath(cr, glowWidth, alpha, glowColor);
157
+ cr.setLineWidth(3);
158
+ cr.setLineCap(Cairo.LineCap.ROUND);
159
+ cr.setLineJoin(Cairo.LineJoin.ROUND);
160
+ setInk(cr, alpha);
161
+ cr.strokePreserve();
162
+ setFill(cr, glowColor, alpha);
163
+ cr.fill();
164
+ }
165
+
166
+ function linePath(cr, points, width = 4, alpha = 1, glowColor = null) {
167
+ if (points.length === 0) return;
168
+ cr.moveTo(points[0][0], points[0][1]);
169
+ for (let i = 1; i < points.length; i++) cr.lineTo(points[i][0], points[i][1]);
170
+ strokePath(cr, width, alpha, glowColor);
171
+ }
172
+
173
+ function roundedRect(cr, x, y, width, height, radius) {
174
+ const r = Math.max(0, Math.min(radius, width / 2, height / 2));
175
+ const k = 0.5522848;
176
+ cr.moveTo(x + r, y);
177
+ cr.lineTo(x + width - r, y);
178
+ cr.curveTo(x + width - r + r * k, y, x + width, y + r - r * k, x + width, y + r);
179
+ cr.lineTo(x + width, y + height - r);
180
+ cr.curveTo(
181
+ x + width,
182
+ y + height - r + r * k,
183
+ x + width - r + r * k,
184
+ y + height,
185
+ x + width - r,
186
+ y + height
187
+ );
188
+ cr.lineTo(x + r, y + height);
189
+ cr.curveTo(x + r - r * k, y + height, x, y + height - r + r * k, x, y + height - r);
190
+ cr.lineTo(x, y + r);
191
+ cr.curveTo(x, y + r - r * k, x + r - r * k, y, x + r, y);
192
+ cr.closePath();
193
+ }
194
+
195
+ function traceCursorBody(cr) {
196
+ cr.moveTo(55, 30);
197
+ cr.curveTo(48, 28, 42, 33, 43, 41);
198
+ cr.lineTo(64, 98);
199
+ cr.curveTo(67, 106, 73, 106, 77, 99);
200
+ cr.lineTo(86, 79);
201
+ cr.curveTo(88, 75, 91, 72, 95, 70);
202
+ cr.lineTo(108, 63);
203
+ cr.curveTo(115, 59, 114, 53, 107, 50);
204
+ cr.closePath();
205
+ }
206
+
207
+ function drawCursorGlowShape(cr, fillColor) {
208
+ const layers = 36;
209
+ const outerWidth = 44;
210
+ const innerWidth = 7;
211
+ const maxOpacity = 0.34;
212
+ let accumulatedOpacity = 0;
213
+
214
+ for (let layer = 0; layer < layers; layer++) {
215
+ const progress = (layer + 1) / layers;
216
+ const width = outerWidth + (innerWidth - outerWidth) * progress;
217
+ const targetOpacity = maxOpacity * Math.pow(progress, 1.65);
218
+ const layerOpacity =
219
+ (targetOpacity - accumulatedOpacity) / Math.max(0.001, 1 - accumulatedOpacity);
220
+ accumulatedOpacity = targetOpacity;
221
+ if (layerOpacity <= 0) continue;
222
+
223
+ traceCursorBody(cr);
224
+ cr.setLineWidth(width);
225
+ cr.setLineCap(Cairo.LineCap.ROUND);
226
+ cr.setLineJoin(Cairo.LineJoin.ROUND);
227
+ setFill(cr, fillColor, layerOpacity);
228
+ cr.stroke();
229
+ }
230
+ }
231
+
232
+ function createGlowSurface(fillColor) {
233
+ const surface = new Cairo.ImageSurface(
234
+ Cairo.Format.ARGB32,
235
+ (CANVAS_SIZE + GLOW_PADDING * 2) * GLOW_SURFACE_SCALE,
236
+ (CANVAS_SIZE + GLOW_PADDING * 2) * GLOW_SURFACE_SCALE
237
+ );
238
+ const cr = new Cairo.Context(surface);
239
+ cr.scale(GLOW_SURFACE_SCALE, GLOW_SURFACE_SCALE);
240
+ cr.translate(GLOW_PADDING, GLOW_PADDING);
241
+ drawCursorGlowShape(cr, fillColor);
242
+ cr.$dispose();
243
+ return surface;
244
+ }
245
+
246
+ function sharedFloatMotion(progress) {
247
+ const angle = progress * Math.PI * 2;
248
+ return {
249
+ dx: Math.sin(angle) * 5,
250
+ dy: Math.cos(angle) * 6 - 5,
251
+ rotation: Math.cos(angle) * ((2.5 * Math.PI) / 180),
252
+ scale: 1,
253
+ };
254
+ }
255
+
256
+ function cursorBodyMotion(progress, action) {
257
+ let dx = 0;
258
+ let dy = 0;
259
+ let rotation = 0;
260
+ let scale = 1;
261
+
262
+ if (action === 'click') {
263
+ if (progress < 0.35) scale = 1 - easeInOut(progress / 0.35) * 0.07;
264
+ else if (progress < 0.6) scale = 0.93 + easeInOut((progress - 0.35) / 0.25) * 0.1;
265
+ else scale = 1.03 - easeInOut((progress - 0.6) / 0.4) * 0.03;
266
+ } else if (action === 'drag') {
267
+ const held = easeInOut(triangleWave(progress));
268
+ dx = held * 7;
269
+ dy = held * 3;
270
+ }
271
+
272
+ return { dx, dy, rotation, scale };
273
+ }
274
+
275
+ function applyCursorBodyMotion(cr, motion) {
276
+ cr.translate(64 + motion.dx, 64 + motion.dy);
277
+ cr.rotate(motion.rotation);
278
+ cr.scale(motion.scale, motion.scale);
279
+ cr.translate(-64, -64);
280
+ }
281
+
282
+ function drawCursorGlow(cr, progress, action, glowSurface) {
283
+ const motion = cursorBodyMotion(progress, action);
284
+ cr.save();
285
+ applyCursorBodyMotion(cr, motion);
286
+ cr.translate(-GLOW_PADDING, -GLOW_PADDING);
287
+ cr.scale(1 / GLOW_SURFACE_SCALE, 1 / GLOW_SURFACE_SCALE);
288
+ cr.setSourceSurface(glowSurface, 0, 0);
289
+ cr.paint();
290
+ cr.restore();
291
+ }
292
+
293
+ function drawCursorBody(cr, progress, action, fillColor) {
294
+ const motion = cursorBodyMotion(progress, action);
295
+ cr.save();
296
+ applyCursorBodyMotion(cr, motion);
297
+ traceCursorBody(cr);
298
+ setFill(cr, fillColor);
299
+ cr.fillPreserve();
300
+ cr.setLineWidth(5);
301
+ cr.setLineCap(Cairo.LineCap.ROUND);
302
+ cr.setLineJoin(Cairo.LineJoin.ROUND);
303
+ setPaper(cr);
304
+ cr.stroke();
305
+ cr.restore();
306
+ }
307
+
308
+ function drawActionCue(cr, action, progress, fillColor) {
309
+ const wave = triangleWave(progress);
310
+ const strokeCue = (width, alpha = 1) => strokePath(cr, width, alpha, fillColor);
311
+ const lineCue = (points, width = 4, alpha = 1) => linePath(cr, points, width, alpha, fillColor);
312
+ cr.save();
313
+ switch (action) {
314
+ case 'observe': {
315
+ const opacity = Math.min(1, progress * 7);
316
+ cr.translate(8, -10);
317
+ cr.moveTo(38, 28);
318
+ cr.curveTo(27, 29, 20, 38, 20, 49);
319
+ strokeCue(4, opacity);
320
+ cr.moveTo(42, 19);
321
+ cr.curveTo(23, 19, 11, 33, 11, 51);
322
+ strokeCue(4, opacity);
323
+ break;
324
+ }
325
+ case 'click': {
326
+ const cueProgress = Math.max(0, Math.min(1, progress / 0.65));
327
+ const opacity = Math.max(
328
+ 0,
329
+ Math.min(1, Math.min(1 - Math.pow(cueProgress, 1.35), cueProgress * 5))
330
+ );
331
+ const cueScale = 1.1 + easeInOut(cueProgress) * 0.4;
332
+ cr.translate(35, 28);
333
+ cr.scale(cueScale, cueScale);
334
+ cr.translate(-25, -25);
335
+ lineCue(
336
+ [
337
+ [35, 20],
338
+ [34, 11],
339
+ ],
340
+ 4,
341
+ opacity
342
+ );
343
+ lineCue(
344
+ [
345
+ [27, 25],
346
+ [19, 19],
347
+ ],
348
+ 4,
349
+ opacity
350
+ );
351
+ lineCue(
352
+ [
353
+ [25, 34],
354
+ [15, 34],
355
+ ],
356
+ 4,
357
+ opacity
358
+ );
359
+ break;
360
+ }
361
+ case 'drag': {
362
+ const offset = easeInOut(wave) * 7;
363
+ cr.translate(offset, offset * 0.43);
364
+ lineCue(
365
+ [
366
+ [28, 38],
367
+ [16, 35],
368
+ ],
369
+ 4,
370
+ 0.2 + wave * 0.8
371
+ );
372
+ lineCue(
373
+ [
374
+ [26, 48],
375
+ [12, 45],
376
+ ],
377
+ 4,
378
+ 0.2 + wave * 0.8
379
+ );
380
+ break;
381
+ }
382
+ case 'scroll': {
383
+ cr.translate(-5, 4 - wave * 8);
384
+ lineCue(
385
+ [
386
+ [23, 31],
387
+ [31, 22],
388
+ [39, 31],
389
+ ],
390
+ 4,
391
+ 0.42 + wave * 0.58
392
+ );
393
+ lineCue(
394
+ [
395
+ [23, 49],
396
+ [31, 58],
397
+ [39, 49],
398
+ ],
399
+ 4,
400
+ 0.42 + wave * 0.58
401
+ );
402
+ break;
403
+ }
404
+ case 'text': {
405
+ const opacity = progress < 0.34 || progress > 0.64 ? 1 : 0.18;
406
+ cr.translate(-4, 0);
407
+ lineCue(
408
+ [
409
+ [31, 22],
410
+ [31, 58],
411
+ ],
412
+ 4,
413
+ opacity
414
+ );
415
+ lineCue(
416
+ [
417
+ [24, 22],
418
+ [38, 22],
419
+ ],
420
+ 4,
421
+ opacity
422
+ );
423
+ lineCue(
424
+ [
425
+ [24, 58],
426
+ [38, 58],
427
+ ],
428
+ 4,
429
+ opacity
430
+ );
431
+ break;
432
+ }
433
+ case 'key': {
434
+ const bounce = Math.sin(progress * Math.PI * 2) * (1 - progress) * 3;
435
+ cr.translate(-9, bounce);
436
+ roundedRect(cr, 14, 25, 28, 28, 6);
437
+ strokeCue(3.5);
438
+ lineCue(
439
+ [
440
+ [23, 32],
441
+ [23, 46],
442
+ [23, 39],
443
+ [33, 32],
444
+ [24, 39],
445
+ [34, 46],
446
+ ],
447
+ 3.5
448
+ );
449
+ break;
450
+ }
451
+ case 'navigate': {
452
+ cr.translate(-10 + easeInOut(progress) * 9, 0);
453
+ lineCue(
454
+ [
455
+ [15, 29],
456
+ [25, 40],
457
+ [15, 51],
458
+ ],
459
+ 4,
460
+ 0.2 + wave * 0.8
461
+ );
462
+ lineCue(
463
+ [
464
+ [29, 29],
465
+ [39, 40],
466
+ [29, 51],
467
+ ],
468
+ 4,
469
+ 0.2 + wave * 0.8
470
+ );
471
+ break;
472
+ }
473
+ case 'app': {
474
+ const s = 0.2 + easeInOut(Math.min(1, progress * 2)) * 0.8;
475
+ cr.translate(21, 39);
476
+ cr.scale(s, s);
477
+ cr.translate(-26, -39);
478
+ for (const [x, y] of [
479
+ [13, 26],
480
+ [29, 26],
481
+ [13, 42],
482
+ [29, 42],
483
+ ]) {
484
+ roundedRect(cr, x, y, 10, 10, 2);
485
+ strokeCue(3.5);
486
+ }
487
+ break;
488
+ }
489
+ case 'transfer':
490
+ cr.translate(-9, 6 - wave * 12);
491
+ lineCue(
492
+ [
493
+ [22, 50],
494
+ [22, 20],
495
+ [14, 28],
496
+ [22, 20],
497
+ [30, 28],
498
+ ],
499
+ 4,
500
+ 0.38 + wave * 0.62
501
+ );
502
+ lineCue(
503
+ [
504
+ [37, 28],
505
+ [37, 58],
506
+ [29, 50],
507
+ [37, 58],
508
+ [45, 50],
509
+ ],
510
+ 4,
511
+ 0.38 + wave * 0.62
512
+ );
513
+ break;
514
+ case 'record':
515
+ cr.translate(-12, 0);
516
+ cr.arc(29, 39, 17, 0, Math.PI * 2);
517
+ strokeCue(4);
518
+ cr.arc(29, 39, 3.6 + wave * 2.1, 0, Math.PI * 2);
519
+ fillPath(cr, (3.6 + wave * 2.1) * 1.25, 0.42 + wave * 0.58, fillColor);
520
+ break;
521
+ case 'system': {
522
+ cr.translate(15, 39);
523
+ cr.rotate(((easeInOut(progress) * 68 - 18) * Math.PI) / 180);
524
+ cr.translate(-29, -39);
525
+ for (const radius of [12, 4]) {
526
+ cr.arc(29, 39, radius, 0, Math.PI * 2);
527
+ strokeCue(3.5);
528
+ }
529
+ for (const points of [
530
+ [
531
+ [29, 20],
532
+ [29, 25],
533
+ ],
534
+ [
535
+ [29, 53],
536
+ [29, 58],
537
+ ],
538
+ [
539
+ [10, 39],
540
+ [15, 39],
541
+ ],
542
+ [
543
+ [43, 39],
544
+ [48, 39],
545
+ ],
546
+ [
547
+ [16, 26],
548
+ [20, 30],
549
+ ],
550
+ [
551
+ [38, 48],
552
+ [42, 52],
553
+ ],
554
+ [
555
+ [16, 52],
556
+ [20, 48],
557
+ ],
558
+ [
559
+ [38, 30],
560
+ [42, 26],
561
+ ],
562
+ ])
563
+ lineCue(points, 3.5);
564
+ break;
565
+ }
566
+ default:
567
+ break;
568
+ }
569
+ cr.restore();
570
+ }
571
+
572
+ function drawBadgeChip(cr, glyph, filled, fillColor) {
573
+ roundedRect(cr, 0.5, 0.5, 17, 17, 5);
574
+ if (filled) {
575
+ cr.setSourceRGBA(fillColor[0] / 255, fillColor[1] / 255, fillColor[2] / 255, 0.86);
576
+ cr.fillPreserve();
577
+ } else {
578
+ cr.setSourceRGBA(1, 1, 1, 0.09);
579
+ cr.fillPreserve();
580
+ }
581
+ cr.setSourceRGBA(1, 1, 1, filled ? 0.72 : 0.42);
582
+ cr.setLineWidth(1);
583
+ cr.stroke();
584
+ cr.setSourceRGBA(1, 1, 1, 0.95);
585
+ cr.setLineWidth(1.35);
586
+ cr.setLineCap(Cairo.LineCap.ROUND);
587
+ cr.setLineJoin(Cairo.LineJoin.ROUND);
588
+
589
+ const stroke = () => cr.stroke();
590
+ const line = (points) => {
591
+ cr.moveTo(points[0][0], points[0][1]);
592
+ for (const [x, y] of points.slice(1)) cr.lineTo(x, y);
593
+ stroke();
594
+ };
595
+ switch (glyph) {
596
+ case 'background':
597
+ roundedRect(cr, 4, 4, 7, 7, 1.8);
598
+ stroke();
599
+ roundedRect(cr, 6.4, 6.4, 7, 7, 1.8);
600
+ stroke();
601
+ break;
602
+ case 'foreground':
603
+ roundedRect(cr, 4, 5, 10, 9, 1.8);
604
+ stroke();
605
+ line([
606
+ [5, 7.6],
607
+ [13, 7.6],
608
+ ]);
609
+ break;
610
+ case 'ax':
611
+ line([
612
+ [9, 5],
613
+ [9, 9],
614
+ [5, 13],
615
+ ]);
616
+ line([
617
+ [9, 9],
618
+ [13, 13],
619
+ ]);
620
+ for (const [x, y] of [
621
+ [9, 5],
622
+ [5, 13],
623
+ [13, 13],
624
+ ]) {
625
+ cr.arc(x, y, 1.35, 0, Math.PI * 2);
626
+ cr.fill();
627
+ }
628
+ break;
629
+ case 'pixel':
630
+ line([
631
+ [4, 7],
632
+ [4, 4],
633
+ [7, 4],
634
+ ]);
635
+ line([
636
+ [11, 4],
637
+ [14, 4],
638
+ [14, 7],
639
+ ]);
640
+ line([
641
+ [14, 11],
642
+ [14, 14],
643
+ [11, 14],
644
+ ]);
645
+ line([
646
+ [7, 14],
647
+ [4, 14],
648
+ [4, 11],
649
+ ]);
650
+ break;
651
+ case 'browser':
652
+ cr.arc(9, 9, 5, 0, Math.PI * 2);
653
+ stroke();
654
+ line([
655
+ [4, 9],
656
+ [14, 9],
657
+ ]);
658
+ line([
659
+ [9, 4],
660
+ [7, 9],
661
+ [9, 14],
662
+ [11, 9],
663
+ [9, 4],
664
+ ]);
665
+ break;
666
+ case 'desktop':
667
+ roundedRect(cr, 4, 4, 10, 7.6, 1.3);
668
+ stroke();
669
+ line([
670
+ [9, 11.6],
671
+ [9, 14],
672
+ [6, 14],
673
+ [12, 14],
674
+ ]);
675
+ break;
676
+ default:
677
+ break;
678
+ }
679
+ }
680
+
681
+ export default class WinRectsExtension extends Extension {
682
+ enable() {
683
+ this._impl = Gio.DBusExportedObject.wrapJSObject(IFACE, this);
684
+ this._impl.export(Gio.DBus.session, '/org/cua/WinRects');
685
+ this._nameId = Gio.bus_own_name(
686
+ Gio.BusType.SESSION,
687
+ 'org.cua.WinRects',
688
+ Gio.BusNameOwnerFlags.REPLACE,
689
+ null,
690
+ null,
691
+ null
692
+ );
693
+ this._action = 'idle';
694
+ this._delivery = '';
695
+ this._target = '';
696
+ this._actionStarted = nowSeconds();
697
+ this._endingAt = null;
698
+ this._fillColor = [94, 192, 232];
699
+ this._fillColorCss = '#5ec0e8';
700
+ this._glowSurface = createGlowSurface(this._fillColor);
701
+ this._cursor = new St.DrawingArea({
702
+ width: ACTOR_SIZE,
703
+ height: ACTOR_SIZE,
704
+ visible: false,
705
+ reactive: false,
706
+ can_focus: false,
707
+ });
708
+ this._cursor.connect('repaint', (area) => {
709
+ const cr = area.get_context();
710
+ const duration = ACTION_DURATIONS[this._action] ?? 1.6;
711
+ const elapsed = nowSeconds() - this._actionStarted;
712
+ const progress = (elapsed % duration) / duration;
713
+ const floatProgress = (elapsed % FLOAT_DURATION) / FLOAT_DURATION;
714
+ cr.save();
715
+ cr.translate(ACTOR_CENTER, ACTOR_CENTER);
716
+ cr.scale(SCALE, SCALE);
717
+ cr.translate(-CANVAS_SIZE / 2, -CANVAS_SIZE / 2);
718
+ applyCursorBodyMotion(cr, sharedFloatMotion(floatProgress));
719
+ drawCursorGlow(cr, progress, this._action, this._glowSurface);
720
+ drawActionCue(cr, this._action, progress, this._fillColor);
721
+ drawCursorBody(cr, progress, this._action, this._fillColor);
722
+ cr.restore();
723
+ cr.$dispose();
724
+ });
725
+ this._cursor.set_pivot_point(0.5, 0.5);
726
+ Main.layoutManager.addTopChrome(this._cursor);
727
+ this._badgeLabel = new St.Label({
728
+ text: '',
729
+ visible: false,
730
+ y_align: Clutter.ActorAlign.CENTER,
731
+ style: 'font-size: 11px; font-weight: 600; color: white;',
732
+ });
733
+ this._deliveryChip = new St.DrawingArea({
734
+ width: BADGE_CHIP_SIZE,
735
+ height: BADGE_CHIP_SIZE,
736
+ visible: false,
737
+ });
738
+ this._deliveryChip.connect('repaint', (area) => {
739
+ const cr = area.get_context();
740
+ drawBadgeChip(cr, this._delivery, true, this._fillColor);
741
+ cr.$dispose();
742
+ });
743
+ this._targetChip = new St.DrawingArea({
744
+ width: BADGE_CHIP_SIZE,
745
+ height: BADGE_CHIP_SIZE,
746
+ visible: false,
747
+ });
748
+ this._targetChip.connect('repaint', (area) => {
749
+ const cr = area.get_context();
750
+ drawBadgeChip(cr, this._target, false, this._fillColor);
751
+ cr.$dispose();
752
+ });
753
+ this._badge = new St.BoxLayout({
754
+ visible: false,
755
+ reactive: false,
756
+ can_focus: false,
757
+ style: badgeStyle(this._fillColor),
758
+ });
759
+ this._badge.add_child(this._badgeLabel);
760
+ this._badge.add_child(this._deliveryChip);
761
+ this._badge.add_child(this._targetChip);
762
+ Main.layoutManager.addTopChrome(this._badge);
763
+ this._cursorX = null;
764
+ this._cursorY = null;
765
+ this._badgeRevealedAt = null;
766
+ this._modifierFadeAt = null;
767
+ this._frameId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 33, () => {
768
+ if (!this._cursor) return GLib.SOURCE_REMOVE;
769
+ const now = nowSeconds();
770
+ if (this._endingAt !== null && now >= this._endingAt) this._setCursorState('idle', '', '');
771
+ if (
772
+ ONE_SHOT_ACTIONS.has(this._action) &&
773
+ now - this._actionStarted >= ACTION_DURATIONS[this._action]
774
+ )
775
+ this._setCursorState('idle', '', '');
776
+ if (this._cursor.visible) this._cursor.queue_repaint();
777
+ let labelAlpha = this._badgeRevealedAt === null ? 0 : 1;
778
+ if (this._badgeRevealedAt !== null) {
779
+ const badgeElapsed = now - this._badgeRevealedAt;
780
+ if (
781
+ badgeElapsed > BADGE_HOLD_SECONDS &&
782
+ badgeElapsed < BADGE_HOLD_SECONDS + BADGE_FADE_SECONDS
783
+ ) {
784
+ const fade = (badgeElapsed - BADGE_HOLD_SECONDS) / BADGE_FADE_SECONDS;
785
+ labelAlpha = 1 - easeInOut(fade);
786
+ } else if (badgeElapsed >= BADGE_HOLD_SECONDS + BADGE_FADE_SECONDS) {
787
+ labelAlpha = 0;
788
+ this._badgeRevealedAt = null;
789
+ }
790
+ }
791
+ let chipAlpha = this._delivery || this._target ? 1 : 0;
792
+ if (this._modifierFadeAt !== null) {
793
+ const fade = (now - this._modifierFadeAt) / BADGE_FADE_SECONDS;
794
+ chipAlpha = 1 - easeInOut(fade);
795
+ if (fade >= 1) {
796
+ chipAlpha = 0;
797
+ this._modifierFadeAt = null;
798
+ this._delivery = '';
799
+ this._target = '';
800
+ this._updateModifierChips();
801
+ }
802
+ }
803
+ if (this._badgeLabel) {
804
+ this._badgeLabel.opacity = Math.round(255 * labelAlpha);
805
+ if (labelAlpha <= 0.001 && this._badgeLabel.visible) {
806
+ this._badgeLabel.hide();
807
+ if (this._cursorX !== null && this._cursorY !== null)
808
+ this._positionBadge(this._cursorX, this._cursorY, 0);
809
+ }
810
+ }
811
+ if (this._deliveryChip) this._deliveryChip.opacity = Math.round(255 * chipAlpha);
812
+ if (this._targetChip) this._targetChip.opacity = Math.round(255 * chipAlpha);
813
+ if (this._badge) {
814
+ if (labelAlpha > 0.001 || chipAlpha > 0.001) {
815
+ this._badge.opacity = 255;
816
+ this._badge.show();
817
+ } else {
818
+ this._badge.hide();
819
+ }
820
+ }
821
+ return GLib.SOURCE_CONTINUE;
822
+ });
823
+ }
824
+ disable() {
825
+ if (this._frameId) {
826
+ GLib.source_remove(this._frameId);
827
+ this._frameId = 0;
828
+ }
829
+ if (this._cursor) {
830
+ this._cursor.destroy();
831
+ this._cursor = null;
832
+ }
833
+ if (this._badge) {
834
+ this._badge.destroy();
835
+ this._badge = null;
836
+ }
837
+ this._badgeLabel = null;
838
+ this._deliveryChip = null;
839
+ this._targetChip = null;
840
+ if (this._glowSurface) {
841
+ this._glowSurface.finish();
842
+ this._glowSurface = null;
843
+ }
844
+ if (this._impl) {
845
+ this._impl.unexport();
846
+ this._impl = null;
847
+ }
848
+ if (this._nameId) {
849
+ Gio.bus_unown_name(this._nameId);
850
+ this._nameId = 0;
851
+ }
852
+ }
853
+ GetVersion() {
854
+ return 8;
855
+ }
856
+ GetRects() {
857
+ const actors = global.get_window_actors();
858
+ const actorByWindow = new Map();
859
+ for (const actor of actors) {
860
+ if (actor.meta_window) actorByWindow.set(actor.meta_window, actor);
861
+ }
862
+ const windows = global.display.sort_windows_by_stacking([...actorByWindow.keys()]);
863
+ const focusedWindow = global.display.focus_window;
864
+ const out = [];
865
+ for (let stacking = 0; stacking < windows.length; stacking++) {
866
+ const w = windows[stacking];
867
+ const actor = actorByWindow.get(w);
868
+ const r = w.get_frame_rect();
869
+ let buffer = r;
870
+ try {
871
+ buffer = w.get_buffer_rect();
872
+ } catch (_error) {
873
+ // Older Shell releases may not expose the buffer rectangle.
874
+ }
875
+ const minimized = Boolean(w.minimized);
876
+ out.push({
877
+ id: w.get_stable_sequence(),
878
+ pid: w.get_pid(),
879
+ title: w.get_title() ?? '',
880
+ x: r.x,
881
+ y: r.y,
882
+ w: r.width,
883
+ h: r.height,
884
+ buffer_x: buffer.x,
885
+ buffer_y: buffer.y,
886
+ focused: focusedWindow === w,
887
+ minimized,
888
+ visible: Boolean(actor?.visible) && !minimized,
889
+ stacking,
890
+ });
891
+ }
892
+ return JSON.stringify(out);
893
+ }
894
+ async CaptureAsync(_params, invocation) {
895
+ try {
896
+ const shooter = new Shell.Screenshot();
897
+ const stream = Gio.MemoryOutputStream.new_resizable();
898
+ const [width, height] = global.display.get_size();
899
+ if (width <= 0 || height <= 0) throw new Error(`Invalid display size ${width}x${height}`);
900
+ // GNOME 50's stage-content capture copies the real cursor sprite
901
+ // after painting the stage. Remote/headless pointer seats can
902
+ // expose a 0x0 sprite, and that copy can crash Shell. The stream
903
+ // API has an explicit include_cursor flag, so keep cursor capture
904
+ // disabled; cua-driver draws its own agent cursor.
905
+ await shooter.screenshot(false, stream);
906
+ stream.close(null);
907
+ const encoded = GLib.base64_encode(stream.steal_as_bytes().get_data());
908
+ invocation.return_value(new GLib.Variant('(s)', [encoded]));
909
+ } catch (error) {
910
+ invocation.return_dbus_error('org.cua.WinRects.CaptureFailed', String(error));
911
+ }
912
+ }
913
+ ActivateAsync([id], invocation) {
914
+ const target = global
915
+ .get_window_actors()
916
+ .map((actor) => actor.meta_window)
917
+ .find((window) => window?.get_stable_sequence() === id);
918
+ if (!target) {
919
+ invocation.return_value(new GLib.Variant('(b)', [false]));
920
+ return;
921
+ }
922
+ target.activate(global.get_current_time());
923
+ GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => {
924
+ invocation.return_value(new GLib.Variant('(b)', [global.display.focus_window === target]));
925
+ return GLib.SOURCE_REMOVE;
926
+ });
927
+ }
928
+ MoveCursor(x, y) {
929
+ if (!this._cursor) return;
930
+ const revealBadge = !this._cursor.visible;
931
+ this._cursorX = x;
932
+ this._cursorY = y;
933
+ this._cursor.show();
934
+ this._cursor.ease({
935
+ x: x - ACTOR_CENTER,
936
+ y: y - ACTOR_CENTER,
937
+ duration: 480,
938
+ mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
939
+ });
940
+ this._positionBadge(x, y, 480);
941
+ if (revealBadge) this._revealBadge();
942
+ }
943
+ ClickPulse(x, y) {
944
+ if (!this._cursor) return;
945
+ const revealBadge = !this._cursor.visible;
946
+ this._cursorX = x;
947
+ this._cursorY = y;
948
+ this._cursor.set_position(x - ACTOR_CENTER, y - ACTOR_CENTER);
949
+ this._positionBadge(x, y, 0);
950
+ if (['idle', 'navigate', 'click'].includes(this._action))
951
+ this._setCursorState('click', this._delivery, this._target);
952
+ this._cursor.show();
953
+ this._cursor.queue_repaint();
954
+ if (revealBadge) this._revealBadge();
955
+ }
956
+ SetCursorColor(fillColor) {
957
+ const match = /^#([0-9a-fA-F]{6})$/.exec(fillColor);
958
+ if (!match) return;
959
+ const rgb = Number.parseInt(match[1], 16);
960
+ this._fillColor = [(rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff];
961
+ this._fillColorCss = `#${match[1].toLowerCase()}`;
962
+ if (this._badge) this._badge.set_style(badgeStyle(this._fillColor));
963
+ this._deliveryChip?.queue_repaint();
964
+ this._targetChip?.queue_repaint();
965
+ if (this._glowSurface) this._glowSurface.finish();
966
+ this._glowSurface = createGlowSurface(this._fillColor);
967
+ if (this._cursor) this._cursor.queue_repaint();
968
+ }
969
+ SetCursorState(action, delivery, target, active) {
970
+ if (!ACTIONS.has(action)) return;
971
+ if (active) {
972
+ this._setCursorState(action, delivery, target);
973
+ } else if (this._action === action && !ONE_SHOT_ACTIONS.has(action)) {
974
+ this._endingAt = nowSeconds() + 0.4;
975
+ }
976
+ }
977
+ SetSessionLabel(label) {
978
+ if (!this._badgeLabel || !this._badge) return;
979
+ this._badgeLabel.set_text(label);
980
+ if (label.length === 0 || !this._cursor?.visible) {
981
+ this._badgeLabel.hide();
982
+ this._badgeRevealedAt = null;
983
+ if (!this._delivery && !this._target) this._badge.hide();
984
+ return;
985
+ }
986
+ if (this._cursorX !== null && this._cursorY !== null) {
987
+ this._positionBadge(this._cursorX, this._cursorY, 0);
988
+ this._revealBadge();
989
+ }
990
+ }
991
+ _positionBadge(x, y, duration) {
992
+ if (!this._badge) return;
993
+ const [, naturalWidth] = this._badge.get_preferred_width(-1);
994
+ const badgeX = Math.round(x - naturalWidth / 2);
995
+ const badgeY = Math.round(y + BADGE_Y_OFFSET);
996
+ if (duration > 0) {
997
+ this._badge.ease({
998
+ x: badgeX,
999
+ y: badgeY,
1000
+ duration,
1001
+ mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
1002
+ });
1003
+ } else {
1004
+ this._badge.set_position(badgeX, badgeY);
1005
+ }
1006
+ }
1007
+ _revealBadge() {
1008
+ if (!this._badge || !this._badgeLabel || this._badgeLabel.get_text().length === 0) return;
1009
+ this._badgeLabel.show();
1010
+ if (this._cursorX !== null && this._cursorY !== null)
1011
+ this._positionBadge(this._cursorX, this._cursorY, 0);
1012
+ this._badgeRevealedAt = nowSeconds();
1013
+ this._badge.opacity = 255;
1014
+ this._badge.show();
1015
+ }
1016
+ _setCursorState(action, delivery, target) {
1017
+ this._action = ACTIONS.has(action) ? action : 'idle';
1018
+ const nextDelivery = delivery ?? '';
1019
+ const nextTarget = target ?? '';
1020
+ if (nextDelivery || nextTarget) {
1021
+ this._delivery = nextDelivery;
1022
+ this._target = nextTarget;
1023
+ this._modifierFadeAt = null;
1024
+ } else if ((this._delivery || this._target) && this._modifierFadeAt === null) {
1025
+ this._modifierFadeAt = nowSeconds();
1026
+ }
1027
+ this._updateModifierChips();
1028
+ this._actionStarted = nowSeconds();
1029
+ this._endingAt = null;
1030
+ if (this._cursor) this._cursor.queue_repaint();
1031
+ if (this._cursorX !== null && this._cursorY !== null)
1032
+ this._positionBadge(this._cursorX, this._cursorY, 0);
1033
+ }
1034
+ _updateModifierChips() {
1035
+ if (this._deliveryChip) {
1036
+ this._deliveryChip.visible = this._delivery.length > 0;
1037
+ this._deliveryChip.queue_repaint();
1038
+ }
1039
+ if (this._targetChip) {
1040
+ this._targetChip.visible = this._target.length > 0;
1041
+ this._targetChip.queue_repaint();
1042
+ }
1043
+ }
1044
+ HideCursor() {
1045
+ if (this._cursor) this._cursor.hide();
1046
+ if (this._badge) this._badge.hide();
1047
+ this._badgeRevealedAt = null;
1048
+ this._modifierFadeAt = null;
1049
+ this._delivery = '';
1050
+ this._target = '';
1051
+ this._updateModifierChips();
1052
+ }
1053
+ }