HazAT/glimpse

GitHub: HazAT/glimpse

原生 macOS 轻量级 UI 框架,可在 50 毫秒内打开 WKWebView 窗口,通过 JSON Lines 协议实现脚本与 HTML 界面的双向通信。

Stars: 300 | Forks: 6

# Glimpse 原生 macOS 脚本和代理微 UI。 https://github.com/user-attachments/assets/57822cd2-4606-4865-a555-d8ccacd31b40 Glimpse 可在 50 毫秒内打开一个原生 WKWebView 窗口,并通过 stdin/stdout 进行双向 JSON Lines 协议通信。无需 Electron,无需浏览器,无运行时依赖 —— 仅需一个微型 Swift 二进制文件和一个 Node.js 封装。 ## 系统要求 - macOS(任意近期版本) - Xcode Command Line Tools:`xcode-select --install` - Node.js 18+ ## 安装 ``` npm install glimpseui ``` `npm install` 会通过 `postinstall` 钩子自动编译 Swift 二进制文件(约 2 秒)。详情请参阅[安装时编译](#compile-on-install)。 ### Pi Agent 包 ``` pi install npm:glimpseui ``` 为 [pi](https://github.com/mariozechner/pi) 安装 Glimpse 技能和伴侣扩展。该伴侣是一个跟随光标的浮动状态药丸,实时显示您的代理正在执行的操作。使用 `/companion` 命令进行切换。 **手动构建:** ``` npm run build # 或直接: swiftc src/glimpse.swift -o src/glimpse ``` ## 快速开始 ``` import { open } from 'glimpseui'; const win = open(`

Hello from Glimpse

`, { width: 400, height: 300, title: 'My App' }); win.on('message', (data) => { console.log('Received:', data); // { action: 'greet' } win.close(); }); win.on('closed', () => process.exit(0)); ``` ## 窗口模式 Glimpse 支持多种窗口样式标志,可以自由组合: | 标志 | 效果 | |------|--------| | `frameless` | 移除标题栏 —— 使用您自己的 HTML 边框 | | `floating` | 始终置于其他窗口之上 | | `transparent` | 透明窗口背景 —— HTML body 需要 `background: transparent` | | `clickThrough` | 窗口忽略所有鼠标事件 | 常见组合: - **浮动 HUD**:`floating: true` —— 状态面板、代理指示器 - **自定义对话框**:`frameless: true` —— 无系统边框的简洁 UI - **覆盖层**:`frameless + transparent` —— 浮动在内容上的异形小部件 - **伴侣小部件**:`frameless + transparent + floating + clickThrough` —— 不干扰桌面的纯视觉覆盖层 ## 跟随光标 将窗口附加到光标上。结合 `transparent + frameless + floating + clickThrough`,可以创建跟随鼠标但不干扰正常使用的视觉伴侣。 ``` import { open } from './src/glimpse.mjs'; const win = open(` `, { width: 60, height: 60, transparent: true, frameless: true, followCursor: true, clickThrough: true, cursorOffset: { x: 20, y: -20 } }); ``` 窗口会跨所有屏幕实时追踪光标。`followCursor` 蕴含 `floating` —— 窗口会自动保持在顶层。 您也可以在窗口打开后动态切换追踪状态: ``` win.followCursor(false); // stop tracking win.followCursor(true); // resume tracking (snap mode) win.followCursor(true, undefined, 'spring'); // resume with spring physics ``` ### 光标锚点吸附位置 使用 `cursorAnchor` 将窗口定位在光标周围 6 个命名吸附点之一,而不是使用原始像素偏移: ``` top-left top-right \ / left -- 🖱️ -- right / \ bottom-left bottom-right ``` 会自动应用固定的**安全区域**,使窗口永远不会与光标图形重叠(考虑到最大的 macOS 系统光标加上 8pt 内边距)。`cursorOffset` 仍可在锚点之上用作微调。 ``` // Window snaps to the right of the cursor with a safe gap const win = open(html, { followCursor: true, cursorAnchor: 'top-right', transparent: true, frameless: true, clickThrough: true, }); // Change anchor at runtime win.followCursor(true, 'bottom-left'); ``` **使用场景:** 动画 SVG 伴侣、代理“思考”指示器、浮动工具提示、自定义光标替换。 ## API 参考 ### `open(html, options?)` 打开一个原生窗口并返回一个 `GlimpseWindow`。HTML 会在 WebView 发出就绪信号后显示。 ``` import { open } from 'glimpseui'; const win = open('...', { width: 800, // default: 800 height: 600, // default: 600 title: 'App', // default: "Glimpse" }); ``` **所有选项:** | 选项 | 类型 | 默认值 | 描述 | |--------|------|---------|-------------| | `width` | number | `800` | 窗口宽度(像素) | | `height` | number | `600` | 窗口高度(像素) | | `title` | string | `"Glimpse"` | 标题栏文本(无边框时忽略) | | `x` | number | — | 水平屏幕位置(省略则居中) | | `y` | number | — | 垂直屏幕位置(省略则居中) | | `frameless` | boolean | `false` | 移除标题栏 | | `floating` | boolean | `false` | 始终置于其他窗口之上 | | `transparent` | boolean | `false` | 透明窗口背景 | | `clickThrough` | boolean | `false` | 窗口忽略所有鼠标事件 | | `followCursor` | boolean | `false` | 实时追踪光标位置 | | `followMode` | string | `"snap"` | 跟随动画模式:`snap`(即时)或 `spring`(iOS 风格弹性带过冲) | | `cursorAnchor` | string | `null` | 光标周围的吸附点:`top-left`, `top-right`, `right`, `bottom-right`, `bottom-left`, `left`。带安全区域间隙定位窗口;覆盖原始偏移定位。 | | `cursorOffset` | `{ x?, y? }` | `{ x: 20, y: -20 }` | 距光标的像素偏移(或基于 `cursorAnchor` 的微调) | | `hidden` | boolean | `false` | 以隐藏状态启动窗口(预热模式) —— 在后台加载 HTML,然后用 `win.show()` 显示 | | `autoClose` | boolean | `false` | 在第一个 `message` 事件后自动关闭窗口 | ### `prompt(html, options?)` 一次性助手 —— 打开窗口,等待第一条消息,然后自动关闭。返回 `Promise`,其中 `data` 是第一条消息的负载,`null` 表示用户在未发送任何内容的情况下关闭了窗口。 ``` import { prompt } from 'glimpseui'; const answer = await prompt(`

Delete this file?

`, { width: 300, height: 150, title: 'Confirm' }); if (answer?.ok) console.log('Deleted!'); ``` 接受与 `open()` 相同的 `options`。可选的 `options.timeout`(毫秒)如果在此时间内没有消息到达,则拒绝 Promise。 ### GlimpseWindow `GlimpseWindow` 继承自 `EventEmitter`。 #### 事件 | 事件 | 负载 | 描述 | |-------|---------|-------------| | `ready` | `info: object` | WebView 已加载 —— 包含屏幕、外观和光标信息 | | `message` | `data: object` | 页面通过 `window.glimpse.send(data)` 发送的消息 | | `info` | `info: object` | 最新的系统信息(响应 `.getInfo()`) | | `closed` | — | 窗口已关闭(由用户或通过 `.close()`) | | `error` | `Error` | 进程错误或协议行格式错误 | ``` win.on('ready', (info) => { console.log(info.screen); // { width, height, scaleFactor, visibleWidth, visibleHeight, ... } console.log(info.appearance); // { darkMode, accentColor, reduceMotion, increaseContrast } console.log(info.cursor); // { x, y } console.log(info.screens); // [{ x, y, width, height, scaleFactor, ... }, ...] console.log(info.cursorTip); // { x, y } in CSS coords (relative to window top-left), or null when not following }); win.on('message', (msg) => console.log('from page:', msg)); win.on('closed', () => process.exit(0)); win.on('error', (err) => console.error(err)); ``` #### 方法 **`win.send(js)`** —— 在 WebView 中执行 JavaScript。 ``` win.send(`document.body.style.background = 'coral'`); win.send(`document.getElementById('status').textContent = 'Done'`); ``` **`win.setHTML(html)`** —— 替换整个页面内容。 ``` win.setHTML('

Step 2

'); ``` **`win.followCursor(enabled, anchor?, mode?)`** —— 在运行时启动或停止光标追踪。可选的 `anchor` 设置吸附点(`top-left`, `top-right`, `right`, `bottom-right`, `bottom-left`, `left`)。可选的 `mode` 设置动画:`snap`(即时)或 `spring`(弹性)。 ``` win.followCursor(true); // attach to cursor (uses offset) win.followCursor(true, 'top-right'); // attach at top-right snap point win.followCursor(true, 'top-right', 'spring'); // spring physics follow win.followCursor(false); // detach ``` **`win.info`** —— 获取最后已知系统信息(屏幕、外观、光标)的 Getter。在 `ready` 之后可用。 ``` const { width, height } = win.info.screen; const isDark = win.info.appearance.darkMode; ``` **`win.getInfo()`** —— 请求最新的系统信息。发出带有更新数据的 `info` 事件。 ``` win.getInfo(); win.on('info', (info) => console.log(info.appearance.darkMode)); ``` **`win.loadFile(path)`** —— 通过绝对路径将本地 HTML 文件加载到 WebView 中。 ``` win.loadFile('/path/to/page.html'); ``` **`win.show(options?)`** —— 显示隐藏的窗口(参见 `hidden` 选项)。激活应用并将窗口置于前台。可选的 `options.title` 设置窗口标题。 ``` win.show(); // reveal with default title win.show({ title: 'Results' }); // reveal and set title ``` **`win.close()`** —— 以编程方式关闭窗口。 ``` win.close(); ``` ### JavaScript 桥接(页内) Glimpse 加载的每个页面在文档开始时都会注入一个 `window.glimpse` 对象: ``` // Send any JSON-serializable value to Node.js → triggers 'message' event window.glimpse.send({ action: 'submit', value: 42 }); // Close the window from inside the page window.glimpse.close(); // Cursor tip position in CSS coordinates (px from window top-left, Y down) // null when follow-cursor is not active; updated on window resize const tip = window.glimpse.cursorTip; // { x: 0, y: 120 } or null ``` ## 协议 Glimpse 使用换行符分隔的 JSON (JSON Lines) 协议。每一行都是一个完整的 JSON 对象。这使得从任何语言驱动二进制文件变得容易。 ### Stdin → Glimpse(命令) **设置 HTML** —— 替换页面内容。HTML 必须进行 base64 编码。 ``` {"type":"html","html":""} ``` **执行 JavaScript** —— 在 WebView 中运行 JS。 ``` {"type":"eval","js":"document.title = 'Updated'"} ``` **跟随光标** —— 在运行时切换光标追踪。可选的 `anchor` 设置吸附点。可选的 `mode` 设置动画:`snap` 或 `spring`。 ``` {"type":"follow-cursor","enabled":true} {"type":"follow-cursor","enabled":true,"anchor":"top-right"} {"type":"follow-cursor","enabled":true,"anchor":"top-right","mode":"spring"} {"type":"follow-cursor","enabled":false} ``` **加载文件** —— 通过绝对路径加载本地 HTML 文件。 ``` {"type":"file","path":"/path/to/page.html"} ``` **获取信息** —— 请求当前系统信息(屏幕、外观、光标)。以 `info` 事件响应。 ``` {"type":"get-info"} ``` **显示** —— 显示隐藏的窗口(使用 `--hidden` 启动)。激活应用并将窗口置于前台。可选的 `title` 设置窗口标题。 ``` {"type":"show"} {"type":"show","title":"Results"} ``` **关闭** —— 关闭窗口并退出。 ``` {"type":"close"} ``` ### Stdout → 宿主(事件) **就绪** —— WebView 完成加载。包含系统信息。 ``` {"type":"ready","screen":{"width":2560,"height":1440,"scaleFactor":2,"visibleX":0,"visibleY":48,"visibleWidth":2560,"visibleHeight":1367},"screens":[...],"appearance":{"darkMode":true,"accentColor":"#007AFF","reduceMotion":false,"increaseContrast":false},"cursor":{"x":500,"y":800},"cursorTip":{"x":0,"y":120}} ``` 当跟随光标处于活动状态时,`cursorTip` 存在。它保存光标尖端位置的 CSS 坐标(距窗口左上角的像素距离,Y 轴向下增加)。否则为 `null`。 **信息** —— 对 `get-info` 命令的响应。形状与 `ready` 相同,但 `type: "info"`。 ``` {"type":"info","screen":{...},"screens":[...],"appearance":{...},"cursor":{...},"cursorTip":{"x":0,"y":120}} ``` **消息** —— 页面通过 `window.glimpse.send(...)` 发送的数据。 ``` {"type":"message","data":{"action":"submit","value":42}} ``` **已关闭** —— 窗口已关闭(由用户或通过关闭命令)。 ``` {"type":"closed"} ``` 诊断日志写入 **stderr**(前缀为 `[glimpse]`),不影响协议。 ## CLI 用法 直接从任何语言驱动二进制文件 —— shell、Python、Ruby 等。 ``` # 基本用法 echo '{"type":"html","html":"PGh0bWw+PGJvZHk+SGVsbG8hPC9ib2R5PjwvaHRtbD4="}' \ | ./src/glimpse --width 400 --height 300 --title "Hello" ``` 可用标志: | 标志 | 默认值 | 描述 | |------|---------|-------------| | `--width N` | `800` | 窗口宽度(像素) | | `--height N` | `600` | 窗口高度(像素) | | `--title STR` | `"Glimpse"` | 窗口标题栏文本 | | `--x N` | — | 水平屏幕位置(省略则居中) | | `--y N` | — | 垂直屏幕位置(省略则居中) | | `--frameless` | off | 移除标题栏 | | `--floating` | off | 始终置于其他窗口之上 | | `--transparent` | off | 透明窗口背景 | | `--click-through` | off | 窗口忽略所有鼠标事件 | | `--follow-cursor` | off | 实时追踪光标位置 | | `--follow-mode ` | `snap` | 跟随动画:`snap`(即时)或 `spring`(带过冲的弹性) | | `--cursor-anchor ` | — | 光标周围的吸附点:`top-left`, `top-right`, `right`, `bottom-right`, `bottom-left`, `left` | | `--cursor-offset-x N` | `20` | 距光标的水平偏移(或基于 `--cursor-anchor` 的微调) | | `--cursor-offset-y N` | `-20` | 距光标的垂直偏移(或基于 `--cursor-anchor` 的微调) | | `--hidden` | off | 以隐藏状态启动窗口(预热模式) —— 在后台加载 HTML,用 `show` 命令显示 | | `--auto-close` | off | 收到页面的第一条消息后退出 | **Shell 示例 —— 编码 HTML 并通过管道传入:** ``` HTML=$(echo '

Hi

' | base64) { echo "{\"type\":\"html\",\"html\":\"$HTML\"}" cat # keep stdin open so the window stays up } | ./src/glimpse --width 600 --height 400 ``` **Python 示例:** ``` import subprocess, base64, json html = b"

Hello from Python

" proc = subprocess.Popen( ["./src/glimpse", "--width", "500", "--height", "400"], stdin=subprocess.PIPE, stdout=subprocess.PIPE ) cmd = json.dumps({"type": "html", "html": base64.b64encode(html).decode()}) proc.stdin.write((cmd + "\n").encode()) proc.stdin.flush() for line in proc.stdout: msg = json.loads(line) if msg["type"] == "ready": print("Window is ready") elif msg["type"] == "message": print("From page:", msg["data"]) elif msg["type"] == "closed": break ``` ## 安装时编译 一旦安装了 Xcode Command Line Tools,每台 Mac 都自带 `swiftc` —— 不需要 Xcode IDE。Glimpse 利用这一点:运行 `npm install` 会触发 `postinstall` 脚本,在大约 2 秒内将 `src/glimpse.swift` 编译为原生二进制文件。 ``` > glimpse@0.1.0 postinstall > npm run build swiftc src/glimpse.swift -o src/glimpse ✓ ``` **如果编译失败**,最常见的原因是缺少 Xcode CLT: ``` xcode-select --install ``` 随时手动重新编译: ``` npm run build ``` ## 性能 测量完整往返的端到端基准测试:生成进程 → 打开原生窗口 → 渲染 HTML → JavaScript 执行 → 响应返回 Node.js。在 Apple Silicon(M 系列 Mac)上测量。 ### 热启动(二进制预编译) 这是典型情况 —— 二进制文件在安装时编译一次。 | 运行 | 时间 | |-----|------| | 空闲后首次 | ~630ms | | 后续(5 次中位数) | **~310ms** | 闲置一段时间后的首次调用较慢(~630ms),因为 macOS 将系统框架(Cocoa, WebKit)加载到内存中。后续运行稳定在 **~310ms** —— 这包括进程生成、窗口服务器、WebKit 初始化、HTML 渲染、JS 执行和通过 stdout 的 JSON 响应,所有这些仅在三分之一秒内完成。 冷启动(从源码编译 + 运行) | 阶段 | 时间 | |-------|------| | `swiftc -O` 编译 | ~1,600ms | | 窗口往返 | ~350ms | | **总计** | **~2,000ms** | 冷启动只发生一次 —— 在 `npm install` 期间(通过 `postinstall`)或手动 `npm run build`。之后,始终是热启动。 ## 许可证 MIT
标签:DNS 反向解析, Electron替代, GNU通用公共许可证, HUD, JSON Lines, LangChain, MITM代理, Node.js, SIRP, Swift, WKWebView, 前端工具, 原生应用, 双向通信, 开源库, 微用户界面, 搜索引擎爬虫, 无运行时依赖, 智能体界面, 暗色界面, 标准输入输出, 桌面应用开发, 浮动窗口, 脚本GUI, 轻量级, 进程间通信