opentray 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,21 +1,32 @@
1
1
  #!/usr/bin/env node
2
- import { t as createClient } from "./client-Db-tztnj.mjs";
3
- import { a as inspectDaemon, c as stopDaemon, i as createNodeDaemonDriver, n as resolveDaemonPaths, o as restartDaemon, r as readPackageVersion, s as startDaemon, t as connectLocalBroker } from "./local-broker-mQmeVLF5.mjs";
2
+ import { t as createClient } from "./client-B_zxQOo5.mjs";
3
+ import { a as inspectDaemon, c as stopDaemon, i as createNodeDaemonDriver, n as resolveDaemonPaths, o as restartDaemon, r as readPackageVersion, s as startDaemon, t as connectLocalBroker } from "./local-broker-CEIHPBcK.mjs";
4
4
  import { homedir } from "node:os";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { existsSync, realpathSync } from "node:fs";
7
7
  import { dirname, join, resolve } from "node:path";
8
8
  //#region src/smoke/daemon-lynx.ts
9
9
  const menuLabels$1 = new Map([
10
- [1, "Show Fit Window"],
11
- [2, "Show Fixed Window"],
12
- [3, "Hide Window"],
10
+ [1, "Show Window"],
11
+ [2, "Hide Window"],
13
12
  [99, "Quit Smoke"]
14
13
  ]);
15
14
  const DEFAULT_LYNX_BUNDLE_ENV = "OPENTRAY_LYNX_BUNDLE";
15
+ const DEFAULT_LYNX_HOST_FEATURES_ENV = "OPENTRAY_LYNX_HOST_FEATURES";
16
16
  const DEFAULT_REVIEW_BUNDLE_RELATIVE_PATH = "assets/lynx-review/main.lynx.bundle";
17
+ const LYNX_HOST_FEATURES = [
18
+ "nativeWindowApi",
19
+ "bindWindowGlobals",
20
+ "nativeScreenApi",
21
+ "bindScreenGlobals",
22
+ "frameless"
23
+ ];
17
24
  const runDaemonLynxSmoke = async (options = {}) => {
18
25
  const bundlePath = resolveLynxBundlePath(options.bundlePath);
26
+ const hostFeatures = resolveLynxHostFeatures(options.featureExpression);
27
+ if (process.env.OPENTRAY_LYNX_DEBUG) console.log(`lynx debug modes: ${process.env.OPENTRAY_LYNX_DEBUG}`);
28
+ if (process.env[DEFAULT_LYNX_HOST_FEATURES_ENV]) console.log(`lynx host features: ${process.env[DEFAULT_LYNX_HOST_FEATURES_ENV]}`);
29
+ if (process.env.OPENTRAY_LYNX_DEBUG_LOG_PATH) console.log(`lynx debug log path: ${process.env.OPENTRAY_LYNX_DEBUG_LOG_PATH}`);
19
30
  const connection = await connectLocalBroker();
20
31
  const client = createClient(connection, { requestIdPrefix: "daemon-lynx" });
21
32
  console.log(`connected: endpoint=${connection.endpoint} session=${connection.sessionId}`);
@@ -37,16 +48,11 @@ const runDaemonLynxSmoke = async (options = {}) => {
37
48
  {
38
49
  type: "item",
39
50
  id: 1,
40
- title: "Show Fit Window"
51
+ title: "Show Window"
41
52
  },
42
53
  {
43
54
  type: "item",
44
55
  id: 2,
45
- title: "Show Fixed Window"
46
- },
47
- {
48
- type: "item",
49
- id: 3,
50
56
  title: "Hide Window"
51
57
  },
52
58
  { type: "separator" },
@@ -69,21 +75,13 @@ const runDaemonLynxSmoke = async (options = {}) => {
69
75
  let closed = false;
70
76
  let exitTimer;
71
77
  let resolveClosed;
72
- const show = async (mode) => {
73
- await tray.commandExtension("lynx", {
74
- type: "show",
78
+ const show = async () => {
79
+ const command = createLynxShowCommand({
75
80
  bundlePath,
76
- fitContentSize: mode === "fit",
77
- width: mode === "fixed" ? 720 : void 0,
78
- height: mode === "fixed" ? 420 : void 0,
79
- minWidth: 320,
80
- minHeight: 220,
81
- maxWidth: 960,
82
- maxHeight: 720,
83
- nativeWindowApi: true,
84
- bindWindowGlobals: true
81
+ hostFeatures
85
82
  });
86
- console.log(`lynx command: show mode=${mode} bundle=${bundlePath}`);
83
+ await tray.commandExtension("lynx", command);
84
+ console.log(`lynx command: show features=${describeLynxHostFeatures(hostFeatures)} bundle=${bundlePath}`);
87
85
  };
88
86
  const hide = async () => {
89
87
  await tray.commandExtension("lynx", { type: "hide" });
@@ -106,12 +104,9 @@ const runDaemonLynxSmoke = async (options = {}) => {
106
104
  const handleMenuClick = async (itemId) => {
107
105
  switch (itemId) {
108
106
  case 1:
109
- await show("fit");
107
+ await show();
110
108
  return;
111
109
  case 2:
112
- await show("fixed");
113
- return;
114
- case 3:
115
110
  await hide();
116
111
  return;
117
112
  case 99:
@@ -143,8 +138,8 @@ const runDaemonLynxSmoke = async (options = {}) => {
143
138
  }, duration);
144
139
  }
145
140
  try {
146
- await show("fit");
147
- console.log("use the tray menu to switch between fit-content and fixed sizing, or press Ctrl+C to exit");
141
+ await show();
142
+ console.log(`use the tray menu to re-show or hide the window, or press Ctrl+C to exit; features=${describeLynxHostFeatures(hostFeatures)}`);
148
143
  } catch (error) {
149
144
  await connection.close();
150
145
  process.off("SIGINT", onSignal);
@@ -166,6 +161,39 @@ const resolveLynxBundlePath = (value) => {
166
161
  if (!existsSync(absolutePath)) throw new Error(`lynx smoke bundle does not exist: ${absolutePath}`);
167
162
  return realpathSync(absolutePath);
168
163
  };
164
+ const resolveLynxHostFeatures = (expression = process.env[DEFAULT_LYNX_HOST_FEATURES_ENV] ?? "") => {
165
+ const features = createEmptyLynxHostFeatures();
166
+ for (const rawToken of expression.split(",")) {
167
+ const token = rawToken.trim();
168
+ if (token.length === 0) continue;
169
+ applyLynxHostFeatureToken(features, token);
170
+ }
171
+ return features;
172
+ };
173
+ const createLynxShowCommand = ({ bundlePath, hostFeatures }) => {
174
+ const command = {
175
+ type: "show",
176
+ bundlePath,
177
+ width: 720,
178
+ height: 420,
179
+ minWidth: 320,
180
+ minHeight: 220,
181
+ maxWidth: 960,
182
+ maxHeight: 720,
183
+ nativeWindowApi: hostFeatures.nativeWindowApi,
184
+ bindWindowGlobals: hostFeatures.bindWindowGlobals,
185
+ nativeScreenApi: hostFeatures.nativeScreenApi,
186
+ bindScreenGlobals: hostFeatures.bindScreenGlobals,
187
+ title: `OpenTray Lynx Smoke (${describeLynxHostFeatures(hostFeatures)})`,
188
+ icon: createVisibleIcon$1()
189
+ };
190
+ if (hostFeatures.frameless) command.style = { frameless: true };
191
+ return command;
192
+ };
193
+ const describeLynxHostFeatures = (features) => {
194
+ const enabled = LYNX_HOST_FEATURES.filter((feature) => features[feature]);
195
+ return enabled.length === 0 ? "baseline" : enabled.join(",");
196
+ };
169
197
  const resolveBundledReviewBundlePath = (moduleUrl = import.meta.url) => {
170
198
  let currentDir = dirname(fileURLToPath(moduleUrl));
171
199
  while (true) {
@@ -175,6 +203,45 @@ const resolveBundledReviewBundlePath = (moduleUrl = import.meta.url) => {
175
203
  currentDir = parent;
176
204
  }
177
205
  };
206
+ function createEmptyLynxHostFeatures() {
207
+ return {
208
+ nativeWindowApi: false,
209
+ bindWindowGlobals: false,
210
+ nativeScreenApi: false,
211
+ bindScreenGlobals: false,
212
+ frameless: false
213
+ };
214
+ }
215
+ function applyLynxHostFeatureToken(features, rawToken) {
216
+ const disabled = rawToken.startsWith("!");
217
+ const token = disabled ? rawToken.slice(1) : rawToken;
218
+ if (token === "*" || token === "full") {
219
+ for (const feature of LYNX_HOST_FEATURES) features[feature] = !disabled;
220
+ return;
221
+ }
222
+ if (token === "baseline") {
223
+ for (const feature of LYNX_HOST_FEATURES) features[feature] = false;
224
+ return;
225
+ }
226
+ if (!isLynxHostFeature(token)) throw new Error(`unsupported Lynx host feature token: ${rawToken}; supported tokens: baseline, full, *, !<feature>, ${LYNX_HOST_FEATURES.join(", ")}`);
227
+ features[token] = !disabled;
228
+ if (token === "nativeWindowApi" && disabled) {
229
+ features.bindWindowGlobals = false;
230
+ return;
231
+ }
232
+ if (token === "nativeScreenApi" && disabled) {
233
+ features.bindScreenGlobals = false;
234
+ return;
235
+ }
236
+ if (token === "bindWindowGlobals" && !disabled) {
237
+ features.nativeWindowApi = true;
238
+ return;
239
+ }
240
+ if (token === "bindScreenGlobals" && !disabled) features.nativeScreenApi = true;
241
+ }
242
+ function isLynxHostFeature(value) {
243
+ return LYNX_HOST_FEATURES.includes(value);
244
+ }
178
245
  function createVisibleIcon$1() {
179
246
  const width = 32;
180
247
  const height = 32;
@@ -194,20 +261,7 @@ function createVisibleIcon$1() {
194
261
  }
195
262
  //#endregion
196
263
  //#region src/smoke/daemon-tray.ts
197
- const menuLabels = new Map([
198
- [1, "Primary Action"],
199
- [3, "Checked Capability"],
200
- [4, "Radio: Active"],
201
- [5, "Radio: Passive"],
202
- [6, "Nested Action"],
203
- [7, "Nested Check"],
204
- [8, "WebView: Show HTML"],
205
- [9, "WebView: Navigate"],
206
- [10, "WebView: Post Message"],
207
- [11, "WebView: Evaluate"],
208
- [12, "WebView: Hide"],
209
- [99, "Quit Demo"]
210
- ]);
264
+ const menuLabels = new Map([[1, "Open WebView"]]);
211
265
  const runDaemonTraySmoke = async () => {
212
266
  const connection = await connectLocalBroker();
213
267
  const client = createClient(connection, { requestIdPrefix: "daemon-smoke" });
@@ -223,93 +277,15 @@ const runDaemonTraySmoke = async () => {
223
277
  title: "OpenTray",
224
278
  tooltip: {
225
279
  title: "OpenTray",
226
- description: "npm-installed daemon smoke with broker-routed menu events"
280
+ description: "Single primary tray action; macOS direct-triggers without opening a menu"
227
281
  },
228
282
  icon: createVisibleIcon(),
229
- menu: { items: [
230
- {
231
- type: "item",
232
- id: 1,
233
- title: "Primary Action"
234
- },
235
- {
236
- type: "item",
237
- id: 2,
238
- title: "Disabled Action",
239
- enabled: false
240
- },
241
- {
242
- type: "check",
243
- id: 3,
244
- title: "Checked Capability",
245
- checked: true
246
- },
247
- {
248
- type: "radio",
249
- id: 4,
250
- title: "Radio: Active",
251
- group: 1,
252
- checked: true
253
- },
254
- {
255
- type: "radio",
256
- id: 5,
257
- title: "Radio: Passive",
258
- group: 1
259
- },
260
- { type: "separator" },
261
- {
262
- type: "submenu",
263
- title: "Nested Actions",
264
- items: [{
265
- type: "item",
266
- id: 6,
267
- title: "Nested Action"
268
- }, {
269
- type: "check",
270
- id: 7,
271
- title: "Nested Check",
272
- checked: false
273
- }]
274
- },
275
- {
276
- type: "submenu",
277
- title: "WebView Commands",
278
- items: [
279
- {
280
- type: "item",
281
- id: 8,
282
- title: "Show HTML"
283
- },
284
- {
285
- type: "item",
286
- id: 9,
287
- title: "Navigate"
288
- },
289
- {
290
- type: "item",
291
- id: 10,
292
- title: "Post Message"
293
- },
294
- {
295
- type: "item",
296
- id: 11,
297
- title: "Evaluate JS"
298
- },
299
- {
300
- type: "item",
301
- id: 12,
302
- title: "Hide"
303
- }
304
- ]
305
- },
306
- { type: "separator" },
307
- {
308
- type: "item",
309
- id: 99,
310
- title: "Quit Demo"
311
- }
312
- ] }
283
+ menu: { items: [{
284
+ type: "item",
285
+ id: 1,
286
+ title: "Open WebView",
287
+ primaryEvent: true
288
+ }] }
313
289
  });
314
290
  console.log(`tray: ${tray.trayId}`);
315
291
  await connection.request({
@@ -320,10 +296,8 @@ const runDaemonTraySmoke = async () => {
320
296
  path: "@opentray/ext-webview"
321
297
  });
322
298
  console.log("webview extension requested through the generic load-ext path");
323
- console.log("open the system tray item and choose any enabled menu item to see routed events");
299
+ console.log("click the tray icon: macOS should direct-trigger the single primary action without opening a menu");
324
300
  let closed = false;
325
- let messageCount = 0;
326
- let evalCount = 0;
327
301
  let exitTimer;
328
302
  const shutdown = async () => {
329
303
  if (closed) return;
@@ -335,63 +309,56 @@ const runDaemonTraySmoke = async () => {
335
309
  await tray.commandExtension("webview", command);
336
310
  };
337
311
  const handleMenuClick = async (itemId) => {
338
- if (itemId === 99) {
339
- console.log("quit item routed; closing smoke connection");
340
- await shutdown();
312
+ if (itemId === 1) {
313
+ const trayBounds = await tray.getBounds();
314
+ console.log(`tray bounds: ${JSON.stringify(trayBounds)}`);
315
+ await commandWebview({
316
+ type: "show",
317
+ html: createWebviewDemoHtml(),
318
+ width: 420,
319
+ height: 260,
320
+ title: "OpenTray WebView Smoke",
321
+ icon: {
322
+ type: "href",
323
+ href: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="
324
+ },
325
+ style: {
326
+ frameless: true,
327
+ transparent: true,
328
+ keepOnTop: true,
329
+ platform: { macos: {
330
+ material: "hudWindow",
331
+ materialState: "active"
332
+ } }
333
+ },
334
+ fallbackRect: trayBounds.rect ?? {
335
+ x: 0,
336
+ y: 0,
337
+ width: 1,
338
+ height: 1
339
+ },
340
+ nativeWindowApi: true,
341
+ bindWindowGlobals: true,
342
+ nativeScreenApi: true,
343
+ bindScreenGlobals: true,
344
+ nativeTrayApi: true,
345
+ titleSync: {
346
+ documentToWindow: true,
347
+ windowToDocument: true
348
+ },
349
+ iconSync: true,
350
+ nativeApiPolicy: {
351
+ defaultSrc: ["'local'"],
352
+ window: ["https://example.com"],
353
+ screen: ["https://example.com"],
354
+ tray: ["https://example.com"],
355
+ titleSync: ["https://example.com"],
356
+ iconSync: ["https://example.com"]
357
+ }
358
+ });
359
+ console.log("primary tray action: webview show");
341
360
  return;
342
361
  }
343
- if (itemId < 8 || itemId > 12) return;
344
- switch (itemId) {
345
- case 8:
346
- await commandWebview({
347
- type: "show",
348
- html: createWebviewDemoHtml(),
349
- width: 420,
350
- height: 260,
351
- fallbackRect: {
352
- x: 0,
353
- y: 0,
354
- width: 1,
355
- height: 1
356
- },
357
- nativeWindowApi: true,
358
- bindWindowGlobals: true
359
- });
360
- console.log("webview command: show");
361
- break;
362
- case 9:
363
- await commandWebview({
364
- type: "navigate",
365
- url: "https://example.com/opentray-status"
366
- });
367
- console.log("webview command: navigate");
368
- break;
369
- case 10:
370
- messageCount += 1;
371
- await commandWebview({
372
- type: "postMessage",
373
- payload: {
374
- kind: "ping",
375
- source: "opentray smoke daemon-tray",
376
- count: messageCount,
377
- sentAt: (/* @__PURE__ */ new Date()).toISOString()
378
- }
379
- });
380
- console.log("webview command: postMessage");
381
- break;
382
- case 11:
383
- evalCount += 1;
384
- await commandWebview({
385
- type: "evaluate",
386
- js: createVisibleEvaluateScript(evalCount)
387
- });
388
- console.log("webview command: evaluate");
389
- break;
390
- case 12:
391
- await commandWebview({ type: "hide" });
392
- console.log("webview command: hide");
393
- break;
394
- }
395
362
  };
396
363
  connection.onEvent((frame) => {
397
364
  console.log(`broker -> client ${JSON.stringify(frame)}`);
@@ -409,19 +376,12 @@ const runDaemonTraySmoke = async () => {
409
376
  }
410
377
  const webviewSmoke = process.env.OPENTRAY_EXAMPLE_WEBVIEW_SMOKE;
411
378
  try {
412
- if (webviewSmoke === "show") await handleMenuClick(8);
413
- else if (webviewSmoke === "1") {
379
+ if (webviewSmoke === "show" || webviewSmoke === "1") await handleMenuClick(1);
380
+ if (webviewSmoke === "1") {
414
381
  if (exitTimer !== void 0) {
415
382
  clearTimeout(exitTimer);
416
383
  exitTimer = void 0;
417
384
  }
418
- for (const itemId of [
419
- 8,
420
- 10,
421
- 11,
422
- 9,
423
- 12
424
- ]) await handleMenuClick(itemId);
425
385
  await sleep(300);
426
386
  await shutdown();
427
387
  }
@@ -510,47 +470,17 @@ function createWebviewDemoHtml() {
510
470
  <body>
511
471
  <main>
512
472
  <h1>OpenTray WebView</h1>
513
- <p>Use the tray menu to mutate this native WebView through extension commands.</p>
473
+ <p>This window was opened by the single primary tray action.</p>
514
474
  <section>
515
475
  <div class="card">
516
- <div class="label">postMessage</div>
517
- <code id="message-status">Waiting for WebView Commands -> Post Message</code>
518
- </div>
519
- <div class="card">
520
- <div class="label">evaluate JS</div>
521
- <code id="eval-status">Waiting for WebView Commands -> Evaluate JS</code>
476
+ <div class="label">primary event</div>
477
+ <code>macOS single primary item direct-triggered menuClick -> WebView show</code>
522
478
  </div>
523
479
  </section>
524
480
  </main>
525
- <script>
526
- window.addEventListener("message", (event) => {
527
- const target = document.getElementById("message-status");
528
- if (target) {
529
- target.textContent = JSON.stringify(event.data, null, 2);
530
- }
531
- });
532
- <\/script>
533
481
  </body>
534
482
  </html>`;
535
483
  }
536
- function createVisibleEvaluateScript(count) {
537
- return `(() => {
538
- let target = document.getElementById("eval-status");
539
- if (!target) {
540
- const panel = document.createElement("div");
541
- panel.style.cssText = "position:fixed;left:16px;right:16px;bottom:16px;z-index:2147483647;padding:12px;border-radius:12px;background:#fff8e7;color:#18220f;box-shadow:0 12px 30px rgba(0,0,0,.18);font:14px ui-rounded, sans-serif;";
542
- panel.textContent = "OpenTray Evaluate JS: ";
543
- target = document.createElement("code");
544
- target.id = "eval-status";
545
- panel.appendChild(target);
546
- document.body.appendChild(panel);
547
- }
548
- if (target) {
549
- target.textContent = "Evaluate JS updated the WebView, count=${count}, at " + new Date().toLocaleTimeString();
550
- }
551
- window.__OPENTRAY_DEMO__ = { evaluated: true, count: ${count} };
552
- })();`;
553
- }
554
484
  function sleep(ms) {
555
485
  return new Promise((resolve) => setTimeout(resolve, ms));
556
486
  }
@@ -566,14 +496,12 @@ const parseCliCommand = (argv) => {
566
496
  name: "daemon-tray"
567
497
  };
568
498
  if (group === "smoke" && action === "daemon-lynx") {
569
- const bundlePath = parseSmokeBundlePath(rest);
570
- return bundlePath === void 0 ? {
571
- type: "smoke",
572
- name: "daemon-lynx"
573
- } : {
499
+ const smokeOptions = parseSmokeOptions(rest);
500
+ return {
574
501
  type: "smoke",
575
502
  name: "daemon-lynx",
576
- bundlePath
503
+ ...smokeOptions.bundlePath ? { bundlePath: smokeOptions.bundlePath } : {},
504
+ ...smokeOptions.featureExpression ? { featureExpression: smokeOptions.featureExpression } : {}
577
505
  };
578
506
  }
579
507
  return { type: "help" };
@@ -597,7 +525,10 @@ const runCli = async (argv) => {
597
525
  }
598
526
  if (command.type === "smoke") {
599
527
  if (command.name === "daemon-tray") await runDaemonTraySmoke();
600
- else await runDaemonLynxSmoke(command.bundlePath === void 0 ? {} : { bundlePath: command.bundlePath });
528
+ else await runDaemonLynxSmoke({
529
+ ...command.bundlePath === void 0 ? {} : { bundlePath: command.bundlePath },
530
+ ...command.featureExpression === void 0 ? {} : { featureExpression: command.featureExpression }
531
+ });
601
532
  return 0;
602
533
  }
603
534
  const driver = createNodeDaemonDriver(cliModulePath);
@@ -657,7 +588,7 @@ const runCli = async (argv) => {
657
588
  const printHelp = () => {
658
589
  console.error("Usage: opentray daemon <start|stop|restart|health>");
659
590
  console.error(" opentray smoke daemon-tray");
660
- console.error(" opentray smoke daemon-lynx [--bundle <path-to-main.lynx.bundle>]");
591
+ console.error(" opentray smoke daemon-lynx [--bundle <path-to-main.lynx.bundle>] [--features <baseline|full|*,!feature,...>]");
661
592
  console.error(" default Lynx smoke uses the packaged review bundle");
662
593
  };
663
594
  const formatDaemonHealthOutput = (health) => {
@@ -683,8 +614,20 @@ const isCliEntrypoint = (argvEntryPath, modulePath) => {
683
614
  return argvEntryPath === modulePath;
684
615
  }
685
616
  };
686
- function parseSmokeBundlePath(argv) {
687
- for (let index = 0; index < argv.length; index += 1) if (argv[index] === "--bundle") return argv[index + 1];
617
+ function parseSmokeOptions(argv) {
618
+ let bundlePath;
619
+ let featureExpression;
620
+ for (let index = 0; index < argv.length; index += 1) {
621
+ if (argv[index] === "--bundle") {
622
+ bundlePath = argv[index + 1];
623
+ continue;
624
+ }
625
+ if (argv[index] === "--features") featureExpression = argv[index + 1];
626
+ }
627
+ return {
628
+ ...bundlePath === void 0 ? {} : { bundlePath },
629
+ ...featureExpression === void 0 ? {} : { featureExpression }
630
+ };
688
631
  }
689
632
  if (isCliEntrypoint(process.argv[1], cliModulePath)) runCli(process.argv.slice(2)).then((code) => {
690
633
  process.exitCode = code;