@midscene/computer 1.12.7-beta-20260911063016.0 → 1.12.7

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/dist/lib/index.js CHANGED
@@ -68,6 +68,73 @@ const img_namespaceObject = require("@midscene/shared/img");
68
68
  const logger_namespaceObject = require("@midscene/shared/logger");
69
69
  const external_screenshot_desktop_namespaceObject = require("screenshot-desktop");
70
70
  var external_screenshot_desktop_default = /*#__PURE__*/ __webpack_require__.n(external_screenshot_desktop_namespaceObject);
71
+ const debugKeyboard = (0, logger_namespaceObject.getDebug)('computer:keyboard');
72
+ const APPLE_SCRIPT_KEY_CODES = {
73
+ return: 36,
74
+ enter: 36,
75
+ tab: 48,
76
+ space: 49,
77
+ backspace: 51,
78
+ delete: 51,
79
+ escape: 53,
80
+ forwarddelete: 117,
81
+ left: 123,
82
+ right: 124,
83
+ down: 125,
84
+ up: 126,
85
+ home: 115,
86
+ end: 119,
87
+ pageup: 116,
88
+ pagedown: 121,
89
+ f1: 122,
90
+ f2: 120,
91
+ f3: 99,
92
+ f4: 118,
93
+ f5: 96,
94
+ f6: 97,
95
+ f7: 98,
96
+ f8: 100,
97
+ f9: 101,
98
+ f10: 109,
99
+ f11: 103,
100
+ f12: 111
101
+ };
102
+ const APPLE_SCRIPT_MODIFIER_KEYS = {
103
+ command: 'command',
104
+ cmd: 'command',
105
+ control: 'control',
106
+ ctrl: 'control',
107
+ shift: 'shift',
108
+ alt: 'option',
109
+ option: 'option',
110
+ meta: 'command'
111
+ };
112
+ function buildKeyCommand(key) {
113
+ const keyCode = APPLE_SCRIPT_KEY_CODES[key.toLowerCase()];
114
+ if (void 0 !== keyCode) return `key code ${keyCode}`;
115
+ const escapedKey = key.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
116
+ return `keystroke "${escapedKey}"`;
117
+ }
118
+ function resolveModifierKeys(modifiers) {
119
+ return modifiers.map((modifier)=>APPLE_SCRIPT_MODIFIER_KEYS[modifier.toLowerCase()]).filter((modifier)=>void 0 !== modifier);
120
+ }
121
+ function buildAppleScriptKeyPress(key, modifiers = []) {
122
+ const modifierKeys = resolveModifierKeys(modifiers);
123
+ const modifierClause = modifierKeys.length ? ` using {${modifierKeys.map((modifier)=>`${modifier} down`).join(', ')}}` : '';
124
+ return `tell application "System Events" to ${buildKeyCommand(key)}${modifierClause}`;
125
+ }
126
+ function sendKeyViaAppleScript(key, modifiers = []) {
127
+ const script = buildAppleScriptKeyPress(key, modifiers);
128
+ debugKeyboard('sendKeyViaAppleScript', {
129
+ key,
130
+ modifiers,
131
+ script
132
+ });
133
+ (0, external_node_child_process_namespaceObject.execFileSync)("osascript", [
134
+ '-e',
135
+ script
136
+ ]);
137
+ }
71
138
  const external_node_assert_namespaceObject = require("node:assert");
72
139
  var external_node_assert_default = /*#__PURE__*/ __webpack_require__.n(external_node_assert_namespaceObject);
73
140
  function _define_property(obj, key, value) {
@@ -142,6 +209,41 @@ class ComputerInputDriver {
142
209
  if (void 0 !== modifiers) lib.keyTap(key, modifiers);
143
210
  else lib.keyTap(key);
144
211
  }
212
+ keyToggle(key, state, modifiers) {
213
+ const lib = this.getLibnutOrThrow('keyToggle');
214
+ if (void 0 !== modifiers) lib.keyToggle(key, state, modifiers);
215
+ else lib.keyToggle(key, state);
216
+ }
217
+ async keyTapWithModifierDelay(key, modifiers, delayMs) {
218
+ const uniqueModifiers = [
219
+ ...new Set(modifiers)
220
+ ];
221
+ if (0 === uniqueModifiers.length) return void this.keyTap(key);
222
+ if (0 === delayMs) return void this.keyTap(key, uniqueModifiers);
223
+ const pressedModifiers = [];
224
+ let failure;
225
+ try {
226
+ for (const modifier of uniqueModifiers){
227
+ this.keyToggle(modifier, 'down');
228
+ pressedModifiers.push(modifier);
229
+ await this.delay(delayMs);
230
+ }
231
+ this.keyTap(key);
232
+ await this.delay(delayMs);
233
+ } catch (error) {
234
+ failure = error;
235
+ }
236
+ for (const modifier of pressedModifiers.reverse()){
237
+ const releaseFailure = this.tryReleaseKey(modifier);
238
+ if (void 0 === failure && void 0 !== releaseFailure) failure = releaseFailure;
239
+ if (void 0 === failure) try {
240
+ await this.delay(delayMs);
241
+ } catch (error) {
242
+ failure = error;
243
+ }
244
+ }
245
+ if (void 0 !== failure) throw failure;
246
+ }
145
247
  typeString(text) {
146
248
  this.getLibnutOrThrow('typeString').typeString(text);
147
249
  }
@@ -214,6 +316,20 @@ class ComputerInputDriver {
214
316
  this.options.debug(`Failed to release mouse button ${button}: ${error}`);
215
317
  }
216
318
  }
319
+ tryReleaseKey(key) {
320
+ try {
321
+ const libnut = this.options.getLibnut();
322
+ external_node_assert_default()(libnut, 'libnut not initialized');
323
+ libnut.keyToggle(key, 'up');
324
+ return;
325
+ } catch (error) {
326
+ const releaseError = new Error(`Failed to release modifier key "${key}"`, {
327
+ cause: error
328
+ });
329
+ this.options.debug(`${releaseError.message}: ${error}`);
330
+ return releaseError;
331
+ }
332
+ }
217
333
  rejectPendingInputDelays() {
218
334
  const error = this.createDestroyedError('in-flight input');
219
335
  for (const waitRef of this.pendingInputDelayWaits){
@@ -231,6 +347,96 @@ class ComputerInputDriver {
231
347
  this.pendingInputDelayWaits = new Set();
232
348
  }
233
349
  }
350
+ const US_SHIFTED_CHARACTER_KEYS = new Map([
351
+ [
352
+ '~',
353
+ '`'
354
+ ],
355
+ [
356
+ '!',
357
+ '1'
358
+ ],
359
+ [
360
+ '@',
361
+ '2'
362
+ ],
363
+ [
364
+ '#',
365
+ '3'
366
+ ],
367
+ [
368
+ '$',
369
+ '4'
370
+ ],
371
+ [
372
+ '%',
373
+ '5'
374
+ ],
375
+ [
376
+ '^',
377
+ '6'
378
+ ],
379
+ [
380
+ '&',
381
+ '7'
382
+ ],
383
+ [
384
+ '*',
385
+ '8'
386
+ ],
387
+ [
388
+ '(',
389
+ '9'
390
+ ],
391
+ [
392
+ ')',
393
+ '0'
394
+ ],
395
+ [
396
+ '_',
397
+ '-'
398
+ ],
399
+ [
400
+ '+',
401
+ '='
402
+ ],
403
+ [
404
+ '{',
405
+ '['
406
+ ],
407
+ [
408
+ '}',
409
+ ']'
410
+ ],
411
+ [
412
+ '|',
413
+ '\\'
414
+ ],
415
+ [
416
+ ':',
417
+ ';'
418
+ ],
419
+ [
420
+ '"',
421
+ "'"
422
+ ],
423
+ [
424
+ '<',
425
+ ','
426
+ ],
427
+ [
428
+ '>',
429
+ '.'
430
+ ],
431
+ [
432
+ '?',
433
+ '/'
434
+ ]
435
+ ]);
436
+ function resolveShiftedKey(character, keyboardLayout) {
437
+ if (/^[A-Z]$/.test(character)) return character.toLowerCase();
438
+ return 'en-US' === keyboardLayout ? US_SHIFTED_CHARACTER_KEYS.get(character) : void 0;
439
+ }
234
440
  const POWERSHELL_TIMEOUT_MS = 15000;
235
441
  const POWERSHELL_MAX_BUFFER = 67108864;
236
442
  const WINDOWS_PHYSICAL_PIXEL_POWERSHELL_PREAMBLE = `
@@ -595,92 +801,6 @@ const LIBNUT_FALLBACK_PIXELS_PER_DETENT = 100;
595
801
  const LIBNUT_FALLBACK_TICK_DELAY_MS = 30;
596
802
  const LIBNUT_FALLBACK_MAX_DETENTS = 200;
597
803
  const LIBNUT_FALLBACK_DETENT_AMOUNT = 'win32' === process.platform ? 120 : 1;
598
- const LINUX_SHIFTED_CHARACTER_KEYS = new Map([
599
- [
600
- '~',
601
- '`'
602
- ],
603
- [
604
- '!',
605
- '1'
606
- ],
607
- [
608
- '@',
609
- '2'
610
- ],
611
- [
612
- '#',
613
- '3'
614
- ],
615
- [
616
- '$',
617
- '4'
618
- ],
619
- [
620
- '%',
621
- '5'
622
- ],
623
- [
624
- '^',
625
- '6'
626
- ],
627
- [
628
- '&',
629
- '7'
630
- ],
631
- [
632
- '*',
633
- '8'
634
- ],
635
- [
636
- '(',
637
- '9'
638
- ],
639
- [
640
- ')',
641
- '0'
642
- ],
643
- [
644
- '_',
645
- '-'
646
- ],
647
- [
648
- '+',
649
- '='
650
- ],
651
- [
652
- '{',
653
- '['
654
- ],
655
- [
656
- '}',
657
- ']'
658
- ],
659
- [
660
- '|',
661
- '\\'
662
- ],
663
- [
664
- ':',
665
- ';'
666
- ],
667
- [
668
- '"',
669
- "'"
670
- ],
671
- [
672
- '<',
673
- ','
674
- ],
675
- [
676
- '>',
677
- '.'
678
- ],
679
- [
680
- '?',
681
- '/'
682
- ]
683
- ]);
684
804
  const LIBNUT_FALLBACK_EDGE_DETENTS = Math.min(LIBNUT_FALLBACK_MAX_DETENTS, Math.max(1, Math.ceil(EDGE_SCROLL_TOTAL_PX / LIBNUT_FALLBACK_PIXELS_PER_DETENT)));
685
805
  const DEFAULT_SCROLL_VIEWPORT_RATIO = 0.7;
686
806
  const EDGE_SCROLL_SPEC = {
@@ -717,67 +837,6 @@ const EDGE_SCROLL_SPEC = {
717
837
  ]
718
838
  }
719
839
  };
720
- const APPLESCRIPT_KEY_CODE_MAP = {
721
- return: 36,
722
- enter: 36,
723
- tab: 48,
724
- space: 49,
725
- backspace: 51,
726
- delete: 51,
727
- escape: 53,
728
- forwarddelete: 117,
729
- left: 123,
730
- right: 124,
731
- down: 125,
732
- up: 126,
733
- home: 115,
734
- end: 119,
735
- pageup: 116,
736
- pagedown: 121,
737
- f1: 122,
738
- f2: 120,
739
- f3: 99,
740
- f4: 118,
741
- f5: 96,
742
- f6: 97,
743
- f7: 98,
744
- f8: 100,
745
- f9: 101,
746
- f10: 109,
747
- f11: 103,
748
- f12: 111
749
- };
750
- const APPLESCRIPT_MODIFIER_MAP = {
751
- command: 'command down',
752
- cmd: 'command down',
753
- control: 'control down',
754
- ctrl: 'control down',
755
- shift: 'shift down',
756
- alt: 'option down',
757
- option: 'option down',
758
- meta: 'command down'
759
- };
760
- function sendKeyViaAppleScript(key, modifiers = []) {
761
- const lowerKey = key.toLowerCase();
762
- const keyCode = APPLESCRIPT_KEY_CODE_MAP[lowerKey];
763
- const modifierParts = modifiers.map((m)=>APPLESCRIPT_MODIFIER_MAP[m.toLowerCase()]).filter(Boolean);
764
- const modifierStr = modifierParts.length > 0 ? ` using {${modifierParts.join(', ')}}` : '';
765
- let script;
766
- if (void 0 !== keyCode) script = `tell application "System Events" to key code ${keyCode}${modifierStr}`;
767
- else {
768
- const escapedKey = key.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
769
- script = `tell application "System Events" to keystroke "${escapedKey}"${modifierStr}`;
770
- }
771
- debugDevice('sendKeyViaAppleScript', {
772
- key,
773
- modifiers,
774
- script
775
- });
776
- (0, external_node_child_process_namespaceObject.execFileSync)("osascript", [
777
- '-e',
778
- script
779
- ]);
780
- }
781
840
  function escapePowershellSingleQuoted(value) {
782
841
  return value.replace(/'/g, "''");
783
842
  }
@@ -1205,7 +1264,7 @@ Available Displays: ${displays.length > 0 ? displays.map((d)=>d.name).join(', ')
1205
1264
  }
1206
1265
  async healthCheck(displays) {
1207
1266
  console.log('[HealthCheck] Starting health check...');
1208
- console.log("[HealthCheck] @midscene/computer v1.12.7-beta-20260911063016.0");
1267
+ console.log("[HealthCheck] @midscene/computer v1.12.7");
1209
1268
  console.log('[HealthCheck] Taking screenshot...');
1210
1269
  const screenshotTimeout = 15000;
1211
1270
  let timeoutId;
@@ -1382,9 +1441,9 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
1382
1441
  ]);
1383
1442
  else {
1384
1443
  const modifier = 'darwin' === process.platform ? 'command' : 'control';
1385
- this.inputDriver.keyTap('v', [
1444
+ await this.inputDriver.keyTapWithModifierDelay('v', [
1386
1445
  modifier
1387
- ]);
1446
+ ], this.options?.keyboardModifierDelay ?? 0);
1388
1447
  }
1389
1448
  await this.inputDriver.delay(100);
1390
1449
  } finally{
@@ -1399,12 +1458,16 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
1399
1458
  }
1400
1459
  async typeStringWithDelay(text, keyboardTypeDelay) {
1401
1460
  await (0, device_namespaceObject.sendTextSequentially)(text.replace(/\r\n?/g, '\n'), {
1402
- sendCharacter: (character)=>{
1403
- const linuxShiftedKey = 'linux' === process.platform ? LINUX_SHIFTED_CHARACTER_KEYS.get(character) : void 0;
1461
+ sendCharacter: async (character)=>{
1462
+ const linuxShiftedKey = 'linux' === process.platform ? US_SHIFTED_CHARACTER_KEYS.get(character) : void 0;
1463
+ const pacedShiftedKey = !this.useAppleScript && (this.options?.keyboardModifierDelay ?? 0) > 0 ? resolveShiftedKey(character, this.options?.keyboardLayout) : void 0;
1404
1464
  if ('\n' === character) this.inputDriver.sendKey('return');
1405
1465
  else if ('\t' === character) this.inputDriver.sendKey('tab');
1406
1466
  else if (' ' === character) this.inputDriver.sendKey('space');
1407
1467
  else if (this.useAppleScript) this.inputDriver.sendKeyViaAppleScript(character);
1468
+ else if (void 0 !== pacedShiftedKey) await this.inputDriver.keyTapWithModifierDelay(pacedShiftedKey, [
1469
+ 'shift'
1470
+ ], this.options?.keyboardModifierDelay ?? 0);
1408
1471
  else if (void 0 !== linuxShiftedKey) this.inputDriver.keyTap(linuxShiftedKey, [
1409
1472
  'shift'
1410
1473
  ]);
@@ -1425,9 +1488,9 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
1425
1488
  return;
1426
1489
  }
1427
1490
  const modifier = 'darwin' === process.platform ? 'command' : 'control';
1428
- this.inputDriver.keyTap('a', [
1491
+ await this.inputDriver.keyTapWithModifierDelay('a', [
1429
1492
  modifier
1430
- ]);
1493
+ ], this.options?.keyboardModifierDelay ?? 0);
1431
1494
  await this.inputDriver.delay(50);
1432
1495
  this.inputDriver.keyTap('backspace');
1433
1496
  }
@@ -1441,6 +1504,7 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
1441
1504
  modifiers,
1442
1505
  driver: this.useAppleScript ? "applescript" : 'libnut'
1443
1506
  });
1507
+ if (!this.useAppleScript && modifiers.length > 0 && (this.options?.keyboardModifierDelay ?? 0) > 0) return void await this.inputDriver.keyTapWithModifierDelay(key, modifiers, this.options?.keyboardModifierDelay ?? 0);
1444
1508
  this.inputDriver.sendKey(key, modifiers);
1445
1509
  }
1446
1510
  resolveUntargetedScrollPoint(screenSize) {
@@ -1594,7 +1658,7 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
1594
1658
  device_define_property(this, "inputDriver", new ComputerInputDriver({
1595
1659
  getLibnut: ()=>device_libnut,
1596
1660
  useAppleScript: ()=>this.useAppleScript,
1597
- sendKeyViaAppleScript,
1661
+ sendKeyViaAppleScript: sendKeyViaAppleScript,
1598
1662
  runPhasedScroll,
1599
1663
  debug: (message)=>debugDevice(message)
1600
1664
  }));
@@ -1720,6 +1784,8 @@ $g.Dispose(); $bmp.Dispose(); $ms.Dispose()
1720
1784
  }
1721
1785
  }
1722
1786
  });
1787
+ if (options?.keyboardModifierDelay !== void 0 && (!Number.isFinite(options.keyboardModifierDelay) || options.keyboardModifierDelay < 0)) throw new Error('keyboardModifierDelay must be a finite non-negative number');
1788
+ if (options?.keyboardLayout !== void 0 && 'en-US' !== options.keyboardLayout) throw new Error('keyboardLayout must be "en-US" when specified');
1723
1789
  this.options = options;
1724
1790
  this.displayId = options?.displayId;
1725
1791
  this.useAppleScript = 'darwin' === process.platform && options?.keyboardDriver !== 'libnut';
@@ -2467,6 +2533,8 @@ function createLocalComputerDevice(opts) {
2467
2533
  keyboardTypeDelay: opts?.keyboardTypeDelay,
2468
2534
  inputStrategy: opts?.inputStrategy,
2469
2535
  keyboardDriver: opts?.keyboardDriver,
2536
+ keyboardModifierDelay: opts?.keyboardModifierDelay,
2537
+ keyboardLayout: opts?.keyboardLayout,
2470
2538
  headless: opts?.headless,
2471
2539
  xvfbResolution: opts?.xvfbResolution,
2472
2540
  keepXvfbAliveUntilProcessExit: opts?.keepXvfbAliveUntilProcessExit
@@ -2527,6 +2595,10 @@ const computerInitArgShape = {
2527
2595
  headless: core_namespaceObject.z.boolean().optional().describe('Start virtual display via Xvfb (Linux local mode only). Ignored when host is set.'),
2528
2596
  keyboardTypeDelay: core_namespaceObject.z.number().finite().nonnegative().optional().describe('Finite non-negative delay in milliseconds between keystrokes. In "legacy" mode, positive values enable key-by-key input; zero or omitted uses clipboard input.'),
2529
2597
  inputStrategy: core_namespaceObject.z["enum"](device_namespaceObject.inputStrategies).optional().describe('Text input strategy. "legacy" (default) preserves current Computer behavior, "sequential" sends one Unicode code point at a time, and "bulk" uses one backend input operation. "bulk" requires keyboardTypeDelay to be omitted or set to 0.'),
2598
+ keyboardModifierDelay: core_namespaceObject.z.number().finite().nonnegative().optional().describe('Finite non-negative delay in milliseconds after each modifier transition and the main key for local libnut keyboard events. Positive values pace shortcuts and the implicit Shift used by uppercase letters during sequential input. Layout-dependent punctuation also requires keyboardLayout. Ignored in RDP mode and by the macOS AppleScript driver.'),
2599
+ keyboardLayout: core_namespaceObject.z["enum"]([
2600
+ 'en-US'
2601
+ ]).optional().describe('Keyboard layout used to resolve layout-dependent shifted punctuation to base keys during sequential local libnut input. Currently only "en-US" is supported. Ignored in RDP mode and by the macOS AppleScript driver.'),
2530
2602
  host: core_namespaceObject.z.string().optional().describe('RDP host (FQDN or IP). Set this to switch into RDP mode.'),
2531
2603
  port: core_namespaceObject.z.number().optional().describe('RDP port (default 3389). Requires host.'),
2532
2604
  username: core_namespaceObject.z.string().optional().describe('RDP username. Requires host.'),
@@ -2543,7 +2615,7 @@ const computerInitArgShape = {
2543
2615
  function adaptComputerInitArgs(extracted) {
2544
2616
  if (!extracted || 0 === Object.keys(extracted).length) return;
2545
2617
  if (extracted.host) {
2546
- const { displayId: _d, headless: _h, ...rdpFields } = extracted;
2618
+ const { displayId: _d, headless: _h, keyboardModifierDelay: _s, keyboardLayout: _l, ...rdpFields } = extracted;
2547
2619
  const host = normalizeRdpHost(extracted.host);
2548
2620
  return {
2549
2621
  mode: 'rdp',
@@ -2557,6 +2629,8 @@ function adaptComputerInitArgs(extracted) {
2557
2629
  headless: extracted.headless,
2558
2630
  keyboardTypeDelay: extracted.keyboardTypeDelay,
2559
2631
  inputStrategy: extracted.inputStrategy,
2632
+ keyboardModifierDelay: extracted.keyboardModifierDelay,
2633
+ keyboardLayout: extracted.keyboardLayout,
2560
2634
  ...(0, agent_behavior_init_args_namespaceObject.extractAgentBehaviorInitArgs)(extracted) ?? {}
2561
2635
  };
2562
2636
  }
@@ -2608,6 +2682,8 @@ class ComputerMidsceneTools extends base_tools_namespaceObject.BaseMidsceneTools
2608
2682
  const headless = opts?.mode === 'local' ? opts.headless : void 0;
2609
2683
  const keyboardTypeDelay = opts?.keyboardTypeDelay;
2610
2684
  const inputStrategy = opts?.inputStrategy;
2685
+ const keyboardModifierDelay = opts?.mode === 'local' ? opts.keyboardModifierDelay : void 0;
2686
+ const keyboardLayout = opts?.mode === 'local' ? opts.keyboardLayout : void 0;
2611
2687
  agent_tools_debug('Creating Computer agent with displayId:', displayId || 'primary');
2612
2688
  const agentOpts = {
2613
2689
  ...displayId ? {
@@ -2622,6 +2698,12 @@ class ComputerMidsceneTools extends base_tools_namespaceObject.BaseMidsceneTools
2622
2698
  ...void 0 !== inputStrategy ? {
2623
2699
  inputStrategy
2624
2700
  } : {},
2701
+ ...void 0 !== keyboardModifierDelay ? {
2702
+ keyboardModifierDelay
2703
+ } : {},
2704
+ ...void 0 !== keyboardLayout ? {
2705
+ keyboardLayout
2706
+ } : {},
2625
2707
  ...this.options.keepXvfbAliveUntilProcessExit ? {
2626
2708
  keepXvfbAliveUntilProcessExit: true
2627
2709
  } : {},
@@ -2703,7 +2785,7 @@ class ComputerMidsceneTools extends base_tools_namespaceObject.BaseMidsceneTools
2703
2785
  }
2704
2786
  const env_namespaceObject = require("@midscene/shared/env");
2705
2787
  function version() {
2706
- const currentVersion = "1.12.7-beta-20260911063016.0";
2788
+ const currentVersion = "1.12.7";
2707
2789
  console.log(`@midscene/computer v${currentVersion}`);
2708
2790
  return currentVersion;
2709
2791
  }
@@ -177,6 +177,25 @@ export declare interface ComputerDeviceOpt extends ComputerDeviceInputOpt {
177
177
  * - 'libnut': Use libnut's keyTap (faster but may not work with some TUI apps)
178
178
  */
179
179
  keyboardDriver?: 'applescript' | 'libnut';
180
+ /**
181
+ * Delay in milliseconds after each explicit modifier transition and the
182
+ * main key for local libnut keyboard events. A positive value changes
183
+ * modified shortcuts and shifted en-US text characters into separately
184
+ * observable modifier-down, main-key, and modifier-up phases. This can help
185
+ * foreground clients whose full-screen keyboard capture misses rapidly
186
+ * synthesized modifier state changes.
187
+ *
188
+ * Ignored by the macOS AppleScript driver and RDP mode.
189
+ * @default 0
190
+ */
191
+ keyboardModifierDelay?: number;
192
+ /**
193
+ * Keyboard layout used to resolve layout-dependent shifted characters
194
+ * during sequential local libnut input. Uppercase Latin letters do not
195
+ * require this option. Other characters keep using the backend's existing
196
+ * input behavior unless a supported layout is declared.
197
+ */
198
+ keyboardLayout?: 'en-US';
180
199
  /**
181
200
  * Headless mode via Xvfb (Linux only).
182
201
  * - true: start Xvfb virtual display
@@ -210,7 +229,7 @@ export declare type ComputerInterface = ComputerDevice | RDPDevice;
210
229
  /** Init args for the local desktop agent (macOS/Windows/Linux). */
211
230
  declare type ComputerLocalInitArgs = {
212
231
  mode: 'local';
213
- } & Pick<ComputerDeviceOpt, 'displayId' | 'headless'> & Pick<ComputerDeviceOpt, 'inputStrategy' | 'keyboardTypeDelay'> & AgentBehaviorInitArgs;
232
+ } & Pick<ComputerDeviceOpt, 'displayId' | 'headless'> & Pick<ComputerDeviceOpt, 'inputStrategy' | 'keyboardTypeDelay' | 'keyboardModifierDelay' | 'keyboardLayout'> & AgentBehaviorInitArgs;
214
233
 
215
234
  /**
216
235
  * Computer-specific tools manager
@@ -92,6 +92,28 @@ if(BUILD_TESTING)
92
92
  NAME rdp-session-policy-test
93
93
  COMMAND rdp-session-policy-test
94
94
  )
95
+
96
+ add_executable(
97
+ rdp-screenshot-test
98
+ tests/screenshot_test.cpp
99
+ src/png.cpp
100
+ src/session.cpp
101
+ )
102
+ target_include_directories(
103
+ rdp-screenshot-test PRIVATE
104
+ ${CMAKE_CURRENT_SOURCE_DIR}/include
105
+ ${FREERDP_INCLUDE_DIRS}
106
+ )
107
+ target_link_libraries(
108
+ rdp-screenshot-test PRIVATE
109
+ ${FREERDP_LIBRARIES}
110
+ Threads::Threads
111
+ )
112
+ if(APPLE)
113
+ target_compile_definitions(rdp-screenshot-test PRIVATE _DARWIN_C_SOURCE)
114
+ endif()
115
+ add_test(NAME rdp-screenshot-test COMMAND rdp-screenshot-test)
116
+ set_tests_properties(rdp-screenshot-test PROPERTIES TIMEOUT 30)
95
117
  endif()
96
118
 
97
119
  if(APPLE)
@@ -0,0 +1,24 @@
1
+ #pragma once
2
+
3
+ #include <algorithm>
4
+ #include <chrono>
5
+
6
+ #include "rdp_helper_session.hpp"
7
+
8
+ namespace midscene::rdp {
9
+
10
+ // FreeRDP embeds its context first; shared by the callback and native tests.
11
+ struct MidsceneRdpContext {
12
+ rdpContext context;
13
+ FreeRdpSessionTransport* owner = nullptr;
14
+ };
15
+
16
+ // Compute the next initial-screenshot wakeup without consulting wall time.
17
+ inline std::chrono::steady_clock::time_point ScreenshotWakeAt(
18
+ std::chrono::steady_clock::time_point started,
19
+ std::chrono::steady_clock::time_point last_update) {
20
+ return std::min(std::max(started, last_update) + std::chrono::milliseconds(300),
21
+ started + std::chrono::seconds(3));
22
+ }
23
+
24
+ } // namespace midscene::rdp
@@ -1,6 +1,7 @@
1
1
  #pragma once
2
2
 
3
3
  #include <atomic>
4
+ #include <chrono>
4
5
  #include <condition_variable>
5
6
  #include <cstdint>
6
7
  #include <mutex>
@@ -76,7 +77,7 @@ class FreeRdpSessionTransport final : public SessionTransport {
76
77
  void HookEndPaint(rdpUpdate* update);
77
78
  // Records that at least one paint has reached the local framebuffer and
78
79
  // wakes Connect().
79
- void MarkFramePainted(std::optional<RawFrame> first_frame = std::nullopt);
80
+ void MarkFramePainted();
80
81
  // Records a framebuffer update even when it is too blank or uniform to count
81
82
  // as the first informative frame.
82
83
  void MarkFramebufferUpdated();
@@ -90,6 +91,7 @@ class FreeRdpSessionTransport final : public SessionTransport {
90
91
 
91
92
  private:
92
93
  friend struct MidsceneRdpContext;
94
+ friend struct RdpScreenshotTestPeer;
93
95
 
94
96
  freerdp* instance_ = nullptr;
95
97
  std::thread event_thread_;
@@ -110,8 +112,10 @@ class FreeRdpSessionTransport final : public SessionTransport {
110
112
  std::atomic<bool> session_active_{false};
111
113
  std::condition_variable frame_cv_;
112
114
  std::mutex frame_mutex_;
113
- std::optional<RawFrame> first_frame_;
114
- bool first_frame_consumed_ = false;
115
+ // Protected by frame_mutex_; use a monotonic clock for screenshot settling.
116
+ std::chrono::steady_clock::time_point last_frame_update_{};
117
+ // Protected by mutex_; reset for each new connection.
118
+ bool first_screenshot_pending_ = true;
115
119
  pEndPaint original_end_paint_ = nullptr;
116
120
  uint16_t mouse_x_ = 0;
117
121
  uint16_t mouse_y_ = 0;
@@ -81,12 +81,15 @@ std::vector<uint8_t> BuildImageData(const RawFrame& frame) {
81
81
  const uint8_t blue = frame.bgra[pixel_offset];
82
82
  const uint8_t green = frame.bgra[pixel_offset + 1];
83
83
  const uint8_t red = frame.bgra[pixel_offset + 2];
84
- const uint8_t alpha = frame.bgra[pixel_offset + 3];
85
84
 
86
85
  image_data.push_back(red);
87
86
  image_data.push_back(green);
88
87
  image_data.push_back(blue);
89
- image_data.push_back(alpha);
88
+ // FreeRDP's PIXEL_FORMAT_BGRA32 is an opaque BGRX desktop surface. The
89
+ // fourth byte is padding and may be zero, so treating it as alpha makes
90
+ // otherwise valid screenshots transparent and turns them black during
91
+ // PNG-to-JPEG conversion.
92
+ image_data.push_back(0xFF);
90
93
  }
91
94
  }
92
95