novac 2.2.0 → 2.2.2

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.
Files changed (122) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +0 -0
  3. package/bin/novac +6 -3
  4. package/bin/nvc +0 -0
  5. package/bin/nvml +0 -0
  6. package/demo.nv +0 -0
  7. package/demo_builtins.nv +0 -0
  8. package/demo_http.nv +0 -0
  9. package/examples/bf.nv +5 -13
  10. package/examples/math.nv +2 -2
  11. package/kits/kitffmpeg/kitdef.js +1174 -0
  12. package/kits/libos/kitdef.js +3135 -0
  13. package/kits/libtasker/kitdef.js +125 -0
  14. package/package.json +1 -1
  15. package/scripts/update-bin.js +0 -0
  16. package/src/core/executor.js +7 -4
  17. package/src/core/lexer.js +2 -2
  18. package/src/index.js +0 -0
  19. package/novac/LICENSE +0 -21
  20. package/novac/README.md +0 -1823
  21. package/novac/bin/novac +0 -950
  22. package/novac/bin/nvc +0 -522
  23. package/novac/bin/nvml +0 -542
  24. package/novac/demo.nv +0 -245
  25. package/novac/demo_builtins.nv +0 -209
  26. package/novac/demo_http.nv +0 -62
  27. package/novac/examples/bf.nv +0 -69
  28. package/novac/examples/math.nv +0 -21
  29. package/novac/kits/kitai/kitdef.js +0 -2185
  30. package/novac/kits/kitansi/kitdef.js +0 -1402
  31. package/novac/kits/kitformat/kitdef.js +0 -1485
  32. package/novac/kits/kitgps/kitdef.js +0 -1862
  33. package/novac/kits/kitlibfs/kitdef.js +0 -231
  34. package/novac/kits/kitlibproc/kitdef.js +0 -78
  35. package/novac/kits/kitmatrix/ex.js +0 -19
  36. package/novac/kits/kitmatrix/kitdef.js +0 -960
  37. package/novac/kits/kitmpatch/kitdef.js +0 -906
  38. package/novac/kits/kitnovacweb/README.md +0 -1572
  39. package/novac/kits/kitnovacweb/demo.nv +0 -12
  40. package/novac/kits/kitnovacweb/demo.nvml +0 -71
  41. package/novac/kits/kitnovacweb/index.nova +0 -12
  42. package/novac/kits/kitnovacweb/kitdef.js +0 -692
  43. package/novac/kits/kitnovacweb/nova.kit.json +0 -8
  44. package/novac/kits/kitnovacweb/nvml/executor.js +0 -739
  45. package/novac/kits/kitnovacweb/nvml/index.js +0 -67
  46. package/novac/kits/kitnovacweb/nvml/lexer.js +0 -263
  47. package/novac/kits/kitnovacweb/nvml/parser.js +0 -508
  48. package/novac/kits/kitnovacweb/nvml/renderer.js +0 -924
  49. package/novac/kits/kitparse/kitdef.js +0 -1688
  50. package/novac/kits/kitregex++/kitdef.js +0 -1353
  51. package/novac/kits/kitrequire/kitdef.js +0 -1599
  52. package/novac/kits/kitx11/kitdef.js +0 -1
  53. package/novac/kits/kitx11/kitx11.js +0 -2472
  54. package/novac/kits/kitx11/kitx11_conn.js +0 -948
  55. package/novac/kits/kitx11/kitx11_worker.js +0 -121
  56. package/novac/kits/libtea/tf.js +0 -2691
  57. package/novac/kits/libterm/ex.js +0 -285
  58. package/novac/kits/libterm/kitdef.js +0 -1927
  59. package/novac/node_modules/chalk/license +0 -9
  60. package/novac/node_modules/chalk/package.json +0 -83
  61. package/novac/node_modules/chalk/readme.md +0 -297
  62. package/novac/node_modules/chalk/source/index.d.ts +0 -325
  63. package/novac/node_modules/chalk/source/index.js +0 -225
  64. package/novac/node_modules/chalk/source/utilities.js +0 -33
  65. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +0 -236
  66. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +0 -223
  67. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +0 -1
  68. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +0 -34
  69. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +0 -55
  70. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +0 -190
  71. package/novac/node_modules/commander/LICENSE +0 -22
  72. package/novac/node_modules/commander/Readme.md +0 -1176
  73. package/novac/node_modules/commander/esm.mjs +0 -16
  74. package/novac/node_modules/commander/index.js +0 -24
  75. package/novac/node_modules/commander/lib/argument.js +0 -150
  76. package/novac/node_modules/commander/lib/command.js +0 -2777
  77. package/novac/node_modules/commander/lib/error.js +0 -39
  78. package/novac/node_modules/commander/lib/help.js +0 -747
  79. package/novac/node_modules/commander/lib/option.js +0 -380
  80. package/novac/node_modules/commander/lib/suggestSimilar.js +0 -101
  81. package/novac/node_modules/commander/package-support.json +0 -19
  82. package/novac/node_modules/commander/package.json +0 -82
  83. package/novac/node_modules/commander/typings/esm.d.mts +0 -3
  84. package/novac/node_modules/commander/typings/index.d.ts +0 -1113
  85. package/novac/node_modules/node-addon-api/LICENSE.md +0 -9
  86. package/novac/node_modules/node-addon-api/README.md +0 -95
  87. package/novac/node_modules/node-addon-api/common.gypi +0 -21
  88. package/novac/node_modules/node-addon-api/except.gypi +0 -25
  89. package/novac/node_modules/node-addon-api/index.js +0 -14
  90. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +0 -186
  91. package/novac/node_modules/node-addon-api/napi-inl.h +0 -7165
  92. package/novac/node_modules/node-addon-api/napi.h +0 -3364
  93. package/novac/node_modules/node-addon-api/node_addon_api.gyp +0 -42
  94. package/novac/node_modules/node-addon-api/node_api.gyp +0 -9
  95. package/novac/node_modules/node-addon-api/noexcept.gypi +0 -26
  96. package/novac/node_modules/node-addon-api/nothing.c +0 -0
  97. package/novac/node_modules/node-addon-api/package-support.json +0 -21
  98. package/novac/node_modules/node-addon-api/package.json +0 -480
  99. package/novac/node_modules/node-addon-api/tools/README.md +0 -73
  100. package/novac/node_modules/node-addon-api/tools/check-napi.js +0 -99
  101. package/novac/node_modules/node-addon-api/tools/clang-format.js +0 -71
  102. package/novac/node_modules/node-addon-api/tools/conversion.js +0 -301
  103. package/novac/node_modules/serialize-javascript/LICENSE +0 -27
  104. package/novac/node_modules/serialize-javascript/README.md +0 -149
  105. package/novac/node_modules/serialize-javascript/index.js +0 -297
  106. package/novac/node_modules/serialize-javascript/package.json +0 -33
  107. package/novac/package.json +0 -27
  108. package/novac/scripts/update-bin.js +0 -24
  109. package/novac/src/core/bstd.js +0 -1035
  110. package/novac/src/core/config.js +0 -155
  111. package/novac/src/core/describe.js +0 -187
  112. package/novac/src/core/emitter.js +0 -499
  113. package/novac/src/core/error.js +0 -86
  114. package/novac/src/core/executor.js +0 -5606
  115. package/novac/src/core/formatter.js +0 -686
  116. package/novac/src/core/lexer.js +0 -1026
  117. package/novac/src/core/nova_builtins.js +0 -717
  118. package/novac/src/core/nova_thread_worker.js +0 -166
  119. package/novac/src/core/parser.js +0 -2181
  120. package/novac/src/core/types.js +0 -112
  121. package/novac/src/index.js +0 -28
  122. package/novac/src/runtime/stdlib.js +0 -244
@@ -0,0 +1,3135 @@
1
+ 'use strict';
2
+
3
+ // ============================================================
4
+ // kitos.js — Full OS Integration Kit for Nova
5
+ // 20 categories of native OS interaction, all cross-platform.
6
+ // Platforms: Linux, macOS, Windows, Android (Termux)
7
+ //
8
+ // import "kitos"
9
+ //
10
+ // Categories exported as namespaced objects:
11
+ // Notify, TTS, Dialog, Menu, Keyboard, Mouse, Screen,
12
+ // Audio, Camera, Power, Brightness, Wifi, Bluetooth,
13
+ // Volume, Trash, Open, Autostart, A11y, IdleTime,
14
+ // Sensors, Location, Vibrate
15
+ //
16
+ // module.exports = { kitdef: { Notify, TTS, Dialog, ... } }
17
+ // ============================================================
18
+
19
+ 'use strict';
20
+
21
+ const { execSync, spawnSync, exec, spawn } = require('child_process');
22
+ const os = require('os');
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+
26
+ // ── Platform detection ────────────────────────────────────────────────────────
27
+
28
+ const PLATFORM = (() => {
29
+ const p = os.platform();
30
+ if (p === 'darwin') return 'macos';
31
+ if (p === 'win32') return 'windows';
32
+ if (p === 'linux') {
33
+ // Termux detection: $PREFIX points into /data/data/com.termux
34
+ if (process.env.PREFIX && process.env.PREFIX.includes('com.termux')) return 'android';
35
+ return 'linux';
36
+ }
37
+ return p;
38
+ })();
39
+
40
+ const IS_MACOS = PLATFORM === 'macos';
41
+ const IS_WINDOWS = PLATFORM === 'windows';
42
+ const IS_LINUX = PLATFORM === 'linux';
43
+ const IS_ANDROID = PLATFORM === 'android';
44
+
45
+ // ── Internal helpers ──────────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * Run a command synchronously, return { code, stdout, stderr }.
49
+ * Never throws.
50
+ */
51
+ function _run(cmd, opts = {}) {
52
+ try {
53
+ const stdout = execSync(cmd, {
54
+ encoding: 'utf8',
55
+ timeout: opts.timeout ?? 15_000,
56
+ env: { ...process.env, ...opts.env },
57
+ stdio: ['ignore', 'pipe', 'pipe'],
58
+ ...(opts.cwd ? { cwd: opts.cwd } : {}),
59
+ }).trimEnd();
60
+ return { code: 0, stdout, stderr: '' };
61
+ } catch (e) {
62
+ return {
63
+ code: e.status ?? 1,
64
+ stdout: (e.stdout ?? '').trimEnd(),
65
+ stderr: (e.stderr ?? '').trimEnd(),
66
+ };
67
+ }
68
+ }
69
+
70
+ /** Run and return stdout string only ('' on failure). */
71
+ function _out(cmd, opts = {}) { return _run(cmd, opts).stdout; }
72
+
73
+ /** Spawn and return { code, stdout, stderr }. Never throws. */
74
+ function _spawn(bin, args = [], opts = {}) {
75
+ const r = spawnSync(bin, args, {
76
+ encoding: 'utf8',
77
+ timeout: opts.timeout ?? 15_000,
78
+ env: { ...process.env, ...opts.env },
79
+ ...(opts.cwd ? { cwd: opts.cwd } : {}),
80
+ });
81
+ return {
82
+ code: r.status ?? 1,
83
+ stdout: (r.stdout ?? '').trimEnd(),
84
+ stderr: (r.stderr ?? '').trimEnd(),
85
+ };
86
+ }
87
+
88
+ /** Escape a string for use inside an AppleScript string literal. */
89
+ function _escAS(s) { return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); }
90
+
91
+ /** Escape a string for safe insertion into a shell command. */
92
+ function _esc(s) { return `'${String(s).replace(/'/g, "'\\''")}'`; }
93
+
94
+ /** Run an AppleScript string. Returns stdout. */
95
+ function _osascript(script) {
96
+ return _spawn('osascript', ['-e', script]).stdout;
97
+ }
98
+
99
+ /** Run a PowerShell command string. Returns stdout. */
100
+ function _pwsh(code) {
101
+ return _spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command', code]).stdout;
102
+ }
103
+
104
+ /** Detect which Linux dialog tool is available. */
105
+ function _linuxDialog() {
106
+ for (const t of ['zenity', 'yad', 'kdialog', 'qarma']) {
107
+ if (_run(`which ${t}`).code === 0) return t;
108
+ }
109
+ return null;
110
+ }
111
+
112
+ /** Which Linux notification tool is available. */
113
+ function _linuxNotify() {
114
+ if (_run('which notify-send').code === 0) return 'notify-send';
115
+ if (_run('which dunstify').code === 0) return 'dunstify';
116
+ return null;
117
+ }
118
+
119
+ // ══════════════════════════════════════════════════════════════════════════════
120
+ // 1. NOTIFY — Desktop Notifications
121
+ // ══════════════════════════════════════════════════════════════════════════════
122
+
123
+ const Notify = {
124
+ /**
125
+ * Send a desktop notification.
126
+ * @param {string} title
127
+ * @param {string} [body='']
128
+ * @param {object} [opts]
129
+ * @param {'low'|'normal'|'critical'} [opts.urgency='normal'] Linux only
130
+ * @param {number} [opts.timeout] ms (Linux), ignored elsewhere
131
+ * @param {string} [opts.icon] icon name or path (Linux)
132
+ * @param {string} [opts.sound] sound name (macOS)
133
+ * @param {string} [opts.subtitle] (macOS)
134
+ * @param {string} [opts.id] notification id (Linux)
135
+ */
136
+ send(title, body = '', opts = {}) {
137
+ title = String(title); body = String(body);
138
+
139
+ if (IS_MACOS) {
140
+ const t = _escAS(title);
141
+ const b = _escAS(body);
142
+ const sub = opts.subtitle ? `with subtitle "${_escAS(opts.subtitle)}"` : '';
143
+ const snd = opts.sound ? `sound name "${_escAS(opts.sound)}"` : '';
144
+ _osascript(
145
+ `display notification "${b}" with title "${t}" ${sub} ${snd}`
146
+ );
147
+ return true;
148
+ }
149
+
150
+ if (IS_WINDOWS) {
151
+ // PowerShell toast via BurntToast-free method (WinRT)
152
+ const t = title.replace(/'/g, "''"), b2 = body.replace(/'/g, "''");
153
+ _pwsh(`
154
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
155
+ [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
156
+ $xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
157
+ $xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${t}')) | Out-Null
158
+ $xml.GetElementsByTagName('text')[1].AppendChild($xml.CreateTextNode('${b2}')) | Out-Null
159
+ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
160
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Nova').Show($toast)
161
+ `);
162
+ return true;
163
+ }
164
+
165
+ if (IS_ANDROID) {
166
+ const args = ['termux-notification', '--title', title, '--content', body];
167
+ if (opts.id) args.push('--id', String(opts.id));
168
+ if (opts.icon) args.push('--icon', opts.icon);
169
+ _spawn(args[0], args.slice(1));
170
+ return true;
171
+ }
172
+
173
+ // Linux
174
+ const tool = _linuxNotify();
175
+ if (!tool) return false;
176
+ const args = [title, body];
177
+ if (opts.urgency) args.unshift('--urgency=' + opts.urgency);
178
+ if (opts.timeout) args.unshift('--expire-time=' + opts.timeout);
179
+ if (opts.icon) args.unshift('--icon=' + opts.icon);
180
+ if (opts.id) args.unshift('--replace-id=' + opts.id);
181
+ _spawn(tool, args);
182
+ return true;
183
+ },
184
+
185
+ /** Remove a notification by id (Linux / Android only). */
186
+ remove(id) {
187
+ if (IS_ANDROID) { _spawn('termux-notification-remove', [String(id)]); return; }
188
+ if (IS_LINUX) { _run(`notify-send --close=${id}`); }
189
+ },
190
+
191
+ /** List active notifications (Android only). */
192
+ list() {
193
+ if (IS_ANDROID) return JSON.parse(_out('termux-notification-list') || '[]');
194
+ return [];
195
+ },
196
+
197
+ platform: PLATFORM,
198
+ };
199
+
200
+ // ══════════════════════════════════════════════════════════════════════════════
201
+ // 2. TTS — Text-to-Speech
202
+ // ══════════════════════════════════════════════════════════════════════════════
203
+
204
+ const TTS = {
205
+ /**
206
+ * Speak text aloud (synchronous by default).
207
+ * @param {string} text
208
+ * @param {object} [opts]
209
+ * @param {string} [opts.voice] voice name
210
+ * @param {number} [opts.rate] words-per-minute or rate (platform-specific)
211
+ * @param {number} [opts.pitch] pitch adjustment (Linux espeak)
212
+ * @param {boolean} [opts.async] if true, don't wait
213
+ */
214
+ speak(text, opts = {}) {
215
+ text = String(text);
216
+
217
+ if (IS_MACOS) {
218
+ const args = [text];
219
+ if (opts.voice) args.push('-v', opts.voice);
220
+ if (opts.rate) args.push('-r', String(opts.rate));
221
+ opts.async ? spawn('say', args, { detached: true, stdio: 'ignore' }).unref()
222
+ : _spawn('say', args);
223
+ return;
224
+ }
225
+
226
+ if (IS_WINDOWS) {
227
+ const rate = opts.rate ? `$s.Rate=${opts.rate};` : '';
228
+ const voice = opts.voice ? `$s.SelectVoice('${opts.voice.replace(/'/g, "''")}');` : '';
229
+ const cmd = `Add-Type -AssemblyName System.speech; $s=New-Object System.Speech.Synthesis.SpeechSynthesizer; ${voice}${rate}$s.Speak('${text.replace(/'/g, "''")}')`;
230
+ opts.async ? spawn('powershell', ['-NoProfile', '-Command', cmd], { detached: true, stdio: 'ignore' }).unref()
231
+ : _pwsh(cmd);
232
+ return;
233
+ }
234
+
235
+ if (IS_ANDROID) {
236
+ const args = ['termux-tts-speak'];
237
+ if (opts.rate) args.push('-r', String(opts.rate));
238
+ if (opts.pitch) args.push('-p', String(opts.pitch));
239
+ if (opts.voice) args.push('-l', opts.voice);
240
+ args.push(text);
241
+ opts.async ? spawn(args[0], args.slice(1), { detached: true, stdio: 'ignore' }).unref()
242
+ : _spawn(args[0], args.slice(1));
243
+ return;
244
+ }
245
+
246
+ // Linux — try espeak, espeak-ng, festival, spd-say
247
+ if (_run('which espeak-ng').code === 0) {
248
+ const args = [text];
249
+ if (opts.voice) args.push('-v', opts.voice);
250
+ if (opts.rate) args.push('-s', String(opts.rate));
251
+ if (opts.pitch) args.push('-p', String(opts.pitch));
252
+ opts.async ? spawn('espeak-ng', args, { detached: true, stdio: 'ignore' }).unref()
253
+ : _spawn('espeak-ng', args);
254
+ } else if (_run('which espeak').code === 0) {
255
+ const args = [text];
256
+ if (opts.voice) args.push('-v', opts.voice);
257
+ if (opts.rate) args.push('-s', String(opts.rate));
258
+ opts.async ? spawn('espeak', args, { detached: true, stdio: 'ignore' }).unref()
259
+ : _spawn('espeak', args);
260
+ } else if (_run('which spd-say').code === 0) {
261
+ _spawn('spd-say', [text]);
262
+ } else if (_run('which festival').code === 0) {
263
+ const r = spawnSync('festival', ['--pipe'], { input: `(SayText "${text.replace(/"/g, '\\"')}")`, encoding: 'utf8' });
264
+ }
265
+ },
266
+
267
+ /** List available voices. */
268
+ voices() {
269
+ if (IS_MACOS) {
270
+ return _out('say -v ?').split('\n')
271
+ .filter(Boolean)
272
+ .map(l => { const m = l.match(/^(\S+)\s+(\S+)/); return m ? { name: m[1], lang: m[2] } : null; })
273
+ .filter(Boolean);
274
+ }
275
+ if (IS_WINDOWS) {
276
+ return _pwsh('Add-Type -AssemblyName System.speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).GetInstalledVoices() | ForEach-Object { $_.VoiceInfo.Name }')
277
+ .split('\n').map(s => s.trim()).filter(Boolean).map(n => ({ name: n }));
278
+ }
279
+ if (IS_ANDROID) {
280
+ return []; // Termux TTS uses Android TTS engine
281
+ }
282
+ // espeak-ng
283
+ return _out('espeak-ng --voices').split('\n').slice(1)
284
+ .filter(Boolean)
285
+ .map(l => { const p = l.trim().split(/\s+/); return { name: p[4], lang: p[1] }; });
286
+ },
287
+
288
+ /** Stop any ongoing speech (best-effort). */
289
+ stop() {
290
+ if (IS_MACOS) _run('killall say 2>/dev/null');
291
+ if (IS_WINDOWS) _pwsh('Add-Type -AssemblyName System.speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).SpeakAsyncCancelAll()');
292
+ if (IS_ANDROID) _spawn('termux-tts-stop');
293
+ if (IS_LINUX) _run('killall espeak espeak-ng spd-say 2>/dev/null');
294
+ },
295
+ };
296
+
297
+ // ══════════════════════════════════════════════════════════════════════════════
298
+ // 3. DIALOG — Native OS Dialogs
299
+ // ══════════════════════════════════════════════════════════════════════════════
300
+
301
+ const Dialog = {
302
+ /**
303
+ * Show an alert dialog.
304
+ * @param {string} message
305
+ * @param {string} [title='Alert']
306
+ */
307
+ alert(message, title = 'Alert') {
308
+ message = String(message); title = String(title);
309
+ if (IS_MACOS) {
310
+ _osascript(`display alert "${_escAS(title)}" message "${_escAS(message)}"`);
311
+ } else if (IS_WINDOWS) {
312
+ _pwsh(`[System.Windows.Forms.MessageBox]::Show('${message.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}') | Out-Null`);
313
+ } else if (IS_ANDROID) {
314
+ _spawn('termux-dialog', ['confirm', '-t', title, '-i', message]);
315
+ } else {
316
+ const t = _linuxDialog();
317
+ if (t === 'zenity') _spawn('zenity', ['--info', `--title=${title}`, `--text=${message}`]);
318
+ else if (t === 'kdialog') _spawn('kdialog', ['--msgbox', message, '--title', title]);
319
+ else if (t === 'yad') _spawn('yad', ['--info', `--title=${title}`, `--text=${message}`]);
320
+ }
321
+ },
322
+
323
+ /**
324
+ * Show a confirm (yes/no) dialog.
325
+ * @returns {boolean}
326
+ */
327
+ confirm(message, title = 'Confirm') {
328
+ message = String(message); title = String(title);
329
+ if (IS_MACOS) {
330
+ const r = _osascript(`display alert "${_escAS(title)}" message "${_escAS(message)}" buttons {"Cancel","OK"} default button "OK"`);
331
+ return r.includes('OK');
332
+ }
333
+ if (IS_WINDOWS) {
334
+ const r = _pwsh(`[System.Windows.Forms.MessageBox]::Show('${message.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}', 'YesNo')`);
335
+ return r.trim() === 'Yes';
336
+ }
337
+ if (IS_ANDROID) {
338
+ const r = _spawn('termux-dialog', ['confirm', '-t', title, '-i', message]);
339
+ try { return JSON.parse(r.stdout).text === 'yes'; } catch { return false; }
340
+ }
341
+ const t = _linuxDialog();
342
+ if (t === 'zenity') return _spawn('zenity', ['--question', `--title=${title}`, `--text=${message}`]).code === 0;
343
+ if (t === 'kdialog') return _spawn('kdialog', ['--yesno', message, '--title', title]).code === 0;
344
+ if (t === 'yad') return _spawn('yad', ['--question', `--title=${title}`, `--text=${message}`]).code === 0;
345
+ return false;
346
+ },
347
+
348
+ /**
349
+ * Show a text prompt dialog.
350
+ * @param {string} message
351
+ * @param {string} [defaultValue='']
352
+ * @returns {string|null} null if cancelled
353
+ */
354
+ prompt(message, defaultValue = '', title = 'Input') {
355
+ message = String(message); defaultValue = String(defaultValue);
356
+ if (IS_MACOS) {
357
+ const r = _osascript(
358
+ `set result to display dialog "${_escAS(message)}" default answer "${_escAS(defaultValue)}" with title "${_escAS(title)}" buttons {"Cancel","OK"} default button "OK"\n` +
359
+ `return text returned of result`
360
+ );
361
+ return r || null;
362
+ }
363
+ if (IS_WINDOWS) {
364
+ const r = _pwsh(
365
+ `Add-Type -AssemblyName Microsoft.VisualBasic; ` +
366
+ `[Microsoft.VisualBasic.Interaction]::InputBox('${message.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}', '${defaultValue.replace(/'/g, "''")}')`
367
+ );
368
+ return r.trim() === '' ? null : r.trim();
369
+ }
370
+ if (IS_ANDROID) {
371
+ const r = _spawn('termux-dialog', ['text', '-t', title, '-i', message, '--hint', defaultValue]);
372
+ try { const j = JSON.parse(r.stdout); return j.text ?? null; } catch { return null; }
373
+ }
374
+ const t = _linuxDialog();
375
+ if (t === 'zenity') {
376
+ const r = _spawn('zenity', ['--entry', `--title=${title}`, `--text=${message}`, `--entry-text=${defaultValue}`]);
377
+ return r.code === 0 ? r.stdout.trim() : null;
378
+ }
379
+ if (t === 'kdialog') {
380
+ const r = _spawn('kdialog', ['--inputbox', message, defaultValue, '--title', title]);
381
+ return r.code === 0 ? r.stdout.trim() : null;
382
+ }
383
+ if (t === 'yad') {
384
+ const r = _spawn('yad', ['--entry', `--title=${title}`, `--text=${message}`, `--entry-text=${defaultValue}`]);
385
+ return r.code === 0 ? r.stdout.trim() : null;
386
+ }
387
+ return null;
388
+ },
389
+
390
+ /**
391
+ * Show a native file picker dialog.
392
+ * @param {object} [opts]
393
+ * @param {boolean} [opts.multiple=false]
394
+ * @param {string} [opts.initialDir]
395
+ * @param {string} [opts.filter] e.g. '*.txt'
396
+ * @param {string} [opts.title]
397
+ * @returns {string|string[]|null}
398
+ */
399
+ filePicker(opts = {}) {
400
+ const title = opts.title ?? 'Select File';
401
+ const initDir= opts.initialDir ?? os.homedir();
402
+ const multi = opts.multiple ?? false;
403
+
404
+ if (IS_MACOS) {
405
+ const multiClause = multi ? 'with multiple selections allowed' : '';
406
+ const r = _osascript(
407
+ `set f to choose file with prompt "${_escAS(title)}" default location "${_escAS(initDir)}" ${multiClause}\n` +
408
+ `POSIX path of f`
409
+ );
410
+ if (!r) return null;
411
+ return multi ? r.split('\n').filter(Boolean) : r.trim();
412
+ }
413
+ if (IS_WINDOWS) {
414
+ const code = `
415
+ Add-Type -AssemblyName System.Windows.Forms
416
+ $d = New-Object System.Windows.Forms.OpenFileDialog
417
+ $d.Title = '${title.replace(/'/g, "''")}'; $d.InitialDirectory = '${initDir.replace(/'/g, "''")}'
418
+ ${multi ? "$d.Multiselect = $true" : ''}
419
+ if ($d.ShowDialog() -eq 'OK') { $d.FileNames -join [char]10 } else { '' }
420
+ `;
421
+ const r = _pwsh(code).trim();
422
+ if (!r) return null;
423
+ const files = r.split('\n').map(s => s.trim()).filter(Boolean);
424
+ return multi ? files : files[0];
425
+ }
426
+ if (IS_ANDROID) {
427
+ const r = _spawn('termux-storage-get', ['/tmp/kitos_file_pick.tmp']);
428
+ return r.code === 0 ? '/tmp/kitos_file_pick.tmp' : null;
429
+ }
430
+ const t = _linuxDialog();
431
+ if (t === 'zenity') {
432
+ const args = ['--file-selection', `--title=${title}`, `--filename=${initDir}/`];
433
+ if (multi) args.push('--multiple', '--separator=\n');
434
+ const r = _spawn('zenity', args);
435
+ if (r.code !== 0) return null;
436
+ const files = r.stdout.split('\n').filter(Boolean);
437
+ return multi ? files : files[0];
438
+ }
439
+ if (t === 'kdialog') {
440
+ const r = _spawn('kdialog', ['--getopenfilename', initDir, opts.filter ?? '*', '--title', title]);
441
+ return r.code === 0 ? r.stdout.trim() : null;
442
+ }
443
+ return null;
444
+ },
445
+
446
+ /**
447
+ * Show a folder/directory picker.
448
+ * @returns {string|null}
449
+ */
450
+ folderPicker(opts = {}) {
451
+ const title = opts.title ?? 'Select Folder';
452
+ const initDir= opts.initialDir ?? os.homedir();
453
+
454
+ if (IS_MACOS) {
455
+ const r = _osascript(`POSIX path of (choose folder with prompt "${_escAS(title)}" default location "${_escAS(initDir)}")`);
456
+ return r ? r.trim() : null;
457
+ }
458
+ if (IS_WINDOWS) {
459
+ const r = _pwsh(`
460
+ Add-Type -AssemblyName System.Windows.Forms
461
+ $d = New-Object System.Windows.Forms.FolderBrowserDialog
462
+ $d.Description = '${title.replace(/'/g, "''")}'; $d.SelectedPath = '${initDir.replace(/'/g, "''")}'
463
+ if ($d.ShowDialog() -eq 'OK') { $d.SelectedPath } else { '' }
464
+ `).trim();
465
+ return r || null;
466
+ }
467
+ const t = _linuxDialog();
468
+ if (t === 'zenity') {
469
+ const r = _spawn('zenity', ['--file-selection', '--directory', `--title=${title}`, `--filename=${initDir}/`]);
470
+ return r.code === 0 ? r.stdout.trim() : null;
471
+ }
472
+ if (t === 'kdialog') {
473
+ const r = _spawn('kdialog', ['--getexistingdirectory', initDir, '--title', title]);
474
+ return r.code === 0 ? r.stdout.trim() : null;
475
+ }
476
+ return null;
477
+ },
478
+
479
+ /**
480
+ * Show a color picker dialog.
481
+ * @returns {{ hex: string, r: number, g: number, b: number }|null}
482
+ */
483
+ colorPicker(opts = {}) {
484
+ const initial = opts.color ?? '#ffffff';
485
+ if (IS_MACOS) {
486
+ // AppleScript doesn't have a native color picker, use choose color
487
+ const r = _osascript(`set c to choose color default color {65535, 65535, 65535}\nreturn c`);
488
+ // Returns e.g. "{65535, 32768, 0}"
489
+ const m = r.match(/(\d+),\s*(\d+),\s*(\d+)/);
490
+ if (!m) return null;
491
+ const [rv, gv, bv] = [Math.round(+m[1]/257), Math.round(+m[2]/257), Math.round(+m[3]/257)];
492
+ const hex = '#' + [rv, gv, bv].map(v => v.toString(16).padStart(2,'0')).join('');
493
+ return { hex, r: rv, g: gv, b: bv };
494
+ }
495
+ if (IS_WINDOWS) {
496
+ const r = _pwsh(`
497
+ Add-Type -AssemblyName System.Windows.Forms
498
+ $d = New-Object System.Windows.Forms.ColorDialog
499
+ if ($d.ShowDialog() -eq 'OK') { "$($d.Color.R) $($d.Color.G) $($d.Color.B)" } else { '' }
500
+ `).trim();
501
+ if (!r) return null;
502
+ const [rv, gv, bv] = r.split(' ').map(Number);
503
+ const hex = '#' + [rv, gv, bv].map(v => v.toString(16).padStart(2,'0')).join('');
504
+ return { hex, r: rv, g: gv, b: bv };
505
+ }
506
+ const t = _linuxDialog();
507
+ if (t === 'zenity') {
508
+ const r = _spawn('zenity', ['--color-selection', `--color=${initial}`]);
509
+ if (r.code !== 0) return null;
510
+ const s = r.stdout.trim(); // "rgb(r,g,b)" or "#RRGGBB"
511
+ const m = s.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/) ?? s.match(/#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i);
512
+ if (!m) return null;
513
+ const [rv, gv, bv] = s.startsWith('rgb') ? [+m[1], +m[2], +m[3]] : [parseInt(m[1],16), parseInt(m[2],16), parseInt(m[3],16)];
514
+ const hex = '#' + [rv, gv, bv].map(v => v.toString(16).padStart(2,'0')).join('');
515
+ return { hex, r: rv, g: gv, b: bv };
516
+ }
517
+ if (t === 'kdialog') {
518
+ const r = _spawn('kdialog', ['--getcolor', '--default', initial]);
519
+ if (r.code !== 0) return null;
520
+ const hex = r.stdout.trim();
521
+ const rv = parseInt(hex.slice(1,3),16), gv = parseInt(hex.slice(3,5),16), bv = parseInt(hex.slice(5,7),16);
522
+ return { hex, r: rv, g: gv, b: bv };
523
+ }
524
+ return null;
525
+ },
526
+
527
+ /**
528
+ * Show a date picker dialog.
529
+ * @returns {string|null} ISO date string 'YYYY-MM-DD'
530
+ */
531
+ datePicker(opts = {}) {
532
+ const title = opts.title ?? 'Select Date';
533
+ if (IS_ANDROID) {
534
+ const r = _spawn('termux-dialog', ['datepicker', '-t', title]);
535
+ try { const j = JSON.parse(r.stdout); return j.text ?? null; } catch { return null; }
536
+ }
537
+ const t = _linuxDialog();
538
+ if (t === 'yad') {
539
+ const r = _spawn('yad', ['--calendar', `--title=${title}`, '--date-format=%Y-%m-%d']);
540
+ return r.code === 0 ? r.stdout.trim() : null;
541
+ }
542
+ if (t === 'zenity') {
543
+ const r = _spawn('zenity', ['--calendar', `--title=${title}`, '--date-format=%Y-%m-%d']);
544
+ return r.code === 0 ? r.stdout.trim() : null;
545
+ }
546
+ if (IS_MACOS) {
547
+ // Fallback: prompt
548
+ return Dialog.prompt('Enter date (YYYY-MM-DD)', new Date().toISOString().slice(0,10), title);
549
+ }
550
+ if (IS_WINDOWS) {
551
+ return Dialog.prompt('Enter date (YYYY-MM-DD)', new Date().toISOString().slice(0,10), title);
552
+ }
553
+ return null;
554
+ },
555
+
556
+ /**
557
+ * Show a list selection dialog.
558
+ * @param {string[]} items
559
+ * @param {object} [opts]
560
+ * @param {boolean} [opts.multiple=false]
561
+ * @returns {string|string[]|null}
562
+ */
563
+ selectList(items, opts = {}) {
564
+ const title = opts.title ?? 'Select';
565
+ const prompt = opts.prompt ?? 'Choose an item:';
566
+ const multi = opts.multiple ?? false;
567
+
568
+ if (IS_MACOS) {
569
+ const list = items.map(i => `"${_escAS(i)}"`).join(', ');
570
+ const r = _osascript(
571
+ `choose from list {${list}} with title "${_escAS(title)}" with prompt "${_escAS(prompt)}"` +
572
+ (multi ? ' with multiple selections allowed' : '')
573
+ );
574
+ if (!r || r === 'false') return null;
575
+ const selected = r.split(', ');
576
+ return multi ? selected : selected[0];
577
+ }
578
+ if (IS_WINDOWS) {
579
+ const list = items.map(i => `'${i.replace(/'/g, "''")}'`).join(',');
580
+ const r = _pwsh(`
581
+ Add-Type -AssemblyName System.Windows.Forms
582
+ $f = New-Object System.Windows.Forms.Form; $f.Text = '${title.replace(/'/g, "''")}'; $f.Width = 400; $f.Height = 500; $f.StartPosition = 'CenterScreen'
583
+ $lb = New-Object System.Windows.Forms.ListBox; $lb.Dock = 'Fill'; $lb.SelectionMode = '${multi ? 'MultiSimple' : 'One'}'
584
+ @(${list}) | ForEach-Object { $lb.Items.Add($_) | Out-Null }
585
+ $ok = New-Object System.Windows.Forms.Button; $ok.Text = 'OK'; $ok.DialogResult = 'OK'; $ok.Dock = 'Bottom'
586
+ $f.Controls.AddRange(@($lb, $ok)); $f.AcceptButton = $ok
587
+ if ($f.ShowDialog() -eq 'OK') { $lb.SelectedItems -join '\\n' } else { '' }
588
+ `).trim();
589
+ if (!r) return null;
590
+ const selected = r.split('\\n').filter(Boolean);
591
+ return multi ? selected : selected[0];
592
+ }
593
+ if (IS_ANDROID) {
594
+ const r = _spawn('termux-dialog', ['radio', '-t', title, '-v', items.join(',')]);
595
+ try { const j = JSON.parse(r.stdout); return j.text ?? null; } catch { return null; }
596
+ }
597
+ const t = _linuxDialog();
598
+ if (t === 'zenity') {
599
+ const colArgs = items.flatMap(i => [multi ? 'TRUE' : 'FALSE', i]);
600
+ const r = _spawn('zenity', ['--list', `--title=${title}`, `--text=${prompt}`,
601
+ '--column=Select', '--column=Item', '--checklist', ...colArgs]);
602
+ if (r.code !== 0) return null;
603
+ const sel = r.stdout.trim().split('|').filter(Boolean);
604
+ return multi ? sel : sel[0] ?? null;
605
+ }
606
+ if (t === 'kdialog') {
607
+ const itemArgs = items.flatMap(i => [i, i, 'off']);
608
+ const r = _spawn('kdialog', [multi ? '--checklist' : '--menu', prompt, '--title', title, ...itemArgs]);
609
+ if (r.code !== 0) return null;
610
+ const sel = r.stdout.trim().split('\n').filter(Boolean);
611
+ return multi ? sel : sel[0] ?? null;
612
+ }
613
+ return null;
614
+ },
615
+
616
+ /** Show a progress bar (returns a handle with .update(n, text) and .close()). */
617
+ progress(title = 'Loading...', total = 100) {
618
+ if (IS_MACOS) {
619
+ // Display a progress bar via AppleScript (display dialog workaround)
620
+ let current = 0;
621
+ const close = () => {};
622
+ const update = (n, text) => { current = n; };
623
+ return { update, close, get value() { return current; } };
624
+ }
625
+ if (IS_LINUX || IS_ANDROID) {
626
+ const t = _linuxDialog();
627
+ if (t === 'zenity') {
628
+ const child = spawn('zenity', ['--progress', `--title=${title}`, '--percentage=0', '--auto-close'], { stdio: ['pipe', 'ignore', 'ignore'] });
629
+ return {
630
+ update(n, text = '') { child.stdin.write(`${Math.round(n)}\\n# ${text}\\n`); },
631
+ close() { child.stdin.end(); child.kill(); },
632
+ };
633
+ }
634
+ }
635
+ return { update() {}, close() {} };
636
+ },
637
+ };
638
+
639
+ // ══════════════════════════════════════════════════════════════════════════════
640
+ // 4. MENU — System Tray / Menu Bar / Context Menu
641
+ // ══════════════════════════════════════════════════════════════════════════════
642
+
643
+ const Menu = {
644
+ /**
645
+ * Show a context/popup menu and return selected item.
646
+ * On platforms without native menus, falls back to Dialog.selectList.
647
+ * @param {string[]} items
648
+ * @param {object} [opts]
649
+ * @returns {string|null}
650
+ */
651
+ context(items, opts = {}) {
652
+ const title = opts.title ?? 'Menu';
653
+ if (IS_MACOS) {
654
+ const list = items.map(i => `"${_escAS(i)}"`).join(', ');
655
+ const r = _osascript(`choose from list {${list}} with title "${_escAS(title)}"`);
656
+ return (!r || r === 'false') ? null : r.trim();
657
+ }
658
+ if (IS_LINUX) {
659
+ const t = _linuxDialog();
660
+ if (t === 'zenity') {
661
+ const args = ['--list', `--title=${title}`, '--column=Item', ...items];
662
+ const r = _spawn('zenity', args);
663
+ return r.code === 0 ? r.stdout.trim() : null;
664
+ }
665
+ if (t === 'yad') {
666
+ const args = ['--list', `--title=${title}`, '--column=Item', ...items, '--no-headers'];
667
+ const r = _spawn('yad', args);
668
+ return r.code === 0 ? r.stdout.trim().replace(/\|$/, '') : null;
669
+ }
670
+ }
671
+ return Dialog.selectList(items, opts);
672
+ },
673
+
674
+ /**
675
+ * Spawn a persistent system tray icon (Linux only via yad --notification).
676
+ * Returns a handle with .update(tooltip) and .destroy().
677
+ */
678
+ tray(icon, tooltip = '', menuItems = []) {
679
+ if (IS_LINUX && _run('which yad').code === 0) {
680
+ const menu = menuItems.map(m => `${m.label}!${m.cmd ?? 'echo ' + m.label}`).join('!');
681
+ const args = ['--notification', `--image=${icon}`, `--text=${tooltip}`];
682
+ if (menu) args.push(`--menu=${menu}`);
683
+ const child = spawn('yad', args, { detached: true, stdio: 'ignore' });
684
+ child.unref();
685
+ return {
686
+ update(tip) { /* yad --notification doesn't support live update easily */ },
687
+ destroy() { child.kill(); },
688
+ };
689
+ }
690
+ if (IS_MACOS) {
691
+ // BitBar/xbar style — can only open/write to plugin directory
692
+ return { update() {}, destroy() {} };
693
+ }
694
+ return { update() {}, destroy() {} };
695
+ },
696
+ };
697
+
698
+ // ══════════════════════════════════════════════════════════════════════════════
699
+ // 5. KEYBOARD — Simulate Keystrokes / Hotkeys
700
+ // ══════════════════════════════════════════════════════════════════════════════
701
+
702
+ const Keyboard = {
703
+ /**
704
+ * Type a string of text.
705
+ * @param {string} text
706
+ */
707
+ type(text) {
708
+ text = String(text);
709
+ if (IS_MACOS) {
710
+ _osascript(`tell application "System Events" to keystroke "${_escAS(text)}"`);
711
+ } else if (IS_WINDOWS) {
712
+ _pwsh(`
713
+ Add-Type -AssemblyName System.Windows.Forms
714
+ [System.Windows.Forms.SendKeys]::SendWait('${text.replace(/[+^%~(){}]/g, '{$&}').replace(/'/g, "''")}')
715
+ `);
716
+ } else if (IS_LINUX) {
717
+ if (_run('which xdotool').code === 0) _spawn('xdotool', ['type', '--clearmodifiers', text]);
718
+ } else if (IS_ANDROID) {
719
+ _spawn('termux-clipboard-set', [text]);
720
+ // Cannot type directly without accessibility service
721
+ }
722
+ },
723
+
724
+ /**
725
+ * Press a key or key combination.
726
+ * @param {string} key e.g. 'Return', 'ctrl+c', 'cmd+shift+4', 'F5', 'Escape'
727
+ */
728
+ press(key) {
729
+ key = String(key);
730
+ if (IS_MACOS) {
731
+ // Map common names to AppleScript key codes / keystroke format
732
+ const AS_KEYS = { Return: 'return', Escape: 'escape', Delete: 'delete', Tab: 'tab', Space: 'space',
733
+ F1:'f1',F2:'f2',F3:'f3',F4:'f4',F5:'f5',F6:'f6',F7:'f7',F8:'f8',F9:'f9',F10:'f10',F11:'f11',F12:'f12' };
734
+ const parts = key.toLowerCase().split('+');
735
+ const main = parts[parts.length - 1];
736
+ const mods = parts.slice(0, -1);
737
+ const modStr = mods.map(m => ({
738
+ ctrl: 'control down', cmd: 'command down', shift: 'shift down', alt: 'option down', option: 'option down',
739
+ }[m] ?? '')).filter(Boolean).join(', ');
740
+ const asKey = AS_KEYS[parts[parts.length - 1]] ?? main;
741
+ _osascript(`tell application "System Events" to keystroke "${asKey}" using {${modStr}}`);
742
+ } else if (IS_WINDOWS) {
743
+ const WIN_KEYS = { ctrl: '^', shift: '+', alt: '%', Return: '~', Escape: '{ESC}', Tab: '{TAB}',
744
+ Delete: '{DELETE}', BackSpace: '{BACKSPACE}', Up: '{UP}', Down: '{DOWN}', Left: '{LEFT}', Right: '{RIGHT}' };
745
+ let combo = key;
746
+ for (const [k, v] of Object.entries(WIN_KEYS)) combo = combo.replace(new RegExp(k, 'gi'), v);
747
+ _pwsh(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${combo.replace(/'/g, "''")}')`);
748
+ } else if (IS_LINUX) {
749
+ if (_run('which xdotool').code === 0) _spawn('xdotool', ['key', '--clearmodifiers', key.replace(/cmd/gi, 'super')]);
750
+ }
751
+ },
752
+
753
+ /**
754
+ * Hold a key down, run fn, then release (macOS / Linux xdotool).
755
+ * @param {string} key
756
+ * @param {Function} fn
757
+ */
758
+ hold(key, fn) {
759
+ if (IS_LINUX && _run('which xdotool').code === 0) {
760
+ _spawn('xdotool', ['keydown', key]);
761
+ try { fn(); } finally { _spawn('xdotool', ['keyup', key]); }
762
+ } else {
763
+ fn();
764
+ }
765
+ },
766
+
767
+ /** Get the clipboard text content. */
768
+ getClipboard() {
769
+ if (IS_MACOS) return _out('pbpaste');
770
+ if (IS_WINDOWS) return _pwsh('Get-Clipboard').trim();
771
+ if (IS_ANDROID) return _out('termux-clipboard-get');
772
+ if (IS_LINUX) {
773
+ if (_run('which xclip').code === 0) return _out('xclip -selection clipboard -o');
774
+ if (_run('which xsel').code === 0) return _out('xsel --clipboard --output');
775
+ if (_run('which wl-paste').code === 0) return _out('wl-paste');
776
+ }
777
+ return '';
778
+ },
779
+
780
+ /** Set clipboard text content. */
781
+ setClipboard(text) {
782
+ text = String(text);
783
+ if (IS_MACOS) { const r = spawnSync('pbcopy', [], { input: text, encoding: 'utf8' }); return; }
784
+ if (IS_WINDOWS) { _pwsh(`Set-Clipboard '${text.replace(/'/g, "''")}'`); return; }
785
+ if (IS_ANDROID) { _spawn('termux-clipboard-set', [text]); return; }
786
+ if (IS_LINUX) {
787
+ if (_run('which xclip').code === 0) { spawnSync('xclip', ['-selection', 'clipboard'], { input: text, encoding: 'utf8' }); return; }
788
+ if (_run('which xsel').code === 0) { spawnSync('xsel', ['--clipboard', '--input'], { input: text, encoding: 'utf8' }); return; }
789
+ if (_run('which wl-copy').code === 0) { spawnSync('wl-copy', [], { input: text, encoding: 'utf8' }); }
790
+ }
791
+ },
792
+ };
793
+
794
+ // ══════════════════════════════════════════════════════════════════════════════
795
+ // 6. MOUSE — Cursor Control
796
+ // ══════════════════════════════════════════════════════════════════════════════
797
+
798
+ const Mouse = {
799
+ /**
800
+ * Move the cursor to absolute screen coordinates.
801
+ * @param {number} x
802
+ * @param {number} y
803
+ */
804
+ move(x, y) {
805
+ x = Math.round(x); y = Math.round(y);
806
+ if (IS_MACOS) _spawn('cliclick', [`m:${x},${y}`]);
807
+ else if (IS_WINDOWS) _pwsh(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${x}, ${y})`);
808
+ else if (IS_LINUX && _run('which xdotool').code === 0) _spawn('xdotool', ['mousemove', String(x), String(y)]);
809
+ },
810
+
811
+ /**
812
+ * Click at current position or given coordinates.
813
+ * @param {number} [x]
814
+ * @param {number} [y]
815
+ * @param {'left'|'right'|'middle'} [button='left']
816
+ */
817
+ click(x, y, button = 'left') {
818
+ const btn = { left: 1, middle: 2, right: 3 };
819
+ if (x !== undefined) Mouse.move(x, y);
820
+ if (IS_MACOS) {
821
+ const action = { left: 'c', right: 'rc', middle: 'mc' }[button] ?? 'c';
822
+ if (x !== undefined) _spawn('cliclick', [`${action}:${Math.round(x)},${Math.round(y)}`]);
823
+ else _spawn('cliclick', [action]);
824
+ } else if (IS_WINDOWS) {
825
+ const btnNum = btn[button] ?? 1;
826
+ _pwsh(`
827
+ Add-Type -AssemblyName System.Windows.Forms
828
+ $pos = [System.Windows.Forms.Cursor]::Position
829
+ Add-Type @'
830
+ using System; using System.Runtime.InteropServices;
831
+ public class Mouse { [DllImport("user32.dll")] public static extern void mouse_event(int f, int x, int y, int d, int e); }
832
+ '@
833
+ [Mouse]::mouse_event(${btnNum === 1 ? 2 : btnNum === 2 ? 8 : 32}, 0, 0, 0, 0)
834
+ [Mouse]::mouse_event(${btnNum === 1 ? 4 : btnNum === 2 ? 16 : 64}, 0, 0, 0, 0)
835
+ `);
836
+ } else if (IS_LINUX && _run('which xdotool').code === 0) {
837
+ _spawn('xdotool', ['click', String(btn[button] ?? 1)]);
838
+ }
839
+ },
840
+
841
+ /** Double-click. */
842
+ doubleClick(x, y) {
843
+ if (IS_MACOS) {
844
+ if (x !== undefined) _spawn('cliclick', [`dc:${Math.round(x)},${Math.round(y)}`]);
845
+ else _spawn('cliclick', ['dc']);
846
+ } else {
847
+ Mouse.click(x, y); Mouse.click(x, y);
848
+ }
849
+ },
850
+
851
+ /** Right-click. */
852
+ rightClick(x, y) { Mouse.click(x, y, 'right'); },
853
+
854
+ /** Scroll up or down. */
855
+ scroll(direction = 'down', amount = 3) {
856
+ if (IS_MACOS) _spawn('cliclick', [`kd:${direction === 'down' ? 'page-down' : 'page-up'}`]);
857
+ else if (IS_LINUX && _run('which xdotool').code === 0) {
858
+ const btn = direction === 'down' ? 5 : 4;
859
+ for (let i = 0; i < amount; i++) _spawn('xdotool', ['click', String(btn)]);
860
+ }
861
+ },
862
+
863
+ /** Get current cursor position. */
864
+ position() {
865
+ if (IS_MACOS) {
866
+ const r = _osascript('tell application "System Events" to return position of mouse cursor');
867
+ const m = r.match(/(\d+),\s*(\d+)/);
868
+ return m ? { x: +m[1], y: +m[2] } : { x: 0, y: 0 };
869
+ }
870
+ if (IS_WINDOWS) {
871
+ const r = _pwsh(`$p=[System.Windows.Forms.Cursor]::Position; "$($p.X),$($p.Y)"`).trim();
872
+ const [x, y] = r.split(',').map(Number);
873
+ return { x, y };
874
+ }
875
+ if (IS_LINUX && _run('which xdotool').code === 0) {
876
+ const r = _out('xdotool getmouselocation --shell');
877
+ const m = r.match(/X=(\d+)\s+Y=(\d+)/);
878
+ return m ? { x: +m[1], y: +m[2] } : { x: 0, y: 0 };
879
+ }
880
+ return { x: 0, y: 0 };
881
+ },
882
+
883
+ /** Drag from (x1,y1) to (x2,y2). */
884
+ drag(x1, y1, x2, y2) {
885
+ if (IS_LINUX && _run('which xdotool').code === 0) {
886
+ _spawn('xdotool', ['mousemove', String(x1), String(y1)]);
887
+ _spawn('xdotool', ['mousedown', '1']);
888
+ _spawn('xdotool', ['mousemove', String(x2), String(y2)]);
889
+ _spawn('xdotool', ['mouseup', '1']);
890
+ } else if (IS_MACOS) {
891
+ _spawn('cliclick', [`dd:${x1},${y1}`, `du:${x2},${y2}`]);
892
+ }
893
+ },
894
+ };
895
+
896
+ // ══════════════════════════════════════════════════════════════════════════════
897
+ // 7. SCREEN — Screenshots / Display Info
898
+ // ══════════════════════════════════════════════════════════════════════════════
899
+
900
+ const Screen = {
901
+ /**
902
+ * Take a screenshot.
903
+ * @param {string} [outputPath] destination file (PNG)
904
+ * @param {object} [opts]
905
+ * @param {object} [opts.region] { x, y, w, h }
906
+ * @returns {string} path to screenshot file
907
+ */
908
+ screenshot(outputPath, opts = {}) {
909
+ outputPath = outputPath ?? path.join(os.tmpdir(), `kitos_screenshot_${Date.now()}.png`);
910
+
911
+ if (IS_MACOS) {
912
+ const args = ['-x']; // silent
913
+ if (opts.region) {
914
+ const { x, y, w, h } = opts.region;
915
+ args.push('-R', `${x},${y},${w},${h}`);
916
+ }
917
+ args.push(outputPath);
918
+ _spawn('screencapture', args);
919
+ } else if (IS_WINDOWS) {
920
+ _pwsh(`
921
+ Add-Type -AssemblyName System.Windows.Forms
922
+ Add-Type -AssemblyName System.Drawing
923
+ $bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
924
+ ${opts.region ? `$bounds = New-Object System.Drawing.Rectangle(${opts.region.x},${opts.region.y},${opts.region.w},${opts.region.h})` : ''}
925
+ $bmp = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
926
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
927
+ $g.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
928
+ $bmp.Save('${outputPath.replace(/'/g, "''")}')
929
+ $g.Dispose(); $bmp.Dispose()
930
+ `);
931
+ } else if (IS_ANDROID) {
932
+ _spawn('termux-screenshot', ['-f', outputPath]);
933
+ } else if (IS_LINUX) {
934
+ if (_run('which scrot').code === 0) {
935
+ const args = [outputPath];
936
+ if (opts.region) {
937
+ const { x, y, w, h } = opts.region;
938
+ args.unshift(`-a`, `${x},${y},${w},${h}`);
939
+ }
940
+ _spawn('scrot', args);
941
+ } else if (_run('which gnome-screenshot').code === 0) {
942
+ _spawn('gnome-screenshot', ['-f', outputPath]);
943
+ } else if (_run('which import').code === 0) { // ImageMagick
944
+ _spawn('import', ['-window', 'root', outputPath]);
945
+ } else if (_run('which spectacle').code === 0) {
946
+ _spawn('spectacle', ['-n', '-o', outputPath]);
947
+ }
948
+ }
949
+ return outputPath;
950
+ },
951
+
952
+ /**
953
+ * Get all monitor/display info.
954
+ * @returns {Array<{ id, width, height, x, y, primary, dpi }>}
955
+ */
956
+ monitors() {
957
+ if (IS_MACOS) {
958
+ const r = _out('system_profiler SPDisplaysDataType');
959
+ // Parse resolution lines: e.g. "Resolution: 2560 x 1440"
960
+ const monitors = [];
961
+ const lines = r.split('\n');
962
+ let current = null;
963
+ for (const line of lines) {
964
+ if (line.match(/\s+Resolution:\s+(\d+) x (\d+)/)) {
965
+ const m = line.match(/(\d+) x (\d+)/);
966
+ if (m) monitors.push({ width: +m[1], height: +m[2], x: 0, y: 0, primary: monitors.length === 0 });
967
+ }
968
+ }
969
+ return monitors.length ? monitors : [{ width: 1920, height: 1080, x: 0, y: 0, primary: true }];
970
+ }
971
+ if (IS_WINDOWS) {
972
+ const r = _pwsh(`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::AllScreens | ForEach-Object { "$($_.Bounds.X),$($_.Bounds.Y),$($_.Bounds.Width),$($_.Bounds.Height),$($_.Primary)" }`);
973
+ return r.trim().split('\n').filter(Boolean).map((line, i) => {
974
+ const [x, y, w, h, primary] = line.trim().split(',');
975
+ return { id: i, x: +x, y: +y, width: +w, height: +h, primary: primary === 'True' };
976
+ });
977
+ }
978
+ if (IS_ANDROID) {
979
+ const r = _out('termux-display-info 2>/dev/null');
980
+ try { return [JSON.parse(r)]; } catch { return [{ width: 1080, height: 1920, primary: true }]; }
981
+ }
982
+ if (IS_LINUX) {
983
+ const r = _out('xrandr 2>/dev/null');
984
+ const monitors = [];
985
+ for (const m of r.matchAll(/(\S+) connected(?: primary)? (\d+)x(\d+)\+(\d+)\+(\d+)/g)) {
986
+ monitors.push({ id: m[1], width: +m[2], height: +m[3], x: +m[4], y: +m[5], primary: r.includes(`${m[1]} connected primary`) });
987
+ }
988
+ return monitors.length ? monitors : [{ width: 1920, height: 1080, primary: true }];
989
+ }
990
+ return [];
991
+ },
992
+
993
+ /** Get primary screen dimensions. */
994
+ size() {
995
+ const m = Screen.monitors().find(m => m.primary) ?? Screen.monitors()[0];
996
+ return m ? { width: m.width, height: m.height } : { width: 0, height: 0 };
997
+ },
998
+
999
+ /**
1000
+ * Get the pixel color at screen coordinates.
1001
+ * @returns {{ r: number, g: number, b: number, hex: string }|null}
1002
+ */
1003
+ pixelColor(x, y) {
1004
+ if (IS_MACOS) {
1005
+ // Use screencapture + sips to read one pixel
1006
+ const tmp = path.join(os.tmpdir(), 'kitos_px.png');
1007
+ _spawn('screencapture', ['-x', '-R', `${x},${y},1,1`, tmp]);
1008
+ const r = _out(`sips -g all ${tmp} 2>/dev/null`);
1009
+ fs.unlinkSync(tmp);
1010
+ // Parse pixel data (hex) from sips
1011
+ const m = r.match(/pixelColor: ([0-9a-f]+)/i);
1012
+ if (!m) return null;
1013
+ const hex = '#' + m[1].slice(0, 6);
1014
+ return { r: parseInt(hex.slice(1,3),16), g: parseInt(hex.slice(3,5),16), b: parseInt(hex.slice(5,7),16), hex };
1015
+ }
1016
+ if (IS_LINUX && _run('which import').code === 0) {
1017
+ const r = _out(`import -window root -crop 1x1+${x}+${y} txt:- 2>/dev/null`);
1018
+ const m = r.match(/#([0-9A-F]{6})/i);
1019
+ if (!m) return null;
1020
+ const hex = '#' + m[1];
1021
+ return { r: parseInt(hex.slice(1,3),16), g: parseInt(hex.slice(3,5),16), b: parseInt(hex.slice(5,7),16), hex };
1022
+ }
1023
+ return null;
1024
+ },
1025
+
1026
+ /** Get DPI of primary monitor. */
1027
+ dpi() {
1028
+ if (IS_MACOS) {
1029
+ const r = _out('system_profiler SPDisplaysDataType');
1030
+ const m = r.match(/(\d+) dpi/i);
1031
+ return m ? +m[1] : 96;
1032
+ }
1033
+ if (IS_LINUX) {
1034
+ const r = _out('xdpyinfo 2>/dev/null');
1035
+ const m = r.match(/(\d+)x(\d+) dots per inch/);
1036
+ return m ? +m[1] : 96;
1037
+ }
1038
+ return 96;
1039
+ },
1040
+ };
1041
+
1042
+ // ══════════════════════════════════════════════════════════════════════════════
1043
+ // 8. AUDIO — Sound Playback / Beep / Volume
1044
+ // ══════════════════════════════════════════════════════════════════════════════
1045
+
1046
+ const Audio = {
1047
+ /**
1048
+ * Play a sound file.
1049
+ * @param {string} filePath
1050
+ * @param {object} [opts]
1051
+ * @param {boolean} [opts.async]
1052
+ * @param {number} [opts.volume] 0.0–1.0
1053
+ */
1054
+ play(filePath, opts = {}) {
1055
+ filePath = path.resolve(filePath);
1056
+ if (IS_MACOS) {
1057
+ const args = [filePath];
1058
+ if (opts.volume !== undefined) args.push('--volume', String(Math.round(opts.volume * 100)));
1059
+ opts.async ? spawn('afplay', args, { detached: true, stdio: 'ignore' }).unref()
1060
+ : _spawn('afplay', args);
1061
+ } else if (IS_WINDOWS) {
1062
+ const cmd = `(New-Object Media.SoundPlayer '${filePath.replace(/'/g, "''")}').PlaySync()`;
1063
+ opts.async ? spawn('powershell', ['-NoProfile', '-Command', cmd], { detached: true, stdio: 'ignore' }).unref()
1064
+ : _pwsh(cmd);
1065
+ } else if (IS_ANDROID) {
1066
+ _spawn('termux-media-player', ['play', filePath]);
1067
+ } else if (IS_LINUX) {
1068
+ for (const player of ['paplay', 'aplay', 'mpg123', 'ffplay', 'mplayer', 'cvlc']) {
1069
+ if (_run(`which ${player}`).code === 0) {
1070
+ const args = player === 'ffplay' ? ['-nodisp', '-autoexit', filePath]
1071
+ : player === 'cvlc' ? ['--play-and-exit', filePath]
1072
+ : [filePath];
1073
+ opts.async ? spawn(player, args, { detached: true, stdio: 'ignore' }).unref()
1074
+ : _spawn(player, args);
1075
+ return;
1076
+ }
1077
+ }
1078
+ }
1079
+ },
1080
+
1081
+ /** Play a system beep / bell sound. */
1082
+ beep(count = 1) {
1083
+ for (let i = 0; i < count; i++) {
1084
+ if (IS_MACOS) _osascript('beep');
1085
+ else if (IS_WINDOWS) _pwsh('[Console]::Beep(800, 200)');
1086
+ else if (IS_ANDROID) _spawn('termux-vibrate', ['-d', '100']);
1087
+ else { process.stdout.write('\x07'); }
1088
+ }
1089
+ },
1090
+
1091
+ /**
1092
+ * Get system volume (0–100).
1093
+ * @returns {number}
1094
+ */
1095
+ getVolume() {
1096
+ if (IS_MACOS) {
1097
+ const r = _out('osascript -e "output volume of (get volume settings)"');
1098
+ return parseInt(r) || 0;
1099
+ }
1100
+ if (IS_WINDOWS) {
1101
+ const r = _pwsh(`(Get-AudioDevice -Playback).Volume`);
1102
+ return Math.round(parseFloat(r) * 100) || 0;
1103
+ }
1104
+ if (IS_ANDROID) {
1105
+ try { return JSON.parse(_out('termux-volume')).find(v => v.stream === 'music')?.volume ?? 50; } catch { return 50; }
1106
+ }
1107
+ if (IS_LINUX) {
1108
+ const r = _out('amixer sget Master 2>/dev/null || pactl get-sink-volume @DEFAULT_SINK@ 2>/dev/null');
1109
+ const m = r.match(/(\d+)%/);
1110
+ return m ? +m[1] : 0;
1111
+ }
1112
+ return 0;
1113
+ },
1114
+
1115
+ /**
1116
+ * Set system volume.
1117
+ * @param {number} level 0–100
1118
+ */
1119
+ setVolume(level) {
1120
+ level = Math.max(0, Math.min(100, Math.round(level)));
1121
+ if (IS_MACOS) {
1122
+ _osascript(`set volume output volume ${level}`);
1123
+ } else if (IS_WINDOWS) {
1124
+ _pwsh(`$obj = New-Object -ComObject WScript.Shell; for($i=0;$i -lt 50;$i++){$obj.SendKeys([char]174)}; for($i=0;$i -lt [math]::Round(${level}/2);$i++){$obj.SendKeys([char]175)}`);
1125
+ } else if (IS_ANDROID) {
1126
+ _spawn('termux-volume', ['music', String(level)]);
1127
+ } else if (IS_LINUX) {
1128
+ if (_run('which pactl').code === 0) _spawn('pactl', ['set-sink-volume', '@DEFAULT_SINK@', `${level}%`]);
1129
+ else _spawn('amixer', ['sset', 'Master', `${level}%`]);
1130
+ }
1131
+ },
1132
+
1133
+ /** Get mute state. */
1134
+ isMuted() {
1135
+ if (IS_MACOS) return _out('osascript -e "output muted of (get volume settings)"').trim() === 'true';
1136
+ if (IS_LINUX) {
1137
+ const r = _out('amixer sget Master 2>/dev/null');
1138
+ return r.includes('[off]');
1139
+ }
1140
+ return false;
1141
+ },
1142
+
1143
+ /** Mute audio. */
1144
+ mute() {
1145
+ if (IS_MACOS) _osascript('set volume with output muted');
1146
+ else if (IS_WINDOWS) _pwsh('(New-Object -ComObject WScript.Shell).SendKeys([char]173)');
1147
+ else if (IS_LINUX) {
1148
+ if (_run('which pactl').code === 0) _spawn('pactl', ['set-sink-mute', '@DEFAULT_SINK@', '1']);
1149
+ else _spawn('amixer', ['sset', 'Master', 'mute']);
1150
+ }
1151
+ },
1152
+
1153
+ /** Unmute audio. */
1154
+ unmute() {
1155
+ if (IS_MACOS) _osascript('set volume without output muted');
1156
+ else if (IS_WINDOWS) _pwsh('(New-Object -ComObject WScript.Shell).SendKeys([char]173)');
1157
+ else if (IS_LINUX) {
1158
+ if (_run('which pactl').code === 0) _spawn('pactl', ['set-sink-mute', '@DEFAULT_SINK@', '0']);
1159
+ else _spawn('amixer', ['sset', 'Master', 'unmute']);
1160
+ }
1161
+ },
1162
+
1163
+ /** Toggle mute. */
1164
+ toggleMute() { Audio.isMuted() ? Audio.unmute() : Audio.mute(); },
1165
+
1166
+ /** List audio output devices. */
1167
+ devices() {
1168
+ if (IS_MACOS) {
1169
+ return _out('system_profiler SPAudioDataType').split('\n')
1170
+ .filter(l => l.match(/^\s{6}\S/))
1171
+ .map(l => l.trim());
1172
+ }
1173
+ if (IS_LINUX && _run('which pactl').code === 0) {
1174
+ return _out('pactl list sinks short').split('\n').filter(Boolean).map(l => {
1175
+ const p = l.trim().split('\t');
1176
+ return { id: p[0], name: p[1], driver: p[2], state: p[4] };
1177
+ });
1178
+ }
1179
+ if (IS_ANDROID) {
1180
+ try { return JSON.parse(_out('termux-volume')); } catch { return []; }
1181
+ }
1182
+ return [];
1183
+ },
1184
+
1185
+ /** Stop media playback (Android). */
1186
+ stop() {
1187
+ if (IS_ANDROID) _spawn('termux-media-player', ['stop']);
1188
+ if (IS_MACOS) _run('killall afplay 2>/dev/null');
1189
+ },
1190
+ };
1191
+
1192
+ // ══════════════════════════════════════════════════════════════════════════════
1193
+ // 9. CAMERA — Webcam / Photo Capture
1194
+ // ══════════════════════════════════════════════════════════════════════════════
1195
+
1196
+ const Camera = {
1197
+ /**
1198
+ * Capture a photo from the default camera.
1199
+ * @param {string} [outputPath]
1200
+ * @param {object} [opts]
1201
+ * @param {string} [opts.device] camera device (Linux)
1202
+ * @returns {string} path to captured image
1203
+ */
1204
+ capture(outputPath, opts = {}) {
1205
+ outputPath = outputPath ?? path.join(os.tmpdir(), `kitos_cam_${Date.now()}.jpg`);
1206
+ if (IS_ANDROID) {
1207
+ _spawn('termux-camera-photo', [outputPath]);
1208
+ return outputPath;
1209
+ }
1210
+ if (IS_MACOS) {
1211
+ // imagesnap (brew install imagesnap)
1212
+ if (_run('which imagesnap').code === 0) {
1213
+ _spawn('imagesnap', ['-q', outputPath]);
1214
+ return outputPath;
1215
+ }
1216
+ }
1217
+ if (IS_LINUX) {
1218
+ const device = opts.device ?? '/dev/video0';
1219
+ if (_run('which ffmpeg').code === 0) {
1220
+ _spawn('ffmpeg', ['-y', '-f', 'v4l2', '-i', device, '-frames:v', '1', outputPath]);
1221
+ return outputPath;
1222
+ }
1223
+ if (_run('which fswebcam').code === 0) {
1224
+ _spawn('fswebcam', ['-d', device, '-r', '1280x720', '--no-banner', outputPath]);
1225
+ return outputPath;
1226
+ }
1227
+ }
1228
+ return outputPath;
1229
+ },
1230
+
1231
+ /**
1232
+ * Capture video.
1233
+ * @param {string} outputPath
1234
+ * @param {number} [duration=5] seconds
1235
+ * @param {object} [opts]
1236
+ * @returns {string} path to video file
1237
+ */
1238
+ captureVideo(outputPath, duration = 5, opts = {}) {
1239
+ outputPath = outputPath ?? path.join(os.tmpdir(), `kitos_vid_${Date.now()}.mp4`);
1240
+ if (IS_ANDROID) {
1241
+ _spawn('termux-camera-video', [outputPath]);
1242
+ return outputPath;
1243
+ }
1244
+ if (IS_LINUX && _run('which ffmpeg').code === 0) {
1245
+ const device = opts.device ?? '/dev/video0';
1246
+ _spawn('ffmpeg', ['-y', '-f', 'v4l2', '-i', device, '-t', String(duration), outputPath]);
1247
+ }
1248
+ return outputPath;
1249
+ },
1250
+
1251
+ /**
1252
+ * List available camera devices.
1253
+ * @returns {string[]}
1254
+ */
1255
+ devices() {
1256
+ if (IS_ANDROID) {
1257
+ try { return JSON.parse(_out('termux-camera-info') || '[]'); } catch { return []; }
1258
+ }
1259
+ if (IS_MACOS && _run('which imagesnap').code === 0) {
1260
+ return _out('imagesnap -l').split('\n').filter(l => l.startsWith('[') || l.match(/^\s+\S/)).map(l => l.trim()).filter(Boolean);
1261
+ }
1262
+ if (IS_LINUX) {
1263
+ const devices = [];
1264
+ for (let i = 0; i < 8; i++) {
1265
+ if (fs.existsSync(`/dev/video${i}`)) devices.push(`/dev/video${i}`);
1266
+ }
1267
+ return devices;
1268
+ }
1269
+ return [];
1270
+ },
1271
+ };
1272
+
1273
+ // ══════════════════════════════════════════════════════════════════════════════
1274
+ // 10. POWER — Battery / Sleep / Shutdown
1275
+ // ══════════════════════════════════════════════════════════════════════════════
1276
+
1277
+ const Power = {
1278
+ /**
1279
+ * Get battery information.
1280
+ * @returns {{ level: number, charging: boolean, full: boolean, timeRemaining: number|null }|null}
1281
+ */
1282
+ battery() {
1283
+ if (IS_MACOS) {
1284
+ const r = _out('pmset -g batt');
1285
+ const levelM = r.match(/(\d+)%/);
1286
+ const chargeM = r.match(/AC Power/) ?? r.match(/charging/i);
1287
+ const timeM = r.match(/(\d+):(\d+) remaining/);
1288
+ return {
1289
+ level: levelM ? +levelM[1] : null,
1290
+ charging: !!chargeM,
1291
+ full: r.includes('fully charged'),
1292
+ timeRemaining: timeM ? +timeM[1] * 60 + +timeM[2] : null,
1293
+ };
1294
+ }
1295
+ if (IS_WINDOWS) {
1296
+ const r = _pwsh(`(Get-WmiObject Win32_Battery) | Select-Object EstimatedChargeRemaining, BatteryStatus | ConvertTo-Csv -NoTypeInformation`);
1297
+ const lines = r.trim().split('\n');
1298
+ if (lines.length < 2) return null;
1299
+ const [level, status] = lines[1].replace(/"/g, '').split(',').map(s => s.trim());
1300
+ return { level: +level, charging: status === '2', full: status === '1', timeRemaining: null };
1301
+ }
1302
+ if (IS_ANDROID) {
1303
+ try { return JSON.parse(_out('termux-battery-status')); } catch { return null; }
1304
+ }
1305
+ if (IS_LINUX) {
1306
+ const battDir = '/sys/class/power_supply';
1307
+ if (!fs.existsSync(battDir)) return null;
1308
+ const batts = fs.readdirSync(battDir).filter(d => d.startsWith('BAT'));
1309
+ if (!batts.length) return null;
1310
+ const batt = path.join(battDir, batts[0]);
1311
+ const read = (f) => { try { return fs.readFileSync(path.join(batt, f), 'utf8').trim(); } catch { return null; } };
1312
+ const cap = read('capacity');
1313
+ const status = read('status');
1314
+ return {
1315
+ level: cap ? +cap : null,
1316
+ charging: status === 'Charging',
1317
+ full: status === 'Full',
1318
+ timeRemaining: null,
1319
+ };
1320
+ }
1321
+ return null;
1322
+ },
1323
+
1324
+ /** Put the system to sleep. */
1325
+ sleep() {
1326
+ if (IS_MACOS) _run('pmset sleepnow');
1327
+ else if (IS_WINDOWS) _pwsh('Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Application]::SetSuspendState("Suspend",$false,$false)');
1328
+ else if (IS_LINUX) _run('systemctl suspend');
1329
+ else if (IS_ANDROID) _spawn('termux-toast', ['Sleep not supported on Android']);
1330
+ },
1331
+
1332
+ /** Shut down the system. */
1333
+ shutdown(delay = 0) {
1334
+ if (IS_MACOS) _run(`shutdown -h +${delay}`);
1335
+ else if (IS_WINDOWS) _run(`shutdown /s /t ${delay * 60}`);
1336
+ else if (IS_LINUX) _run(`shutdown -h +${delay}`);
1337
+ },
1338
+
1339
+ /** Restart the system. */
1340
+ restart(delay = 0) {
1341
+ if (IS_MACOS) _run(`shutdown -r +${delay}`);
1342
+ else if (IS_WINDOWS) _run(`shutdown /r /t ${delay * 60}`);
1343
+ else if (IS_LINUX) _run(`shutdown -r +${delay}`);
1344
+ },
1345
+
1346
+ /** Lock the screen. */
1347
+ lock() {
1348
+ if (IS_MACOS) {
1349
+ _run('/System/Library/CoreServices/"Menu Extras"/User.menu/Contents/Resources/CGSession -suspend 2>/dev/null || pmset displaysleepnow');
1350
+ } else if (IS_WINDOWS) {
1351
+ _run('rundll32.exe user32.dll,LockWorkStation');
1352
+ } else if (IS_ANDROID) {
1353
+ _spawn('termux-toast', ['Screen lock not directly supported']);
1354
+ } else if (IS_LINUX) {
1355
+ const lockers = ['gnome-screensaver-command -l', 'xdg-screensaver lock', 'loginctl lock-session', 'xscreensaver-command -lock', 'i3lock'];
1356
+ for (const cmd of lockers) { if (_run(cmd).code === 0) break; }
1357
+ }
1358
+ },
1359
+
1360
+ /** Prevent the system from sleeping (returns a stopper function). */
1361
+ preventSleep(reason = 'Nova process') {
1362
+ let child;
1363
+ if (IS_MACOS) {
1364
+ child = spawn('caffeinate', ['-d', '-i'], { detached: true, stdio: 'ignore' });
1365
+ child.unref();
1366
+ } else if (IS_WINDOWS) {
1367
+ child = spawn('powershell', ['-NoProfile', '-Command',
1368
+ `while ($true) { (New-Object -ComObject WScript.Shell).SendKeys([char]0); Start-Sleep -Seconds 60 }`
1369
+ ], { detached: true, stdio: 'ignore' });
1370
+ child.unref();
1371
+ } else if (IS_LINUX) {
1372
+ if (_run('which systemd-inhibit').code === 0) {
1373
+ child = spawn('systemd-inhibit', ['--what=sleep:idle', '--why=' + reason, 'sleep', 'infinity'], { detached: true, stdio: 'ignore' });
1374
+ child.unref();
1375
+ } else if (_run('which caffeine').code === 0) {
1376
+ child = spawn('caffeine', [], { detached: true, stdio: 'ignore' });
1377
+ child.unref();
1378
+ }
1379
+ } else if (IS_ANDROID) {
1380
+ _spawn('termux-wake-lock');
1381
+ }
1382
+ return () => {
1383
+ if (child) { try { child.kill(); } catch {} }
1384
+ if (IS_ANDROID) _spawn('termux-wake-unlock');
1385
+ };
1386
+ },
1387
+
1388
+ /** Cancel a scheduled shutdown. */
1389
+ cancelShutdown() {
1390
+ if (IS_MACOS || IS_LINUX) _run('shutdown -c 2>/dev/null');
1391
+ else if (IS_WINDOWS) _run('shutdown /a');
1392
+ },
1393
+ };
1394
+
1395
+ // ══════════════════════════════════════════════════════════════════════════════
1396
+ // 11. BRIGHTNESS — Display Brightness
1397
+ // ══════════════════════════════════════════════════════════════════════════════
1398
+
1399
+ const Brightness = {
1400
+ /**
1401
+ * Get display brightness (0–100).
1402
+ * @param {number} [monitor=0] monitor index
1403
+ * @returns {number}
1404
+ */
1405
+ get(monitor = 0) {
1406
+ if (IS_MACOS && _run('which brightness').code === 0) {
1407
+ const r = _out('brightness -l');
1408
+ const m = r.match(/display \d+: brightness ([\d.]+)/);
1409
+ return m ? Math.round(parseFloat(m[1]) * 100) : 100;
1410
+ }
1411
+ if (IS_WINDOWS) {
1412
+ const r = _pwsh('(Get-WmiObject -Namespace root/WMI -Class WmiMonitorBrightness).CurrentBrightness');
1413
+ return parseInt(r) || 100;
1414
+ }
1415
+ if (IS_ANDROID) {
1416
+ try { return JSON.parse(_out('termux-brightness'))?.brightness ?? 100; } catch { return 100; }
1417
+ }
1418
+ if (IS_LINUX) {
1419
+ // Try /sys/class/backlight
1420
+ const bl = '/sys/class/backlight';
1421
+ if (fs.existsSync(bl)) {
1422
+ const devices = fs.readdirSync(bl);
1423
+ if (devices.length > 0) {
1424
+ const dev = path.join(bl, devices[monitor] ?? devices[0]);
1425
+ const cur = +fs.readFileSync(path.join(dev, 'brightness'), 'utf8').trim();
1426
+ const max = +fs.readFileSync(path.join(dev, 'max_brightness'), 'utf8').trim();
1427
+ return Math.round((cur / max) * 100);
1428
+ }
1429
+ }
1430
+ if (_run('which xrandr').code === 0) {
1431
+ const r = _out('xrandr --verbose 2>/dev/null');
1432
+ const m = r.match(/Brightness: ([\d.]+)/);
1433
+ return m ? Math.round(parseFloat(m[1]) * 100) : 100;
1434
+ }
1435
+ }
1436
+ return 100;
1437
+ },
1438
+
1439
+ /**
1440
+ * Set display brightness.
1441
+ * @param {number} level 0–100
1442
+ * @param {number} [monitor=0]
1443
+ */
1444
+ set(level, monitor = 0) {
1445
+ level = Math.max(0, Math.min(100, Math.round(level)));
1446
+ if (IS_MACOS && _run('which brightness').code === 0) {
1447
+ _spawn('brightness', [String(level / 100)]);
1448
+ } else if (IS_WINDOWS) {
1449
+ _pwsh(`(Get-WmiObject -Namespace root/WMI -Class WmiMonitorBrightnessMethods).WmiSetBrightness(1, ${level})`);
1450
+ } else if (IS_ANDROID) {
1451
+ _spawn('termux-brightness', [String(Math.round(level * 2.55))]);
1452
+ } else if (IS_LINUX) {
1453
+ const bl = '/sys/class/backlight';
1454
+ if (fs.existsSync(bl)) {
1455
+ const devices = fs.readdirSync(bl);
1456
+ if (devices.length > 0) {
1457
+ const dev = path.join(bl, devices[monitor] ?? devices[0]);
1458
+ const max = +fs.readFileSync(path.join(dev, 'max_brightness'), 'utf8').trim();
1459
+ const val = Math.round((level / 100) * max);
1460
+ try { fs.writeFileSync(path.join(dev, 'brightness'), String(val)); return; } catch {}
1461
+ }
1462
+ }
1463
+ if (_run('which xrandr').code === 0) {
1464
+ const r = _out('xrandr --listactivemonitors 2>/dev/null');
1465
+ const m = r.match(/\d+:\s+\S+\s+(\S+)/g);
1466
+ const displayName = m ? m[monitor ?? 0]?.split(/\s+/).pop() : null;
1467
+ if (displayName) _spawn('xrandr', ['--output', displayName, '--brightness', String(level / 100)]);
1468
+ }
1469
+ }
1470
+ },
1471
+ };
1472
+
1473
+ // ══════════════════════════════════════════════════════════════════════════════
1474
+ // 12. WIFI — Wireless Network
1475
+ // ══════════════════════════════════════════════════════════════════════════════
1476
+
1477
+ const Wifi = {
1478
+ /**
1479
+ * Get current SSID.
1480
+ * @returns {string|null}
1481
+ */
1482
+ currentSSID() {
1483
+ if (IS_MACOS) {
1484
+ return _out('/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I 2>/dev/null').match(/\s+SSID: (.+)/)?.[1]?.trim() ?? null;
1485
+ }
1486
+ if (IS_WINDOWS) {
1487
+ return _out('netsh wlan show interfaces').match(/\s+SSID\s+:\s+(.+)/)?.[1]?.trim() ?? null;
1488
+ }
1489
+ if (IS_ANDROID) {
1490
+ try { return JSON.parse(_out('termux-wifi-connectioninfo'))?.ssid ?? null; } catch { return null; }
1491
+ }
1492
+ if (IS_LINUX) {
1493
+ return (_out('iwgetid -r 2>/dev/null').trim() ||
1494
+ _out('nmcli -t -f active,ssid dev wifi 2>/dev/null').split('\n').find(l => l.startsWith('yes:'))?.split(':')[1]) ?? null;
1495
+ }
1496
+ return null;
1497
+ },
1498
+
1499
+ /**
1500
+ * Get detailed connection info.
1501
+ * @returns {object|null}
1502
+ */
1503
+ connectionInfo() {
1504
+ if (IS_ANDROID) {
1505
+ try { return JSON.parse(_out('termux-wifi-connectioninfo')); } catch { return null; }
1506
+ }
1507
+ if (IS_MACOS) {
1508
+ const r = _out('/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I 2>/dev/null');
1509
+ const info = {};
1510
+ for (const m of r.matchAll(/\s+(\S+): (.+)/g)) info[m[1].trim()] = m[2].trim();
1511
+ return info;
1512
+ }
1513
+ if (IS_LINUX) {
1514
+ return { ssid: Wifi.currentSSID(), signal: Wifi.signalStrength() };
1515
+ }
1516
+ return null;
1517
+ },
1518
+
1519
+ /**
1520
+ * Scan for available WiFi networks.
1521
+ * @returns {Array<{ ssid, signal, security, channel }>}
1522
+ */
1523
+ scan() {
1524
+ if (IS_ANDROID) {
1525
+ try { return JSON.parse(_out('termux-wifi-scaninfo') || '[]'); } catch { return []; }
1526
+ }
1527
+ if (IS_MACOS) {
1528
+ const r = _out('/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s 2>/dev/null');
1529
+ return r.split('\n').slice(1).filter(Boolean).map(l => {
1530
+ const parts = l.trim().split(/\s+/);
1531
+ return { ssid: parts[0], bssid: parts[1], signal: +parts[2], channel: +parts[3], security: parts.slice(5).join(' ') };
1532
+ });
1533
+ }
1534
+ if (IS_LINUX) {
1535
+ const r = _out('nmcli -t -f SSID,SIGNAL,SECURITY,CHAN dev wifi list 2>/dev/null');
1536
+ return r.split('\n').filter(Boolean).map(l => {
1537
+ const [ssid, signal, security, channel] = l.split(':');
1538
+ return { ssid, signal: +signal, security, channel: +channel };
1539
+ });
1540
+ }
1541
+ if (IS_WINDOWS) {
1542
+ const r = _out('netsh wlan show networks mode=bssid');
1543
+ const networks = [];
1544
+ let current = {};
1545
+ for (const line of r.split('\n')) {
1546
+ const m = line.match(/\s+SSID\s+\d+\s+:\s+(.+)/); if (m) current = { ssid: m[1].trim() };
1547
+ const s = line.match(/\s+Signal\s+:\s+(\d+)%/); if (s) current.signal = +s[1];
1548
+ const a = line.match(/\s+Authentication\s+:\s+(.+)/); if (a) { current.security = a[1].trim(); networks.push(current); }
1549
+ }
1550
+ return networks;
1551
+ }
1552
+ return [];
1553
+ },
1554
+
1555
+ /**
1556
+ * Get signal strength in dBm or percent.
1557
+ * @returns {number|null}
1558
+ */
1559
+ signalStrength() {
1560
+ if (IS_ANDROID) {
1561
+ try { return JSON.parse(_out('termux-wifi-connectioninfo'))?.rssi ?? null; } catch { return null; }
1562
+ }
1563
+ if (IS_LINUX) {
1564
+ const r = _out('iwconfig 2>/dev/null');
1565
+ const m = r.match(/Signal level=([\-\d]+) dBm/);
1566
+ return m ? +m[1] : null;
1567
+ }
1568
+ return null;
1569
+ },
1570
+
1571
+ /** Enable WiFi (Android + Linux nmcli). */
1572
+ enable() {
1573
+ if (IS_ANDROID) _spawn('termux-wifi-enable', ['true']);
1574
+ else if (IS_LINUX && _run('which nmcli').code === 0) _spawn('nmcli', ['radio', 'wifi', 'on']);
1575
+ else if (IS_WINDOWS) _run('netsh interface set interface Wi-Fi admin=enabled');
1576
+ },
1577
+
1578
+ /** Disable WiFi. */
1579
+ disable() {
1580
+ if (IS_ANDROID) _spawn('termux-wifi-enable', ['false']);
1581
+ else if (IS_LINUX && _run('which nmcli').code === 0) _spawn('nmcli', ['radio', 'wifi', 'off']);
1582
+ else if (IS_WINDOWS) _run('netsh interface set interface Wi-Fi admin=disabled');
1583
+ },
1584
+ };
1585
+
1586
+ // ══════════════════════════════════════════════════════════════════════════════
1587
+ // 13. BLUETOOTH — Bluetooth Control
1588
+ // ══════════════════════════════════════════════════════════════════════════════
1589
+
1590
+ const Bluetooth = {
1591
+ /**
1592
+ * Toggle Bluetooth on/off.
1593
+ * @param {boolean} on
1594
+ */
1595
+ setEnabled(on) {
1596
+ if (IS_MACOS) _run(`blueutil --power ${on ? 1 : 0} 2>/dev/null || echo "install blueutil: brew install blueutil"`);
1597
+ else if (IS_WINDOWS) _pwsh(`Add-Type -AssemblyName System.Runtime.WindowsRuntime; [Windows.Devices.Radios.Radio,Windows.Devices.Radios, ContentType=WindowsRuntime] | Out-Null`);
1598
+ else if (IS_ANDROID) _spawn('termux-bluetooth-scan', on ? ['-le'] : []);
1599
+ else if (IS_LINUX) _run(`rfkill ${on ? 'unblock' : 'block'} bluetooth 2>/dev/null`);
1600
+ },
1601
+
1602
+ /** Is Bluetooth enabled? */
1603
+ isEnabled() {
1604
+ if (IS_MACOS) return _out('blueutil --power 2>/dev/null').trim() === '1';
1605
+ if (IS_LINUX) return _out('rfkill list bluetooth 2>/dev/null').includes('Soft blocked: no');
1606
+ if (IS_ANDROID) { try { return JSON.parse(_out('termux-bluetooth-info 2>/dev/null') || '{}').enabled ?? false; } catch { return false; } }
1607
+ return false;
1608
+ },
1609
+
1610
+ /**
1611
+ * List paired devices.
1612
+ * @returns {Array<{ id, name, connected, paired }>}
1613
+ */
1614
+ pairedDevices() {
1615
+ if (IS_MACOS) {
1616
+ const r = _out('system_profiler SPBluetoothDataType 2>/dev/null');
1617
+ const devices = [];
1618
+ for (const m of r.matchAll(/(\S[\w\s]+):\s*\n\s+Address: ([\w:]+)/g)) {
1619
+ devices.push({ name: m[1].trim(), id: m[2], paired: true, connected: r.includes(`${m[1].trim()}`) });
1620
+ }
1621
+ return devices;
1622
+ }
1623
+ if (IS_LINUX && _run('which bluetoothctl').code === 0) {
1624
+ const r = _out('bluetoothctl devices');
1625
+ return r.split('\n').filter(Boolean).map(l => {
1626
+ const m = l.match(/Device ([\w:]+) (.+)/);
1627
+ return m ? { id: m[1], name: m[2], paired: true } : null;
1628
+ }).filter(Boolean);
1629
+ }
1630
+ if (IS_ANDROID) {
1631
+ try { return JSON.parse(_out('termux-bluetooth-paired-devices 2>/dev/null') || '[]'); } catch { return []; }
1632
+ }
1633
+ return [];
1634
+ },
1635
+
1636
+ /** Scan for nearby Bluetooth devices (Linux bluetoothctl). */
1637
+ scan(durationMs = 5000) {
1638
+ if (IS_LINUX && _run('which bluetoothctl').code === 0) {
1639
+ _run(`bluetoothctl scan on & sleep ${Math.round(durationMs/1000)} && bluetoothctl scan off`);
1640
+ return Bluetooth.pairedDevices();
1641
+ }
1642
+ if (IS_ANDROID) {
1643
+ try { return JSON.parse(_out('termux-bluetooth-scan -d 5000 2>/dev/null') || '[]'); } catch { return []; }
1644
+ }
1645
+ return [];
1646
+ },
1647
+
1648
+ /**
1649
+ * Connect to a device by MAC address.
1650
+ * @param {string} address e.g. 'AA:BB:CC:DD:EE:FF'
1651
+ */
1652
+ connect(address) {
1653
+ if (IS_LINUX && _run('which bluetoothctl').code === 0) {
1654
+ _run(`bluetoothctl connect ${address}`);
1655
+ } else if (IS_ANDROID) {
1656
+ _spawn('termux-bluetooth-connect', [address]);
1657
+ }
1658
+ },
1659
+
1660
+ /** Disconnect from a device. */
1661
+ disconnect(address) {
1662
+ if (IS_LINUX && _run('which bluetoothctl').code === 0) {
1663
+ _run(`bluetoothctl disconnect ${address}`);
1664
+ }
1665
+ },
1666
+ };
1667
+
1668
+ // ══════════════════════════════════════════════════════════════════════════════
1669
+ // 14. VOLUME — Fine-grained audio volume control
1670
+ // ══════════════════════════════════════════════════════════════════════════════
1671
+
1672
+ const Volume = {
1673
+ /** Get volume for a specific stream type (Android: music, ring, alarm, notification). */
1674
+ getStream(stream = 'music') {
1675
+ if (IS_ANDROID) {
1676
+ try {
1677
+ const vols = JSON.parse(_out('termux-volume'));
1678
+ const s = vols.find(v => v.stream === stream);
1679
+ return s ? s.volume : null;
1680
+ } catch { return null; }
1681
+ }
1682
+ return Audio.getVolume();
1683
+ },
1684
+
1685
+ /** Set volume for a specific stream type. */
1686
+ setStream(stream, level) {
1687
+ if (IS_ANDROID) { _spawn('termux-volume', [stream, String(level)]); return; }
1688
+ Audio.setVolume(level);
1689
+ },
1690
+
1691
+ /** Get microphone input level (0–100). */
1692
+ getMicLevel() {
1693
+ if (IS_LINUX && _run('which amixer').code === 0) {
1694
+ const r = _out('amixer sget Capture 2>/dev/null || amixer sget "Mic" 2>/dev/null');
1695
+ const m = r.match(/(\d+)%/);
1696
+ return m ? +m[1] : null;
1697
+ }
1698
+ if (IS_MACOS) {
1699
+ const r = _out('osascript -e "input volume of (get volume settings)"').trim();
1700
+ return parseInt(r) ?? null;
1701
+ }
1702
+ if (IS_WINDOWS) {
1703
+ return parseInt(_pwsh(`[math]::Round((Get-AudioDevice -Microphone).Volume * 100)`) ?? '0');
1704
+ }
1705
+ return null;
1706
+ },
1707
+
1708
+ /** Set microphone input level (0–100). */
1709
+ setMicLevel(level) {
1710
+ level = Math.max(0, Math.min(100, Math.round(level)));
1711
+ if (IS_MACOS) _osascript(`set volume input volume ${level}`);
1712
+ else if (IS_LINUX && _run('which amixer').code === 0) {
1713
+ _spawn('amixer', ['sset', 'Capture', `${level}%`]);
1714
+ } else if (IS_WINDOWS) {
1715
+ _pwsh(`(Get-AudioDevice -Microphone).Volume = ${level / 100}`);
1716
+ }
1717
+ },
1718
+
1719
+ /** List output devices. */
1720
+ outputDevices() { return Audio.devices(); },
1721
+
1722
+ /** Switch default audio output device (Linux PulseAudio). */
1723
+ switchOutput(sinkName) {
1724
+ if (IS_LINUX && _run('which pactl').code === 0) {
1725
+ _spawn('pactl', ['set-default-sink', sinkName]);
1726
+ }
1727
+ },
1728
+
1729
+ // All basic ops delegate to Audio
1730
+ get: () => Audio.getVolume(),
1731
+ set: (l) => Audio.setVolume(l),
1732
+ mute: () => Audio.mute(),
1733
+ unmute: () => Audio.unmute(),
1734
+ toggle: () => Audio.toggleMute(),
1735
+ isMuted: () => Audio.isMuted(),
1736
+ };
1737
+
1738
+ // ══════════════════════════════════════════════════════════════════════════════
1739
+ // 15. TRASH — Send Files to Trash
1740
+ // ══════════════════════════════════════════════════════════════════════════════
1741
+
1742
+ const Trash = {
1743
+ /**
1744
+ * Move a file or directory to the system trash.
1745
+ * @param {string} filePath
1746
+ * @returns {boolean}
1747
+ */
1748
+ send(filePath) {
1749
+ filePath = path.resolve(filePath);
1750
+ if (IS_MACOS) {
1751
+ const r = _osascript(`tell application "Finder" to delete POSIX file "${_escAS(filePath)}"`);
1752
+ return !r.includes('error');
1753
+ }
1754
+ if (IS_WINDOWS) {
1755
+ const r = _pwsh(`
1756
+ Add-Type -AssemblyName Microsoft.VisualBasic
1757
+ [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('${filePath.replace(/'/g, "''")}', 'OnlyErrorDialogs', 'SendToRecycleBin')
1758
+ `);
1759
+ return r.trim() === '';
1760
+ }
1761
+ if (IS_ANDROID) {
1762
+ // Termux has no trash; move to ~/_trash
1763
+ const trashDir = path.join(os.homedir(), '_trash');
1764
+ fs.mkdirSync(trashDir, { recursive: true });
1765
+ fs.renameSync(filePath, path.join(trashDir, path.basename(filePath) + '.' + Date.now()));
1766
+ return true;
1767
+ }
1768
+ if (IS_LINUX) {
1769
+ // Try trash-cli
1770
+ if (_run('which trash-put').code === 0) return _spawn('trash-put', [filePath]).code === 0;
1771
+ if (_run('which gio').code === 0) return _spawn('gio', ['trash', filePath]).code === 0;
1772
+ // XDG Trash spec fallback
1773
+ const trashDir = path.join(os.homedir(), '.local', 'share', 'Trash');
1774
+ const files = path.join(trashDir, 'files');
1775
+ const info = path.join(trashDir, 'info');
1776
+ fs.mkdirSync(files, { recursive: true });
1777
+ fs.mkdirSync(info, { recursive: true });
1778
+ const name = path.basename(filePath);
1779
+ const dest = path.join(files, name);
1780
+ fs.renameSync(filePath, dest);
1781
+ const trashInfo = `[Trash Info]\nPath=${filePath}\nDeletionDate=${new Date().toISOString().replace(/\..+/, '')}\n`;
1782
+ fs.writeFileSync(path.join(info, name + '.trashinfo'), trashInfo, 'utf8');
1783
+ return true;
1784
+ }
1785
+ return false;
1786
+ },
1787
+
1788
+ /**
1789
+ * List items in the trash.
1790
+ * @returns {Array<{ name, path, date }>}
1791
+ */
1792
+ list() {
1793
+ if (IS_MACOS) {
1794
+ const trashPath = path.join(os.homedir(), '.Trash');
1795
+ return fs.readdirSync(trashPath).map(f => ({ name: f, path: path.join(trashPath, f) }));
1796
+ }
1797
+ if (IS_LINUX) {
1798
+ if (_run('which trash-list').code === 0) {
1799
+ return _out('trash-list').split('\n').filter(Boolean).map(l => {
1800
+ const m = l.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (.+)/);
1801
+ return m ? { date: m[1], path: m[2], name: path.basename(m[2]) } : null;
1802
+ }).filter(Boolean);
1803
+ }
1804
+ const infoDir = path.join(os.homedir(), '.local', 'share', 'Trash', 'info');
1805
+ if (!fs.existsSync(infoDir)) return [];
1806
+ return fs.readdirSync(infoDir).map(f => ({ name: f.replace('.trashinfo', '') }));
1807
+ }
1808
+ if (IS_WINDOWS) {
1809
+ const r = _pwsh(`(New-Object -ComObject Shell.Application).NameSpace(10).Items() | ForEach-Object { $_.Path }`);
1810
+ return r.split('\n').filter(Boolean).map(p => ({ name: path.basename(p.trim()), path: p.trim() }));
1811
+ }
1812
+ if (IS_ANDROID) {
1813
+ const td = path.join(os.homedir(), '_trash');
1814
+ if (!fs.existsSync(td)) return [];
1815
+ return fs.readdirSync(td).map(f => ({ name: f, path: path.join(td, f) }));
1816
+ }
1817
+ return [];
1818
+ },
1819
+
1820
+ /** Empty the trash. */
1821
+ empty() {
1822
+ if (IS_MACOS) {
1823
+ _osascript('tell application "Finder" to empty trash');
1824
+ } else if (IS_LINUX) {
1825
+ if (_run('which trash-empty').code === 0) _run('trash-empty');
1826
+ else {
1827
+ const td = path.join(os.homedir(), '.local', 'share', 'Trash');
1828
+ _run(`rm -rf "${td}/files/"* "${td}/info/"* 2>/dev/null`);
1829
+ }
1830
+ } else if (IS_WINDOWS) {
1831
+ _pwsh('(New-Object -ComObject Shell.Application).NameSpace(10).Items() | ForEach-Object { Remove-Item $_.Path -Recurse -Force }');
1832
+ } else if (IS_ANDROID) {
1833
+ const td = path.join(os.homedir(), '_trash');
1834
+ _run(`rm -rf ${_esc(td)}/* 2>/dev/null`);
1835
+ }
1836
+ },
1837
+ };
1838
+
1839
+ // ══════════════════════════════════════════════════════════════════════════════
1840
+ // 16. OPEN — Open Files / URLs / Apps
1841
+ // ══════════════════════════════════════════════════════════════════════════════
1842
+
1843
+ const Open = {
1844
+ /**
1845
+ * Open a file, URL, or application with the system default handler.
1846
+ * @param {string} target file path, URL, or app name
1847
+ * @param {object} [opts]
1848
+ * @param {string} [opts.app] specific app to open with
1849
+ * @param {boolean} [opts.reveal] reveal in file manager instead of opening
1850
+ */
1851
+ open(target, opts = {}) {
1852
+ target = String(target);
1853
+ if (opts.reveal) return Open.reveal(target);
1854
+
1855
+ if (IS_MACOS) {
1856
+ const args = [target];
1857
+ if (opts.app) args.unshift('-a', opts.app);
1858
+ spawn('open', args, { detached: true, stdio: 'ignore' }).unref();
1859
+ } else if (IS_WINDOWS) {
1860
+ if (opts.app) spawn('cmd', ['/c', 'start', '', opts.app, target], { detached: true, stdio: 'ignore' }).unref();
1861
+ else spawn('cmd', ['/c', 'start', '', target], { detached: true, stdio: 'ignore' }).unref();
1862
+ } else if (IS_ANDROID) {
1863
+ _spawn('termux-open', [target]);
1864
+ } else if (IS_LINUX) {
1865
+ const opener = _run('which xdg-open').code === 0 ? 'xdg-open'
1866
+ : _run('which gio').code === 0 ? 'gio'
1867
+ : _run('which mimeopen').code === 0 ? 'mimeopen'
1868
+ : null;
1869
+ if (opener) spawn(opener, opener === 'gio' ? ['open', target] : [target], { detached: true, stdio: 'ignore' }).unref();
1870
+ }
1871
+ },
1872
+
1873
+ /** Open a URL in the default browser. */
1874
+ url(url) { Open.open(url); },
1875
+
1876
+ /** Open a file with its default application. */
1877
+ file(filePath) { Open.open(path.resolve(filePath)); },
1878
+
1879
+ /**
1880
+ * Reveal a file in Finder / Explorer / Nautilus / Files.
1881
+ * @param {string} filePath
1882
+ */
1883
+ reveal(filePath) {
1884
+ filePath = path.resolve(filePath);
1885
+ if (IS_MACOS) {
1886
+ _osascript(`tell application "Finder" to reveal POSIX file "${_escAS(filePath)}"\ntell application "Finder" to activate`);
1887
+ } else if (IS_WINDOWS) {
1888
+ _run(`explorer /select,"${filePath.replace(/"/g, '\\"')}"`);
1889
+ } else if (IS_LINUX) {
1890
+ const parent = path.dirname(filePath);
1891
+ if (_run('which nautilus').code === 0) spawn('nautilus', ['--select', filePath], { detached: true, stdio: 'ignore' }).unref();
1892
+ else if (_run('which dolphin').code === 0) spawn('dolphin', ['--select', filePath], { detached: true, stdio: 'ignore' }).unref();
1893
+ else if (_run('which thunar').code === 0) spawn('thunar', [parent], { detached: true, stdio: 'ignore' }).unref();
1894
+ else spawn('xdg-open', [parent], { detached: true, stdio: 'ignore' }).unref();
1895
+ }
1896
+ },
1897
+
1898
+ /** Open a terminal window (optionally with a command). */
1899
+ terminal(cmd, opts = {}) {
1900
+ if (IS_MACOS) {
1901
+ if (cmd) _osascript(`tell application "Terminal" to do script "${_escAS(cmd)}"\ntell application "Terminal" to activate`);
1902
+ else _osascript('tell application "Terminal" to activate');
1903
+ } else if (IS_WINDOWS) {
1904
+ if (cmd) spawn('cmd', ['/c', 'start', 'cmd', '/k', cmd], { detached: true, stdio: 'ignore' }).unref();
1905
+ else spawn('cmd', ['/c', 'start', 'cmd'], { detached: true, stdio: 'ignore' }).unref();
1906
+ } else if (IS_LINUX) {
1907
+ const terms = ['gnome-terminal', 'konsole', 'xterm', 'xfce4-terminal', 'lxterminal', 'alacritty', 'kitty'];
1908
+ for (const t of terms) {
1909
+ if (_run(`which ${t}`).code === 0) {
1910
+ const args = cmd ? (t === 'gnome-terminal' ? ['--', 'bash', '-c', cmd] : ['-e', cmd]) : [];
1911
+ spawn(t, args, { detached: true, stdio: 'ignore' }).unref();
1912
+ return;
1913
+ }
1914
+ }
1915
+ }
1916
+ },
1917
+ };
1918
+
1919
+ // ══════════════════════════════════════════════════════════════════════════════
1920
+ // 17. AUTOSTART — Startup Items
1921
+ // ══════════════════════════════════════════════════════════════════════════════
1922
+
1923
+ const Autostart = {
1924
+ /**
1925
+ * Add a startup item.
1926
+ * @param {string} name identifier / display name
1927
+ * @param {string} command command to run (full path recommended)
1928
+ * @param {object} [opts]
1929
+ * @param {boolean} [opts.hidden] (macOS Login Item)
1930
+ */
1931
+ add(name, command, opts = {}) {
1932
+ name = String(name); command = String(command);
1933
+ if (IS_MACOS) {
1934
+ // Create a LaunchAgent plist in ~/Library/LaunchAgents
1935
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `com.nova.${name}.plist`);
1936
+ const args = command.split(' ');
1937
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
1938
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1939
+ <plist version="1.0"><dict>
1940
+ <key>Label</key><string>com.nova.${name}</string>
1941
+ <key>ProgramArguments</key><array>${args.map(a => `<string>${a}</string>`).join('')}</array>
1942
+ <key>RunAtLoad</key><true/>
1943
+ ${opts.hidden ? '<key>LSUIElement</key><true/>' : ''}
1944
+ </dict></plist>`;
1945
+ fs.writeFileSync(plistPath, plist, 'utf8');
1946
+ _run(`launchctl load "${plistPath}" 2>/dev/null`);
1947
+ return true;
1948
+ }
1949
+ if (IS_WINDOWS) {
1950
+ _run(`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v "${name}" /t REG_SZ /d "${command.replace(/"/g, '\\"')}" /f`);
1951
+ return true;
1952
+ }
1953
+ if (IS_ANDROID) {
1954
+ // Termux:Boot
1955
+ const bootDir = path.join(os.homedir(), '.termux', 'boot');
1956
+ fs.mkdirSync(bootDir, { recursive: true });
1957
+ const scriptPath = path.join(bootDir, `${name}.sh`);
1958
+ fs.writeFileSync(scriptPath, `#!/data/data/com.termux/files/usr/bin/bash\n${command}\n`, 'utf8');
1959
+ fs.chmodSync(scriptPath, 0o755);
1960
+ return true;
1961
+ }
1962
+ if (IS_LINUX) {
1963
+ const autostartDir = path.join(os.homedir(), '.config', 'autostart');
1964
+ fs.mkdirSync(autostartDir, { recursive: true });
1965
+ const desktop = `[Desktop Entry]\nType=Application\nName=${name}\nExec=${command}\nX-GNOME-Autostart-enabled=true\nHidden=false\n`;
1966
+ fs.writeFileSync(path.join(autostartDir, `${name}.desktop`), desktop, 'utf8');
1967
+ return true;
1968
+ }
1969
+ return false;
1970
+ },
1971
+
1972
+ /**
1973
+ * Remove a startup item.
1974
+ * @param {string} name
1975
+ */
1976
+ remove(name) {
1977
+ name = String(name);
1978
+ if (IS_MACOS) {
1979
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `com.nova.${name}.plist`);
1980
+ if (fs.existsSync(plistPath)) {
1981
+ _run(`launchctl unload "${plistPath}" 2>/dev/null`);
1982
+ fs.unlinkSync(plistPath);
1983
+ return true;
1984
+ }
1985
+ return false;
1986
+ }
1987
+ if (IS_WINDOWS) {
1988
+ _run(`reg delete "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v "${name}" /f 2>nul`);
1989
+ return true;
1990
+ }
1991
+ if (IS_ANDROID) {
1992
+ const scriptPath = path.join(os.homedir(), '.termux', 'boot', `${name}.sh`);
1993
+ if (fs.existsSync(scriptPath)) { fs.unlinkSync(scriptPath); return true; }
1994
+ return false;
1995
+ }
1996
+ if (IS_LINUX) {
1997
+ const desktop = path.join(os.homedir(), '.config', 'autostart', `${name}.desktop`);
1998
+ if (fs.existsSync(desktop)) { fs.unlinkSync(desktop); return true; }
1999
+ return false;
2000
+ }
2001
+ return false;
2002
+ },
2003
+
2004
+ /**
2005
+ * List all startup items added via kitos.
2006
+ * @returns {Array<{ name, command, enabled }>}
2007
+ */
2008
+ list() {
2009
+ if (IS_MACOS) {
2010
+ const dir = path.join(os.homedir(), 'Library', 'LaunchAgents');
2011
+ if (!fs.existsSync(dir)) return [];
2012
+ return fs.readdirSync(dir).filter(f => f.startsWith('com.nova.') && f.endsWith('.plist')).map(f => ({ name: f.replace(/^com\.nova\./, '').replace(/\.plist$/, ''), file: path.join(dir, f) }));
2013
+ }
2014
+ if (IS_LINUX || IS_ANDROID) {
2015
+ const dir = IS_ANDROID
2016
+ ? path.join(os.homedir(), '.termux', 'boot')
2017
+ : path.join(os.homedir(), '.config', 'autostart');
2018
+ if (!fs.existsSync(dir)) return [];
2019
+ return fs.readdirSync(dir).map(f => ({ name: f.replace(/\.(desktop|sh)$/, ''), file: path.join(dir, f) }));
2020
+ }
2021
+ if (IS_WINDOWS) {
2022
+ const r = _run('reg query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"').stdout;
2023
+ return r.split('\n').filter(l => l.trim() && !l.includes('HKEY')).map(l => {
2024
+ const m = l.trim().match(/^(\S+)\s+REG_SZ\s+(.+)/);
2025
+ return m ? { name: m[1], command: m[2] } : null;
2026
+ }).filter(Boolean);
2027
+ }
2028
+ return [];
2029
+ },
2030
+
2031
+ /** Enable/disable a startup item (Linux .desktop files). */
2032
+ setEnabled(name, enabled) {
2033
+ if (IS_LINUX) {
2034
+ const desktop = path.join(os.homedir(), '.config', 'autostart', `${name}.desktop`);
2035
+ if (!fs.existsSync(desktop)) return false;
2036
+ let content = fs.readFileSync(desktop, 'utf8');
2037
+ content = content.replace(/X-GNOME-Autostart-enabled=\w+/, `X-GNOME-Autostart-enabled=${enabled ? 'true' : 'false'}`);
2038
+ content = content.replace(/Hidden=\w+/, `Hidden=${enabled ? 'false' : 'true'}`);
2039
+ fs.writeFileSync(desktop, content, 'utf8');
2040
+ return true;
2041
+ }
2042
+ return false;
2043
+ },
2044
+ };
2045
+
2046
+ // ══════════════════════════════════════════════════════════════════════════════
2047
+ // 18. A11Y — Accessibility
2048
+ // ══════════════════════════════════════════════════════════════════════════════
2049
+
2050
+ const A11y = {
2051
+ /**
2052
+ * Announce a message via the screen reader.
2053
+ * @param {string} message
2054
+ * @param {boolean} [interrupt=false]
2055
+ */
2056
+ announce(message, interrupt = false) {
2057
+ message = String(message);
2058
+ if (IS_MACOS) {
2059
+ _osascript(`tell application "VoiceOver" to output "${_escAS(message)}"`);
2060
+ } else if (IS_LINUX) {
2061
+ // AT-SPI / spd-say
2062
+ if (_run('which spd-say').code === 0) {
2063
+ _spawn('spd-say', interrupt ? ['-C', message] : [message]);
2064
+ } else if (_run('which espeakup').code === 0) {
2065
+ _spawn('espeakup', ['--device', 'default', message]);
2066
+ }
2067
+ } else if (IS_WINDOWS) {
2068
+ _pwsh(`Add-Type -AssemblyName System.speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; $s.Speak('${message.replace(/'/g, "''")}'); $s.Dispose()`);
2069
+ } else if (IS_ANDROID) {
2070
+ TTS.speak(message);
2071
+ }
2072
+ },
2073
+
2074
+ /**
2075
+ * Get focused element info (best-effort via platform tools).
2076
+ * @returns {object|null}
2077
+ */
2078
+ focusedElement() {
2079
+ if (IS_LINUX && _run('which xdotool').code === 0) {
2080
+ const winId = _out('xdotool getactivewindow 2>/dev/null').trim();
2081
+ const name = _out(`xdotool getwindowname ${winId} 2>/dev/null`).trim();
2082
+ const pid = _out(`xdotool getwindowpid ${winId} 2>/dev/null`).trim();
2083
+ return { windowId: winId, title: name, pid: +pid };
2084
+ }
2085
+ if (IS_MACOS) {
2086
+ const r = _osascript(
2087
+ 'tell application "System Events" to return {name, description} of first UI element whose focused is true of (first process whose frontmost is true)'
2088
+ );
2089
+ return { raw: r };
2090
+ }
2091
+ return null;
2092
+ },
2093
+
2094
+ /**
2095
+ * Get the active window title.
2096
+ * @returns {string|null}
2097
+ */
2098
+ activeWindowTitle() {
2099
+ if (IS_MACOS) {
2100
+ return _osascript('tell application "System Events" to return name of first process whose frontmost is true').trim() || null;
2101
+ }
2102
+ if (IS_LINUX && _run('which xdotool').code === 0) {
2103
+ return _out('xdotool getactivewindow getwindowname 2>/dev/null').trim() || null;
2104
+ }
2105
+ if (IS_WINDOWS) {
2106
+ return _pwsh('(Get-Process | Where-Object {$_.MainWindowTitle -ne ""} | Where-Object {$_.MainWindowHandle -ne 0} | Sort-Object CPU -desc | Select-Object -First 1).MainWindowTitle').trim() || null;
2107
+ }
2108
+ return null;
2109
+ },
2110
+
2111
+ /**
2112
+ * List running application windows.
2113
+ * @returns {Array<{ pid, name, title }>}
2114
+ */
2115
+ windows() {
2116
+ if (IS_LINUX && _run('which wmctrl').code === 0) {
2117
+ return _out('wmctrl -l -p').split('\n').filter(Boolean).map(l => {
2118
+ const [id, desktop, pid, host, ...title] = l.trim().split(/\s+/);
2119
+ return { id, desktop, pid: +pid, host, title: title.join(' ') };
2120
+ });
2121
+ }
2122
+ if (IS_MACOS) {
2123
+ const r = _osascript('tell application "System Events" to return {name, unix id} of every process whose background only is false');
2124
+ return r ? [{ raw: r }] : [];
2125
+ }
2126
+ if (IS_WINDOWS) {
2127
+ return _pwsh('Get-Process | Where-Object {$_.MainWindowTitle -ne ""} | ForEach-Object { "$($_.Id)|$($_.ProcessName)|$($_.MainWindowTitle)" }')
2128
+ .split('\n').filter(Boolean).map(l => {
2129
+ const [pid, name, title] = l.split('|');
2130
+ return { pid: +pid, name, title };
2131
+ });
2132
+ }
2133
+ return [];
2134
+ },
2135
+ };
2136
+
2137
+ // ══════════════════════════════════════════════════════════════════════════════
2138
+ // 19. IDLETIME — User Idle Detection
2139
+ // ══════════════════════════════════════════════════════════════════════════════
2140
+
2141
+ const IdleTime = {
2142
+ /**
2143
+ * Get the number of milliseconds the user has been idle.
2144
+ * @returns {number}
2145
+ */
2146
+ get() {
2147
+ if (IS_MACOS) {
2148
+ const r = _out('ioreg -c IOHIDSystem 2>/dev/null | awk \'/HIDIdleTime/{print $NF; exit}\'');
2149
+ // Value in nanoseconds
2150
+ return r ? Math.round(+r / 1_000_000) : 0;
2151
+ }
2152
+ if (IS_LINUX) {
2153
+ if (_run('which xprintidle').code === 0) {
2154
+ return +(_out('xprintidle').trim()) || 0;
2155
+ }
2156
+ if (_run('which xssstate').code === 0) {
2157
+ return +(_out('xssstate -i').trim()) || 0;
2158
+ }
2159
+ // Read /proc/uptime and last input from xinput or loginctl
2160
+ return 0;
2161
+ }
2162
+ if (IS_WINDOWS) {
2163
+ const r = _pwsh(`
2164
+ Add-Type @'
2165
+ using System; using System.Runtime.InteropServices;
2166
+ public class Idle {
2167
+ [DllImport("user32.dll")] static extern bool GetLastInputInfo(ref LASTINPUTINFO p);
2168
+ [StructLayout(LayoutKind.Sequential)] struct LASTINPUTINFO { public uint cbSize; public int dwTime; }
2169
+ public static int IdleMs() {
2170
+ var l = new LASTINPUTINFO(); l.cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf(l);
2171
+ GetLastInputInfo(ref l); return Environment.TickCount - l.dwTime;
2172
+ }
2173
+ }
2174
+ '@
2175
+ [Idle]::IdleMs()
2176
+ `).trim();
2177
+ return +r || 0;
2178
+ }
2179
+ if (IS_ANDROID) {
2180
+ // No direct API; return 0
2181
+ return 0;
2182
+ }
2183
+ return 0;
2184
+ },
2185
+
2186
+ /**
2187
+ * Is the user idle for more than a given threshold?
2188
+ * @param {number} thresholdMs
2189
+ * @returns {boolean}
2190
+ */
2191
+ isIdle(thresholdMs) { return IdleTime.get() >= thresholdMs; },
2192
+
2193
+ /**
2194
+ * Get last input timestamp.
2195
+ * @returns {Date}
2196
+ */
2197
+ lastInputAt() { return new Date(Date.now() - IdleTime.get()); },
2198
+
2199
+ /**
2200
+ * Watch idle state, calling onIdle when idle for thresholdMs, onActive when active again.
2201
+ * @param {number} thresholdMs
2202
+ * @param {Function} onIdle
2203
+ * @param {Function} [onActive]
2204
+ * @param {number} [pollMs=5000]
2205
+ * @returns {{ stop: Function }}
2206
+ */
2207
+ watch(thresholdMs, onIdle, onActive, pollMs = 5000) {
2208
+ let wasIdle = false;
2209
+ const timer = setInterval(() => {
2210
+ const idle = IdleTime.isIdle(thresholdMs);
2211
+ if (idle && !wasIdle) { wasIdle = true; onIdle(); }
2212
+ else if (!idle && wasIdle) { wasIdle = false; if (onActive) onActive(); }
2213
+ }, pollMs);
2214
+ return { stop: () => clearInterval(timer) };
2215
+ },
2216
+ };
2217
+
2218
+ // ══════════════════════════════════════════════════════════════════════════════
2219
+ // 20. SENSORS — Hardware Sensors (CPU temp, fan, GPU)
2220
+ // ══════════════════════════════════════════════════════════════════════════════
2221
+
2222
+ const Sensors = {
2223
+ /**
2224
+ * Get CPU temperature in Celsius.
2225
+ * @returns {number|null}
2226
+ */
2227
+ cpuTemp() {
2228
+ if (IS_MACOS) {
2229
+ // iStats (gem install iStats) or osx-cpu-temp
2230
+ if (_run('which istats').code === 0) {
2231
+ const r = _out('istats cpu temp --value-only 2>/dev/null');
2232
+ return parseFloat(r) || null;
2233
+ }
2234
+ if (_run('which osx-cpu-temp').code === 0) {
2235
+ return parseFloat(_out('osx-cpu-temp')) || null;
2236
+ }
2237
+ // Try powermetrics (requires sudo)
2238
+ return null;
2239
+ }
2240
+ if (IS_LINUX) {
2241
+ // /sys/class/thermal
2242
+ const thermal = '/sys/class/thermal';
2243
+ if (fs.existsSync(thermal)) {
2244
+ const zones = fs.readdirSync(thermal).filter(d => d.startsWith('thermal_zone'));
2245
+ for (const zone of zones) {
2246
+ const type = _out(`cat /sys/class/thermal/${zone}/type 2>/dev/null`).trim();
2247
+ if (!type.includes('cpu') && !type.includes('x86') && !type.includes('acpi')) continue;
2248
+ const temp = _out(`cat /sys/class/thermal/${zone}/temp 2>/dev/null`).trim();
2249
+ if (temp) return +temp / 1000;
2250
+ }
2251
+ // Fallback: first zone
2252
+ if (zones.length > 0) {
2253
+ const temp = _out(`cat /sys/class/thermal/${zones[0]}/temp 2>/dev/null`).trim();
2254
+ if (temp) return +temp / 1000;
2255
+ }
2256
+ }
2257
+ // lm-sensors
2258
+ if (_run('which sensors').code === 0) {
2259
+ const r = _out('sensors 2>/dev/null');
2260
+ const m = r.match(/Core 0:\s+\+?([\d.]+)°C/);
2261
+ return m ? parseFloat(m[1]) : null;
2262
+ }
2263
+ }
2264
+ if (IS_WINDOWS) {
2265
+ const r = _pwsh('(Get-WmiObject -Namespace root/wmi -Class MSAcpi_ThermalZoneTemperature).CurrentTemperature');
2266
+ return r ? Math.round((+r.trim() - 2732) / 10) : null;
2267
+ }
2268
+ if (IS_ANDROID) {
2269
+ const paths = ['/sys/class/thermal/thermal_zone0/temp', '/sys/devices/virtual/thermal/thermal_zone0/temp'];
2270
+ for (const p of paths) {
2271
+ if (fs.existsSync(p)) {
2272
+ const raw = +fs.readFileSync(p, 'utf8').trim();
2273
+ return raw > 1000 ? raw / 1000 : raw;
2274
+ }
2275
+ }
2276
+ }
2277
+ return null;
2278
+ },
2279
+
2280
+ /**
2281
+ * Get all available temperatures.
2282
+ * @returns {Array<{ name, temp, high, critical }>}
2283
+ */
2284
+ temperatures() {
2285
+ if (IS_LINUX && _run('which sensors').code === 0) {
2286
+ const r = _out('sensors -j 2>/dev/null');
2287
+ try {
2288
+ const data = JSON.parse(r);
2289
+ const result = [];
2290
+ for (const [chip, sensors] of Object.entries(data)) {
2291
+ for (const [sensor, values] of Object.entries(sensors)) {
2292
+ if (typeof values !== 'object') continue;
2293
+ const temp = Object.values(values).find(v => String(v).match(/\d+\.\d+/) && typeof v === 'number');
2294
+ if (temp) result.push({ chip, sensor, temp });
2295
+ }
2296
+ }
2297
+ return result;
2298
+ } catch {}
2299
+ }
2300
+ if (IS_ANDROID) {
2301
+ const base = '/sys/class/thermal';
2302
+ if (!fs.existsSync(base)) return [];
2303
+ return fs.readdirSync(base).filter(d => d.startsWith('thermal_zone')).map(zone => {
2304
+ const type = _out(`cat ${base}/${zone}/type 2>/dev/null`).trim();
2305
+ const temp = _out(`cat ${base}/${zone}/temp 2>/dev/null`).trim();
2306
+ return temp ? { name: type || zone, temp: +temp > 1000 ? +temp/1000 : +temp } : null;
2307
+ }).filter(Boolean);
2308
+ }
2309
+ const cpu = Sensors.cpuTemp();
2310
+ return cpu !== null ? [{ name: 'CPU', temp: cpu }] : [];
2311
+ },
2312
+
2313
+ /**
2314
+ * Get GPU temperature in Celsius.
2315
+ * @returns {number|null}
2316
+ */
2317
+ gpuTemp() {
2318
+ if (IS_LINUX) {
2319
+ // NVIDIA
2320
+ if (_run('which nvidia-smi').code === 0) {
2321
+ const r = _out('nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits 2>/dev/null');
2322
+ return r ? +r.trim() : null;
2323
+ }
2324
+ // AMD via sensors
2325
+ if (_run('which sensors').code === 0) {
2326
+ const r = _out('sensors 2>/dev/null');
2327
+ const m = r.match(/edge:\s+\+?([\d.]+)°C/);
2328
+ return m ? parseFloat(m[1]) : null;
2329
+ }
2330
+ }
2331
+ if (IS_WINDOWS) {
2332
+ const r = _pwsh('(Get-WmiObject -Namespace root/OpenHardwareMonitor -Class Sensor | Where-Object {$_.SensorType -eq "Temperature" -and $_.Name -like "*GPU*"} | Select-Object -First 1).Value');
2333
+ return r ? parseFloat(r) : null;
2334
+ }
2335
+ return null;
2336
+ },
2337
+
2338
+ /**
2339
+ * Get CPU fan speed(s) in RPM.
2340
+ * @returns {number|null}
2341
+ */
2342
+ fanSpeed() {
2343
+ if (IS_MACOS && _run('which istats').code === 0) {
2344
+ const r = _out('istats fan speed --value-only 2>/dev/null');
2345
+ return parseFloat(r) || null;
2346
+ }
2347
+ if (IS_LINUX && _run('which sensors').code === 0) {
2348
+ const r = _out('sensors 2>/dev/null');
2349
+ const m = r.match(/fan\d+:\s+([\d]+) RPM/i);
2350
+ return m ? +m[1] : null;
2351
+ }
2352
+ if (IS_WINDOWS) {
2353
+ const r = _pwsh('(Get-WmiObject Win32_Fan).DesiredSpeed');
2354
+ return r ? +r.trim() : null;
2355
+ }
2356
+ return null;
2357
+ },
2358
+
2359
+ /**
2360
+ * Get all sensor readings.
2361
+ * @returns {object}
2362
+ */
2363
+ all() {
2364
+ return {
2365
+ cpuTemp: Sensors.cpuTemp(),
2366
+ gpuTemp: Sensors.gpuTemp(),
2367
+ fanSpeed: Sensors.fanSpeed(),
2368
+ temps: Sensors.temperatures(),
2369
+ };
2370
+ },
2371
+ };
2372
+
2373
+ // ══════════════════════════════════════════════════════════════════════════════
2374
+ // 21. LOCATION — GPS / IP Geolocation
2375
+ // ══════════════════════════════════════════════════════════════════════════════
2376
+
2377
+ const Location = {
2378
+ /**
2379
+ * Get current location.
2380
+ * Falls back from GPS → CoreLocation → IP-based.
2381
+ * @param {object} [opts]
2382
+ * @param {boolean} [opts.ipFallback=true]
2383
+ * @returns {Promise<{ lat, lon, accuracy, source }>}
2384
+ */
2385
+ async get(opts = {}) {
2386
+ const ipFallback = opts.ipFallback !== false;
2387
+
2388
+ if (IS_ANDROID) {
2389
+ try {
2390
+ const r = _out('termux-location -r once -p gps 2>/dev/null');
2391
+ const j = JSON.parse(r);
2392
+ if (j.latitude !== undefined) return { lat: j.latitude, lon: j.longitude, accuracy: j.accuracy, altitude: j.altitude, source: 'gps' };
2393
+ } catch {}
2394
+ // Network fallback
2395
+ try {
2396
+ const r = _out('termux-location -r once -p network 2>/dev/null');
2397
+ const j = JSON.parse(r);
2398
+ if (j.latitude !== undefined) return { lat: j.latitude, lon: j.longitude, accuracy: j.accuracy, source: 'network' };
2399
+ } catch {}
2400
+ }
2401
+
2402
+ if (IS_MACOS && _run('which CoreLocationCLI').code === 0) {
2403
+ try {
2404
+ const r = _out('CoreLocationCLI -format "%latitude,%longitude" 2>/dev/null');
2405
+ const [lat, lon] = r.split(',').map(Number);
2406
+ if (!isNaN(lat)) return { lat, lon, accuracy: null, source: 'corelocation' };
2407
+ } catch {}
2408
+ }
2409
+
2410
+ if (ipFallback) return Location.ipLocation();
2411
+ return null;
2412
+ },
2413
+
2414
+ /**
2415
+ * Get location via IP geolocation API.
2416
+ * @returns {Promise<{ lat, lon, city, country, ip, source }>}
2417
+ */
2418
+ async ipLocation() {
2419
+ const http = require('http');
2420
+ const https = require('https');
2421
+ return new Promise((resolve) => {
2422
+ const req = https.get('https://ipapi.co/json/', { timeout: 5000 }, (res) => {
2423
+ let data = '';
2424
+ res.on('data', c => data += c);
2425
+ res.on('end', () => {
2426
+ try {
2427
+ const j = JSON.parse(data);
2428
+ resolve({ lat: j.latitude, lon: j.longitude, city: j.city, country: j.country_name, ip: j.ip, source: 'ip' });
2429
+ } catch { resolve(null); }
2430
+ });
2431
+ });
2432
+ req.on('error', () => {
2433
+ // Fallback: ip-api.com (HTTP)
2434
+ const r2 = http.get('http://ip-api.com/json/', { timeout: 5000 }, (res) => {
2435
+ let data = '';
2436
+ res.on('data', c => data += c);
2437
+ res.on('end', () => {
2438
+ try {
2439
+ const j = JSON.parse(data);
2440
+ resolve({ lat: j.lat, lon: j.lon, city: j.city, country: j.country, ip: j.query, source: 'ip' });
2441
+ } catch { resolve(null); }
2442
+ });
2443
+ });
2444
+ r2.on('error', () => resolve(null));
2445
+ });
2446
+ });
2447
+ },
2448
+
2449
+ /**
2450
+ * Calculate distance between two coordinates in km (Haversine).
2451
+ * @param {number} lat1
2452
+ * @param {number} lon1
2453
+ * @param {number} lat2
2454
+ * @param {number} lon2
2455
+ * @returns {number}
2456
+ */
2457
+ distance(lat1, lon1, lat2, lon2) {
2458
+ const R = 6371;
2459
+ const dLat = (lat2 - lat1) * Math.PI / 180;
2460
+ const dLon = (lon2 - lon1) * Math.PI / 180;
2461
+ const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLon/2)**2;
2462
+ return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
2463
+ },
2464
+ };
2465
+
2466
+ // ══════════════════════════════════════════════════════════════════════════════
2467
+ // 22. VIBRATE — Haptic Feedback
2468
+ // ══════════════════════════════════════════════════════════════════════════════
2469
+
2470
+ const Vibrate = {
2471
+ /**
2472
+ * Vibrate the device.
2473
+ * @param {number} [durationMs=500]
2474
+ * @param {object} [opts]
2475
+ * @param {number} [opts.force=1.0] 0.0–1.0 intensity (Android only)
2476
+ */
2477
+ vibrate(durationMs = 500, opts = {}) {
2478
+ durationMs = Math.max(1, Math.round(durationMs));
2479
+ if (IS_ANDROID) {
2480
+ const args = ['termux-vibrate', '-d', String(durationMs)];
2481
+ if (opts.force !== undefined) args.push('-f');
2482
+ _spawn(args[0], args.slice(1));
2483
+ } else if (IS_LINUX) {
2484
+ // Some Linux devices support vibration via /sys/class/leds
2485
+ const vibPath = '/sys/class/leds/vibrator';
2486
+ if (fs.existsSync(vibPath)) {
2487
+ try {
2488
+ fs.writeFileSync(path.join(vibPath, 'brightness'), '255');
2489
+ setTimeout(() => { try { fs.writeFileSync(path.join(vibPath, 'brightness'), '0'); } catch {} }, durationMs);
2490
+ } catch {}
2491
+ }
2492
+ // Fallback: beep (at least give audio feedback)
2493
+ else Audio.beep(1);
2494
+ } else {
2495
+ // macOS / Windows: audio bell as fallback
2496
+ Audio.beep(1);
2497
+ }
2498
+ },
2499
+
2500
+ /**
2501
+ * Vibrate a pattern.
2502
+ * @param {number[]} pattern alternating on/off durations in ms e.g. [100, 200, 100]
2503
+ */
2504
+ pattern(pattern) {
2505
+ if (IS_ANDROID) {
2506
+ // Termux doesn't support pattern directly; simulate with delays
2507
+ let delay = 0;
2508
+ for (let i = 0; i < pattern.length; i++) {
2509
+ if (i % 2 === 0) {
2510
+ setTimeout(() => Vibrate.vibrate(pattern[i]), delay);
2511
+ }
2512
+ delay += pattern[i];
2513
+ }
2514
+ } else {
2515
+ for (let i = 0; i < pattern.length; i += 2) {
2516
+ if (i < pattern.length) setTimeout(() => Vibrate.vibrate(pattern[i]), i * 500);
2517
+ }
2518
+ }
2519
+ },
2520
+
2521
+ /** Check if vibration is supported. */
2522
+ isSupported() {
2523
+ if (IS_ANDROID) return _run('which termux-vibrate').code === 0;
2524
+ if (IS_LINUX) return fs.existsSync('/sys/class/leds/vibrator');
2525
+ return false;
2526
+ },
2527
+ };
2528
+
2529
+ // ══════════════════════════════════════════════════════════════════════════════
2530
+ // KITX11 — Low-level X11 window management (Linux only)
2531
+ // ══════════════════════════════════════════════════════════════════════════════
2532
+
2533
+ const X11 = {
2534
+ /**
2535
+ * Is X11 available?
2536
+ * @returns {boolean}
2537
+ */
2538
+ isAvailable() {
2539
+ return (IS_LINUX || IS_ANDROID) &&
2540
+ (!!process.env.DISPLAY || !!process.env.WAYLAND_DISPLAY) &&
2541
+ _run('which xdotool').code === 0;
2542
+ },
2543
+
2544
+ // ── Window management ─────────────────────────────────────────────────────
2545
+
2546
+ /**
2547
+ * List all open windows.
2548
+ * @returns {Array<{ id, pid, title, desktop, x, y, w, h }>}
2549
+ */
2550
+ listWindows() {
2551
+ const wmctrl = _run('which wmctrl').code === 0;
2552
+ const xdotool = _run('which xdotool').code === 0;
2553
+ if (wmctrl) {
2554
+ return _out('wmctrl -l -G -p').split('\n').filter(Boolean).map(l => {
2555
+ const [id, desktop, pid, x, y, w, h, ...title] = l.trim().split(/\s+/);
2556
+ return { id, desktop: +desktop, pid: +pid, x: +x, y: +y, w: +w, h: +h, title: title.join(' ') };
2557
+ });
2558
+ }
2559
+ if (xdotool) {
2560
+ const ids = _out('xdotool search --onlyvisible --name "" 2>/dev/null').split('\n').filter(Boolean);
2561
+ return ids.map(id => {
2562
+ const title = _out(`xdotool getwindowname ${id} 2>/dev/null`).trim();
2563
+ const pid = _out(`xdotool getwindowpid ${id} 2>/dev/null`).trim();
2564
+ return { id, pid: +pid, title };
2565
+ });
2566
+ }
2567
+ return [];
2568
+ },
2569
+
2570
+ /**
2571
+ * Get the active (focused) window ID.
2572
+ * @returns {string|null}
2573
+ */
2574
+ activeWindow() {
2575
+ if (_run('which xdotool').code === 0) return _out('xdotool getactivewindow 2>/dev/null').trim() || null;
2576
+ if (_run('which wmctrl').code === 0) return _out('wmctrl -d').match(/\* (\w+)/)?.[1] ?? null;
2577
+ return null;
2578
+ },
2579
+
2580
+ /**
2581
+ * Focus a window by its ID.
2582
+ * @param {string} windowId
2583
+ */
2584
+ focusWindow(windowId) {
2585
+ if (_run('which xdotool').code === 0) _spawn('xdotool', ['windowactivate', '--sync', String(windowId)]);
2586
+ else if (_run('which wmctrl').code === 0) _spawn('wmctrl', ['-ia', String(windowId)]);
2587
+ },
2588
+
2589
+ /**
2590
+ * Find windows by title pattern.
2591
+ * @param {string} pattern regex or substring
2592
+ * @returns {string[]} window IDs
2593
+ */
2594
+ findByTitle(pattern) {
2595
+ if (_run('which xdotool').code === 0) {
2596
+ return _out(`xdotool search --name "${pattern.replace(/"/g, '\\"')}" 2>/dev/null`).split('\n').filter(Boolean);
2597
+ }
2598
+ if (_run('which wmctrl').code === 0) {
2599
+ return _out('wmctrl -l').split('\n').filter(l => l.toLowerCase().includes(pattern.toLowerCase()))
2600
+ .map(l => l.trim().split(/\s+/)[0]);
2601
+ }
2602
+ return [];
2603
+ },
2604
+
2605
+ /**
2606
+ * Find windows by PID.
2607
+ * @param {number} pid
2608
+ * @returns {string[]} window IDs
2609
+ */
2610
+ findByPID(pid) {
2611
+ if (_run('which xdotool').code === 0) {
2612
+ return _out(`xdotool search --pid ${pid} 2>/dev/null`).split('\n').filter(Boolean);
2613
+ }
2614
+ return [];
2615
+ },
2616
+
2617
+ /**
2618
+ * Close a window gracefully.
2619
+ * @param {string} windowId
2620
+ */
2621
+ closeWindow(windowId) {
2622
+ if (_run('which wmctrl').code === 0) _spawn('wmctrl', ['-ic', String(windowId)]);
2623
+ else if (_run('which xdotool').code === 0) _spawn('xdotool', ['windowclose', String(windowId)]);
2624
+ },
2625
+
2626
+ /**
2627
+ * Kill a window (force close).
2628
+ * @param {string} windowId
2629
+ */
2630
+ killWindow(windowId) {
2631
+ if (_run('which xdotool').code === 0) _spawn('xdotool', ['windowkill', String(windowId)]);
2632
+ else _spawn('xkill', []);
2633
+ },
2634
+
2635
+ /**
2636
+ * Move a window to given coordinates.
2637
+ * @param {string} windowId
2638
+ * @param {number} x
2639
+ * @param {number} y
2640
+ */
2641
+ moveWindow(windowId, x, y) {
2642
+ if (_run('which wmctrl').code === 0) {
2643
+ _spawn('wmctrl', ['-ir', String(windowId), '-e', `0,${x},${y},-1,-1`]);
2644
+ } else if (_run('which xdotool').code === 0) {
2645
+ _spawn('xdotool', ['windowmove', String(windowId), String(x), String(y)]);
2646
+ }
2647
+ },
2648
+
2649
+ /**
2650
+ * Resize a window.
2651
+ * @param {string} windowId
2652
+ * @param {number} w
2653
+ * @param {number} h
2654
+ */
2655
+ resizeWindow(windowId, w, h) {
2656
+ if (_run('which wmctrl').code === 0) {
2657
+ _spawn('wmctrl', ['-ir', String(windowId), '-e', `0,-1,-1,${w},${h}`]);
2658
+ } else if (_run('which xdotool').code === 0) {
2659
+ _spawn('xdotool', ['windowsize', String(windowId), String(w), String(h)]);
2660
+ }
2661
+ },
2662
+
2663
+ /**
2664
+ * Move and resize in one call.
2665
+ * @param {string} windowId
2666
+ * @param {number} x
2667
+ * @param {number} y
2668
+ * @param {number} w
2669
+ * @param {number} h
2670
+ */
2671
+ setGeometry(windowId, x, y, w, h) {
2672
+ if (_run('which wmctrl').code === 0) {
2673
+ _spawn('wmctrl', ['-ir', String(windowId), '-e', `0,${x},${y},${w},${h}`]);
2674
+ } else if (_run('which xdotool').code === 0) {
2675
+ _spawn('xdotool', ['windowmove', String(windowId), String(x), String(y)]);
2676
+ _spawn('xdotool', ['windowsize', String(windowId), String(w), String(h)]);
2677
+ }
2678
+ },
2679
+
2680
+ /**
2681
+ * Get window geometry.
2682
+ * @param {string} windowId
2683
+ * @returns {{ x, y, w, h }|null}
2684
+ */
2685
+ getGeometry(windowId) {
2686
+ if (_run('which xdotool').code === 0) {
2687
+ const geo = _out(`xdotool getwindowgeometry ${windowId} 2>/dev/null`);
2688
+ const pos = geo.match(/Position: (\d+),(\d+)/);
2689
+ const siz = geo.match(/Geometry: (\d+)x(\d+)/);
2690
+ if (pos && siz) return { x: +pos[1], y: +pos[2], w: +siz[1], h: +siz[2] };
2691
+ }
2692
+ if (_run('which wmctrl').code === 0) {
2693
+ const r = _out(`wmctrl -l -G | grep ${windowId}`);
2694
+ const p = r.trim().split(/\s+/);
2695
+ if (p.length >= 8) return { x: +p[3], y: +p[4], w: +p[5], h: +p[6] };
2696
+ }
2697
+ return null;
2698
+ },
2699
+
2700
+ /**
2701
+ * Maximize a window.
2702
+ * @param {string} windowId
2703
+ */
2704
+ maximize(windowId) {
2705
+ if (_run('which wmctrl').code === 0) {
2706
+ _spawn('wmctrl', ['-ir', String(windowId), '-b', 'add,maximized_vert,maximized_horz']);
2707
+ } else if (_run('which xdotool').code === 0) {
2708
+ _spawn('xdotool', ['windowmove', String(windowId), '0', '0']);
2709
+ const s = Screen.size();
2710
+ _spawn('xdotool', ['windowsize', String(windowId), String(s.width), String(s.height)]);
2711
+ }
2712
+ },
2713
+
2714
+ /**
2715
+ * Minimize a window.
2716
+ * @param {string} windowId
2717
+ */
2718
+ minimize(windowId) {
2719
+ if (_run('which xdotool').code === 0) _spawn('xdotool', ['windowminimize', String(windowId)]);
2720
+ else if (_run('which wmctrl').code === 0) _spawn('wmctrl', ['-ir', String(windowId), '-b', 'add,hidden']);
2721
+ },
2722
+
2723
+ /**
2724
+ * Restore/unminimize a window.
2725
+ * @param {string} windowId
2726
+ */
2727
+ restore(windowId) {
2728
+ if (_run('which wmctrl').code === 0) _spawn('wmctrl', ['-ir', String(windowId), '-b', 'remove,maximized_vert,maximized_horz,hidden']);
2729
+ else if (_run('which xdotool').code === 0) _spawn('xdotool', ['windowactivate', String(windowId)]);
2730
+ },
2731
+
2732
+ /**
2733
+ * Always-on-top (raise above all windows).
2734
+ * @param {string} windowId
2735
+ * @param {boolean} [on=true]
2736
+ */
2737
+ alwaysOnTop(windowId, on = true) {
2738
+ if (_run('which wmctrl').code === 0) {
2739
+ _spawn('wmctrl', ['-ir', String(windowId), '-b', `${on ? 'add' : 'remove'},above`]);
2740
+ }
2741
+ },
2742
+
2743
+ /**
2744
+ * Set window opacity (0.0–1.0). Requires a compositor.
2745
+ * @param {string} windowId
2746
+ * @param {number} opacity
2747
+ */
2748
+ setOpacity(windowId, opacity) {
2749
+ opacity = Math.max(0, Math.min(1, opacity));
2750
+ const value = Math.round(opacity * 0xFFFFFFFF);
2751
+ if (_run('which xprop').code === 0) {
2752
+ _spawn('xprop', ['-id', String(windowId), '-f', '_NET_WM_WINDOW_OPACITY', '32c', '-set', '_NET_WM_WINDOW_OPACITY', String(value)]);
2753
+ }
2754
+ },
2755
+
2756
+ /**
2757
+ * Get window class / application name.
2758
+ * @param {string} windowId
2759
+ * @returns {{ class: string, name: string }|null}
2760
+ */
2761
+ getClass(windowId) {
2762
+ if (_run('which xprop').code === 0) {
2763
+ const r = _out(`xprop -id ${windowId} WM_CLASS 2>/dev/null`);
2764
+ const m = r.match(/WM_CLASS.*= "([^"]+)", "([^"]+)"/);
2765
+ return m ? { class: m[1], name: m[2] } : null;
2766
+ }
2767
+ return null;
2768
+ },
2769
+
2770
+ // ── Virtual Desktops / Workspaces ─────────────────────────────────────────
2771
+
2772
+ /**
2773
+ * Get current desktop/workspace number.
2774
+ * @returns {number}
2775
+ */
2776
+ currentDesktop() {
2777
+ if (_run('which wmctrl').code === 0) {
2778
+ const r = _out('wmctrl -d');
2779
+ const m = r.match(/^(\d+)\s+\*/m);
2780
+ return m ? +m[1] : 0;
2781
+ }
2782
+ if (_run('which xdotool').code === 0) {
2783
+ return +(_out('xdotool get_desktop 2>/dev/null').trim()) || 0;
2784
+ }
2785
+ return 0;
2786
+ },
2787
+
2788
+ /**
2789
+ * Switch to a desktop by number.
2790
+ * @param {number} n 0-indexed
2791
+ */
2792
+ switchDesktop(n) {
2793
+ if (_run('which wmctrl').code === 0) _spawn('wmctrl', ['-s', String(n)]);
2794
+ else if (_run('which xdotool').code === 0) _spawn('xdotool', ['set_desktop', String(n)]);
2795
+ },
2796
+
2797
+ /**
2798
+ * Move window to a desktop.
2799
+ * @param {string} windowId
2800
+ * @param {number} desktop 0-indexed
2801
+ */
2802
+ moveToDesktop(windowId, desktop) {
2803
+ if (_run('which wmctrl').code === 0) _spawn('wmctrl', ['-ir', String(windowId), '-t', String(desktop)]);
2804
+ else if (_run('which xdotool').code === 0) _spawn('xdotool', ['set_desktop_for_window', String(windowId), String(desktop)]);
2805
+ },
2806
+
2807
+ /**
2808
+ * List all virtual desktops.
2809
+ * @returns {Array<{ id, name, active }>}
2810
+ */
2811
+ listDesktops() {
2812
+ if (_run('which wmctrl').code === 0) {
2813
+ return _out('wmctrl -d').split('\n').filter(Boolean).map(l => {
2814
+ const parts = l.trim().split(/\s+/);
2815
+ return { id: +parts[0], active: parts[1] === '*', name: parts.slice(-1)[0] };
2816
+ });
2817
+ }
2818
+ return [];
2819
+ },
2820
+
2821
+ // ── X11 Properties ────────────────────────────────────────────────────────
2822
+
2823
+ /**
2824
+ * Get an X11 property from a window.
2825
+ * @param {string} windowId
2826
+ * @param {string} property e.g. '_NET_WM_STATE', 'WM_NAME'
2827
+ * @returns {string}
2828
+ */
2829
+ getProperty(windowId, property) {
2830
+ if (_run('which xprop').code === 0) {
2831
+ return _out(`xprop -id ${windowId} ${property} 2>/dev/null`);
2832
+ }
2833
+ return '';
2834
+ },
2835
+
2836
+ /**
2837
+ * Set an X11 string property on a window.
2838
+ * @param {string} windowId
2839
+ * @param {string} property
2840
+ * @param {string} value
2841
+ */
2842
+ setProperty(windowId, property, value) {
2843
+ if (_run('which xprop').code === 0) {
2844
+ _spawn('xprop', ['-id', String(windowId), '-f', property, '8s', '-set', property, value]);
2845
+ }
2846
+ },
2847
+
2848
+ /**
2849
+ * Send an X11 ClientMessage event to a window.
2850
+ * @param {string} windowId
2851
+ * @param {string} msgType e.g. '_NET_CLOSE_WINDOW'
2852
+ * @param {number[]} data up to 5 longs
2853
+ */
2854
+ sendClientMessage(windowId, msgType, data = [0,0,0,0,0]) {
2855
+ if (_run('which xdotool').code === 0) {
2856
+ _spawn('xdotool', ['windowactivate', String(windowId)]);
2857
+ }
2858
+ // Use xdotool or direct Xlib via xdpyinfo — best effort
2859
+ },
2860
+
2861
+ // ── Screen / Display ──────────────────────────────────────────────────────
2862
+
2863
+ /**
2864
+ * Get info about connected X11 outputs (monitors).
2865
+ * @returns {Array<{ name, connected, primary, resolution, position, hz }>}
2866
+ */
2867
+ outputs() {
2868
+ const r = _out('xrandr 2>/dev/null');
2869
+ const outputs = [];
2870
+ for (const line of r.split('\n')) {
2871
+ const m = line.match(/^(\S+)\s+(connected|disconnected)(?: primary)?\s*(?:(\d+)x(\d+)\+(\d+)\+(\d+))?/);
2872
+ if (m) {
2873
+ outputs.push({
2874
+ name: m[1], connected: m[2] === 'connected', primary: line.includes('primary'),
2875
+ resolution: m[3] ? `${m[3]}x${m[4]}` : null,
2876
+ position: m[5] ? { x: +m[5], y: +m[6] } : null,
2877
+ });
2878
+ }
2879
+ }
2880
+ return outputs;
2881
+ },
2882
+
2883
+ /**
2884
+ * Set display resolution for an output.
2885
+ * @param {string} outputName e.g. 'HDMI-1'
2886
+ * @param {string} resolution e.g. '1920x1080'
2887
+ * @param {string} [rate] e.g. '60'
2888
+ */
2889
+ setResolution(outputName, resolution, rate) {
2890
+ const args = ['--output', outputName, '--mode', resolution];
2891
+ if (rate) args.push('--rate', rate);
2892
+ _spawn('xrandr', args);
2893
+ },
2894
+
2895
+ /**
2896
+ * Turn an output on or off.
2897
+ * @param {string} outputName
2898
+ * @param {boolean} on
2899
+ */
2900
+ setOutputEnabled(outputName, on) {
2901
+ _spawn('xrandr', ['--output', outputName, on ? '--auto' : '--off']);
2902
+ },
2903
+
2904
+ /**
2905
+ * Set display rotation.
2906
+ * @param {string} outputName
2907
+ * @param {'normal'|'left'|'right'|'inverted'} rotation
2908
+ */
2909
+ setRotation(outputName, rotation) {
2910
+ _spawn('xrandr', ['--output', outputName, '--rotate', rotation]);
2911
+ },
2912
+
2913
+ // ── Input / XDG ──────────────────────────────────────────────────────────
2914
+
2915
+ /**
2916
+ * List X11 input devices.
2917
+ * @returns {Array<{ id, name, type }>}
2918
+ */
2919
+ listInputDevices() {
2920
+ if (_run('which xinput').code === 0) {
2921
+ return _out('xinput list --short').split('\n').filter(Boolean).map(l => {
2922
+ const m = l.match(/^\s+(.+?)\s+id=(\d+)\s+\[(.+?)\]/);
2923
+ return m ? { name: m[1].trim(), id: +m[2], type: m[3].trim() } : null;
2924
+ }).filter(Boolean);
2925
+ }
2926
+ return [];
2927
+ },
2928
+
2929
+ /**
2930
+ * Enable or disable an input device (e.g. touchpad).
2931
+ * @param {number} deviceId
2932
+ * @param {boolean} enabled
2933
+ */
2934
+ setInputEnabled(deviceId, enabled) {
2935
+ if (_run('which xinput').code === 0) {
2936
+ _spawn('xinput', [enabled ? 'enable' : 'disable', String(deviceId)]);
2937
+ }
2938
+ },
2939
+
2940
+ /**
2941
+ * Set mouse pointer speed/acceleration.
2942
+ * @param {number} speed -1.0 to 1.0 (libinput) or acceleration factor
2943
+ */
2944
+ setPointerSpeed(speed) {
2945
+ if (_run('which xinput').code === 0) {
2946
+ // Try libinput
2947
+ const devices = X11.listInputDevices().filter(d => d.type.includes('pointer'));
2948
+ for (const d of devices) {
2949
+ _spawn('xinput', ['set-prop', String(d.id), 'libinput Accel Speed', String(speed)]);
2950
+ }
2951
+ }
2952
+ },
2953
+
2954
+ // ── Clipboard (X11) ───────────────────────────────────────────────────────
2955
+
2956
+ /** Get X11 clipboard content (CLIPBOARD selection). */
2957
+ getClipboard() { return Keyboard.getClipboard(); },
2958
+
2959
+ /** Get X11 primary selection (middle-click paste). */
2960
+ getPrimary() {
2961
+ if (_run('which xsel').code === 0) return _out('xsel --primary --output');
2962
+ if (_run('which xclip').code === 0) return _out('xclip -selection primary -o');
2963
+ return '';
2964
+ },
2965
+
2966
+ /** Set X11 primary selection. */
2967
+ setPrimary(text) {
2968
+ if (_run('which xsel').code === 0) spawnSync('xsel', ['--primary', '--input'], { input: text, encoding: 'utf8' });
2969
+ else if (_run('which xclip').code === 0) spawnSync('xclip', ['-selection', 'primary'], { input: text, encoding: 'utf8' });
2970
+ },
2971
+
2972
+ // ── Compositor / Effects ──────────────────────────────────────────────────
2973
+
2974
+ /**
2975
+ * Is a compositor running?
2976
+ * @returns {boolean}
2977
+ */
2978
+ hasCompositor() {
2979
+ if (_run('which xprop').code === 0) {
2980
+ return _out('xprop -root _NET_WM_CM_S0 2>/dev/null').trim() !== '';
2981
+ }
2982
+ return false;
2983
+ },
2984
+
2985
+ /**
2986
+ * Get the X11 display name.
2987
+ * @returns {string}
2988
+ */
2989
+ display() { return process.env.DISPLAY ?? ':0'; },
2990
+
2991
+ /**
2992
+ * Get screen DPI via xdpyinfo.
2993
+ * @returns {{ x: number, y: number }|null}
2994
+ */
2995
+ dpi() {
2996
+ const r = _out('xdpyinfo 2>/dev/null');
2997
+ const m = r.match(/resolution:\s+(\d+)x(\d+) dots per inch/);
2998
+ return m ? { x: +m[1], y: +m[2] } : null;
2999
+ },
3000
+
3001
+ /**
3002
+ * Bell / ring terminal bell via X11.
3003
+ */
3004
+ bell() { _run('xbell 2>/dev/null || xdotool key XF86AudioPlay 2>/dev/null'); },
3005
+
3006
+ // ── xdg-open / MIME ───────────────────────────────────────────────────────
3007
+
3008
+ /**
3009
+ * Get default app for a MIME type.
3010
+ * @param {string} mimeType e.g. 'text/html'
3011
+ * @returns {string|null}
3012
+ */
3013
+ defaultApp(mimeType) {
3014
+ if (_run('which xdg-mime').code === 0) {
3015
+ return _out(`xdg-mime query default ${mimeType}`).trim() || null;
3016
+ }
3017
+ return null;
3018
+ },
3019
+
3020
+ /**
3021
+ * Set default app for a MIME type.
3022
+ * @param {string} mimeType
3023
+ * @param {string} desktopFile e.g. 'firefox.desktop'
3024
+ */
3025
+ setDefaultApp(mimeType, desktopFile) {
3026
+ if (_run('which xdg-mime').code === 0) {
3027
+ _spawn('xdg-mime', ['default', desktopFile, mimeType]);
3028
+ }
3029
+ },
3030
+
3031
+ /**
3032
+ * Query MIME type of a file.
3033
+ * @param {string} filePath
3034
+ * @returns {string|null}
3035
+ */
3036
+ mimeType(filePath) {
3037
+ if (_run('which xdg-mime').code === 0) {
3038
+ return _out(`xdg-mime query filetype ${_esc(filePath)}`).trim() || null;
3039
+ }
3040
+ if (_run('which file').code === 0) {
3041
+ return _out(`file --mime-type -b ${_esc(filePath)}`).trim() || null;
3042
+ }
3043
+ return null;
3044
+ },
3045
+ };
3046
+
3047
+ // ══════════════════════════════════════════════════════════════════════════════
3048
+ // PLATFORM CONSTANT
3049
+ // ══════════════════════════════════════════════════════════════════════════════
3050
+
3051
+ const KITOS_PLATFORM = PLATFORM;
3052
+
3053
+ // ══════════════════════════════════════════════════════════════════════════════
3054
+ // EXPORTS
3055
+ // ══════════════════════════════════════════════════════════════════════════════
3056
+
3057
+ module.exports = {
3058
+ kitdef: {
3059
+ // ── 1. Notifications ─────────────────────────────
3060
+ Notify,
3061
+
3062
+ // ── 2. Text-to-Speech ────────────────────────────
3063
+ TTS,
3064
+
3065
+ // ── 3. Native Dialogs ────────────────────────────
3066
+ Dialog,
3067
+
3068
+ // ── 4. System Menu / Tray ────────────────────────
3069
+ Menu,
3070
+
3071
+ // ── 5. Keyboard Simulation ───────────────────────
3072
+ Keyboard,
3073
+
3074
+ // ── 6. Mouse Control ─────────────────────────────
3075
+ Mouse,
3076
+
3077
+ // ── 7. Screen / Screenshots ──────────────────────
3078
+ Screen,
3079
+
3080
+ // ── 8. Audio Playback ────────────────────────────
3081
+ Audio,
3082
+
3083
+ // ── 9. Camera ────────────────────────────────────
3084
+ Camera,
3085
+
3086
+ // ── 10. Power / Battery ──────────────────────────
3087
+ Power,
3088
+
3089
+ // ── 11. Display Brightness ───────────────────────
3090
+ Brightness,
3091
+
3092
+ // ── 12. WiFi ─────────────────────────────────────
3093
+ Wifi,
3094
+
3095
+ // ── 13. Bluetooth ────────────────────────────────
3096
+ Bluetooth,
3097
+
3098
+ // ── 14. Volume (fine-grained) ────────────────────
3099
+ Volume,
3100
+
3101
+ // ── 15. Trash / Recycle ──────────────────────────
3102
+ Trash,
3103
+
3104
+ // ── 16. Open / Reveal ────────────────────────────
3105
+ Open,
3106
+
3107
+ // ── 17. Autostart ────────────────────────────────
3108
+ Autostart,
3109
+
3110
+ // ── 18. Accessibility ────────────────────────────
3111
+ A11y,
3112
+
3113
+ // ── 19. Idle Time ────────────────────────────────
3114
+ IdleTime,
3115
+
3116
+ // ── 20. Hardware Sensors ─────────────────────────
3117
+ Sensors,
3118
+
3119
+ // ── 21. Location ─────────────────────────────────
3120
+ Location,
3121
+
3122
+ // ── 22. Haptic Vibration ─────────────────────────
3123
+ Vibrate,
3124
+
3125
+ // ── 23. X11 Window Manager ───────────────────────
3126
+ X11,
3127
+
3128
+ // ── Platform info ─────────────────────────────────
3129
+ platform: KITOS_PLATFORM,
3130
+ IS_MACOS,
3131
+ IS_WINDOWS,
3132
+ IS_LINUX,
3133
+ IS_ANDROID,
3134
+ }
3135
+ };