@remcp/runtime 0.2.152 → 0.2.154

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.
@@ -7,6 +7,8 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { runtimeConfigDir } from './config.mjs';
9
9
  import { ToolError } from './util.mjs';
10
+ import { createWaylandInput } from './wayland-remote-desktop-input.mjs';
11
+ import { cleanError, unwrap, token, withTimeout } from './wayland-remote-desktop-utils.mjs';
10
12
 
11
13
  const PORTAL_NAME = 'org.freedesktop.portal.Desktop';
12
14
  const PORTAL_PATH = '/org/freedesktop/portal/desktop';
@@ -26,31 +28,6 @@ let active = null;
26
28
  let starting = null;
27
29
  let dbusModule = null;
28
30
 
29
- function cleanError(error) {
30
- return (error instanceof Error ? error.message : String(error)).replace(/\s+/g, ' ').trim().slice(0, 500) || 'unknown error';
31
- }
32
-
33
- function unwrap(value) {
34
- if (value && typeof value === 'object' && Object.hasOwn(value, 'value')) return unwrap(value.value);
35
- if (Array.isArray(value)) return value.map(unwrap);
36
- if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, unwrap(item)]));
37
- return value;
38
- }
39
-
40
- function token(prefix) {
41
- return `${prefix}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`.replace(/[^A-Za-z0-9_]/g, '_');
42
- }
43
-
44
- function withTimeout(promise, timeoutMs, label) {
45
- return new Promise((resolve, reject) => {
46
- const timer = setTimeout(() => reject(new ToolError(`${label} timed out after ${timeoutMs} ms`)), timeoutMs);
47
- Promise.resolve(promise).then(
48
- value => { clearTimeout(timer); resolve(value); },
49
- error => { clearTimeout(timer); reject(error); },
50
- );
51
- });
52
- }
53
-
54
31
  async function loadDbus() {
55
32
  dbusModule ||= await import('@jellybrick/dbus-next');
56
33
  return dbusModule;
@@ -471,161 +448,15 @@ export async function ensureWaylandRemoteDesktop(options = {}) {
471
448
  return starting;
472
449
  }
473
450
 
474
- function keysymForToken(tokenValue) {
475
- const token = String(tokenValue || '').trim();
476
- const normalized = token.toLowerCase();
477
- const named = {
478
- ctrl: 0xffe3, control: 0xffe3, shift: 0xffe1, alt: 0xffe9, option: 0xffe9,
479
- super: 0xffeb, meta: 0xffeb, cmd: 0xffeb, command: 0xffeb,
480
- enter: 0xff0d, return: 0xff0d, esc: 0xff1b, escape: 0xff1b, tab: 0xff09,
481
- backspace: 0xff08, delete: 0xffff, up: 0xff52, down: 0xff54, left: 0xff51, right: 0xff53,
482
- home: 0xff50, end: 0xff57, pageup: 0xff55, pagedown: 0xff56, space: 0x20,
483
- };
484
- if (named[normalized]) return named[normalized];
485
- const fn = normalized.match(/^f(\d{1,2})$/);
486
- if (fn) {
487
- const number = Number(fn[1]);
488
- if (number >= 1 && number <= 35) return 0xffbd + number;
489
- }
490
- const chars = [...token];
491
- if (chars.length !== 1) throw new ToolError(`Unsupported keyboard token: ${token}`);
492
- const codePoint = chars[0].codePointAt(0);
493
- return codePoint <= 0xff ? codePoint : (0x01000000 | codePoint);
494
- }
495
-
496
- async function notifyKeysym(state, keysym, keyState) {
497
- if (state.backend === 'xdg-eis') throw new ToolError('EIS 1.2 keyboard input requires evdev keycodes, not X11 keysyms');
498
- await state.remote.NotifyKeyboardKeysym(state.session, {}, keysym, keyState);
499
- }
500
-
501
- async function notifyKeycode(state, keycode, keyState) {
502
- if (state.backend === 'xdg-eis') {
503
- await state.send({ op:'key', keycode:Number(keycode), pressed:keyState === 1 });
504
- return;
505
- }
506
- await state.remote.NotifyKeyboardKeycode(state.session, {}, keycode, keyState);
507
- }
508
-
509
- function keycodeForToken(tokenValue) {
510
- const token = String(tokenValue || '').trim().toLowerCase();
511
- const named = {
512
- esc:1, escape:1,
513
- '1':2, '2':3, '3':4, '4':5, '5':6, '6':7, '7':8, '8':9, '9':10, '0':11,
514
- minus:12, '-':12, equal:13, '=':13, backspace:14, tab:15,
515
- q:16, w:17, e:18, r:19, t:20, y:21, u:22, i:23, o:24, p:25,
516
- '[':26, ']':27, enter:28, return:28, ctrl:29, control:29,
517
- a:30, s:31, d:32, f:33, g:34, h:35, j:36, k:37, l:38,
518
- ';':39, "'":40, grave:41, backtick:41, '`':41, shift:42, '\\':43,
519
- z:44, x:45, c:46, v:47, b:48, n:49, m:50, ',':51, '.':52, '/':53,
520
- alt:56, option:56, space:57,
521
- f1:59, f2:60, f3:61, f4:62, f5:63, f6:64, f7:65, f8:66, f9:67, f10:68,
522
- f11:87, f12:88,
523
- home:102, up:103, pageup:104, left:105, right:106, end:107, down:108, pagedown:109,
524
- insert:110, delete:111,
525
- super:125, meta:125, cmd:125, command:125,
526
- };
527
- return Number.isInteger(named[token]) ? named[token] : null;
528
- }
529
-
530
- async function notifyToken(state, token, keyState) {
531
- if (state.backend === 'xdg-eis') {
532
- const keycode = keycodeForToken(token);
533
- if (!Number.isInteger(keycode)) throw new ToolError(`Unsupported EIS keyboard token: ${token}`);
534
- return notifyKeycode(state, keycode, keyState);
535
- }
536
- return notifyKeysym(state, keysymForToken(token), keyState);
537
- }
538
-
539
- export async function portalShortcut(shortcut, options = {}) {
540
- const state = options.state || await ensureWaylandRemoteDesktop(options);
541
- const tokens = String(shortcut || '').split('+').map(value => value.trim()).filter(Boolean);
542
- if (!tokens.length) throw new ToolError('shortcut or key is required');
543
- const modifiers = tokens.slice(0, -1);
544
- const key = tokens.at(-1);
545
- for (const modifier of modifiers) await notifyToken(state, modifier, 1);
546
- await notifyToken(state, key, 1);
547
- await notifyToken(state, key, 0);
548
- for (const modifier of [...modifiers].reverse()) await notifyToken(state, modifier, 0);
549
- }
550
-
551
- function eisStrokeForChar(char) {
552
- if (char === '\n') return { keycode:28, shift:false };
553
- if (char === '\t') return { keycode:15, shift:false };
554
- if (char === ' ') return { keycode:57, shift:false };
555
- if (/^[a-z]$/.test(char)) return { keycode:keycodeForToken(char), shift:false };
556
- if (/^[A-Z]$/.test(char)) return { keycode:keycodeForToken(char), shift:true };
557
- if (/^[0-9]$/.test(char)) return { keycode:keycodeForToken(char), shift:false };
558
- const plain = { '-':12, '=':13, '[':26, ']':27, ';':39, "'":40, '`':41, '\\':43, ',':51, '.':52, '/':53 };
559
- if (Number.isInteger(plain[char])) return { keycode:plain[char], shift:false };
560
- const shifted = { '!':2, '@':3, '#':4, '$':5, '%':6, '^':7, '&':8, '*':9, '(':10, ')':11, '_':12, '+':13, '{':26, '}':27, ':':39, '"':40, '~':41, '|':43, '<':51, '>':52, '?':53 };
561
- if (Number.isInteger(shifted[char])) return { keycode:shifted[char], shift:true };
562
- return null;
563
- }
564
-
565
- export async function portalTypeText(value, { delayMs = 1, timeoutMs, state: suppliedState } = {}) {
566
- const state = suppliedState || await ensureWaylandRemoteDesktop({ timeoutMs });
567
- const input = String(value ?? '');
568
- if (state.backend === 'xdg-eis') {
569
- const strokes = [...input].map(eisStrokeForChar);
570
- if (strokes.some(stroke => !stroke)) {
571
- throw new ToolError('EIS 1.2 direct typing supports ASCII only; use accessibility or clipboard-paste input for Unicode text');
572
- }
573
- for (const stroke of strokes) {
574
- if (stroke.shift) await notifyKeycode(state, 42, 1);
575
- await notifyKeycode(state, stroke.keycode, 1);
576
- await notifyKeycode(state, stroke.keycode, 0);
577
- if (stroke.shift) await notifyKeycode(state, 42, 0);
578
- if (delayMs > 0) await new Promise(resolve => setTimeout(resolve, Math.min(1000, delayMs)));
579
- }
580
- return;
581
- }
582
- for (const char of input) {
583
- const keysym = char === '\n' ? 0xff0d : char === '\t' ? 0xff09 : keysymForToken(char);
584
- await notifyKeysym(state, keysym, 1);
585
- await notifyKeysym(state, keysym, 0);
586
- if (delayMs > 0) await new Promise(resolve => setTimeout(resolve, Math.min(1000, delayMs)));
587
- }
588
- }
589
-
590
- export async function portalPointerMotion(dx, dy, options = {}) {
591
- const state = options.state || await ensureWaylandRemoteDesktop(options);
592
- if (state.backend === 'xdg-eis') {
593
- await state.send({ op:'motion', dx:Number(dx), dy:Number(dy) });
594
- return;
595
- }
596
- await state.remote.NotifyPointerMotion(state.session, {}, Number(dx), Number(dy));
597
- }
598
-
599
- export async function portalPointerMotionAbsolute(x, y, options = {}) {
600
- const state = options.state || await ensureWaylandRemoteDesktop(options);
601
- if (state.backend === 'xdg-eis') {
602
- await state.send({ op:'motion_absolute', x:Number(x), y:Number(y) });
603
- return;
604
- }
605
- throw new ToolError('Absolute pointer motion requires the EIS Remote Desktop backend');
606
- }
607
-
608
- export async function portalPointerButton(button, pressed, options = {}) {
609
- const state = options.state || await ensureWaylandRemoteDesktop(options);
610
- const code = String(button || 'left').toLowerCase() === 'right' ? 0x111 : String(button || 'left').toLowerCase() === 'middle' ? 0x112 : 0x110;
611
- if (state.backend === 'xdg-eis') {
612
- await state.send({ op:'button', button:code, pressed:Boolean(pressed) });
613
- return;
614
- }
615
- await state.remote.NotifyPointerButton(state.session, {}, code, pressed ? 1 : 0);
616
- }
617
-
618
- export async function portalScroll(dx, dy, options = {}) {
619
- const state = options.state || await ensureWaylandRemoteDesktop(options);
620
- const steps = delta => delta ? Math.sign(delta) * Math.max(1, Math.round(Math.abs(delta) / 120)) : 0;
621
- const x = steps(dx), y = steps(dy);
622
- if (state.backend === 'xdg-eis') {
623
- await state.send({ op:'scroll', dx:x * 120, dy:y * 120 });
624
- return;
625
- }
626
- if (x) await state.remote.NotifyPointerAxisDiscrete(state.session, {}, 1, x);
627
- if (y) await state.remote.NotifyPointerAxisDiscrete(state.session, {}, 0, y);
628
- }
451
+ // Keep portal and EIS input bound to the existing grant/session lifecycle.
452
+ const {
453
+ portalShortcut, portalTypeText, portalPointerMotion, portalPointerMotionAbsolute,
454
+ portalPointerButton, portalScroll,
455
+ } = createWaylandInput(ensureWaylandRemoteDesktop);
456
+ export {
457
+ portalShortcut, portalTypeText, portalPointerMotion, portalPointerMotionAbsolute,
458
+ portalPointerButton, portalScroll,
459
+ };
629
460
 
630
461
  export function hasWaylandRemoteDesktopRestoreToken() {
631
462
  return existsSync(TOKEN_FILE);