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,334 @@
1
+ // Vimium-style hint mode
2
+ // Alt+F or Ctrl+L shows labels on clickable/focusable elements, select by typing the label
3
+
4
+ import { dispatchClick } from './click.js';
5
+
6
+ const HINT_CHARS = ['a', 's', 'd', 'f', 'j', 'k', 'l'];
7
+ const MAX_HINTS = HINT_CHARS.length * HINT_CHARS.length; // 49
8
+
9
+ // Collect elements + generate labels + inject overlay in a single evaluate call
10
+ function makeCollectAndOverlayScript(hintChars, maxHints) {
11
+ return `(() => {
12
+ try {
13
+ const old = document.getElementById('__casty_hints');
14
+ if (old) old.remove();
15
+ for (const el of document.querySelectorAll('[data-casty-hint-id]')) {
16
+ el.removeAttribute('data-casty-hint-id');
17
+ }
18
+
19
+ const CLICKABLE = 'a, button, [role="button"], [onclick], summary, [role="link"], [role="tab"], [tabindex]';
20
+ const FOCUSABLE = 'input, textarea, select, [contenteditable]';
21
+ const all = document.querySelectorAll(CLICKABLE + ', ' + FOCUSABLE);
22
+
23
+ const elems = [];
24
+ for (const el of all) {
25
+ if (elems.length >= ${maxHints}) break;
26
+ const style = getComputedStyle(el);
27
+ if (style.display === 'none' || style.visibility === 'hidden'
28
+ || style.pointerEvents === 'none' || Number(style.opacity) === 0) continue;
29
+
30
+ // getClientRects handles wrapped links better than one large bounding
31
+ // box whose centre may be whitespace or belong to another element.
32
+ const rects = [...el.getClientRects()];
33
+ if (!rects.length && el.firstElementChild) rects.push(...el.firstElementChild.getClientRects());
34
+ let point = null;
35
+ for (const rect of rects) {
36
+ const left = Math.max(0, rect.left);
37
+ const top = Math.max(0, rect.top);
38
+ const right = Math.min(innerWidth, rect.right);
39
+ const bottom = Math.min(innerHeight, rect.bottom);
40
+ if (right <= left || bottom <= top) continue;
41
+ const cx = Math.round((left + right) / 2);
42
+ const cy = Math.round((top + bottom) / 2);
43
+ const hit = document.elementFromPoint(cx, cy);
44
+ if (hit === el || el.contains(hit)) {
45
+ point = { x: Math.round(left), y: Math.round(top), cx, cy };
46
+ break;
47
+ }
48
+ }
49
+ if (!point) continue;
50
+
51
+ const isFocusable = el.matches('input, textarea, select, [contenteditable]');
52
+ const id = String(elems.length);
53
+ el.setAttribute('data-casty-hint-id', id);
54
+ elems.push({
55
+ id,
56
+ ...point,
57
+ type: isFocusable ? 'focus' : 'click',
58
+ });
59
+ }
60
+
61
+ if (elems.length === 0) return JSON.stringify([]);
62
+
63
+ // Generate labels
64
+ const chars = ${JSON.stringify(hintChars)};
65
+ const labels = [];
66
+ if (elems.length <= chars.length) {
67
+ for (let i = 0; i < elems.length; i++) labels.push(chars[i]);
68
+ } else {
69
+ for (const c1 of chars) {
70
+ for (const c2 of chars) {
71
+ labels.push(c1 + c2);
72
+ if (labels.length >= elems.length) break;
73
+ }
74
+ if (labels.length >= elems.length) break;
75
+ }
76
+ }
77
+
78
+ // Inject overlay
79
+ const container = document.createElement('div');
80
+ container.id = '__casty_hints';
81
+ container.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;pointer-events:none;';
82
+ for (let i = 0; i < elems.length; i++) {
83
+ const h = elems[i];
84
+ h.label = labels[i];
85
+ const span = document.createElement('span');
86
+ span.textContent = labels[i].toUpperCase();
87
+ span.dataset.label = labels[i];
88
+ span.style.cssText = 'position:fixed;background:#FFEE00;color:#000;font:bold 12px monospace;border:1px solid #C38A00;border-radius:3px;padding:0 2px;z-index:2147483647;pointer-events:none;line-height:1.4;'
89
+ + 'left:' + h.x + 'px;top:' + h.y + 'px;';
90
+ container.appendChild(span);
91
+ }
92
+ document.documentElement.appendChild(container);
93
+
94
+ return JSON.stringify(elems);
95
+ } catch (e) {
96
+ return JSON.stringify([]);
97
+ }
98
+ })()`;
99
+ }
100
+
101
+ const COLLECT_OVERLAY_SCRIPT = makeCollectAndOverlayScript(HINT_CHARS, MAX_HINTS);
102
+
103
+ // Dim non-matching labels
104
+ function makeDimScript(buffer) {
105
+ return `(() => {
106
+ const c = document.getElementById('__casty_hints');
107
+ if (!c) return;
108
+ for (const span of c.children) {
109
+ const label = span.dataset.label;
110
+ span.style.opacity = label.startsWith('${buffer}') ? '1' : '0.2';
111
+ }
112
+ })()`;
113
+ }
114
+
115
+ // Remove overlay script
116
+ const REMOVE_OVERLAY_SCRIPT = `(() => {
117
+ const el = document.getElementById('__casty_hints');
118
+ if (el) el.remove();
119
+ })()`;
120
+
121
+ const CLEANUP_HINTS_SCRIPT = `(() => {
122
+ const overlay = document.getElementById('__casty_hints');
123
+ if (overlay) overlay.remove();
124
+ for (const el of document.querySelectorAll('[data-casty-hint-id]')) {
125
+ el.removeAttribute('data-casty-hint-id');
126
+ }
127
+ })()`;
128
+
129
+ // Re-resolve the selected element after the overlay is gone. Wait for a
130
+ // stable, unobscured point so animations and late layout do not leave us
131
+ // clicking the coordinates captured when hint mode began.
132
+ function makeResolveHintScript(id, focus) {
133
+ return `(async () => {
134
+ const deadline = performance.now() + 2000;
135
+ let previous = null;
136
+ for (;;) {
137
+ const el = document.querySelector('[data-casty-hint-id="${id}"]');
138
+ if (!el) throw new Error('hint target disappeared');
139
+ const style = getComputedStyle(el);
140
+ if (style.display !== 'none' && style.visibility !== 'hidden'
141
+ && style.pointerEvents !== 'none' && Number(style.opacity) !== 0) {
142
+ const rects = [...el.getClientRects()];
143
+ if (!rects.length && el.firstElementChild) rects.push(...el.firstElementChild.getClientRects());
144
+ for (const rect of rects) {
145
+ const left = Math.max(0, rect.left), top = Math.max(0, rect.top);
146
+ const right = Math.min(innerWidth, rect.right), bottom = Math.min(innerHeight, rect.bottom);
147
+ if (right <= left || bottom <= top) continue;
148
+ const x = (left + right) / 2, y = (top + bottom) / 2;
149
+ const hit = document.elementFromPoint(x, y);
150
+ const stable = previous && previous.x === x && previous.y === y;
151
+ previous = { x, y };
152
+ if (stable && (hit === el || el.contains(hit))) {
153
+ ${focus ? 'el.focus();' : ''}
154
+ const anchor = el.closest('a[target="_blank"]');
155
+ if (anchor) anchor.target = '_self';
156
+ const form = el.closest('form[target="_blank"]');
157
+ if (form) form.target = '_self';
158
+ // Layout coordinates to input (visual viewport) coordinates:
159
+ // they differ when the page is zoomed (pinch zoom, or buninu-browser --mobile).
160
+ const viewport = window.visualViewport;
161
+ const scale = viewport?.scale || 1;
162
+ return { x: (x - (viewport?.offsetLeft || 0)) * scale, y: (y - (viewport?.offsetTop || 0)) * scale };
163
+ }
164
+ break;
165
+ }
166
+ }
167
+ if (performance.now() >= deadline) throw new Error('hint target is not actionable');
168
+ await new Promise(resolve => requestAnimationFrame(resolve));
169
+ }
170
+ })()`;
171
+ }
172
+
173
+ export class HintMode {
174
+ constructor(forceCapture) {
175
+ this.active = false;
176
+ this.buffer = '';
177
+ this._resolve = null;
178
+ this._client = null;
179
+ this._hints = [];
180
+ this._rawForceCapture = forceCapture || (() => {});
181
+ }
182
+
183
+ // Ensure frame capture after DOM changes (immediate + delayed retry)
184
+ _forceCapture() {
185
+ this._rawForceCapture();
186
+ setTimeout(() => this._rawForceCapture(), 200);
187
+ }
188
+
189
+ // Start hint mode — waits until user selects or cancels
190
+ async start(client) {
191
+ this._client = client;
192
+ this.buffer = '';
193
+
194
+ // Single evaluate: collect elements + generate labels + inject overlay
195
+ let hints;
196
+ try {
197
+ const { result } = await client.send('Runtime.evaluate', {
198
+ expression: COLLECT_OVERLAY_SCRIPT,
199
+ });
200
+ hints = JSON.parse(result.value);
201
+ } catch (e) {
202
+ console.error(`casty: hints failed: ${e.message}`);
203
+ return;
204
+ }
205
+
206
+ if (!hints || hints.length === 0) {
207
+ console.error('casty: hints: no elements found');
208
+ return;
209
+ }
210
+
211
+ console.error(`casty: hints: ${hints.length} elements`);
212
+ this._hints = hints;
213
+ this.active = true;
214
+ this._forceCapture();
215
+
216
+ let resolve;
217
+ const promise = new Promise(r => { resolve = r; });
218
+ this._resolve = resolve;
219
+ return promise;
220
+ }
221
+
222
+ // Handle input (only called while active)
223
+ async handleInput(str) {
224
+ if (!this.active) return false;
225
+
226
+ // Escape → cancel
227
+ if (str === '\x1b') {
228
+ await this._cancel();
229
+ return true;
230
+ }
231
+
232
+ // Backspace → remove last buffer char
233
+ if (str === '\x7f' || str === '\x08') {
234
+ if (this.buffer.length > 0) {
235
+ this.buffer = this.buffer.slice(0, -1);
236
+ await this._updateDim();
237
+ }
238
+ return true;
239
+ }
240
+
241
+ // Ignore non-hint characters
242
+ const ch = str.toLowerCase();
243
+ if (ch.length !== 1 || !HINT_CHARS.includes(ch)) {
244
+ return true;
245
+ }
246
+
247
+ this.buffer += ch;
248
+
249
+ // Exact match check
250
+ const exact = this._hints.find(h => h.label === this.buffer);
251
+ if (exact) {
252
+ await this._selectHint(exact);
253
+ return true;
254
+ }
255
+
256
+ // Prefix match check
257
+ const partial = this._hints.some(h => h.label.startsWith(this.buffer));
258
+ if (!partial) {
259
+ this.buffer = '';
260
+ await this._updateDim();
261
+ return true;
262
+ }
263
+
264
+ // Partial match — dim non-matching labels
265
+ await this._updateDim();
266
+ return true;
267
+ }
268
+
269
+ async _updateDim() {
270
+ try {
271
+ await this._client.send('Runtime.evaluate', {
272
+ expression: this.buffer ? makeDimScript(this.buffer) : makeDimScript(''),
273
+ });
274
+ this._forceCapture();
275
+ } catch {}
276
+ }
277
+
278
+ async _selectHint(hint) {
279
+ await this._removeOverlay();
280
+
281
+ try {
282
+ const { result, exceptionDetails } = await this._client.send('Runtime.evaluate', {
283
+ expression: makeResolveHintScript(hint.id, hint.type === 'focus'),
284
+ awaitPromise: true,
285
+ returnByValue: true,
286
+ });
287
+ if (exceptionDetails) {
288
+ throw new Error(exceptionDetails.exception?.description
289
+ || exceptionDetails.text || 'hint target evaluation failed');
290
+ }
291
+ const point = result?.value;
292
+ if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
293
+ throw new Error('hint target returned no clickable point');
294
+ }
295
+ await dispatchClick(this._client, point.x, point.y, { source: `hint ${hint.label}` });
296
+ } catch (error) {
297
+ console.error(`casty: hint click failed: ${error.message}`);
298
+ } finally {
299
+ await this._cleanupHints();
300
+ }
301
+
302
+ this._done();
303
+ this._forceCapture();
304
+ }
305
+
306
+ async _cancel() {
307
+ await this._cleanupHints();
308
+ this._done();
309
+ this._forceCapture();
310
+ }
311
+
312
+ _done() {
313
+ this.active = false;
314
+ this.buffer = '';
315
+ this._hints = [];
316
+ if (this._resolve) { this._resolve(); this._resolve = null; }
317
+ }
318
+
319
+ async _removeOverlay() {
320
+ try {
321
+ await this._client.send('Runtime.evaluate', {
322
+ expression: REMOVE_OVERLAY_SCRIPT,
323
+ });
324
+ } catch {}
325
+ }
326
+
327
+ async _cleanupHints() {
328
+ try {
329
+ await this._client.send('Runtime.evaluate', {
330
+ expression: CLEANUP_HINTS_SCRIPT,
331
+ });
332
+ } catch {}
333
+ }
334
+ }
@@ -0,0 +1,56 @@
1
+ // Key binding configuration
2
+ // Customizable via ~/.casty/keys.json
3
+ // Only defines actions casty intercepts; everything else passes through to Chrome.
4
+
5
+ import { homedir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { loadJsonFile } from './config.js';
8
+
9
+ const configPath = join(homedir(), '.casty', 'keys.json');
10
+
11
+ // Default key bindings
12
+ // Key names: "ctrl+c", "alt+left", "f5", "ctrl+shift+r", etc.
13
+ const DEFAULTS = {
14
+ 'ctrl+q': 'quit',
15
+ 'alt+left': 'back',
16
+ 'alt+right': 'forward',
17
+ 'ctrl+u': 'back',
18
+ 'ctrl+k': 'forward',
19
+ 'alt+l': 'url_bar',
20
+ 'alt+c': 'copy',
21
+ 'ctrl+v': 'paste',
22
+ 'alt+f': 'hints',
23
+ 'ctrl+l': 'hints',
24
+ 'ctrl+e': 'command',
25
+ };
26
+ // Other actions for keys.json: reload, home, copy_url, paste_url, download, eruda, zoom_in, zoom_out, zoom_reset
27
+
28
+ // Load bindings (falls back to defaults)
29
+ export function loadKeyBindings() {
30
+ return { ...DEFAULTS, ...loadJsonFile(configPath, {}) };
31
+ }
32
+
33
+ // Build a human-readable key name from parsed input
34
+ // { key: 'ArrowLeft', modifiers: 1 } → "alt+left"
35
+ // { key: 'r', modifiers: 2 } → "ctrl+r"
36
+ // { key: 'F5', modifiers: 0 } → "f5"
37
+ export function toKeyName(info) {
38
+ const parts = [];
39
+ if (info.modifiers & 2) parts.push('ctrl');
40
+ if (info.modifiers & 1) parts.push('alt');
41
+ if (info.modifiers & 8) parts.push('shift');
42
+ if (info.modifiers & 4) parts.push('meta');
43
+
44
+ // Normalize key name
45
+ const k = info.key;
46
+ const normalized =
47
+ k === 'ArrowUp' ? 'up' :
48
+ k === 'ArrowDown' ? 'down' :
49
+ k === 'ArrowLeft' ? 'left' :
50
+ k === 'ArrowRight' ? 'right' :
51
+ k === ' ' ? 'space' :
52
+ k.toLowerCase();
53
+
54
+ parts.push(normalized);
55
+ return parts.join('+');
56
+ }
@@ -0,0 +1,80 @@
1
+ // Extracted unchanged from Casty lib/input.js (MIT), except exporting the helpers.
2
+ export const FIND_SCRIPT = `(query) => {
3
+ const state = window.__castyFind || (window.__castyFind = { text: '', index: -1 });
4
+ if (query) { if (query !== state.text) state.index = -1; state.text = query; }
5
+ if (!state.text) return { error: 'Usage: find <text>' };
6
+ // Search the concatenated text, so a match may span nodes (pages that wrap
7
+ // each letter in its own element, for one)
8
+ const nodes = [], starts = [];
9
+ let all = '';
10
+ const walk = (node) => {
11
+ for (const child of node.childNodes) {
12
+ if (child.nodeType === 3) { nodes.push(child); starts.push(all.length); all += child.data; }
13
+ else if (child.nodeType === 1 && !/^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE)$/i.test(child.tagName)) walk(child);
14
+ }
15
+ };
16
+ if (document.body) walk(document.body);
17
+ const haystack = all.toLowerCase(), needle = state.text.toLowerCase();
18
+ const matches = [];
19
+ for (let at = haystack.indexOf(needle); at >= 0; at = haystack.indexOf(needle, at + needle.length)) matches.push(at);
20
+ if (!matches.length) return { text: state.text, total: 0, index: -1 };
21
+ state.index = (state.index + 1) % matches.length;
22
+ // Text node and offset holding character \`at\` (the last node starting at or before it)
23
+ const locate = (at, end) => {
24
+ let i = 0;
25
+ while (i + 1 < nodes.length && (starts[i + 1] < at || (!end && starts[i + 1] === at))) i++;
26
+ return [nodes[i], at - starts[i]];
27
+ };
28
+ const from = matches[state.index];
29
+ const [startNode, startOffset] = locate(from, false);
30
+ const [endNode, endOffset] = locate(from + needle.length, true);
31
+ try {
32
+ const range = document.createRange();
33
+ range.setStart(startNode, startOffset);
34
+ range.setEnd(endNode, endOffset);
35
+ const selection = window.getSelection();
36
+ selection.removeAllRanges();
37
+ selection.addRange(range);
38
+ } catch {}
39
+ startNode.parentElement?.scrollIntoView?.({ block: 'center' });
40
+ return { text: state.text, total: matches.length, index: state.index };
41
+ }`;
42
+
43
+ export async function findInPage(client, query) {
44
+ const { result, exceptionDetails } = await client.send('Runtime.evaluate', {
45
+ expression: `(${FIND_SCRIPT})(${JSON.stringify(query)})`,
46
+ returnByValue: true,
47
+ });
48
+ if (exceptionDetails) return { error: 'Find failed' };
49
+ return result?.value ?? { error: 'Find failed' };
50
+ }
51
+
52
+ // Ported from ../exeapk's ERUDA_TOGGLE_JS: loads eruda (github.com/liriliri/eruda)
53
+ // from a CDN on first use, then only shows or hides it. Resolves to a status
54
+ // instead of alert()ing, which would block a headless page.
55
+ const ERUDA_SCRIPT = `new Promise(resolve => {
56
+ const toggle = () => {
57
+ window.__erudaShown = !window.__erudaShown;
58
+ if (window.__erudaShown) eruda.show(); else eruda.hide();
59
+ resolve(window.__erudaShown ? 'Eruda shown' : 'Eruda hidden');
60
+ };
61
+ if (window.eruda) { toggle(); return; }
62
+ const s = document.createElement('script');
63
+ s.src = 'https://cdn.jsdelivr.net/npm/eruda';
64
+ s.onload = () => {
65
+ try { eruda.init(); window.__erudaShown = false; toggle(); }
66
+ catch (e) { resolve('Eruda failed: ' + e.message); }
67
+ };
68
+ s.onerror = () => resolve('Eruda load failed (need network, or blocked by the page)');
69
+ (document.body || document.documentElement).appendChild(s);
70
+ })`;
71
+
72
+ export async function toggleEruda(client) {
73
+ const { result, exceptionDetails } = await client.send('Runtime.evaluate', {
74
+ expression: ERUDA_SCRIPT,
75
+ awaitPromise: true,
76
+ returnByValue: true,
77
+ });
78
+ if (exceptionDetails) return 'Eruda failed';
79
+ return result?.value ?? 'Eruda failed';
80
+ }
@@ -0,0 +1,148 @@
1
+ // Terminal display widths for the address bar, copied from jsmdcui's command
2
+ // prompt (MIT, Copyright (c) 2016-2020 Zachary Yedidia, et al.; Copyright (c)
3
+ // 2026 Dr. John). Indices are UTF-16 code units, as in jsmdcui.
4
+
5
+ function isWideCodePoint(cp) {
6
+ if (cp < 0x1100) return false;
7
+ return (
8
+ cp <= 0x115F ||
9
+ cp === 0x2329 || cp === 0x232A ||
10
+ (cp >= 0x2E80 && cp <= 0x303E) ||
11
+ (cp >= 0x3040 && cp <= 0x33FF) ||
12
+ (cp >= 0x3400 && cp <= 0x4DBF) ||
13
+ (cp >= 0x4E00 && cp <= 0xA4C6) ||
14
+ (cp >= 0xA960 && cp <= 0xA97C) ||
15
+ (cp >= 0xAC00 && cp <= 0xD7A3) ||
16
+ (cp >= 0xF900 && cp <= 0xFAFF) ||
17
+ (cp >= 0xFE10 && cp <= 0xFE19) ||
18
+ (cp >= 0xFE30 && cp <= 0xFE4F) ||
19
+ (cp >= 0xFF01 && cp <= 0xFF60) ||
20
+ (cp >= 0xFFE0 && cp <= 0xFFE6) ||
21
+ (cp >= 0x1B000 && cp <= 0x1B0FF) ||
22
+ (cp >= 0x1F004 && cp <= 0x1F0CF) ||
23
+ (cp >= 0x1F18F && cp <= 0x1F19A) ||
24
+ (cp >= 0x1F200 && cp <= 0x1F2FF) ||
25
+ (cp >= 0x1F300 && cp <= 0x1FAFF) ||
26
+ (cp >= 0x20000 && cp <= 0x2FFFD) ||
27
+ (cp >= 0x30000 && cp <= 0x3FFFD)
28
+ );
29
+ }
30
+
31
+ function isZeroWidthCodePoint(cp) {
32
+ return (
33
+ cp === 0x200D ||
34
+ (cp >= 0x0300 && cp <= 0x036F) ||
35
+ (cp >= 0x1AB0 && cp <= 0x1AFF) ||
36
+ (cp >= 0x1DC0 && cp <= 0x1DFF) ||
37
+ (cp >= 0x20D0 && cp <= 0x20FF) ||
38
+ (cp >= 0xFE00 && cp <= 0xFE0F) ||
39
+ (cp >= 0xFE20 && cp <= 0xFE2F) ||
40
+ (cp >= 0xE0100 && cp <= 0xE01EF)
41
+ );
42
+ }
43
+
44
+ function isEmojiVariationBase(cp) {
45
+ return (
46
+ cp === 0x00A9 || cp === 0x00AE ||
47
+ cp === 0x203C || cp === 0x2049 ||
48
+ cp === 0x2122 || cp === 0x2139 ||
49
+ (cp >= 0x2194 && cp <= 0x21AA) ||
50
+ (cp >= 0x231A && cp <= 0x231B) ||
51
+ cp === 0x2328 || cp === 0x23CF ||
52
+ (cp >= 0x23E9 && cp <= 0x23F3) ||
53
+ (cp >= 0x23F8 && cp <= 0x23FA) ||
54
+ cp === 0x24C2 ||
55
+ (cp >= 0x25AA && cp <= 0x25AB) ||
56
+ cp === 0x25B6 || cp === 0x25C0 ||
57
+ (cp >= 0x25FB && cp <= 0x25FE) ||
58
+ (cp >= 0x2600 && cp <= 0x27BF) ||
59
+ (cp >= 0x2934 && cp <= 0x2935) ||
60
+ (cp >= 0x2B05 && cp <= 0x2B55) ||
61
+ cp === 0x3030 || cp === 0x303D ||
62
+ cp === 0x3297 || cp === 0x3299
63
+ );
64
+ }
65
+
66
+ const TAB_WIDTH = 4;
67
+
68
+ export function charWidth(ch) {
69
+ if (!ch) return 0;
70
+ const cp = ch.codePointAt(0);
71
+ if (cp === 9) return TAB_WIDTH;
72
+ if (cp < 32 || (cp >= 0x7f && cp < 0xa0) || isZeroWidthCodePoint(cp)) return 0;
73
+ if (isWideCodePoint(cp)) return 2;
74
+ return 1;
75
+ }
76
+
77
+ function displayUnitAt(text, idx) {
78
+ const cp = text.codePointAt(idx);
79
+ if (cp == null) return { text: '', width: 0, length: 0 };
80
+ let length = cp > 0xFFFF ? 2 : 1;
81
+ let unit = String.fromCodePoint(cp);
82
+ let width = charWidth(unit);
83
+ const nextCp = text.codePointAt(idx + length);
84
+ if (nextCp === 0xFE0F && isEmojiVariationBase(cp)) {
85
+ unit += String.fromCodePoint(nextCp);
86
+ length += 1;
87
+ width = 2;
88
+ }
89
+ return { text: unit, width, length };
90
+ }
91
+
92
+ export function displayWidth(text) {
93
+ let width = 0;
94
+ for (let i = 0; i < text.length;) {
95
+ const unit = displayUnitAt(text, i);
96
+ if (unit.length <= 0) break;
97
+ width += unit.width;
98
+ i += unit.length;
99
+ }
100
+ return width;
101
+ }
102
+
103
+ // Convert a screen-column offset (visualCol) into a string char-unit index,
104
+ // starting from startIdx (code-unit index) in line and walking forward.
105
+ // Snaps to the start of a wide character when the click lands on its right cell.
106
+ export function visualColToCharIdx(line, startIdx, visualCol) {
107
+ let col = 0;
108
+ let i = startIdx;
109
+ while (i < line.length) {
110
+ if (col >= visualCol) break;
111
+ const cp = line.codePointAt(i);
112
+ const charLen = cp > 0xFFFF ? 2 : 1;
113
+ const w = charWidth(String.fromCodePoint(cp));
114
+ if (col + w > visualCol) break;
115
+ col += w;
116
+ i += charLen;
117
+ }
118
+ return Math.min(i, line.length);
119
+ }
120
+
121
+ /**
122
+ * jsmdcui's prompt line: the label and value scroll horizontally so the
123
+ * cursor stays on screen. Returns the new scroll offset, the code-unit index
124
+ * the visible part starts at, and the cursor's screen column (0-based).
125
+ */
126
+ export function promptView(label, value, cursor, cols, scrollX = 0) {
127
+ const total = label + value;
128
+ const cursorInTotal = displayWidth(label) + displayWidth(value.slice(0, cursor));
129
+ if (cursorInTotal > scrollX + cols - 1) scrollX = cursorInTotal - (cols - 1);
130
+ if (cursorInTotal < scrollX) scrollX = cursorInTotal;
131
+ scrollX = Math.max(0, scrollX);
132
+ let start = scrollX > 0 ? visualColToCharIdx(total, 0, scrollX) : 0;
133
+ // A wide character cut by the left edge is not drawn: its visible half
134
+ // becomes a blank column (lead), so the text and cursor columns agree.
135
+ let lead = 0;
136
+ const startWidth = displayWidth(total.slice(0, start));
137
+ if (startWidth < scrollX) {
138
+ const cp = total.codePointAt(start);
139
+ start += cp > 0xFFFF ? 2 : 1;
140
+ lead = startWidth + charWidth(String.fromCodePoint(cp)) - scrollX;
141
+ }
142
+ return { scrollX, start, lead, cursorCol: cursorInTotal - scrollX };
143
+ }
144
+
145
+ /** The leading part of `text` that fits in `cols` columns. */
146
+ export function fitWidth(text, cols) {
147
+ return text.slice(0, visualColToCharIdx(text, 0, cols));
148
+ }