@pointylab/dsh 0.0.0-stage → 0.4.4
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/ASSETS.md +20 -0
- package/LICENSE +21 -0
- package/README.md +85 -2
- package/cordis.patch.yml +13 -0
- package/lib/client.js +33 -0
- package/package.json +38 -4
- package/pet/app/card/card.css +365 -0
- package/pet/app/card/card.html +68 -0
- package/pet/app/card/card.js +896 -0
- package/pet/app/card/markdown.js +64 -0
- package/pet/app/card/mascot/blink.png +0 -0
- package/pet/app/card/mascot/done.png +0 -0
- package/pet/app/card/mascot/error.png +0 -0
- package/pet/app/card/mascot/idle.png +0 -0
- package/pet/app/card/mascot/peek.png +0 -0
- package/pet/app/card/mascot/think.png +0 -0
- package/pet/app/card/preload.js +21 -0
- package/pet/app/fullscreen.js +131 -0
- package/pet/app/main.js +1284 -0
- package/pet/app/native/uia-selection.ps1 +158 -0
- package/pet/app/notice/notice.html +37 -0
- package/pet/app/notice/notice.js +31 -0
- package/pet/app/notice/preload.js +7 -0
- package/pet/app/overlay/overlay.html +30 -0
- package/pet/app/overlay/overlay.js +42 -0
- package/pet/app/overlay/preload.js +7 -0
- package/pet/app/pill/pill.css +73 -0
- package/pet/app/pill/pill.html +11 -0
- package/pet/app/pill/pill.js +35 -0
- package/pet/app/pill/preload.js +8 -0
- package/pet/app/preload.js +16 -0
- package/pet/app/renderer/index.html +24 -0
- package/pet/app/renderer/lines.js +25 -0
- package/pet/app/renderer/pet.js +292 -0
- package/pet/app/renderer/renderer2d.js +71 -0
- package/pet/app/renderer/style.css +157 -0
- package/pet/app/selection.js +246 -0
- package/pet/app/shake.js +76 -0
- package/pet/app/uipick.js +314 -0
- package/pet/assets/cursor/cursor-32.cur +0 -0
- package/pet/assets/cursor/cursor-32.png +0 -0
- package/pet/assets/cursor/cursor-48.cur +0 -0
- package/pet/assets/cursor/cursor-48.png +0 -0
- package/pet/assets/cursor/cursor-64.cur +0 -0
- package/pet/assets/cursor/cursor-64.png +0 -0
- package/pet/assets/cursor/cursor-clean.png +0 -0
- package/pet/assets/cursor/make_cursor.py +85 -0
- package/pet/assets/cursor/source.png +0 -0
- package/pet/assets/pointy.ico +0 -0
- package/pet/assets/tray.png +0 -0
- package/pet/assets/tray@2x.png +0 -0
- package/pet/boot.js +6 -0
- package/pet/extension/background.js +252 -0
- package/pet/extension/cursor-32.png +0 -0
- package/pet/extension/cursor-48.png +0 -0
- package/pet/extension/cursor-64.png +0 -0
- package/pet/extension/icon.png +0 -0
- package/pet/extension/manifest.json +12 -0
- package/pet/extension/pick.js +391 -0
- package/pet/extension/popup.html +29 -0
- package/pet/extension/popup.js +30 -0
- package/pet/package.json +11 -0
- package/pet/vendor/koffi/LICENSE.txt +22 -0
- package/pet/vendor/koffi/build/koffi/win32_x64/koffi.exp +0 -0
- package/pet/vendor/koffi/build/koffi/win32_x64/koffi.lib +0 -0
- package/pet/vendor/koffi/build/koffi/win32_x64/koffi.node +0 -0
- package/pet/vendor/koffi/index.js +634 -0
- package/pet/vendor/koffi/indirect.js +533 -0
- package/pet/vendor/koffi/package.json +35 -0
- package/pet/vendor/ws/LICENSE +20 -0
- package/pet/vendor/ws/index.js +22 -0
- package/pet/vendor/ws/lib/buffer-util.js +142 -0
- package/pet/vendor/ws/lib/constants.js +19 -0
- package/pet/vendor/ws/lib/event-target.js +292 -0
- package/pet/vendor/ws/lib/extension.js +203 -0
- package/pet/vendor/ws/lib/limiter.js +55 -0
- package/pet/vendor/ws/lib/permessage-deflate.js +530 -0
- package/pet/vendor/ws/lib/receiver.js +743 -0
- package/pet/vendor/ws/lib/sender.js +610 -0
- package/pet/vendor/ws/lib/stream.js +161 -0
- package/pet/vendor/ws/lib/subprotocol.js +62 -0
- package/pet/vendor/ws/lib/validation.js +152 -0
- package/pet/vendor/ws/lib/websocket-server.js +562 -0
- package/pet/vendor/ws/lib/websocket.js +1426 -0
- package/pet/vendor/ws/package.json +74 -0
- package/pet/vendor/ws/wrapper.mjs +21 -0
- package/src/node.js +732 -0
- package/src/runtime.js +178 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// 卡片里够用的 Markdown:代码块、表格、引用块、列表、标题、粗体、行内代码、链接。
|
|
2
|
+
// 先转义再拼标签,不会把回复里的 HTML 当真;链接只给 http(s) 和本机绝对路径,点击由卡片交给主进程处理。
|
|
3
|
+
|
|
4
|
+
const escapeHtml = (s) => s.replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c])
|
|
5
|
+
|
|
6
|
+
// [文字](链接) 或 [文字](<带空格的路径>),在转义之后的文本上匹配
|
|
7
|
+
const LINK = /\[([^\]\n]+)\]\((?:<([^\n]+?)>|([^)\s]+))\)/g
|
|
8
|
+
|
|
9
|
+
function linkTarget(raw) {
|
|
10
|
+
const href = raw.replace(/&/g, '&')
|
|
11
|
+
if (/^https?:\/\//i.test(href)) return href
|
|
12
|
+
if (/^[a-z]:[\\/]/i.test(href)) return href
|
|
13
|
+
return undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function inline(text) {
|
|
17
|
+
return text
|
|
18
|
+
.replace(/`([^`\n]+)`/g, '<code>$1</code>')
|
|
19
|
+
.replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>')
|
|
20
|
+
.replace(LINK, (m, label, angled, plain) => {
|
|
21
|
+
const href = linkTarget(angled ?? plain)
|
|
22
|
+
return href ? `<a href="#" data-href="${href.replace(/"/g, '"')}" title="${href.replace(/"/g, '"')}">${label}</a>` : m
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const cells = (line) => line.trim().replace(/^\||\|$/g, '').split('|').map(c => c.trim())
|
|
27
|
+
const isTableSep = (line) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(line)
|
|
28
|
+
|
|
29
|
+
/** 非代码部分按行处理:表格、引用块、标题、列表,其余原样(卡片用 pre-wrap 保留换行) */
|
|
30
|
+
function blocks(text) {
|
|
31
|
+
const lines = text.split('\n')
|
|
32
|
+
const out = []
|
|
33
|
+
for (let i = 0; i < lines.length; i++) {
|
|
34
|
+
const line = lines[i]
|
|
35
|
+
if (line.trim().startsWith('|') && i + 1 < lines.length && isTableSep(lines[i + 1])) {
|
|
36
|
+
const head = cells(line)
|
|
37
|
+
const rows = []
|
|
38
|
+
i += 2
|
|
39
|
+
while (i < lines.length && lines[i].trim().startsWith('|')) rows.push(cells(lines[i++]))
|
|
40
|
+
i--
|
|
41
|
+
out.push(`<table><tr>${head.map(h => `<th>${inline(h)}</th>`).join('')}</tr>${rows.map(r => `<tr>${r.map(c => `<td>${inline(c)}</td>`).join('')}</tr>`).join('')}</table>`)
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
if (/^>\s?/.test(line)) {
|
|
45
|
+
const quoted = []
|
|
46
|
+
while (i < lines.length && /^>\s?/.test(lines[i])) quoted.push(lines[i++].replace(/^>\s?/, ''))
|
|
47
|
+
i--
|
|
48
|
+
out.push(`<blockquote>${quoted.map(inline).join('\n')}</blockquote>`)
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
const heading = /^#{1,6}\s+(.+)$/.exec(line)
|
|
52
|
+
if (heading) { out.push(`<b class="h">${inline(heading[1])}</b>`); continue }
|
|
53
|
+
out.push(inline(line.replace(/^(\s*)[-*]\s+/, '$1• ')))
|
|
54
|
+
}
|
|
55
|
+
// 表格和引用块自己是块级元素,前后的换行会多出空行,去掉
|
|
56
|
+
return out.join('\n').replace(/\n?(<table>|<blockquote>)/g, '$1').replace(/(<\/table>|<\/blockquote>)\n?/g, '$1')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function renderMarkdown(md) {
|
|
60
|
+
return md.split(/```[^\n]*\n?/).map((part, i) => {
|
|
61
|
+
const html = escapeHtml(part)
|
|
62
|
+
return i % 2 === 1 ? `<pre><code>${html}</code></pre>` : blocks(html)
|
|
63
|
+
}).join('')
|
|
64
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron')
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld('card', {
|
|
4
|
+
onInit: (fn) => ipcRenderer.on('card-init', (_e, data) => fn(data)),
|
|
5
|
+
onDsh: (fn) => ipcRenderer.on('dsh', (_e, msg) => fn(msg)),
|
|
6
|
+
send: (msg) => ipcRenderer.invoke('card-to-plugin', msg),
|
|
7
|
+
close: () => ipcRenderer.send('card-close'),
|
|
8
|
+
openDsh: (sessionId) => ipcRenderer.send('card-open-dsh', sessionId),
|
|
9
|
+
copy: (text) => ipcRenderer.send('card-copy', text),
|
|
10
|
+
resize: (height) => ipcRenderer.send('card-resize', height),
|
|
11
|
+
menuRoom: (height) => ipcRenderer.send('card-menu-room', height),
|
|
12
|
+
rendered: (name) => ipcRenderer.send('card-rendered', name),
|
|
13
|
+
keep: (info) => ipcRenderer.send('card-keep', info),
|
|
14
|
+
onSetAside: (fn) => ipcRenderer.on('card-set-aside', () => fn()),
|
|
15
|
+
onRestore: (fn) => ipcRenderer.on('card-restore', () => fn()),
|
|
16
|
+
openLink: (href) => ipcRenderer.send('card-open-link', href),
|
|
17
|
+
gripStart: () => ipcRenderer.send('card-grip-start'),
|
|
18
|
+
gripMove: () => ipcRenderer.send('card-grip-move'),
|
|
19
|
+
gripEnd: () => ipcRenderer.send('card-grip-end'),
|
|
20
|
+
asked: () => ipcRenderer.send('card-asked'),
|
|
21
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// 判断前台是不是全屏应用(游戏、演示、全屏视频),Pointy在这期间要藏起来
|
|
2
|
+
const koffi = require('koffi')
|
|
3
|
+
|
|
4
|
+
const QUNS_RUNNING_D3D_FULL_SCREEN = 3
|
|
5
|
+
const QUNS_PRESENTATION_MODE = 4
|
|
6
|
+
// 桌面和任务栏本身也是"铺满屏幕的前台窗口",不算全屏应用
|
|
7
|
+
const SHELL_CLASSES = new Set(['Progman', 'WorkerW', 'Shell_TrayWnd', 'Shell_SecondaryTrayWnd'])
|
|
8
|
+
const MONITOR_DEFAULTTONEAREST = 2
|
|
9
|
+
|
|
10
|
+
const shell32 = koffi.load('shell32.dll')
|
|
11
|
+
const user32 = koffi.load('user32.dll')
|
|
12
|
+
const RECT = koffi.struct('RECT', { left: 'int32', top: 'int32', right: 'int32', bottom: 'int32' })
|
|
13
|
+
const MONITORINFO = koffi.struct('MONITORINFO', { cbSize: 'uint32', rcMonitor: RECT, rcWork: RECT, dwFlags: 'uint32' })
|
|
14
|
+
|
|
15
|
+
const SHQueryUserNotificationState = shell32.func('int32 __stdcall SHQueryUserNotificationState(_Out_ int32 *state)')
|
|
16
|
+
const GetForegroundWindow = user32.func('void * __stdcall GetForegroundWindow()')
|
|
17
|
+
const GetWindowRect = user32.func('bool __stdcall GetWindowRect(void *hwnd, _Out_ RECT *rect)')
|
|
18
|
+
const MonitorFromWindow = user32.func('void * __stdcall MonitorFromWindow(void *hwnd, uint32 flags)')
|
|
19
|
+
const GetMonitorInfoW = user32.func('bool __stdcall GetMonitorInfoW(void *monitor, _Inout_ MONITORINFO *info)')
|
|
20
|
+
const GetClassNameW = user32.func('int __stdcall GetClassNameW(void *hwnd, _Out_ uint16 *name, int max)')
|
|
21
|
+
|
|
22
|
+
function notificationState() {
|
|
23
|
+
const out = [0]
|
|
24
|
+
return SHQueryUserNotificationState(out) === 0 ? out[0] : 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function className(hwnd) {
|
|
28
|
+
const buf = Buffer.alloc(512)
|
|
29
|
+
const n = GetClassNameW(hwnd, buf, 256)
|
|
30
|
+
return buf.toString('utf16le', 0, n * 2)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 前台窗口是否盖满它所在的整块显示器(无边框全屏游戏常常不报 D3D 全屏状态) */
|
|
34
|
+
function foregroundCoversMonitor(ownHwnds) {
|
|
35
|
+
const hwnd = GetForegroundWindow()
|
|
36
|
+
if (!hwnd) return false
|
|
37
|
+
const addr = koffi.address(hwnd)
|
|
38
|
+
if (ownHwnds.some(h => h === addr)) return false
|
|
39
|
+
if (SHELL_CLASSES.has(className(hwnd))) return false
|
|
40
|
+
const rect = {}
|
|
41
|
+
if (!GetWindowRect(hwnd, rect)) return false
|
|
42
|
+
const info = { cbSize: koffi.sizeof(MONITORINFO), rcMonitor: {}, rcWork: {}, dwFlags: 0 }
|
|
43
|
+
if (!GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), info)) return false
|
|
44
|
+
const m = info.rcMonitor
|
|
45
|
+
return rect.left <= m.left && rect.top <= m.top && rect.right >= m.right && rect.bottom >= m.bottom
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param ownHwnds 本程序自己的窗口句柄地址(BigInt),自己不算 */
|
|
49
|
+
function isFullscreenActive(ownHwnds = []) {
|
|
50
|
+
const state = notificationState()
|
|
51
|
+
if (state === QUNS_RUNNING_D3D_FULL_SCREEN || state === QUNS_PRESENTATION_MODE) return true
|
|
52
|
+
// QUNS_BUSY 不算数:它只说明"有个置顶窗口铺满了屏幕",Claude 的 computer use 覆盖层、录屏工具的遮罩都会触发,
|
|
53
|
+
// 不一定是用户在看的东西。真正的全屏应用会是前台窗口,下面这条会抓到
|
|
54
|
+
return foregroundCoversMonitor(ownHwnds)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 置顶维护:Electron 窗口 hide/showInactive 或别的窗口激活后,Windows 上的 TOPMOST 会掉
|
|
58
|
+
const SetWindowPos = user32.func('bool __stdcall SetWindowPos(intptr hwnd, intptr after, int x, int y, int cx, int cy, uint32 flags)')
|
|
59
|
+
const GetWindowLongPtrW = user32.func('intptr __stdcall GetWindowLongPtrW(intptr hwnd, int index)')
|
|
60
|
+
const HWND_TOPMOST = -1n
|
|
61
|
+
const HWND_NOTOPMOST = -2n
|
|
62
|
+
const GWL_EXSTYLE = -20
|
|
63
|
+
const WS_EX_TOPMOST = 0x8n
|
|
64
|
+
const SWP_NOSIZE = 0x1
|
|
65
|
+
const SWP_NOMOVE = 0x2
|
|
66
|
+
const SWP_NOACTIVATE = 0x10
|
|
67
|
+
const SWP_NOOWNERZORDER = 0x200
|
|
68
|
+
|
|
69
|
+
const isTopmost = (hwnd) => (BigInt(GetWindowLongPtrW(hwnd, GWL_EXSTYLE)) & WS_EX_TOPMOST) !== 0n
|
|
70
|
+
|
|
71
|
+
/** 把窗口重新压到所有普通窗口上面,不激活、不移动 */
|
|
72
|
+
function assertTopmost(hwnd) {
|
|
73
|
+
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 退到所有置顶窗口下面(仍在普通窗口上面),不激活、不移动 */
|
|
77
|
+
function yieldTopmost(hwnd) {
|
|
78
|
+
SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOOWNERZORDER)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 系统弹出层:右键菜单、托盘溢出面板、Win11 的 XAML 弹出层、Alt+Tab、输入法切换。
|
|
82
|
+
// 开着的时候Pointy要让到它们下面,不然会挡住托盘图标和菜单
|
|
83
|
+
const SHELL_POPUP_CLASSES = ['#32768', 'TopLevelWindowForOverflowXamlIsland', 'NotifyIconOverflowWindow', 'Xaml_WindowedPopupClass', 'XamlExplorerHostIslandWindow', 'MultitaskingViewFrame', 'Shell_InputSwitchTopLevelWindow']
|
|
84
|
+
// 开始菜单、搜索、通知中心、快速设置在前台时是顶层 CoreWindow(普通 UWP 应用的顶层是 ApplicationFrameWindow)
|
|
85
|
+
const SHELL_FOREGROUND_CLASSES = new Set([...SHELL_POPUP_CLASSES, 'Shell_TrayWnd', 'Shell_SecondaryTrayWnd', 'ForegroundStaging', 'Windows.UI.Core.CoreWindow', 'ControlCenterWindow'])
|
|
86
|
+
const FindWindowExW = user32.func('void * __stdcall FindWindowExW(void *parent, void *after, const char16_t *cls, const char16_t *title)')
|
|
87
|
+
const IsWindowVisible = user32.func('bool __stdcall IsWindowVisible(void *hwnd)')
|
|
88
|
+
|
|
89
|
+
function anyVisible(cls) {
|
|
90
|
+
for (let h = FindWindowExW(null, null, cls, null); h; h = FindWindowExW(null, h, cls, null)) {
|
|
91
|
+
if (IsWindowVisible(h)) return true
|
|
92
|
+
}
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 前台是任务栏/系统弹出层,或者屏幕上开着一个系统弹出层 */
|
|
97
|
+
function shellPopupOpen() {
|
|
98
|
+
const fg = GetForegroundWindow()
|
|
99
|
+
if (fg && SHELL_FOREGROUND_CLASSES.has(className(fg))) return true
|
|
100
|
+
return SHELL_POPUP_CLASSES.some(anyVisible)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const foregroundAddress = () => {
|
|
104
|
+
const hwnd = GetForegroundWindow()
|
|
105
|
+
return hwnd ? koffi.address(hwnd) : 0n
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 让别的进程(这里是被 dsh:// 唤起的 DSH)能把自己提到前台;只有前台进程才有资格放行
|
|
109
|
+
const AllowSetForegroundWindow = user32.func('bool __stdcall AllowSetForegroundWindow(uint32 pid)')
|
|
110
|
+
const ASFW_ANY = 0xFFFFFFFF
|
|
111
|
+
const allowAnyForeground = () => AllowSetForegroundWindow(ASFW_ANY)
|
|
112
|
+
|
|
113
|
+
// 把卡片挂到来源程序下面当"被拥有的窗口":Windows 保证它在那个程序之上,
|
|
114
|
+
// 切到别的程序时跟着那个程序一起被盖住,回到那个程序时又跟着出来
|
|
115
|
+
const SetWindowLongPtrW = user32.func('intptr __stdcall SetWindowLongPtrW(intptr hwnd, int index, intptr value)')
|
|
116
|
+
const GA_ROOT = 2
|
|
117
|
+
const GetAncestor = user32.func('intptr __stdcall GetAncestor(intptr hwnd, uint32 flags)')
|
|
118
|
+
const GWLP_HWNDPARENT = -8
|
|
119
|
+
const setOwner = (hwnd, owner) => SetWindowLongPtrW(hwnd, GWLP_HWNDPARENT, owner ? GetAncestor(owner, GA_ROOT) : 0n)
|
|
120
|
+
|
|
121
|
+
// 来源程序窗口的左上角(物理像素),窗口没了返回 undefined;卡片跟着它挪
|
|
122
|
+
const GetWindowRectAt = user32.func('bool __stdcall GetWindowRect(intptr hwnd, _Out_ RECT *rect)')
|
|
123
|
+
const IsWindow = user32.func('bool __stdcall IsWindow(intptr hwnd)')
|
|
124
|
+
function windowOrigin(hwnd) {
|
|
125
|
+
const root = GetAncestor(hwnd, GA_ROOT)
|
|
126
|
+
if (!root || !IsWindow(root)) return undefined
|
|
127
|
+
const r = {}
|
|
128
|
+
return GetWindowRectAt(root, r) ? { x: r.left, y: r.top } : undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { isFullscreenActive, isTopmost, assertTopmost, yieldTopmost, shellPopupOpen, foregroundAddress, allowAnyForeground, setOwner, windowOrigin }
|