@pointylab/dsh 0.0.0-stage → 0.4.3

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 (88) hide show
  1. package/ASSETS.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +85 -2
  4. package/cordis.patch.yml +13 -0
  5. package/lib/client.js +33 -0
  6. package/package.json +38 -4
  7. package/pet/app/card/card.css +365 -0
  8. package/pet/app/card/card.html +68 -0
  9. package/pet/app/card/card.js +896 -0
  10. package/pet/app/card/markdown.js +64 -0
  11. package/pet/app/card/mascot/blink.png +0 -0
  12. package/pet/app/card/mascot/done.png +0 -0
  13. package/pet/app/card/mascot/error.png +0 -0
  14. package/pet/app/card/mascot/idle.png +0 -0
  15. package/pet/app/card/mascot/peek.png +0 -0
  16. package/pet/app/card/mascot/think.png +0 -0
  17. package/pet/app/card/preload.js +21 -0
  18. package/pet/app/fullscreen.js +131 -0
  19. package/pet/app/main.js +1252 -0
  20. package/pet/app/native/uia-selection.ps1 +158 -0
  21. package/pet/app/notice/notice.html +37 -0
  22. package/pet/app/notice/notice.js +31 -0
  23. package/pet/app/notice/preload.js +7 -0
  24. package/pet/app/overlay/overlay.html +30 -0
  25. package/pet/app/overlay/overlay.js +42 -0
  26. package/pet/app/overlay/preload.js +7 -0
  27. package/pet/app/pill/pill.css +73 -0
  28. package/pet/app/pill/pill.html +11 -0
  29. package/pet/app/pill/pill.js +35 -0
  30. package/pet/app/pill/preload.js +8 -0
  31. package/pet/app/preload.js +16 -0
  32. package/pet/app/renderer/index.html +24 -0
  33. package/pet/app/renderer/lines.js +25 -0
  34. package/pet/app/renderer/pet.js +292 -0
  35. package/pet/app/renderer/renderer2d.js +71 -0
  36. package/pet/app/renderer/style.css +157 -0
  37. package/pet/app/selection.js +246 -0
  38. package/pet/app/shake.js +76 -0
  39. package/pet/app/uipick.js +314 -0
  40. package/pet/assets/cursor/cursor-32.cur +0 -0
  41. package/pet/assets/cursor/cursor-32.png +0 -0
  42. package/pet/assets/cursor/cursor-48.cur +0 -0
  43. package/pet/assets/cursor/cursor-48.png +0 -0
  44. package/pet/assets/cursor/cursor-64.cur +0 -0
  45. package/pet/assets/cursor/cursor-64.png +0 -0
  46. package/pet/assets/cursor/cursor-clean.png +0 -0
  47. package/pet/assets/cursor/make_cursor.py +85 -0
  48. package/pet/assets/cursor/source.png +0 -0
  49. package/pet/assets/pointy.ico +0 -0
  50. package/pet/assets/tray.png +0 -0
  51. package/pet/assets/tray@2x.png +0 -0
  52. package/pet/boot.js +6 -0
  53. package/pet/extension/background.js +252 -0
  54. package/pet/extension/cursor-32.png +0 -0
  55. package/pet/extension/cursor-48.png +0 -0
  56. package/pet/extension/cursor-64.png +0 -0
  57. package/pet/extension/icon.png +0 -0
  58. package/pet/extension/manifest.json +12 -0
  59. package/pet/extension/pick.js +391 -0
  60. package/pet/extension/popup.html +29 -0
  61. package/pet/extension/popup.js +30 -0
  62. package/pet/package.json +11 -0
  63. package/pet/vendor/koffi/LICENSE.txt +22 -0
  64. package/pet/vendor/koffi/build/koffi/win32_x64/koffi.exp +0 -0
  65. package/pet/vendor/koffi/build/koffi/win32_x64/koffi.lib +0 -0
  66. package/pet/vendor/koffi/build/koffi/win32_x64/koffi.node +0 -0
  67. package/pet/vendor/koffi/index.js +634 -0
  68. package/pet/vendor/koffi/indirect.js +533 -0
  69. package/pet/vendor/koffi/package.json +35 -0
  70. package/pet/vendor/ws/LICENSE +20 -0
  71. package/pet/vendor/ws/index.js +22 -0
  72. package/pet/vendor/ws/lib/buffer-util.js +142 -0
  73. package/pet/vendor/ws/lib/constants.js +19 -0
  74. package/pet/vendor/ws/lib/event-target.js +292 -0
  75. package/pet/vendor/ws/lib/extension.js +203 -0
  76. package/pet/vendor/ws/lib/limiter.js +55 -0
  77. package/pet/vendor/ws/lib/permessage-deflate.js +530 -0
  78. package/pet/vendor/ws/lib/receiver.js +743 -0
  79. package/pet/vendor/ws/lib/sender.js +610 -0
  80. package/pet/vendor/ws/lib/stream.js +161 -0
  81. package/pet/vendor/ws/lib/subprotocol.js +62 -0
  82. package/pet/vendor/ws/lib/validation.js +152 -0
  83. package/pet/vendor/ws/lib/websocket-server.js +562 -0
  84. package/pet/vendor/ws/lib/websocket.js +1426 -0
  85. package/pet/vendor/ws/package.json +74 -0
  86. package/pet/vendor/ws/wrapper.mjs +21 -0
  87. package/src/node.js +732 -0
  88. package/src/runtime.js +178 -0
package/src/runtime.js ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Pointy needs a full Electron runtime (windows, tray, screenshots), which the
3
+ * DSH plugin host does not have: it runs DSH's own binary in node mode. So the
4
+ * plugin carries Pointy's code in ../pet and, the first time, downloads the
5
+ * stock Electron build that Pointy was tested with into ~/.pointy/runtime.
6
+ *
7
+ * The zip is checked against the SHA-256 pinned in pet/package.json at pack
8
+ * time, so a mirror can only be slow, never substitute a different binary.
9
+ */
10
+ import { spawn } from 'node:child_process'
11
+ import crypto from 'node:crypto'
12
+ import fs from 'node:fs'
13
+ import os from 'node:os'
14
+ import path from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+
17
+ export const PET_APP_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'pet')
18
+ const RUNTIME_DIR = path.join(os.homedir(), '.pointy', 'runtime')
19
+
20
+ // npmmirror first: GitHub release downloads are often slow or blocked in mainland China.
21
+ const MIRRORS = [
22
+ 'https://npmmirror.com/mirrors/electron/',
23
+ 'https://github.com/electron/electron/releases/download/',
24
+ ]
25
+ const DOWNLOAD_TIMEOUT_MS = 10 * 60_000
26
+ /** electron.exe is renamed and rebranded so Task Manager shows "Pointy", not "Electron". */
27
+ const EXE_NAME = 'Pointy.exe'
28
+
29
+ /** `{ version, sha256 }` of the Electron zip pinned by tools/pack-plugin.mjs, or undefined in a dev checkout. */
30
+ export function pinnedRuntime() {
31
+ try {
32
+ const pkg = JSON.parse(fs.readFileSync(path.join(PET_APP_DIR, 'package.json'), 'utf8'))
33
+ const { version, sha256 } = pkg.pointy?.electron ?? {}
34
+ if (typeof version === 'string' && /^[0-9a-f]{64}$/.test(sha256 ?? '')) return { version, sha256 }
35
+ } catch {}
36
+ return undefined
37
+ }
38
+
39
+ const zipName = (version) => `electron-v${version}-win32-x64.zip`
40
+ const runtimeDir = (version) => path.join(RUNTIME_DIR, `electron-v${version}-win32-x64`)
41
+
42
+ /** electron.exe for the pinned version if it is already unpacked. */
43
+ export function installedRuntime(pin = pinnedRuntime()) {
44
+ if (!pin) return undefined
45
+ const exe = path.join(runtimeDir(pin.version), EXE_NAME)
46
+ return fs.existsSync(exe) ? exe : undefined
47
+ }
48
+
49
+ async function download(url, file, logger) {
50
+ const res = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) })
51
+ if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`)
52
+ const total = Number(res.headers.get('content-length')) || 0
53
+ const hash = crypto.createHash('sha256')
54
+ const out = fs.createWriteStream(file)
55
+ let got = 0
56
+ let nextLog = 0.25
57
+ try {
58
+ for await (const chunk of res.body) {
59
+ hash.update(chunk)
60
+ got += chunk.length
61
+ if (!out.write(chunk)) await new Promise((r) => out.once('drain', r))
62
+ if (total && got / total >= nextLog) {
63
+ logger.info(`pointy: downloading Electron ${Math.round((got / total) * 100)}%`)
64
+ nextLog += 0.25
65
+ }
66
+ }
67
+ } finally {
68
+ await new Promise((r) => out.end(r))
69
+ }
70
+ return hash.digest('hex')
71
+ }
72
+
73
+ function unzip(zip, dir) {
74
+ // Windows 10+ ships bsdtar, which reads zip archives.
75
+ const tar = path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'tar.exe')
76
+ return new Promise((resolve, reject) => {
77
+ const child = spawn(tar, ['-xf', zip, '-C', dir], { stdio: 'ignore', windowsHide: true })
78
+ child.on('error', reject)
79
+ child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`tar exited with ${code}`))))
80
+ })
81
+ }
82
+
83
+ /**
84
+ * Rename electron.exe to Pointy.exe and rewrite its version strings and icon.
85
+ * Task Manager's process list shows the FileDescription, its details tab the
86
+ * file name. The official signature no longer matches afterwards, so it is
87
+ * dropped; Windows does not check signatures of programs started by another
88
+ * program. If the resource edit fails, the renamed stock binary still runs.
89
+ */
90
+ async function brand(dir, logger) {
91
+ const from = path.join(dir, 'electron.exe')
92
+ const to = path.join(dir, EXE_NAME)
93
+ try {
94
+ const ResEdit = await import('resedit')
95
+ const exe = ResEdit.NtExecutable.from(fs.readFileSync(from), { ignoreCert: true })
96
+ const res = ResEdit.NtExecutableResource.from(exe)
97
+ const icoFile = path.join(PET_APP_DIR, 'assets', 'pointy.ico')
98
+ if (fs.existsSync(icoFile)) {
99
+ const icons = ResEdit.Data.IconFile.from(fs.readFileSync(icoFile)).icons.map((icon) => icon.data)
100
+ for (const group of ResEdit.Resource.IconGroupEntry.fromEntries(res.entries)) {
101
+ ResEdit.Resource.IconGroupEntry.replaceIconsForResource(res.entries, group.id, group.lang, icons)
102
+ }
103
+ }
104
+ for (const vi of ResEdit.Resource.VersionInfo.fromEntries(res.entries)) {
105
+ for (const lang of vi.getAllLanguagesForStringValues()) {
106
+ vi.setStringValues(lang, { FileDescription: 'Pointy', ProductName: 'Pointy', InternalName: 'Pointy', OriginalFilename: EXE_NAME })
107
+ }
108
+ vi.outputToResourceEntries(res.entries)
109
+ }
110
+ res.outputResource(exe)
111
+ fs.writeFileSync(to, Buffer.from(exe.generate()))
112
+ fs.rmSync(from)
113
+ } catch (error) {
114
+ logger.warn(`pointy: could not rebrand electron.exe (${error.message}); using it renamed`)
115
+ fs.rmSync(to, { force: true })
116
+ fs.renameSync(from, to)
117
+ }
118
+ }
119
+
120
+ let installing
121
+ /**
122
+ * Make sure the pinned Electron is unpacked; returns the path of electron.exe.
123
+ * One download at a time per DSH run; older runtimes are removed afterwards.
124
+ */
125
+ export function ensureRuntime(logger) {
126
+ const pin = pinnedRuntime()
127
+ if (!pin) return Promise.reject(new Error(`no pinned Electron in ${PET_APP_DIR}`))
128
+ if (process.platform !== 'win32' || process.arch !== 'x64') return Promise.reject(new Error('Pointy only supports Windows x64'))
129
+ const ready = installedRuntime(pin)
130
+ if (ready) return Promise.resolve(ready)
131
+ installing ??= install(pin, logger).finally(() => { installing = undefined })
132
+ return installing
133
+ }
134
+
135
+ async function install(pin, logger) {
136
+ fs.mkdirSync(RUNTIME_DIR, { recursive: true })
137
+ // Leftovers of a download that was cut off (DSH closed midway).
138
+ for (const name of fs.readdirSync(RUNTIME_DIR)) {
139
+ const full = path.join(RUNTIME_DIR, name)
140
+ if (name.startsWith('tmp-') && Date.now() - fs.statSync(full).mtimeMs > 3600_000) fs.rmSync(full, { recursive: true, force: true })
141
+ }
142
+ const tmp = fs.mkdtempSync(path.join(RUNTIME_DIR, 'tmp-'))
143
+ try {
144
+ const zip = path.join(tmp, zipName(pin.version))
145
+ let lastError
146
+ for (const mirror of MIRRORS) {
147
+ const url = `${mirror}v${pin.version}/${zipName(pin.version)}`
148
+ try {
149
+ logger.info(`pointy: downloading Electron ${pin.version} from ${url}`)
150
+ const sha = await download(url, zip, logger)
151
+ if (sha !== pin.sha256) throw new Error(`checksum mismatch (${sha})`)
152
+ lastError = undefined
153
+ break
154
+ } catch (error) {
155
+ lastError = error
156
+ logger.warn(`pointy: ${url} failed: ${error.message}`)
157
+ fs.rmSync(zip, { force: true })
158
+ }
159
+ }
160
+ if (lastError) throw lastError
161
+ const unpacked = path.join(tmp, 'unpacked')
162
+ fs.mkdirSync(unpacked)
163
+ await unzip(zip, unpacked)
164
+ if (!fs.existsSync(path.join(unpacked, 'electron.exe'))) throw new Error('electron.exe missing from the zip')
165
+ await brand(unpacked, logger)
166
+ const dest = runtimeDir(pin.version)
167
+ fs.rmSync(dest, { recursive: true, force: true })
168
+ fs.renameSync(unpacked, dest)
169
+ logger.info(`pointy: Electron ${pin.version} ready in ${dest}`)
170
+ for (const name of fs.readdirSync(RUNTIME_DIR)) {
171
+ const full = path.join(RUNTIME_DIR, name)
172
+ if (full !== dest && /^electron-v/.test(name)) fs.rmSync(full, { recursive: true, force: true })
173
+ }
174
+ return path.join(dest, EXE_NAME)
175
+ } finally {
176
+ fs.rmSync(tmp, { recursive: true, force: true })
177
+ }
178
+ }