buninu-browser 0.3.0 → 0.4.0

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 (239) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +19 -4
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +123 -0
  5. package/frontends/wayland/casty/LICENSE +21 -0
  6. package/frontends/wayland/casty/bookmarks.js +32 -0
  7. package/frontends/wayland/casty/click.js +96 -0
  8. package/frontends/wayland/casty/commands.js +61 -0
  9. package/frontends/wayland/casty/config.js +45 -0
  10. package/frontends/wayland/casty/download-directory.js +28 -0
  11. package/frontends/wayland/casty/download.js +49 -0
  12. package/frontends/wayland/casty/hints.js +334 -0
  13. package/frontends/wayland/casty/keys.js +56 -0
  14. package/frontends/wayland/casty/page-commands.js +80 -0
  15. package/frontends/wayland/casty/prompt-width.js +148 -0
  16. package/frontends/wayland/casty/urlbar.js +606 -0
  17. package/frontends/wayland/client.js +88 -0
  18. package/frontends/wayland/clipboard.js +80 -0
  19. package/frontends/wayland/index.js +116 -0
  20. package/frontends/wayland/input.js +56 -0
  21. package/frontends/wayland/protocol.js +55 -0
  22. package/frontends/wayland/protocols/wayland.xml +3248 -0
  23. package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
  24. package/frontends/wayland/transport.js +120 -0
  25. package/frontends/wayland/urlbar.js +158 -0
  26. package/index.js +28 -1
  27. package/lib/clipboard-command.js +10 -0
  28. package/lib/clipboard-powershell.js +22 -0
  29. package/lib/clipboard.js +37 -2
  30. package/lib/document-types.js +11 -0
  31. package/lib/headless-shell.js +47 -8
  32. package/lib/layout/text-layout.js +17 -9
  33. package/lib/paint/display-list.js +5 -1
  34. package/lib/qml/README.md +971 -0
  35. package/lib/qml/builtins.js +700 -0
  36. package/lib/qml/canvas-dom.js +115 -0
  37. package/lib/qml/document.js +1176 -0
  38. package/lib/qml/format.js +9 -0
  39. package/lib/qml/host.js +324 -0
  40. package/lib/qml/images.js +199 -0
  41. package/lib/qml/launcher.js +132 -0
  42. package/lib/qml/loader.js +517 -0
  43. package/lib/qml/media-renderer.js +49 -0
  44. package/lib/qml/media.js +86 -0
  45. package/lib/qml/models.js +622 -0
  46. package/lib/qml/parser.js +381 -0
  47. package/lib/qml/qs +5 -0
  48. package/lib/qml/qscasty +3 -0
  49. package/lib/qml/qsheadless +3 -0
  50. package/lib/qml/qswayland +3 -0
  51. package/lib/qml/reactive.js +159 -0
  52. package/lib/qml/runtime.js +3381 -0
  53. package/lib/qml/shapes.js +243 -0
  54. package/lib/qml/text.js +109 -0
  55. package/lib/qml/textures.js +123 -0
  56. package/lib/qml/wayland-clipboard.js +32 -0
  57. package/lib/qml/worker-thread.js +70 -0
  58. package/lib/render-tree/index.js +16 -1
  59. package/lib/renderer/bindings.js +11 -0
  60. package/lib/renderer/canvas-2d.js +13 -0
  61. package/lib/renderer/host.js +31 -6
  62. package/lib/renderer/main.js +19 -6
  63. package/lib/renderer/page-realm.js +16 -1
  64. package/lib/renderer/page-renderer.js +230 -13
  65. package/lib/renderer/text-input-paint.js +54 -0
  66. package/lib/renderer/text-input.js +57 -0
  67. package/lib/style/computed-style.js +2 -2
  68. package/package.json +1 -1
  69. package/test/clipboard.test.js +19 -0
  70. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  71. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  72. package/test/fixtures/flea-helpers/bin/bat +3 -0
  73. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  74. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  75. package/test/fixtures/flea-helpers/bin/glow +3 -0
  76. package/test/fixtures/flea-helpers/bin/qs +3 -0
  77. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  78. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  79. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  80. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  81. package/test/fixtures/flea-helpers/check.mjs +36 -0
  82. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  83. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  84. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  85. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  86. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  87. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  88. package/test/fixtures/qml/flea/LICENSE +21 -0
  89. package/test/fixtures/qml/flea/README.md +25 -0
  90. package/test/fixtures/qml/flea/Theme.qml +8 -0
  91. package/test/fixtures/qml/flea/demo.qml +24 -0
  92. package/test/fixtures/qml/flea/qmldir +3 -0
  93. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  94. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  95. package/test/fixtures/qml/flea-theme/README.md +70 -0
  96. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  97. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  98. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  99. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  100. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  101. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  102. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  103. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  104. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  105. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  106. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  107. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  108. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  109. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  110. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  111. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  112. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  113. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  114. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  115. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  116. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  117. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  118. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  119. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  120. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  121. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  122. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  123. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  124. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  125. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  126. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  127. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  128. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  129. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  130. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  131. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  132. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  133. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  134. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  135. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  136. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  137. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  138. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  139. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  140. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  141. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  142. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  143. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  144. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  145. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  146. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  147. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  148. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  167. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  168. package/test/fixtures/qml/hello.qml +26 -0
  169. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  170. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  171. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  172. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  173. package/test/fixtures/qml/imports/logic/base.js +2 -0
  174. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  175. package/test/fixtures/qml/imports/logic/math.js +3 -0
  176. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  177. package/test/fixtures/qml/imports/main.qml +21 -0
  178. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  179. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  180. package/test/fixtures/qml/multiply.qml +61 -0
  181. package/test/fixtures/qml-canvas-dom.js +90 -0
  182. package/test/fixtures/qml-media-audio.js +39 -0
  183. package/test/helpers/flea-sandbox.js +48 -0
  184. package/test/helpers/quickshell-fixture.js +125 -0
  185. package/test/html-clipboard-wayland.test.js +47 -0
  186. package/test/index.test.js +13 -0
  187. package/test/manual-casty-without-qml.js +87 -0
  188. package/test/manual-flea-application.js +49 -0
  189. package/test/manual-flea-binary.js +62 -0
  190. package/test/manual-flea-qml.js +9 -0
  191. package/test/manual-flea-settings.js +40 -0
  192. package/test/manual-flea-theme.js +40 -0
  193. package/test/manual-qml-removal.js +46 -0
  194. package/test/manual-without-qml.js +45 -0
  195. package/test/qml-canvas-dom.test.js +37 -0
  196. package/test/qml-clipboard-bridge.test.js +25 -0
  197. package/test/qml-clipboard-wayland.test.js +40 -0
  198. package/test/qml-connections-timing.test.js +73 -0
  199. package/test/qml-dynamic.test.js +401 -0
  200. package/test/qml-flea-app.test.js +146 -0
  201. package/test/qml-flea-backend.test.js +78 -0
  202. package/test/qml-flea-columns-backend.test.js +38 -0
  203. package/test/qml-flea-compile.test.js +78 -0
  204. package/test/qml-flea-launcher.test.js +88 -0
  205. package/test/qml-flea-markdown-backend.test.js +31 -0
  206. package/test/qml-flea-media.test.js +28 -0
  207. package/test/qml-flea-navigation-backend.test.js +35 -0
  208. package/test/qml-flea-navigation-timing.test.js +147 -0
  209. package/test/qml-flea-preview-backend.test.js +53 -0
  210. package/test/qml-flea-previews.test.js +163 -0
  211. package/test/qml-flea-rows.test.js +101 -0
  212. package/test/qml-flea-textures.test.js +59 -0
  213. package/test/qml-flea-view-switch.test.js +33 -0
  214. package/test/qml-flea-wayland.test.js +68 -0
  215. package/test/qml-flea.test.js +34 -0
  216. package/test/qml-host.test.js +238 -0
  217. package/test/qml-imports.test.js +102 -0
  218. package/test/qml-inherited-handlers.test.js +51 -0
  219. package/test/qml-input-selection.test.js +66 -0
  220. package/test/qml-interactions.test.js +309 -0
  221. package/test/qml-language.test.js +167 -0
  222. package/test/qml-launcher.test.js +54 -0
  223. package/test/qml-layout-text.test.js +205 -0
  224. package/test/qml-media.test.js +63 -0
  225. package/test/qml-persisted-delegates.test.js +45 -0
  226. package/test/qml-quickshell-controls.test.js +65 -0
  227. package/test/qml-quickshell-settings.test.js +62 -0
  228. package/test/qml-repeater.test.js +72 -0
  229. package/test/qml-style-updates.test.js +124 -0
  230. package/test/qml-textures.test.js +66 -0
  231. package/test/qml-views.test.js +332 -0
  232. package/test/qml-worker.test.js +69 -0
  233. package/test/qml.test.js +83 -0
  234. package/test/renderer-host.test.js +11 -0
  235. package/test/text-input-pointer.test.js +31 -0
  236. package/test/text-input-scroll.test.js +29 -0
  237. package/test/text-input.test.js +47 -0
  238. package/test/wayland-frontend.test.js +83 -0
  239. package/test/wayland-urlbar.test.js +38 -0
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016-2020: Zachary Yedidia, et al.
4
+ Copyright (c) 2026 Dr. John (醫者小智)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -10,7 +10,14 @@ directly as JavaScript and WebAssembly.
10
10
  > [!WARNING]
11
11
  > Buninu Browser is in an early experimental stage and is not yet a
12
12
  > general-purpose browser engine or replacement for Chromium. The headless
13
- > shell is the initial frontend; native windowed frontends are planned later.
13
+ > shell is the initial frontend; a standard Wayland frontend is experimental.
14
+
15
+ ## Experimental Wayland frontend
16
+
17
+ `bun index.js --wayland URL` opens a standard Wayland SHM window using
18
+ `WAYLAND_DISPLAY` and `XDG_RUNTIME_DIR`. Casty remains the default.
19
+ See [frontend details and limitations](frontends/wayland/README.md).
20
+ This does not yet provide full Quickshell/Flea/Omastorm compatibility.
14
21
 
15
22
  ## Usage
16
23
 
@@ -218,11 +225,19 @@ not start a browser process or CDP listener.
218
225
 
219
226
  ### Page sandbox
220
227
 
221
- Pages can copy text with `navigator.clipboard.writeText()` after a user click
222
- or key press on HTTPS (or localhost). The controller writes the clipboard,
228
+ Pages can copy or read text with `navigator.clipboard.writeText()` and
229
+ `navigator.clipboard.readText()` after a user click or key press on HTTPS
230
+ (or localhost). Each operation consumes that activation. The controller accesses the clipboard,
223
231
  preferring `xclip -selection clipboard`, with platform clipboard commands as
224
232
  fallbacks. Clipboard command access stays outside the page sandbox; unavailable
225
- providers reject the promise instead of reporting a successful copy.
233
+ providers reject the promise instead of reporting success. Reads happen only
234
+ on request, with no clipboard polling. HTML inputs and textareas share selection,
235
+ caret blinking, Shift+arrows, and Ctrl+C/X/V editing with QML controls. Paste
236
+ replaces the selection, or inserts at the cursor. Up/Down currently follow
237
+ explicit textarea line breaks, not soft-wrapped visual lines.
238
+
239
+ PowerShell clipboard quoting and newline handling are reused from jsmdcui
240
+ under its [MIT license](LICENSES/jsmdcui.txt).
226
241
 
227
242
  Page JavaScript runs behind three layers:
228
243
 
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dr. John (醫者小智)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,123 @@
1
+ # Experimental Wayland frontend / 實驗性 Wayland 前端
2
+
3
+ ```sh
4
+ XDG_RUNTIME_DIR=/tmp WAYLAND_DISPLAY=wayland-buninu \
5
+ bun index.js --wayland file:///absolute/path/to/hello.qml
6
+ # HTML works through the same frontend:
7
+ bun index.js --wayland https://example.com --window-size=800,600
8
+ ```
9
+
10
+ Uses the existing browser controller and renderer. No Qt, libwayland, native addon,
11
+ new dependency, PNG encoding, or base64. Only libc is loaded, because Bun sockets
12
+ cannot send/receive SCM_RIGHTS ancillary file descriptors. Linux/Android 64-bit only.
13
+ The default launcher remains Casty. External integration blocks in index.js are
14
+ marked `wayland handle`; the frontend lives entirely in this directory.
15
+
16
+ 沿用現有 Browser controller 與 renderer,不新增依賴;僅為傳送 fd 使用 libc。
17
+ 預設仍是 Casty,只有指定 `--wayland` 才啟用。入口整合標記為 `wayland handle`。
18
+
19
+ ## Address bar / 網址列
20
+
21
+ Ordinary pages have a 32-pixel address bar. Click the URL or press Alt+L to edit;
22
+ click `@` or press Ctrl+E for `cmd>`. Tab switches URL/command mode. Casty's editor
23
+ provides horizontal scrolling, Shift-arrow and mouse-drag selection, double-click
24
+ history/submit gestures, and Ctrl+C / Alt+C copying plus Ctrl+V insertion (replacing
25
+ selected text). Escape cancels editing; `q` quits. `help` lists applicable commands;
26
+ CDP and terminal transport commands are omitted. QML applications have no bar.
27
+
28
+ 一般網頁頂部有 32 像素高的網址列。點網址或按 Alt+L 編輯,點 `@` 或按 Ctrl+E
29
+ 進入 `cmd>`;Tab 切換模式。沿用 Casty 的水平捲動、Shift 方向鍵與滑鼠拖曳選取、
30
+ 雙擊歷史/送出操作,以及 Ctrl+C/Alt+C 複製、Ctrl+V 插入(有選取時取代選取)。
31
+ Escape 取消編輯,`q` 退出;`help` 列出適用命令,不包含 CDP 與終端傳輸命令。
32
+ QML 應用不顯示網址列。
33
+
34
+ Editing the address bar never pauses page frame updates. Only chrome-only changes
35
+ reuse the latest page frame. The cached bar is rasterized separately; raw BGRA
36
+ page bytes go straight to SHM beside the bar, without PNG or full-page canvas
37
+ composition. The bar's small RGBA raster is converted to BGRA; page channels are
38
+ not converted. Presentation still writes a complete SHM buffer.
39
+
40
+ 編輯網址列不會暫停頁面更新。只有網址列本身改變時才沿用上一張頁面;網址列獨立
41
+ 繪製並快取,頁面的 raw BGRA 直接與它分段寫入 SHM,不經 PNG 或整頁 Canvas
42
+ 合成。只有網址列的小圖需要 RGBA 轉 BGRA;提交仍需寫入完整 SHM buffer。
43
+
44
+ `casty/` vendors Casty's MIT-licensed editor and helpers (license included).
45
+ The editor changes replace terminal cursor output with a no-op and terminal column
46
+ lookup with an injectable width; its rendering is overridden by `urlbar.js`.
47
+ Page find/console helpers are extracted from Casty's input module. Navigation,
48
+ clipboard and drawing are adapted here to the existing browser controller.
49
+
50
+ `casty/` 保留 Casty 的 MIT 授權編輯器與輔助程式;移除終端游標輸出,欄寬改為
51
+ 可指定,繪製由 `urlbar.js` 覆寫。搜尋/console 輔助函式取自 Casty input 模組。
52
+ 導航、剪貼板與繪製則由此處的薄接層呼叫現有 browser controller。
53
+
54
+ ## Pixels and lifetime / 像素與生命週期
55
+
56
+ `xdg_wm_base` → `xdg_surface` → `xdg_toplevel`; acknowledge configure before commit.
57
+ `wl_shm` XRGB8888 buffers contain BGRX bytes on supported little-endian platforms.
58
+ The browser currently supplies BGRA; XRGB deliberately ignores alpha (opaque window).
59
+ At most two buffers are outstanding. A buffer is reused only after `wl_buffer.release`;
60
+ frame callbacks pace commits and dirty events coalesce. Resize uses fresh buffers.
61
+ SHM files are mode 0600 and unlinked immediately after opening.
62
+
63
+ Current copies: renderer → browser raw temporary file → JS read → shared buffer →
64
+ compositor. This first frontend does not promise zero-copy or GPU acceleration.
65
+ The producer's raw file is removed after consumption, including error paths.
66
+
67
+ 使用標準共享記憶體、configure/ack、frame callback 與 buffer release。最多兩個
68
+ 尚未釋放的 buffer;不會覆寫 compositor 正在使用的 buffer。視窗目前不透明。
69
+ 目前仍有 raw 暫存檔讀取與共享 buffer 寫入,尚非零拷貝或 GPU 路徑。
70
+
71
+ ## Input and scope / 輸入與範圍
72
+
73
+ Pointer motion, left click, scrolling, basic XKB keymap symbols and modifiers.
74
+ Keyboard input uses the controller's existing paired keydown/keyup operation.
75
+ Not full XKB: no compose, IME, layout groups, repeat, or independently held keys.
76
+ No client cursor, drag/drop, popup, layer-shell or fractional scaling yet.
77
+ Text controls, navigator.clipboard and the address bar share the Wayland data-device
78
+ clipboard. Host tools are a fallback when no Wayland selection is offered; successful
79
+ nonempty host reads are published to Wayland so Land can retain its text cache.
80
+ Clipboard API security/user-activation checks remain in effect.
81
+ 文字框、navigator.clipboard 與網址列共用 Wayland 剪貼簿;沒有 Wayland 選取來源時
82
+ 才讀主機工具,成功取得的非空文字也送回 Wayland,讓 Land 可以快取。
83
+ 網頁 API 原本的安全來源與使用者操作檢查仍保留。
84
+ Connection and window are separate objects, but the launcher currently opens one
85
+ browser document/window. QML `ShellRoot` and multiple windows are not implemented.
86
+
87
+ 支援基本滑鼠、左鍵、捲動與鍵盤;尚非完整 XKB/IME。按鍵沿用 controller 的成對
88
+ 按下/放開,不支援獨立長按。啟動器目前只有單一文件/視窗。
89
+
90
+ ## Compatibility targets / 相容性目標
91
+
92
+ Neither Flea nor Omastorm runs unmodified on the current QML subset.
93
+ Flea 和 Omastorm 目前都還不能直接以現有 QML 子集執行。
94
+
95
+ * Flea: ShellRoot, custom components/imports, models, controls/layouts, Quickshell.Io
96
+ process/IPC and multiple windows. Tab tearoff also uses PanelWindow/WlrLayershell.
97
+ * Omastorm (`../omastorm`, MIT reference checkout): its manifest exposes bar-widget
98
+ and panel entry points. Requires custom components, Controls/Layouts, FileView,
99
+ Process, Socket/SplitParser, IPC, timers, Image/Canvas and ShaderEffect.
100
+ `docs/protocol.md` defines newline-delimited JSON v2 over
101
+ `$XDG_RUNTIME_DIR/omastorm/engine.sock`, served by the separate Rust engine.
102
+ GPU texture files and shaders require rendering compatibility beyond DOM parsing.
103
+
104
+ Flea 需要多視窗、元件、模型與程序/IPC;分離分頁另需 layer-shell。
105
+ Omastorm 還需要 Unix socket JSON 橋接及雷達 shader/貼圖支援;只有 parser
106
+ 與標準 Wayland 視窗不足以適配。不會為一般網頁開放主機檔案或程序權限。
107
+
108
+ References: https://github.com/thisisgm/flea and https://github.com/wesleygrimes/omastorm
109
+
110
+ ## Verification / 測試
111
+
112
+ ```sh
113
+ bun test test/wayland-frontend.test.js
114
+ # Optional integration with the existing sibling buninu-land checkout:
115
+ BUNINU_TEST_WAYLAND=1 bun test test/wayland-frontend.test.js
116
+ ```
117
+
118
+ The integration test creates its own compositor/socket; it does not interrupt the
119
+ user's compositor. Checks SHM presentation, pointer click, keyboard and resize.
120
+ 整合測試使用獨立 socket,不影響使用者正在執行的 Land。
121
+
122
+ Protocol XML retains upstream license notices. Transport/protocol code adapted
123
+ from Buninu Land (MIT), see LICENSE-land. XML is interpreted at runtime with Bun.XML.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hironobu Sano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,32 @@
1
+ // Bookmarks
2
+ // Reads ~/.casty/bookmarks.json, searchable via /b in the address bar
3
+
4
+ import { homedir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { loadJsonFile } from './config.js';
7
+
8
+ const bookmarkPath = join(homedir(), '.casty', 'bookmarks.json');
9
+
10
+ // Load bookmarks (normalized to [{ name, url }, ...])
11
+ export function loadBookmarks() {
12
+ const data = loadJsonFile(bookmarkPath, []);
13
+ // { "name": "url", ... } object format
14
+ if (!Array.isArray(data) && typeof data === 'object') {
15
+ return Object.entries(data).map(([name, url]) => ({ name, url }));
16
+ }
17
+ // [{ name, url }, ...] array format
18
+ if (Array.isArray(data)) {
19
+ return data.filter(e => e.name && e.url);
20
+ }
21
+ return [];
22
+ }
23
+
24
+ // Search bookmarks by query (case-insensitive partial match on name/URL)
25
+ export function searchBookmarks(query) {
26
+ const bookmarks = loadBookmarks();
27
+ if (!query) return bookmarks;
28
+ const q = query.toLowerCase();
29
+ return bookmarks.filter(b =>
30
+ b.name.toLowerCase().includes(q) || b.url.toLowerCase().includes(q)
31
+ );
32
+ }
@@ -0,0 +1,96 @@
1
+ // Shared trusted-click synthesis for Chrome DevTools Protocol.
2
+ import { appendFileSync, mkdirSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ // CASTY_DEBUG_CLICK=1 logs each click to ~/.casty/click.log
7
+ const DEBUG_CLICK = !!process.env.CASTY_DEBUG_CLICK;
8
+ const DEBUG_LOG = join(homedir(), '.casty', 'click.log');
9
+
10
+ const DESCRIBE = `const d = el => !el ? null : el.tagName.toLowerCase()
11
+ + (el.id ? '#' + el.id : '')
12
+ + (typeof el.className === 'string' && el.className.trim() ? '.' + el.className.trim().split(/\\s+/).slice(0, 2).join('.') : '')
13
+ + ' "' + (el.innerText || el.value || '').trim().slice(0, 30).replace(/\\s+/g, ' ') + '"';`;
14
+
15
+ // Input coordinates are in the visual viewport; the page's are layout
16
+ // coordinates, which differ when the page is zoomed.
17
+ const TO_LAYOUT = `const toLayout = (x, y) => {
18
+ const v = window.visualViewport, s = v?.scale || 1;
19
+ return [x / s + (v?.offsetLeft || 0), y / s + (v?.offsetTop || 0)];
20
+ };`;
21
+
22
+ async function debugBefore(client, x, y) {
23
+ const expression = `(() => { ${DESCRIBE}
24
+ window.__castyClickLog = null;
25
+ document.addEventListener('click', e => {
26
+ window.__castyClickLog = { target: d(e.target), clientX: e.clientX, clientY: e.clientY };
27
+ }, { capture: true, once: true });
28
+ ${TO_LAYOUT}
29
+ return { atPoint: d(document.elementFromPoint(...toLayout(${x}, ${y}))), vw: innerWidth, vh: innerHeight, dpr: devicePixelRatio, sx: scrollX, sy: scrollY };
30
+ })()`;
31
+ const { result } = await client.send('Runtime.evaluate', { expression, returnByValue: true });
32
+ return result?.value;
33
+ }
34
+
35
+ async function debugAfter(client) {
36
+ const { result } = await client.send('Runtime.evaluate', {
37
+ expression: 'window.__castyClickLog', returnByValue: true,
38
+ });
39
+ return result?.value ?? null;
40
+ }
41
+
42
+ export async function showClickMarker(client, x, y, duration = 800) {
43
+ const expression = `(async () => {
44
+ ${TO_LAYOUT}
45
+ const [left, top] = toLayout(${x}, ${y});
46
+ const old = document.getElementById('__casty_click_marker');
47
+ if (old) old.remove();
48
+ const marker = document.createElement('div');
49
+ marker.id = '__casty_click_marker';
50
+ marker.style.cssText = [
51
+ 'position:fixed',
52
+ 'left:' + left + 'px',
53
+ 'top:' + top + 'px',
54
+ 'width:14px',
55
+ 'height:14px',
56
+ 'margin-left:-7px',
57
+ 'margin-top:-7px',
58
+ 'box-sizing:border-box',
59
+ 'border:2px solid white',
60
+ 'border-radius:50%',
61
+ 'background:#f00',
62
+ 'box-shadow:0 0 0 2px rgba(255,0,0,.45)',
63
+ 'pointer-events:none',
64
+ 'z-index:2147483647',
65
+ ].join(';');
66
+ document.documentElement.appendChild(marker);
67
+ setTimeout(() => marker.remove(), ${duration});
68
+ await new Promise(resolve => requestAnimationFrame(resolve));
69
+ })()`;
70
+ await client.send('Runtime.evaluate', { expression, awaitPromise: true });
71
+ }
72
+
73
+ export async function dispatchClick(client, x, y, { clickCount = 1, button = 'left', source = '?' } = {}) {
74
+ const before = DEBUG_CLICK ? await debugBefore(client, x, y).catch(e => ({ error: e.message })) : null;
75
+ // The marker is cosmetic. CDP engines without a page DOM (e.g. a Wayland
76
+ // compositor) can reject this evaluation but still support real input.
77
+ try { await showClickMarker(client, x, y); } catch {}
78
+ // Input coordinates are CSS pixels (see startBrowser for why they stay so)
79
+ const common = { x, y, button, clickCount };
80
+ await client.send('Input.dispatchMouseEvent', {
81
+ type: 'mousePressed', ...common, buttons: button === 'right' ? 2 : 1,
82
+ });
83
+ await client.send('Input.dispatchMouseEvent', {
84
+ type: 'mouseReleased', ...common, buttons: 0,
85
+ });
86
+ if (DEBUG_CLICK) {
87
+ const after = await debugAfter(client).catch(e => ({ error: e.message }));
88
+ const line = JSON.stringify({
89
+ t: new Date().toISOString(), source, button, css: [x, y], clickCount, before, clicked: after,
90
+ });
91
+ try {
92
+ mkdirSync(join(homedir(), '.casty'), { recursive: true });
93
+ appendFileSync(DEBUG_LOG, line + '\n');
94
+ } catch {}
95
+ }
96
+ }
@@ -0,0 +1,61 @@
1
+ // Command line: Ctrl+E turns the address bar into a "cmd> " prompt.
2
+ // Each command maps to an action in input.js; `help` lists them.
3
+
4
+ export const COMMANDS = [
5
+ { name: 'open', aliases: ['o', 'go'], usage: 'open <url|query>', help: 'Open a URL or search' },
6
+ { name: 'back', aliases: ['b'], usage: 'back', help: 'Go back' },
7
+ { name: 'forward', aliases: ['f'], usage: 'forward', help: 'Go forward' },
8
+ { name: 'reload', aliases: ['r'], usage: 'reload', help: 'Reload the page' },
9
+ { name: 'home', aliases: [], usage: 'home', help: 'Open the home page' },
10
+ { name: 'bookmark', aliases: ['bm'], usage: 'bookmark [query]', help: 'Open the first matching bookmark' },
11
+ { name: 'links', aliases: ['hints', 'l'], usage: 'links', help: 'Hint mode: pick a link to click' },
12
+ { name: 'copy', aliases: ['c'], usage: 'copy', help: 'Copy the selected text' },
13
+ { name: 'copyurl', aliases: ['yank', 'y'], usage: 'copyurl', help: 'Copy the page URL' },
14
+ { name: 'paste', aliases: ['p'], usage: 'paste', help: 'Paste the clipboard into the page' },
15
+ { name: 'pasteurl', aliases: ['pu'], usage: 'pasteurl', help: 'Put the clipboard into the address bar to edit or send' },
16
+ { name: 'download', aliases: ['dl', 'save'], usage: 'download', help: 'Save the page as the browser received it to the Downloads folder' },
17
+ { name: 'find', aliases: ['/'], usage: 'find [text] (or /text)', help: 'Find text in the page; again for the next match' },
18
+ { name: 'eruda', aliases: ['devtools'], usage: 'eruda', help: 'Show or hide the eruda console in the page (loaded from a CDN)' },
19
+ { name: 'zoom', aliases: ['z'], usage: 'zoom [n|n%|+|-|reset]', help: 'Show or set the zoom (1 = automatic)' },
20
+ { name: 'cdp', aliases: [], usage: 'cdp [ws]', help: 'Show CDP address, state and engine; ws shows the full WebSocket URL' },
21
+ { name: 'kitty', aliases: [], usage: 'kitty [inline|file]', help: 'Show image transport and capture format, or switch transport' },
22
+ { name: 'pixelmouse', aliases: [], usage: 'pixelmouse [on|off]', help: 'Query and enable pixel mouse, or use cell coordinates' },
23
+ { name: 'help', aliases: ['?', 'h'], usage: 'help', help: 'List commands' },
24
+ { name: 'quit', aliases: ['q', 'exit'], usage: 'quit', help: 'Quit casty' },
25
+ ];
26
+
27
+ const BY_NAME = new Map(COMMANDS.flatMap(c => [[c.name, c], ...c.aliases.map(a => [a, c])]));
28
+
29
+ // "zoom 1.5" → { name: 'zoom', arg: '1.5' }; "/text" finds; unknown → { error }
30
+ export function parseCommand(line) {
31
+ const text = line.trim();
32
+ if (text.startsWith('/')) return { name: 'find', arg: text.slice(1).trim() };
33
+ const m = text.match(/^(\S+)\s*(.*)$/);
34
+ if (!m) return { error: 'Empty command' };
35
+ const command = BY_NAME.get(m[1].toLowerCase());
36
+ if (!command) return { error: `Unknown command: ${m[1]} (try help)` };
37
+ return { name: command.name, arg: m[2] };
38
+ }
39
+
40
+ // One-line command summary for the status bar
41
+ export function helpText() {
42
+ return COMMANDS.map(c => c.usage).join(' · ');
43
+ }
44
+
45
+ // Browser-style zoom steps for "zoom +" / "zoom -"
46
+ const ZOOM_STEPS = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5];
47
+ export const MIN_ZOOM = ZOOM_STEPS[0];
48
+ export const MAX_ZOOM = ZOOM_STEPS.at(-1);
49
+
50
+ // New zoom factor for a "zoom" argument, or null if it is not one.
51
+ // "1.5" and "150%" are absolute; "+"/"-" step; "reset" (or "0") goes back to 1.
52
+ export function parseZoom(arg, current = 1) {
53
+ const text = arg.trim().toLowerCase();
54
+ if (text === 'reset' || text === '0') return 1;
55
+ if (text === '+' || text === 'in') return ZOOM_STEPS.find(s => s > current + 1e-9) ?? MAX_ZOOM;
56
+ if (text === '-' || text === 'out') return ZOOM_STEPS.findLast(s => s < current - 1e-9) ?? MIN_ZOOM;
57
+ const m = text.match(/^(\d+(?:\.\d+)?|\.\d+)(%?)$/);
58
+ if (!m) return null;
59
+ const value = Number(m[1]) / (m[2] ? 100 : 1);
60
+ return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, value));
61
+ }
@@ -0,0 +1,45 @@
1
+ // Configuration
2
+ // Customizable via ~/.casty/config.json (falls back to defaults)
3
+
4
+ import { readFileSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
6
+ import { join } from 'node:path';
7
+
8
+ // Safely load and parse a JSON file (returns fallback on parse error or missing file)
9
+ export function loadJsonFile(filePath, fallback) {
10
+ try { return JSON.parse(readFileSync(filePath, 'utf8')); }
11
+ catch { return fallback; }
12
+ }
13
+
14
+ const configPath = join(homedir(), '.casty', 'config.json');
15
+
16
+ // Detect system locale (e.g. "en-US", "ja", "zh-CN")
17
+ function detectLocale() {
18
+ // Environment variables first (Intl may lack ICU data and return en-US)
19
+ const env = process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || '';
20
+ const m = env.match(/^([a-z]{2})(?:[_-]([A-Z]{2}))?/);
21
+ if (m) return m[2] ? `${m[1]}-${m[2]}` : m[1];
22
+ // Fallback to Intl API
23
+ try {
24
+ const locale = Intl.DateTimeFormat().resolvedOptions().locale;
25
+ if (locale) return locale;
26
+ } catch {}
27
+ return 'en-US';
28
+ }
29
+
30
+ const DEFAULTS = {
31
+ homeUrl: 'https://github.com/jjtseng93/casty',
32
+ searchUrl: 'https://www.google.com/search?q=',
33
+ transport: 'auto', // 'auto' | 'file' | 'inline'
34
+ format: 'auto', // 'auto' | 'png' | 'jpeg'
35
+ media: false, // Enable camera/mic via ffmpeg for WebRTC (requires ffmpeg)
36
+ language: detectLocale(), // System locale (override: "en-US", "ja", etc.)
37
+ };
38
+
39
+ let _cache = null;
40
+
41
+ export function loadConfig() {
42
+ if (_cache) return _cache;
43
+ _cache = { ...DEFAULTS, ...loadJsonFile(configPath, {}) };
44
+ return _cache;
45
+ }
@@ -0,0 +1,28 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, win32 } from 'node:path';
3
+
4
+ // Windows can move the Downloads known folder away from %USERPROFILE%. Query
5
+ // its registered location through PowerShell so non-ASCII paths stay UTF-8.
6
+ const WINDOWS_DOWNLOADS = [
7
+ '$OutputEncoding = [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)',
8
+ "$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders')",
9
+ "if ($key) { $path = $key.GetValue('{374DE290-123F-4565-9164-39C4925E467B}'); if ($path) { [Console]::Write([Environment]::ExpandEnvironmentVariables($path)) } }",
10
+ ].join('; ');
11
+
12
+ function windowsDownloads() {
13
+ try {
14
+ const result = Bun.spawnSync({ cmd: ['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', WINDOWS_DOWNLOADS],
15
+ stdout: 'pipe', stderr: 'ignore' });
16
+ return result.exitCode === 0 ? new TextDecoder().decode(result.stdout).trim() : null;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ export function downloadsDirectory({ platform = process.platform, home = homedir(), queryWindows = windowsDownloads } = {}) {
23
+ if (platform === 'win32') {
24
+ const known = queryWindows();
25
+ return known && win32.isAbsolute(known) ? known : win32.join(home, 'Downloads');
26
+ }
27
+ return join(home, 'Downloads');
28
+ }
@@ -0,0 +1,49 @@
1
+ // "download" (Ctrl+E): save the current page's document as the browser
2
+ // received it, via CDP Page.getResourceContent (Chrome and buninu-browser),
3
+ // into the same folder as browser downloads.
4
+
5
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
6
+ import { extname, join } from 'node:path';
7
+ import { downloadsDirectory } from './download-directory.js';
8
+
9
+ export const DOWNLOAD_DIR = downloadsDirectory();
10
+
11
+ // A file name for a URL: its last path segment, else its host; characters
12
+ // Windows and POSIX reject become "_", and an HTML page without an extension
13
+ // gets ".html".
14
+ export function fileNameFor(url, mimeType = '') {
15
+ let name = '';
16
+ let host = 'page';
17
+ try {
18
+ const parsed = new URL(url);
19
+ host = parsed.hostname || host;
20
+ const segment = parsed.pathname.split('/').filter(Boolean).pop() ?? '';
21
+ try { name = decodeURIComponent(segment); } catch { name = segment; }
22
+ } catch {}
23
+ // A host name ("example.com") has no file extension of its own.
24
+ const fromHost = !name;
25
+ name = (name || host).replace(/[\\/:*?"<>|\x00-\x1f]/g, '_').replace(/^\.+/, '_').slice(0, 200);
26
+ if ((fromHost || !extname(name)) && /html/i.test(mimeType)) name += '.html';
27
+ return name;
28
+ }
29
+
30
+ // dir/name, or dir/name (2).ext, (3)… when it exists.
31
+ export function uniquePath(dir, name, exists = existsSync) {
32
+ const extension = extname(name);
33
+ const base = name.slice(0, name.length - extension.length);
34
+ let candidate = join(dir, name);
35
+ for (let n = 2; exists(candidate); n++) candidate = join(dir, `${base} (${n})${extension}`);
36
+ return candidate;
37
+ }
38
+
39
+ // Saves the main frame's document; returns the path written.
40
+ export async function downloadPage(client, dir = DOWNLOAD_DIR) {
41
+ const { frameTree } = await client.send('Page.getFrameTree');
42
+ const { id: frameId, url, mimeType } = frameTree.frame;
43
+ if (!/^(https?|file|data):/.test(url)) throw new Error(`Nothing to download for ${url}`);
44
+ const { content, base64Encoded } = await client.send('Page.getResourceContent', { frameId, url });
45
+ mkdirSync(dir, { recursive: true });
46
+ const path = uniquePath(dir, fileNameFor(url, mimeType));
47
+ writeFileSync(path, base64Encoded ? Buffer.from(content, 'base64') : Buffer.from(content, 'utf8'));
48
+ return path;
49
+ }