buninu-browser 0.3.0 → 0.5.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 (296) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +40 -4
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +152 -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 +130 -0
  20. package/frontends/wayland/input.js +75 -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 +162 -0
  26. package/index.js +28 -1
  27. package/lib/audio/shared-connection.js +31 -0
  28. package/lib/cdp-server.js +15 -2
  29. package/lib/clipboard-command.js +10 -0
  30. package/lib/clipboard-powershell.js +22 -0
  31. package/lib/clipboard.js +37 -2
  32. package/lib/document-types.js +11 -0
  33. package/lib/headless-shell.js +54 -12
  34. package/lib/layout/text-layout.js +17 -9
  35. package/lib/paint/display-list.js +6 -1
  36. package/lib/qml/README.md +1427 -0
  37. package/lib/qml/builtins.js +771 -0
  38. package/lib/qml/canvas-dom.js +115 -0
  39. package/lib/qml/canvas.js +76 -0
  40. package/lib/qml/document.js +1199 -0
  41. package/lib/qml/fonts.js +69 -0
  42. package/lib/qml/format.js +9 -0
  43. package/lib/qml/host.js +352 -0
  44. package/lib/qml/images.js +199 -0
  45. package/lib/qml/launcher.js +132 -0
  46. package/lib/qml/loader.js +521 -0
  47. package/lib/qml/media-renderer.js +49 -0
  48. package/lib/qml/media.js +86 -0
  49. package/lib/qml/models.js +622 -0
  50. package/lib/qml/parser.js +381 -0
  51. package/lib/qml/qs +5 -0
  52. package/lib/qml/qscasty +3 -0
  53. package/lib/qml/qsheadless +3 -0
  54. package/lib/qml/qswayland +3 -0
  55. package/lib/qml/reactive.js +182 -0
  56. package/lib/qml/runtime.js +3603 -0
  57. package/lib/qml/shader/fragment.js +83 -0
  58. package/lib/qml/shader/qsb.js +27 -0
  59. package/lib/qml/shader/renderer.js +39 -0
  60. package/lib/qml/shader/runtime.js +61 -0
  61. package/lib/qml/shapes.js +251 -0
  62. package/lib/qml/shortcuts.js +65 -0
  63. package/lib/qml/text.js +109 -0
  64. package/lib/qml/textures.js +126 -0
  65. package/lib/qml/wayland-clipboard.js +32 -0
  66. package/lib/qml/window-constraints.js +16 -0
  67. package/lib/qml/worker-thread.js +70 -0
  68. package/lib/render-tree/index.js +16 -1
  69. package/lib/renderer/bindings.js +16 -1
  70. package/lib/renderer/canvas-2d.js +29 -0
  71. package/lib/renderer/document-selection.js +19 -0
  72. package/lib/renderer/host.js +45 -17
  73. package/lib/renderer/main.js +21 -6
  74. package/lib/renderer/page-realm.js +20 -10
  75. package/lib/renderer/page-renderer.js +497 -42
  76. package/lib/renderer/pointer-move.js +5 -3
  77. package/lib/renderer/render-interval.js +9 -0
  78. package/lib/renderer/text-input-paint.js +54 -0
  79. package/lib/renderer/text-input.js +57 -0
  80. package/lib/style/computed-style.js +10 -4
  81. package/package.json +1 -1
  82. package/test/audio-connection-lifetime.test.js +46 -0
  83. package/test/canvas-paint.test.js +8 -0
  84. package/test/clipboard.test.js +19 -0
  85. package/test/computed-style.test.js +12 -0
  86. package/test/document-selection.test.js +27 -0
  87. package/test/double-click.test.js +85 -0
  88. package/test/fixtures/canvas-damage.js +40 -0
  89. package/test/fixtures/fake-renderer.js +1 -0
  90. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  91. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  92. package/test/fixtures/flea-helpers/bin/bat +3 -0
  93. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  94. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  95. package/test/fixtures/flea-helpers/bin/glow +3 -0
  96. package/test/fixtures/flea-helpers/bin/qs +3 -0
  97. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  98. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  99. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  100. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  101. package/test/fixtures/flea-helpers/check.mjs +36 -0
  102. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  103. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  104. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  105. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  106. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  107. package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
  108. package/test/fixtures/omastorm-helpers/check.mjs +56 -0
  109. package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
  110. package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
  111. package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
  112. package/test/fixtures/pointer-paint-batching.js +23 -0
  113. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  114. package/test/fixtures/qml/flea/LICENSE +21 -0
  115. package/test/fixtures/qml/flea/README.md +25 -0
  116. package/test/fixtures/qml/flea/Theme.qml +8 -0
  117. package/test/fixtures/qml/flea/demo.qml +24 -0
  118. package/test/fixtures/qml/flea/qmldir +3 -0
  119. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  120. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  121. package/test/fixtures/qml/flea-theme/README.md +70 -0
  122. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  123. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  124. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  125. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  126. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  127. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  128. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  129. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  130. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  131. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  132. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  133. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  134. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  135. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  136. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  137. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  138. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  139. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  140. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  141. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  142. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  143. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  144. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  145. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  146. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  147. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  148. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  167. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  168. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  169. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  170. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  171. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  172. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  173. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  174. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  175. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  176. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  177. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  178. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  179. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  180. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  181. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  182. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  183. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  184. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  185. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  186. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  187. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  188. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  189. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  190. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  191. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  192. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  193. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  194. package/test/fixtures/qml/hello.qml +26 -0
  195. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  196. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  197. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  198. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  199. package/test/fixtures/qml/imports/logic/base.js +2 -0
  200. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  201. package/test/fixtures/qml/imports/logic/math.js +3 -0
  202. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  203. package/test/fixtures/qml/imports/main.qml +21 -0
  204. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  205. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  206. package/test/fixtures/qml/multiply.qml +61 -0
  207. package/test/fixtures/qml-canvas-dom.js +90 -0
  208. package/test/fixtures/qml-fonts/main.qml +10 -0
  209. package/test/fixtures/qml-locale/check.mjs +6 -0
  210. package/test/fixtures/qml-locale/main.qml +8 -0
  211. package/test/fixtures/qml-media-audio.js +39 -0
  212. package/test/fixtures/qml-shader/grab.qml +14 -0
  213. package/test/fixtures/qml-shader/gradient.frag +5 -0
  214. package/test/fixtures/qml-shader/main.qml +14 -0
  215. package/test/fixtures/qml-shader/texture-grab.qml +25 -0
  216. package/test/fixtures/qml-shader/texture-size.frag +5 -0
  217. package/test/fixtures/qml-shader-check.mjs +24 -0
  218. package/test/fixtures/title-no-paint.js +20 -0
  219. package/test/helpers/flea-sandbox.js +48 -0
  220. package/test/helpers/quickshell-fixture.js +125 -0
  221. package/test/html-clipboard-wayland.test.js +47 -0
  222. package/test/index.test.js +13 -0
  223. package/test/manual-casty-without-qml.js +87 -0
  224. package/test/manual-double-click.html +21 -0
  225. package/test/manual-flea-application.js +49 -0
  226. package/test/manual-flea-binary.js +62 -0
  227. package/test/manual-flea-qml.js +9 -0
  228. package/test/manual-flea-settings.js +40 -0
  229. package/test/manual-flea-theme.js +40 -0
  230. package/test/manual-mouse-buttons.html +18 -0
  231. package/test/manual-omastorm-engine.mjs +19 -0
  232. package/test/manual-omastorm-shaders.mjs +28 -0
  233. package/test/manual-omastorm-software.mjs +25 -0
  234. package/test/manual-pointer-scroll.html +43 -0
  235. package/test/manual-qml-removal.js +46 -0
  236. package/test/manual-without-qml.js +45 -0
  237. package/test/pointer-drag.test.js +104 -0
  238. package/test/qml-canvas-dom.test.js +37 -0
  239. package/test/qml-canvas.test.js +21 -0
  240. package/test/qml-clipboard-bridge.test.js +25 -0
  241. package/test/qml-clipboard-wayland.test.js +40 -0
  242. package/test/qml-connections-timing.test.js +95 -0
  243. package/test/qml-controls-window.test.js +182 -0
  244. package/test/qml-dynamic.test.js +401 -0
  245. package/test/qml-flea-app.test.js +146 -0
  246. package/test/qml-flea-backend.test.js +78 -0
  247. package/test/qml-flea-columns-backend.test.js +38 -0
  248. package/test/qml-flea-compile.test.js +78 -0
  249. package/test/qml-flea-launcher.test.js +88 -0
  250. package/test/qml-flea-markdown-backend.test.js +31 -0
  251. package/test/qml-flea-media.test.js +28 -0
  252. package/test/qml-flea-navigation-backend.test.js +35 -0
  253. package/test/qml-flea-navigation-timing.test.js +147 -0
  254. package/test/qml-flea-preview-backend.test.js +53 -0
  255. package/test/qml-flea-previews.test.js +163 -0
  256. package/test/qml-flea-rows.test.js +101 -0
  257. package/test/qml-flea-textures.test.js +59 -0
  258. package/test/qml-flea-view-switch.test.js +33 -0
  259. package/test/qml-flea-wayland.test.js +68 -0
  260. package/test/qml-flea.test.js +34 -0
  261. package/test/qml-fonts.test.js +31 -0
  262. package/test/qml-grid-multiline.test.js +48 -0
  263. package/test/qml-host.test.js +238 -0
  264. package/test/qml-imports.test.js +102 -0
  265. package/test/qml-inherited-handlers.test.js +51 -0
  266. package/test/qml-input-selection.test.js +66 -0
  267. package/test/qml-interactions.test.js +309 -0
  268. package/test/qml-language.test.js +167 -0
  269. package/test/qml-launcher.test.js +54 -0
  270. package/test/qml-layout-text.test.js +205 -0
  271. package/test/qml-locale.test.js +45 -0
  272. package/test/qml-media.test.js +63 -0
  273. package/test/qml-nonvisual-visible.test.js +19 -0
  274. package/test/qml-persisted-delegates.test.js +45 -0
  275. package/test/qml-quickshell-controls.test.js +65 -0
  276. package/test/qml-quickshell-settings.test.js +62 -0
  277. package/test/qml-repeater.test.js +72 -0
  278. package/test/qml-shader-runtime.test.js +6 -0
  279. package/test/qml-shortcuts.test.js +39 -0
  280. package/test/qml-socket.test.js +60 -0
  281. package/test/qml-software-shader.test.js +55 -0
  282. package/test/qml-style-updates.test.js +124 -0
  283. package/test/qml-textures.test.js +66 -0
  284. package/test/qml-views.test.js +332 -0
  285. package/test/qml-window-update.test.js +36 -0
  286. package/test/qml-worker.test.js +69 -0
  287. package/test/qml.test.js +83 -0
  288. package/test/render-interval.test.js +33 -0
  289. package/test/renderer-host.test.js +11 -0
  290. package/test/text-input-pointer.test.js +31 -0
  291. package/test/text-input-scroll.test.js +29 -0
  292. package/test/text-input.test.js +47 -0
  293. package/test/title-no-paint.test.js +6 -0
  294. package/test/wayland-frontend.test.js +83 -0
  295. package/test/wayland-scroll.test.js +40 -0
  296. package/test/wayland-urlbar.test.js +61 -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,17 @@ 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
+ See the [experimental QML runtime and Flea/Omastorm container recipes](lib/qml/README.md)
21
+ for local QML applications. This does not yet provide full Quickshell/Flea/Omastorm
22
+ compatibility. For input checks, open `test/manual-pointer-scroll.html` through
23
+ either frontend.
14
24
 
15
25
  ## Usage
16
26
 
@@ -218,11 +228,19 @@ not start a browser process or CDP listener.
218
228
 
219
229
  ### Page sandbox
220
230
 
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,
231
+ Pages can copy or read text with `navigator.clipboard.writeText()` and
232
+ `navigator.clipboard.readText()` after a user click or key press on HTTPS
233
+ (or localhost). Each operation consumes that activation. The controller accesses the clipboard,
223
234
  preferring `xclip -selection clipboard`, with platform clipboard commands as
224
235
  fallbacks. Clipboard command access stays outside the page sandbox; unavailable
225
- providers reject the promise instead of reporting a successful copy.
236
+ providers reject the promise instead of reporting success. Reads happen only
237
+ on request, with no clipboard polling. HTML inputs and textareas share selection,
238
+ caret blinking, Shift+arrows, and Ctrl+C/X/V editing with QML controls. Paste
239
+ replaces the selection, or inserts at the cursor. Up/Down currently follow
240
+ explicit textarea line breaks, not soft-wrapped visual lines.
241
+
242
+ PowerShell clipboard quoting and newline handling are reused from jsmdcui
243
+ under its [MIT license](LICENSES/jsmdcui.txt).
226
244
 
227
245
  Page JavaScript runs behind three layers:
228
246
 
@@ -420,6 +438,24 @@ additional unreported downstream latency, such as Bluetooth output latency.
420
438
  With `BUNINU_LOG`, `media sync` records reported audio progress, the latest
421
439
  rendered video position, and the output queue/latency for diagnosis.
422
440
 
441
+ ### Render scheduling
442
+
443
+ Set `BUNINU_RENDER_MIN_INTERVAL` to adjust the minimum delay before coalesced
444
+ DOM/Canvas redraws. The default is 16 ms; try 8 ms for more frequent updates:
445
+
446
+ ```sh
447
+ env BUNINU_RENDER_MIN_INTERVAL=8 bun index.js --wayland URL
448
+ env BUNINU_RENDER_MIN_INTERVAL=8 bun index.js URL
449
+ ```
450
+
451
+ Accepts integer milliseconds from 0 to 60000. Invalid values warn and fall back
452
+ to 16 ms. Zero removes the minimum delay, but retains asynchronous coalescing
453
+ and the adaptive delay based on the previous layout cost. This is not an FPS
454
+ guarantee or Casty's capture interval; shorter waits can increase CPU usage.
455
+ Restart the browser after changing it. The setting also applies to iframe
456
+ renderers, and only its validated numeric value is forwarded to the renderer
457
+ process; the ordinary renderer does not inherit the host environment.
458
+
423
459
  ### Diagnostics
424
460
 
425
461
  [`test/fixtures/browser-raw-demo.html`](test/fixtures/browser-raw-demo.html)
@@ -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,152 @@
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/right/middle clicks, double clicks, scrolling on both axes,
74
+ basic XKB keymap symbols and modifiers. Right clicks dispatch `contextmenu`;
75
+ non-primary clicks dispatch `auxclick` without left-click navigation or form actions.
76
+ Press, movement and release are delivered separately; held-button state reaches
77
+ mousemove/pointermove so HTML canvas and QML drag handlers can draw before release.
78
+ Focus occurs on an uncancelled left press. Click/auxclick uses the common ancestor
79
+ of the press and release targets; removing the press target prevents activation.
80
+ The same page event handling is available through CDP/Casty. QML MouseArea and
81
+ TapHandler receive accepted non-primary buttons. Middle click does not yet paste
82
+ PRIMARY selection, and selecting text does not publish a PRIMARY clipboard.
83
+ Keyboard input uses the controller's existing paired keydown/keyup operation.
84
+ Not full XKB: no compose, IME, layout groups, repeat, or independently held keys.
85
+ No client cursor, drag/drop, popup, layer-shell or fractional scaling yet.
86
+ Text controls, navigator.clipboard and the address bar share the Wayland data-device
87
+ clipboard. Host tools are a fallback when no Wayland selection is offered; successful
88
+ nonempty host reads are published to Wayland so Land can retain its text cache.
89
+ Clipboard API security/user-activation checks remain in effect.
90
+ 文字框、navigator.clipboard 與網址列共用 Wayland 剪貼簿;沒有 Wayland 選取來源時
91
+ 才讀主機工具,成功取得的非空文字也送回 Wayland,讓 Land 可以快取。
92
+ 網頁 API 原本的安全來源與使用者操作檢查仍保留。
93
+ Connection and window are separate objects, but the launcher currently opens one
94
+ browser document/window. QML `ShellRoot` can group logical objects, but multiple independent native windows
95
+ are not implemented.
96
+
97
+ 支援滑鼠移動、左/右/中鍵、雙擊及雙軸捲動。按下、移動與放開分別即時傳遞,
98
+ mousemove/pointermove 保留按住狀態,讓 Canvas 與 QML 可在放開前處理拖曳。
99
+ 未取消的左鍵按下會取得焦點;click/auxclick 以按下與放開元素的共同祖先為目標,
100
+ 按下元素已被移除時不觸發點擊。右鍵送出 contextmenu,非左鍵送出
101
+ auxclick,不執行左鍵導航或表單動作;CDP/Casty 也共用這些事件。QML MouseArea
102
+ 與 TapHandler 可收到允許的非左鍵按鈕。選取文字尚不發布 PRIMARY,中鍵也不會
103
+ 自動貼上 PRIMARY。尚非完整 XKB/IME;按鍵沿用 controller 的成對
104
+ 按下/放開,不支援獨立長按。啟動器目前只有單一文件/視窗。
105
+
106
+ ## Compatibility targets / 相容性目標
107
+
108
+ The official Flea binary can launch the experimental QML wrapper; directory
109
+ navigation, previews and selected file operations have been exercised. Omastorm
110
+ has a software ShaderEffect experiment and a reproducible container recipe.
111
+ See [QML setup and limitations](../../lib/qml/README.md) for required backends,
112
+ fonts, external tools and the explicit Omastorm software-path adaptation.
113
+ Neither result implies complete Qt/Quickshell compatibility: independent native
114
+ windows, tab tearoff/layer-shell and arbitrary shaders remain outside this scope.
115
+ Ordinary web pages do not acquire the local QML host's file/process permissions.
116
+
117
+ 官方 Flea binary 已可啟動實驗性 QML wrapper,並測試目錄導航、預覽與部分檔案操作。
118
+ Omastorm 已有軟體 ShaderEffect 實驗及容器教學;所需後端、字型、外部工具與
119
+ Omastorm 的軟體路徑調整,見 [QML 說明](../../lib/qml/README.md)。這不代表完整
120
+ Qt/Quickshell 相容;獨立原生多視窗、分離分頁/layer-shell、任意 shader 仍有限制。
121
+ 一般網頁不會因此取得本地 QML 的檔案或程序權限。
122
+
123
+ References: https://github.com/thisisgm/flea and https://github.com/wesleygrimes/omastorm
124
+
125
+ ## Verification / 測試
126
+
127
+ ```sh
128
+ bun test test/wayland-frontend.test.js test/double-click.test.js test/wayland-urlbar.test.js test/pointer-drag.test.js
129
+ # Optional integration with the existing sibling buninu-land checkout:
130
+ BUNINU_TEST_WAYLAND=1 bun test test/wayland-frontend.test.js test/wayland-scroll.test.js test/pointer-drag.test.js
131
+ # Optional reference comparison with /usr/local/bin/chromium-headless-shell:
132
+ BUNINU_TEST_CHROMIUM=1 bun test test/pointer-drag.test.js
133
+ ```
134
+
135
+ The integration test creates its own compositor/socket; it does not interrupt the
136
+ user's compositor. Checks SHM presentation, pointer click, keyboard and resize.
137
+ 整合測試使用獨立 socket,不影響使用者正在執行的 Land。
138
+
139
+ Protocol XML retains upstream license notices. Transport/protocol code adapted
140
+ from Buninu Land (MIT), see LICENSE-land. XML is interpreted at runtime with Bun.XML.
141
+
142
+ Manual mouse/scroll test / 手動滑鼠與捲動測試:
143
+
144
+ ```sh
145
+ bun index.js --wayland file://"$PWD"/test/manual-pointer-scroll.html
146
+ # Omit --wayland to use the default Casty frontend.
147
+ ```
148
+
149
+ The page includes vertical/horizontal/two-axis scroll boxes, button and double-click
150
+ counters, a drag canvas, text controls and bounded event logs. It reports received
151
+ modifiers without swapping wheel axes itself. 頁面不自行轉換滾輪方向,可從紀錄
152
+ 確認外層是否真的送來水平軸與修飾鍵;預設 Casty 路徑可省略 `--wayland`。
@@ -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
+ }