@blastlauncher/native 3.0.0-alpha.0 → 3.0.0-alpha.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/index.d.ts CHANGED
@@ -9,8 +9,12 @@ export type Runner = (command: string, args: string[], options: {
9
9
  stdout: string;
10
10
  code: number;
11
11
  }>;
12
- export declare function createNativeHandlers({ platform, run, env, }?: {
12
+ export type BeforePaste = (options: {
13
+ closeMainWindow: boolean;
14
+ }) => void | Promise<void>;
15
+ export declare function createNativeHandlers({ platform, run, env, beforePaste, }?: {
13
16
  platform?: NodeJS.Platform;
14
17
  run?: Runner;
15
18
  env?: Record<string, string | undefined>;
19
+ beforePaste?: BeforePaste;
16
20
  }): Record<string, CapabilityHandler>;
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ const text = (value) => {
24
24
  throw new Error("Expected string without NUL");
25
25
  return value;
26
26
  };
27
- export function createNativeHandlers({ platform = process.platform, run = defaultRun, env = process.env, } = {}) {
27
+ export function createNativeHandlers({ platform = process.platform, run = defaultRun, env = process.env, beforePaste, } = {}) {
28
28
  const windows = platform === "win32", mac = platform === "darwin";
29
29
  const paths = windows ? win32 : posix;
30
30
  const absolute = (value) => {
@@ -107,7 +107,9 @@ export function createNativeHandlers({ platform = process.platform, run = defaul
107
107
  await copy("clipboard.copy", object(params));
108
108
  },
109
109
  "clipboard.paste": async (params) => {
110
- await copy("clipboard.paste", object(params));
110
+ const pasteParams = object(params);
111
+ await copy("clipboard.paste", pasteParams);
112
+ await beforePaste?.({ closeMainWindow: pasteParams.closeMainWindow !== false });
111
113
  if (mac)
112
114
  await command("clipboard.paste", "osascript", [
113
115
  "-e",
@@ -1,3 +1,5 @@
1
1
  import type { JsonValue } from "@blastlauncher/protocol";
2
2
  import { type DesktopTools } from "./desktop.js";
3
+ /** The one script used when yabai is unavailable. Inputs are op and JSON params argv. */
4
+ export declare const macWindowsFallback: string;
3
5
  export declare function macWindows(t: DesktopTools, cap: string, op: string, p: Record<string, JsonValue>): Promise<JsonValue>;
@@ -1,7 +1,129 @@
1
1
  import { params, string } from "./desktop.js";
2
2
  // yabai supplies actual Space/display identities that System Events lacks.
3
+ // The fallback below uses display identities only and cannot distinguish Spaces.
3
4
  // No daemon is installed or configured by Blast.
5
+ /** The one script used when yabai is unavailable. Inputs are op and JSON params argv. */
6
+ export const macWindowsFallback = String.raw `
7
+ ObjC.import('AppKit');
8
+ ObjC.import('CoreGraphics');
9
+ const args = $args;
10
+ const op = args[0];
11
+ const input = JSON.parse(args[1] || '{}');
12
+ const cg = ObjC.unwrap($.CGWindowListCopyWindowInfo($.kCGWindowListOptionOnScreenOnly, $.kCGNullWindowID));
13
+ const number = (value) => Number(ObjC.unwrap(value));
14
+ const rect = (value) => {
15
+ const r = ObjC.unwrap(value);
16
+ return { position: { x: r.X, y: r.Y }, size: { width: r.Width, height: r.Height } };
17
+ };
18
+ const screens = $.NSScreen.screens;
19
+ const screenInfo = [];
20
+ for (let i = 0; i < screens.count; i++) {
21
+ const screen = screens.objectAtIndex(i);
22
+ const description = screen.deviceDescription;
23
+ const display = number(description.objectForKey('NSScreenNumber'));
24
+ const uuid = ObjC.unwrap($.CGDisplayCreateUUIDFromDisplayID(display).UUIDString);
25
+ // CG window bounds and CGDisplayBounds share global top-left coordinates.
26
+ // CGDisplayBounds is a CGRect struct (origin/size), not a bounds dictionary.
27
+ const d = $.CGDisplayBounds(display);
28
+ const frame = { position: { x: d.origin.x, y: d.origin.y }, size: { width: d.size.width, height: d.size.height } };
29
+ screenInfo.push({ id: 'screen:' + uuid, screenId: uuid, display, frame });
30
+ }
31
+ const screenFor = (bounds) => {
32
+ let best = screenInfo[0], bestArea = -1;
33
+ for (const screen of screenInfo) {
34
+ const f = screen.frame;
35
+ const width = Math.max(0, Math.min(bounds.X + bounds.Width, f.position.x + f.size.width) - Math.max(bounds.X, f.position.x));
36
+ const height = Math.max(0, Math.min(bounds.Y + bounds.Height, f.position.y + f.size.height) - Math.max(bounds.Y, f.position.y));
37
+ const area = width * height;
38
+ if (area > bestArea) { best = screen; bestArea = area; }
39
+ }
40
+ return best;
41
+ };
42
+ const systemEvents = Application('System Events');
43
+ const axCache = new Map();
44
+ // Reads stay available without Accessibility; capability flags are then false.
45
+ const trusted = !!systemEvents.uiElementsEnabled();
46
+ const axWindows = (pid) => {
47
+ if (!trusted) return [];
48
+ if (!axCache.has(pid)) axCache.set(pid, systemEvents.applicationProcesses.byId(pid).windows());
49
+ return axCache.get(pid);
50
+ };
51
+ const sameFrame = (window, bounds) => {
52
+ const p = window.position(), s = window.size();
53
+ return Math.abs(p.x - bounds.X) < 2 && Math.abs(p.y - bounds.Y) < 2 && Math.abs(s.width - bounds.Width) < 2 && Math.abs(s.height - bounds.Height) < 2;
54
+ };
55
+ const attribute = (window, name) => {
56
+ try { return window.attributes.byName(name); } catch (_) { return undefined; }
57
+ };
58
+ const canSet = (attr) => {
59
+ if (!attr) return false;
60
+ try { if (typeof attr.settable === 'function') return !!attr.settable(); } catch (_) {}
61
+ try { return attr.value() !== undefined; } catch (_) { return false; }
62
+ };
63
+ const frontmostWindow = () => {
64
+ const pid = number($.NSWorkspace.sharedWorkspace.frontmostApplication.processIdentifier);
65
+ return windows.find((window) => number(window.kCGWindowOwnerPID) === pid);
66
+ };
67
+ const requireAccessibility = () => {
68
+ if (!trusted) throw new Error('Accessibility permission is required');
69
+ };
70
+ const windows = cg.filter((w) => number(w.kCGWindowLayer) === 0 && !!ObjC.unwrap(w.kCGWindowIsOnscreen));
71
+ const convert = (w, active) => {
72
+ const bounds = ObjC.unwrap(w.kCGWindowBounds);
73
+ const screen = screenFor(bounds);
74
+ let positionable = false, resizable = false, fullScreenSettable = false;
75
+ const match = axWindows(number(w.kCGWindowOwnerPID)).find((window) => sameFrame(window, bounds));
76
+ if (match) {
77
+ positionable = canSet(attribute(match, 'AXPosition'));
78
+ resizable = canSet(attribute(match, 'AXSize'));
79
+ fullScreenSettable = !!attribute(match, 'AXFullScreen');
80
+ }
81
+ return { id: String(number(w.kCGWindowNumber)), desktopId: screen.id, active, fullScreenSettable, resizable, positionable, bounds: rect(w.kCGWindowBounds) };
82
+ };
83
+ const front = frontmostWindow();
84
+ if (op === 'desktops') {
85
+ const frontScreen = front ? screenFor(ObjC.unwrap(front.kCGWindowBounds)) : undefined;
86
+ console.log(JSON.stringify(screenInfo.map((s) => ({ id: s.id, screenId: s.screenId, active: s === frontScreen, type: 'User', size: s.frame.size }))));
87
+ } else if (op === 'list' || op === 'active') {
88
+ if (op === 'active' && !front) throw new Error('No active window');
89
+ console.log(JSON.stringify(op === 'active' ? convert(front, true) : windows.map((w) => convert(w, w === front))));
90
+ } else if (op === 'setBounds') {
91
+ requireAccessibility();
92
+ const id = String(input.id);
93
+ const target = windows.find((w) => String(number(w.kCGWindowNumber)) === id);
94
+ if (!target) throw new Error('Window not found');
95
+ const current = convert(target, false);
96
+ if (input.desktopId !== undefined && input.desktopId !== current.desktopId) throw new Error('Window cannot move to a different desktop without yabai');
97
+ const candidates = axWindows(number(target.kCGWindowOwnerPID));
98
+ const frame = ObjC.unwrap(target.kCGWindowBounds);
99
+ const match = candidates.find((window) => sameFrame(window, frame));
100
+ if (!match) throw new Error('Accessibility cannot identify this window');
101
+ if (input.bounds === 'fullscreen') {
102
+ const fullscreen = attribute(match, 'AXFullScreen');
103
+ if (!fullscreen) throw new Error('Window does not support fullscreen');
104
+ fullscreen.setValue(true);
105
+ } else {
106
+ const b = input.bounds || {}, p = b.position || {}, s = b.size || {};
107
+ if (p.x !== undefined || p.y !== undefined) match.position = [p.x ?? frame.X, p.y ?? frame.Y];
108
+ if (s.width !== undefined || s.height !== undefined) match.size = [s.width ?? frame.Width, s.height ?? frame.Height];
109
+ }
110
+ console.log('null');
111
+ }`;
4
112
  export async function macWindows(t, cap, op, p) {
113
+ // Display IDs are only produced by the fallback backend; do not ask yabai
114
+ // to interpret one as a Mission Control Space index.
115
+ if (op === "setBounds" && typeof p.desktopId === "string" && p.desktopId.startsWith("screen:"))
116
+ return JSON.parse(await t.jxa(cap, macWindowsFallback, [op, JSON.stringify(p)]));
117
+ try {
118
+ return await yabaiWindows(t, cap, op, p);
119
+ }
120
+ catch (error) {
121
+ if (!missingExecutable(error))
122
+ throw error;
123
+ return JSON.parse(await t.jxa(cap, macWindowsFallback, [op, JSON.stringify(p)]));
124
+ }
125
+ }
126
+ async function yabaiWindows(t, cap, op, p) {
5
127
  const send = (args) => t.command(cap, "yabai", ["-m", ...args]);
6
128
  const query = async (args) => JSON.parse(await send(["query", ...args]));
7
129
  if (op === "desktops") {
@@ -69,3 +191,9 @@ function convert(w) {
69
191
  : { position: { x: f.x, y: f.y }, size: { width: f.w, height: f.h } },
70
192
  };
71
193
  }
194
+ function missingExecutable(error) {
195
+ if (!error || typeof error !== "object")
196
+ return false;
197
+ const value = error;
198
+ return value.code === "ENOENT" || missingExecutable(value.cause);
199
+ }
package/dist/windows.js CHANGED
@@ -9,8 +9,10 @@ export function windowHandlers(t) {
9
9
  async (value) => {
10
10
  const p = params(value);
11
11
  if (t.platform === "darwin") {
12
- if (op === "setBounds")
12
+ if (op === "setBounds") {
13
13
  validateBounds(p);
14
+ validateMacBounds(p);
15
+ }
14
16
  return macWindows(t, cap, op, p);
15
17
  }
16
18
  if (t.platform === "win32") {
@@ -144,3 +146,11 @@ function validateBounds(p) {
144
146
  }
145
147
  }
146
148
  }
149
+ function validateMacBounds(p) {
150
+ if (!/^\d+$/.test(string(p.id)))
151
+ throw new Error("Invalid window id");
152
+ if (p.desktopId !== undefined &&
153
+ !/^\d+$/.test(string(p.desktopId)) &&
154
+ !/^screen:[^\s\0]+$/.test(string(p.desktopId)))
155
+ throw new Error("Invalid desktop id");
156
+ }
package/package.json CHANGED
@@ -9,10 +9,16 @@
9
9
  }
10
10
  },
11
11
  "dependencies": {
12
- "@blastlauncher/protocol": "3.0.0-alpha.0"
12
+ "@blastlauncher/protocol": "3.0.0-alpha.1"
13
13
  },
14
- "version": "3.0.0-alpha.0",
14
+ "version": "3.0.0-alpha.1",
15
15
  "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/BlastLauncher/blast.git",
19
+ "directory": "packages/native"
20
+ },
21
+ "homepage": "https://github.com/BlastLauncher/blast#readme",
16
22
  "engines": {
17
23
  "node": ">=24.20.0"
18
24
  },