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.
- package/LICENSES/jsmdcui.txt +22 -0
- package/README.md +40 -4
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +152 -0
- package/frontends/wayland/casty/LICENSE +21 -0
- package/frontends/wayland/casty/bookmarks.js +32 -0
- package/frontends/wayland/casty/click.js +96 -0
- package/frontends/wayland/casty/commands.js +61 -0
- package/frontends/wayland/casty/config.js +45 -0
- package/frontends/wayland/casty/download-directory.js +28 -0
- package/frontends/wayland/casty/download.js +49 -0
- package/frontends/wayland/casty/hints.js +334 -0
- package/frontends/wayland/casty/keys.js +56 -0
- package/frontends/wayland/casty/page-commands.js +80 -0
- package/frontends/wayland/casty/prompt-width.js +148 -0
- package/frontends/wayland/casty/urlbar.js +606 -0
- package/frontends/wayland/client.js +88 -0
- package/frontends/wayland/clipboard.js +80 -0
- package/frontends/wayland/index.js +130 -0
- package/frontends/wayland/input.js +75 -0
- package/frontends/wayland/protocol.js +55 -0
- package/frontends/wayland/protocols/wayland.xml +3248 -0
- package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
- package/frontends/wayland/transport.js +120 -0
- package/frontends/wayland/urlbar.js +162 -0
- package/index.js +28 -1
- package/lib/audio/shared-connection.js +31 -0
- package/lib/cdp-server.js +15 -2
- package/lib/clipboard-command.js +10 -0
- package/lib/clipboard-powershell.js +22 -0
- package/lib/clipboard.js +37 -2
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +54 -12
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +6 -1
- package/lib/qml/README.md +1427 -0
- package/lib/qml/builtins.js +771 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/canvas.js +76 -0
- package/lib/qml/document.js +1199 -0
- package/lib/qml/fonts.js +69 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +352 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +521 -0
- package/lib/qml/media-renderer.js +49 -0
- package/lib/qml/media.js +86 -0
- package/lib/qml/models.js +622 -0
- package/lib/qml/parser.js +381 -0
- package/lib/qml/qs +5 -0
- package/lib/qml/qscasty +3 -0
- package/lib/qml/qsheadless +3 -0
- package/lib/qml/qswayland +3 -0
- package/lib/qml/reactive.js +182 -0
- package/lib/qml/runtime.js +3603 -0
- package/lib/qml/shader/fragment.js +83 -0
- package/lib/qml/shader/qsb.js +27 -0
- package/lib/qml/shader/renderer.js +39 -0
- package/lib/qml/shader/runtime.js +61 -0
- package/lib/qml/shapes.js +251 -0
- package/lib/qml/shortcuts.js +65 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +126 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/window-constraints.js +16 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +16 -1
- package/lib/renderer/canvas-2d.js +29 -0
- package/lib/renderer/document-selection.js +19 -0
- package/lib/renderer/host.js +45 -17
- package/lib/renderer/main.js +21 -6
- package/lib/renderer/page-realm.js +20 -10
- package/lib/renderer/page-renderer.js +497 -42
- package/lib/renderer/pointer-move.js +5 -3
- package/lib/renderer/render-interval.js +9 -0
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +10 -4
- package/package.json +1 -1
- package/test/audio-connection-lifetime.test.js +46 -0
- package/test/canvas-paint.test.js +8 -0
- package/test/clipboard.test.js +19 -0
- package/test/computed-style.test.js +12 -0
- package/test/document-selection.test.js +27 -0
- package/test/double-click.test.js +85 -0
- package/test/fixtures/canvas-damage.js +40 -0
- package/test/fixtures/fake-renderer.js +1 -0
- package/test/fixtures/flea-helpers/Dockerfile +36 -0
- package/test/fixtures/flea-helpers/bat.mjs +43 -0
- package/test/fixtures/flea-helpers/bin/bat +3 -0
- package/test/fixtures/flea-helpers/bin/bwrap +10 -0
- package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
- package/test/fixtures/flea-helpers/bin/glow +3 -0
- package/test/fixtures/flea-helpers/bin/qs +3 -0
- package/test/fixtures/flea-helpers/bin/unsupported +3 -0
- package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
- package/test/fixtures/flea-helpers/check-media.mjs +29 -0
- package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
- package/test/fixtures/flea-helpers/check.mjs +36 -0
- package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
- package/test/fixtures/flea-helpers/source-mime.xml +28 -0
- package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
- package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
- package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
- package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
- package/test/fixtures/omastorm-helpers/check.mjs +56 -0
- package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
- package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
- package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
- package/test/fixtures/pointer-paint-batching.js +23 -0
- package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea/LICENSE +21 -0
- package/test/fixtures/qml/flea/README.md +25 -0
- package/test/fixtures/qml/flea/Theme.qml +8 -0
- package/test/fixtures/qml/flea/demo.qml +24 -0
- package/test/fixtures/qml/flea/qmldir +3 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
- package/test/fixtures/qml/flea-theme/README.md +70 -0
- package/test/fixtures/qml/flea-theme/button.qml +17 -0
- package/test/fixtures/qml/flea-theme/demo.qml +27 -0
- package/test/fixtures/qml/flea-theme/field.qml +21 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
- package/test/fixtures/qml/flea-theme/segment.qml +17 -0
- package/test/fixtures/qml/flea-theme/settings.qml +58 -0
- package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
- package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
- package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
- package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
- package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
- package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
- package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
- package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
- package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
- package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
- package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
- package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
- package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
- package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
- package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
- package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
- package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
- package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
- package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
- package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
- package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
- package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
- package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
- package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
- package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
- package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
- package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
- package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
- package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
- package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
- package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
- package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
- package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
- package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
- package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
- package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
- package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
- package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
- package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
- package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
- package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
- package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
- package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
- package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
- package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
- package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
- package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
- package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
- package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
- package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
- package/test/fixtures/qml/hello.qml +26 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
- package/test/fixtures/qml/imports/components/Badge.qml +15 -0
- package/test/fixtures/qml/imports/logic/base.js +2 -0
- package/test/fixtures/qml/imports/logic/counter.js +4 -0
- package/test/fixtures/qml/imports/logic/math.js +3 -0
- package/test/fixtures/qml/imports/logic/shared.js +3 -0
- package/test/fixtures/qml/imports/main.qml +21 -0
- package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
- package/test/fixtures/qml/imports/theme/qmldir +2 -0
- package/test/fixtures/qml/multiply.qml +61 -0
- package/test/fixtures/qml-canvas-dom.js +90 -0
- package/test/fixtures/qml-fonts/main.qml +10 -0
- package/test/fixtures/qml-locale/check.mjs +6 -0
- package/test/fixtures/qml-locale/main.qml +8 -0
- package/test/fixtures/qml-media-audio.js +39 -0
- package/test/fixtures/qml-shader/grab.qml +14 -0
- package/test/fixtures/qml-shader/gradient.frag +5 -0
- package/test/fixtures/qml-shader/main.qml +14 -0
- package/test/fixtures/qml-shader/texture-grab.qml +25 -0
- package/test/fixtures/qml-shader/texture-size.frag +5 -0
- package/test/fixtures/qml-shader-check.mjs +24 -0
- package/test/fixtures/title-no-paint.js +20 -0
- package/test/helpers/flea-sandbox.js +48 -0
- package/test/helpers/quickshell-fixture.js +125 -0
- package/test/html-clipboard-wayland.test.js +47 -0
- package/test/index.test.js +13 -0
- package/test/manual-casty-without-qml.js +87 -0
- package/test/manual-double-click.html +21 -0
- package/test/manual-flea-application.js +49 -0
- package/test/manual-flea-binary.js +62 -0
- package/test/manual-flea-qml.js +9 -0
- package/test/manual-flea-settings.js +40 -0
- package/test/manual-flea-theme.js +40 -0
- package/test/manual-mouse-buttons.html +18 -0
- package/test/manual-omastorm-engine.mjs +19 -0
- package/test/manual-omastorm-shaders.mjs +28 -0
- package/test/manual-omastorm-software.mjs +25 -0
- package/test/manual-pointer-scroll.html +43 -0
- package/test/manual-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/pointer-drag.test.js +104 -0
- package/test/qml-canvas-dom.test.js +37 -0
- package/test/qml-canvas.test.js +21 -0
- package/test/qml-clipboard-bridge.test.js +25 -0
- package/test/qml-clipboard-wayland.test.js +40 -0
- package/test/qml-connections-timing.test.js +95 -0
- package/test/qml-controls-window.test.js +182 -0
- package/test/qml-dynamic.test.js +401 -0
- package/test/qml-flea-app.test.js +146 -0
- package/test/qml-flea-backend.test.js +78 -0
- package/test/qml-flea-columns-backend.test.js +38 -0
- package/test/qml-flea-compile.test.js +78 -0
- package/test/qml-flea-launcher.test.js +88 -0
- package/test/qml-flea-markdown-backend.test.js +31 -0
- package/test/qml-flea-media.test.js +28 -0
- package/test/qml-flea-navigation-backend.test.js +35 -0
- package/test/qml-flea-navigation-timing.test.js +147 -0
- package/test/qml-flea-preview-backend.test.js +53 -0
- package/test/qml-flea-previews.test.js +163 -0
- package/test/qml-flea-rows.test.js +101 -0
- package/test/qml-flea-textures.test.js +59 -0
- package/test/qml-flea-view-switch.test.js +33 -0
- package/test/qml-flea-wayland.test.js +68 -0
- package/test/qml-flea.test.js +34 -0
- package/test/qml-fonts.test.js +31 -0
- package/test/qml-grid-multiline.test.js +48 -0
- package/test/qml-host.test.js +238 -0
- package/test/qml-imports.test.js +102 -0
- package/test/qml-inherited-handlers.test.js +51 -0
- package/test/qml-input-selection.test.js +66 -0
- package/test/qml-interactions.test.js +309 -0
- package/test/qml-language.test.js +167 -0
- package/test/qml-launcher.test.js +54 -0
- package/test/qml-layout-text.test.js +205 -0
- package/test/qml-locale.test.js +45 -0
- package/test/qml-media.test.js +63 -0
- package/test/qml-nonvisual-visible.test.js +19 -0
- package/test/qml-persisted-delegates.test.js +45 -0
- package/test/qml-quickshell-controls.test.js +65 -0
- package/test/qml-quickshell-settings.test.js +62 -0
- package/test/qml-repeater.test.js +72 -0
- package/test/qml-shader-runtime.test.js +6 -0
- package/test/qml-shortcuts.test.js +39 -0
- package/test/qml-socket.test.js +60 -0
- package/test/qml-software-shader.test.js +55 -0
- package/test/qml-style-updates.test.js +124 -0
- package/test/qml-textures.test.js +66 -0
- package/test/qml-views.test.js +332 -0
- package/test/qml-window-update.test.js +36 -0
- package/test/qml-worker.test.js +69 -0
- package/test/qml.test.js +83 -0
- package/test/render-interval.test.js +33 -0
- package/test/renderer-host.test.js +11 -0
- package/test/text-input-pointer.test.js +31 -0
- package/test/text-input-scroll.test.js +29 -0
- package/test/text-input.test.js +47 -0
- package/test/title-no-paint.test.js +6 -0
- package/test/wayland-frontend.test.js +83 -0
- package/test/wayland-scroll.test.js +40 -0
- package/test/wayland-urlbar.test.js +61 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Adapted from buninu-land/lib/transport.js (MIT; LICENSE-land).
|
|
2
|
+
// Linux 64-bit ABI. Only libc is loaded. recvmsg is required for SCM_RIGHTS;
|
|
3
|
+
// Bun's stream sockets do not expose ancillary file descriptors.
|
|
4
|
+
import { dlopen, ptr } from 'bun:ffi';
|
|
5
|
+
|
|
6
|
+
if (!['arm64', 'x64'].includes(process.arch) || !['linux', 'android'].includes(process.platform)) throw Error('Requires 64-bit Linux or Android');
|
|
7
|
+
const signature = (args, returns = 'i32') => ({ args, returns });
|
|
8
|
+
let library;
|
|
9
|
+
let errnoSymbol;
|
|
10
|
+
// Same architecture-specific libc names as buninu-linux's /lib/dlopen.js.
|
|
11
|
+
const muslLibc = `libc.musl-${process.arch === 'arm64' ? 'aarch64' : 'x86_64'}.so.1`;
|
|
12
|
+
const candidates = process.platform === 'android'
|
|
13
|
+
? [['libc.so', '__errno']]
|
|
14
|
+
: [
|
|
15
|
+
['libc.so.6', '__errno_location'],
|
|
16
|
+
[muslLibc, '__errno_location'],
|
|
17
|
+
[`/lib/${muslLibc}`, '__errno_location'],
|
|
18
|
+
['libc.so', '__errno_location'],
|
|
19
|
+
['libc.so', '__errno'],
|
|
20
|
+
];
|
|
21
|
+
const failures = [];
|
|
22
|
+
for (const [name, symbol] of candidates) {
|
|
23
|
+
try { library = dlopen(name, {
|
|
24
|
+
connect: signature(['i32','ptr','u32']), socket: signature(['i32','i32','i32']),
|
|
25
|
+
recvmsg: signature(['i32','ptr','i32'], 'i64'), sendmsg: signature(['i32','ptr','i32'], 'i64'),
|
|
26
|
+
dup: signature(['i32']), send: signature(['i32','ptr','u64','i32'], 'i64'), close: signature(['i32']),
|
|
27
|
+
|
|
28
|
+
// Standard Wayland clipboard FD transfer.
|
|
29
|
+
pipe2: signature(['ptr','i32']), fcntl: signature(['i32','i32','i32']),
|
|
30
|
+
|
|
31
|
+
[symbol]: signature([], 'ptr'),
|
|
32
|
+
}); errnoSymbol = symbol; break; }
|
|
33
|
+
catch (error) { failures.push(`${name} (${symbol}): ${error.message}`); }
|
|
34
|
+
}
|
|
35
|
+
if (!library) throw Error(`Cannot load libc on ${process.platform}:\n${failures.join('\n')}`);
|
|
36
|
+
export const libc = library.symbols;
|
|
37
|
+
import { toArrayBuffer } from 'bun:ffi';
|
|
38
|
+
export const errno = () => new Int32Array(toArrayBuffer(libc[errnoSymbol](), 0, 4))[0];
|
|
39
|
+
const retry = () => [4,11].includes(errno());
|
|
40
|
+
export class Connection {
|
|
41
|
+
constructor(fd, removed) {
|
|
42
|
+
this.fd = fd; this.removed = removed; this.fds = []; this.input = Buffer.alloc(0); this.output = []; this.queuedBytes = 0;
|
|
43
|
+
this.data = Buffer.alloc(65536); this.control = Buffer.alloc(4096); this.iov = Buffer.alloc(16); this.msg = Buffer.alloc(56);
|
|
44
|
+
this.iov.writeBigUInt64LE(BigInt(ptr(this.data))); this.iov.writeBigUInt64LE(BigInt(this.data.length), 8);
|
|
45
|
+
this.msg.writeBigUInt64LE(BigInt(ptr(this.iov)), 16); this.msg.writeBigUInt64LE(1n, 24);
|
|
46
|
+
this.msg.writeBigUInt64LE(BigInt(ptr(this.control)), 32);
|
|
47
|
+
}
|
|
48
|
+
send(bytes, fds = []) {
|
|
49
|
+
if (this.closed) return;
|
|
50
|
+
const owned = [];
|
|
51
|
+
try { for (const fd of fds) { const copy = libc.dup(fd); if (copy < 0) throw Error('Cannot duplicate outgoing FD'); owned.push(copy); } }
|
|
52
|
+
catch (e) { for (const fd of owned) libc.close(fd); throw e; }
|
|
53
|
+
this.output.push({ bytes, fds: owned }); this.queuedBytes += bytes.length;
|
|
54
|
+
if (this.queuedBytes > 4*1024*1024) this.close();
|
|
55
|
+
}
|
|
56
|
+
flush() {
|
|
57
|
+
for (let round = 0; round < 64 && this.output.length; round++) {
|
|
58
|
+
const item = this.output[0];
|
|
59
|
+
let n;
|
|
60
|
+
if (item.fds.length) {
|
|
61
|
+
const iov = Buffer.alloc(16), msg = Buffer.alloc(56);
|
|
62
|
+
const size = 16 + item.fds.length * 4, control = Buffer.alloc(Math.ceil(size / 8) * 8);
|
|
63
|
+
iov.writeBigUInt64LE(BigInt(ptr(item.bytes))); iov.writeBigUInt64LE(BigInt(item.bytes.length), 8);
|
|
64
|
+
control.writeBigUInt64LE(BigInt(size)); control.writeInt32LE(1,8); control.writeInt32LE(1,12);
|
|
65
|
+
item.fds.forEach((fd,i) => control.writeInt32LE(fd,16+i*4));
|
|
66
|
+
msg.writeBigUInt64LE(BigInt(ptr(iov)),16); msg.writeBigUInt64LE(1n,24);
|
|
67
|
+
msg.writeBigUInt64LE(BigInt(ptr(control)),32); msg.writeBigUInt64LE(BigInt(control.length),40);
|
|
68
|
+
n = Number(libc.sendmsg(this.fd,msg,0x4000));
|
|
69
|
+
} else n = Number(libc.send(this.fd,item.bytes,item.bytes.length,0x4000));
|
|
70
|
+
if (n < 0) { if (!retry()) this.close(); return; }
|
|
71
|
+
if (!n) return this.close();
|
|
72
|
+
// SCM_RIGHTS is delivered with the first successfully sent byte, even on partial writes.
|
|
73
|
+
for (const fd of item.fds) libc.close(fd); item.fds = [];
|
|
74
|
+
this.queuedBytes -= n;
|
|
75
|
+
if (n === item.bytes.length) this.output.shift(); else item.bytes = item.bytes.subarray(n);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
poll() {
|
|
79
|
+
if (this.closed) return;
|
|
80
|
+
try {
|
|
81
|
+
for (let round = 0; round < 16; round++) {
|
|
82
|
+
this.msg.writeBigUInt64LE(BigInt(this.control.length), 40); this.msg.writeUInt32LE(0, 48);
|
|
83
|
+
const n = Number(libc.recvmsg(this.fd, this.msg, 0x40000000));
|
|
84
|
+
if (n < 0) { if (retry()) break; return this.close(); }
|
|
85
|
+
if (!n) return this.close();
|
|
86
|
+
const end = Number(this.msg.readBigUInt64LE(40));
|
|
87
|
+
for (let p = 0; p + 16 <= end;) {
|
|
88
|
+
const size = Number(this.control.readBigUInt64LE(p)); if (size < 16 || p + size > end) throw Error('Invalid ancillary data');
|
|
89
|
+
if (this.control.readInt32LE(p+8) === 1 && this.control.readInt32LE(p+12) === 1)
|
|
90
|
+
for (let q = p + 16; q + 4 <= p + size; q += 4) this.fds.push(this.control.readInt32LE(q));
|
|
91
|
+
p += Math.ceil(size / 8) * 8;
|
|
92
|
+
}
|
|
93
|
+
if (this.msg.readUInt32LE(48) & 8) throw Error('Truncated ancillary data');
|
|
94
|
+
this.input = Buffer.concat([this.input, this.data.subarray(0,n)]);
|
|
95
|
+
if (this.input.length > 1024*1024 || this.fds.length > 256) throw Error('Client queue limit');
|
|
96
|
+
this.onData();
|
|
97
|
+
if (this.closed) return;
|
|
98
|
+
}
|
|
99
|
+
this.flush();
|
|
100
|
+
} catch (e) { console.error(`Wayland client: ${e.message}`);
|
|
101
|
+
// Flush queued requests before disconnecting.
|
|
102
|
+
this.flush();
|
|
103
|
+
this.close(); }
|
|
104
|
+
}
|
|
105
|
+
close() {
|
|
106
|
+
if (this.closed) return; this.closed = true; libc.close(this.fd);
|
|
107
|
+
for (const item of this.output) for (const fd of item.fds) libc.close(fd);
|
|
108
|
+
this.output = []; this.queuedBytes = 0;
|
|
109
|
+
for (const fd of this.fds) libc.close(fd); this.fds = []; this.onClose?.(); this.removed();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function connectUnix(path) {
|
|
114
|
+
if (Buffer.byteLength(path)>107 || path.includes('\0')) throw Error('Invalid Wayland socket path');
|
|
115
|
+
const fd=libc.socket(1,1|0x800|0x80000,0);
|
|
116
|
+
if(fd<0)throw Error(`socket: errno ${errno()}`);
|
|
117
|
+
const address=Buffer.alloc(110); address.writeUInt16LE(1);address.write(path,2);
|
|
118
|
+
if(libc.connect(fd,address,Buffer.byteLength(path)+3)<0){const code=errno();libc.close(fd);throw Error(`connect ${path}: errno ${code}`);}
|
|
119
|
+
return new Connection(fd,()=>{});
|
|
120
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Casty's editor is vendored beside this adapter. Only rendering and browser
|
|
2
|
+
// actions live here; selection, history, dragging and double clicks stay there.
|
|
3
|
+
import {UrlBar,PromptHistory,toURL} from './casty/urlbar.js';
|
|
4
|
+
import {COMMANDS,parseCommand,parseZoom} from './casty/commands.js';
|
|
5
|
+
import {loadKeyBindings,toKeyName} from './casty/keys.js';
|
|
6
|
+
import {loadConfig} from './casty/config.js';
|
|
7
|
+
import {searchBookmarks} from './casty/bookmarks.js';
|
|
8
|
+
import {displayWidth,fitWidth} from './casty/prompt-width.js';
|
|
9
|
+
import {downloadPage} from './casty/download.js';
|
|
10
|
+
import {HintMode} from './casty/hints.js';
|
|
11
|
+
import {findInPage,toggleEruda} from './casty/page-commands.js';
|
|
12
|
+
import {readClipboard,writeClipboard} from '../../lib/clipboard.js';
|
|
13
|
+
|
|
14
|
+
export const BAR_HEIGHT=32;
|
|
15
|
+
export const WAYLAND_COMMANDS=COMMANDS.filter(c=>!['cdp','kitty','pixelmouse'].includes(c.name));
|
|
16
|
+
export function editorKey(key,modifiers=0) {
|
|
17
|
+
const letters={ArrowUp:'A',ArrowDown:'B',ArrowRight:'C',ArrowLeft:'D',Home:'H',End:'F'};
|
|
18
|
+
if(letters[key])return `\x1b[${modifiers?`1;${1+(modifiers&8?1:0)+(modifiers&1?2:0)+(modifiers&2?4:0)}`:''}${letters[key]}`;
|
|
19
|
+
if(key==='Delete')return '\x1b[3~';
|
|
20
|
+
if(key==='Backspace')return '\x7f';
|
|
21
|
+
if(key==='Enter')return '\r';if(key==='Escape')return '\x1b';if(key==='Tab')return '\t';
|
|
22
|
+
if(modifiers&2&&/^[a-z]$/i.test(key))return String.fromCharCode(key.toLowerCase().charCodeAt(0)-96);
|
|
23
|
+
return key.length===1||[...key].length===1?key:'';
|
|
24
|
+
}
|
|
25
|
+
class PixelEditor extends UrlBar {
|
|
26
|
+
constructor(onChange,history) {super({history,commandEntry:true});this.changed=onChange;this.columns=80;}
|
|
27
|
+
render(){this._dirty=false;this.changed?.();}
|
|
28
|
+
}
|
|
29
|
+
export class WaylandUrlBar {
|
|
30
|
+
constructor(context,{changed=()=>{},close=()=>{},clipboard=null,history=new PromptHistory()}={}) {
|
|
31
|
+
this.context=context;this.changed=changed;this.quit=close;this.height=BAR_HEIGHT;
|
|
32
|
+
this.clipboard=clipboard||{read:readClipboard,write:writeClipboard};
|
|
33
|
+
this.editor=new PixelEditor(()=>{this.dirty=true;this.caret=true;changed();},history);
|
|
34
|
+
this.bindings=loadKeyBindings();this.zoom=1;this.width=0;this.cellWidth=10;this.cache=new Map();
|
|
35
|
+
this.client={send:(method,params)=>this.send(method,params)};
|
|
36
|
+
this.hints=new HintMode(changed);
|
|
37
|
+
this.timer=setInterval(()=>{if(this.editor.editing){this.caret=!this.caret;this.dirty=true;changed();}},500);
|
|
38
|
+
this.timer.unref?.();this.dirty=true;
|
|
39
|
+
}
|
|
40
|
+
async initialize() {
|
|
41
|
+
const {loadGraphics}=await import('../../lib/renderer/page-renderer.js');
|
|
42
|
+
this.graphics=await loadGraphics();
|
|
43
|
+
const paragraph=this.paragraph('M','#ffffff');this.cellWidth=paragraph.getMaxIntrinsicWidth();
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
updateURL(url){if(url!==this.editor.currentUrl)this.editor.setUrl(url||'about:blank');}
|
|
47
|
+
flash(text){clearTimeout(this.statusTimer);this.editor.setStatus(String(text));this.editor.render();this.statusTimer=setTimeout(()=>{this.editor.clearStatus();this.editor.render();},5000);}
|
|
48
|
+
open(command=false,text=null){if(this.editor.editing)this.editor.cancelEditing();this.editor.startEditing({command,text}).then(result=>{
|
|
49
|
+
if(result?.mode==='cmd')return this.command(result.line);
|
|
50
|
+
if(result?.url)return this.navigate(result.url);
|
|
51
|
+
}).catch(error=>this.flash(error.message));}
|
|
52
|
+
async navigate(url){this.editor.loading=true;this.editor.render();try{await this.context.navigate(url);this.updateURL(this.context.url);}finally{this.editor.loading=false;this.editor.render();this.changed();}}
|
|
53
|
+
async press(key,options={}) {
|
|
54
|
+
const m=options.modifiers||0,name=toKeyName({key,modifiers:m});
|
|
55
|
+
if(this.editor.editing) {
|
|
56
|
+
if(name==='ctrl+c'||name==='alt+c')await this.clipboard.write(this.editor.selectedText||this.editor.text);
|
|
57
|
+
else if(name==='ctrl+x') {if(this.editor.selectedText){await this.clipboard.write(this.editor.selectedText);this.editor._clearIfSelected();this.editor.render();}}
|
|
58
|
+
else if(name==='ctrl+v')this.editor.insertText((await this.clipboard.read()).replace(/[\r\n]+/g,' '));
|
|
59
|
+
else this.editor.handleInput(editorKey(key,m));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if(this.hints.active){await this.hints.handleInput(editorKey(key,m));return;}
|
|
63
|
+
const action=this.bindings[name];
|
|
64
|
+
// Native page editing must receive Ctrl-V itself (including preventDefault,
|
|
65
|
+
// selection replacement and beforeinput), using the frontend clipboard.
|
|
66
|
+
if(action==='paste')return this.context.press(key,options);
|
|
67
|
+
if(action){await this.action(action);return;}
|
|
68
|
+
return this.context.press(key,options);
|
|
69
|
+
}
|
|
70
|
+
async pointerButton(x,y,button,state,options={}) {
|
|
71
|
+
if(button<272||button>274)return;
|
|
72
|
+
if((y<this.height&&!this.pageButtons)||this.editor.dragging) {
|
|
73
|
+
if(button!==272)return;
|
|
74
|
+
const col=Math.max(1,Math.floor(x/this.cellWidth)+1);
|
|
75
|
+
if(state) {
|
|
76
|
+
if(!this.editor.editing){this.open(col<=3);if(col>3)this.editor.armDrag(col);}
|
|
77
|
+
else this.editor.pointerDown(col);
|
|
78
|
+
}else this.editor.pointerUp();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if(state&&this.editor.editing)this.editor.cancelEditing();
|
|
82
|
+
const mask=button===272?1:button===273?2:4;
|
|
83
|
+
this.pageButtons=state?(this.pageButtons||0)|mask:(this.pageButtons||0)&~mask;
|
|
84
|
+
if(this.context.pointerButton)return this.context.pointerButton(x/this.zoom,(y-this.height)/this.zoom,button,state,options);
|
|
85
|
+
if(!state)return this.context.click(x/this.zoom,(y-this.height)/this.zoom,button===272?options:{...options,button:button===273?"right":"middle"});
|
|
86
|
+
}
|
|
87
|
+
move(x,y,options={}){if(this.editor.dragging){this.editor.pointerMove(Math.max(1,Math.floor(x/this.cellWidth)+1));return;}if(y>=this.height||this.pageButtons)return this.context.move(x/this.zoom,(y-this.height)/this.zoom,options);}
|
|
88
|
+
scroll(dx,dy,at){if(at?.y<this.height){if(this.editor.editing){if(dy<0)this.editor.historyUp();else this.editor.historyDown();}return;}return this.context.scroll(dx/this.zoom,dy/this.zoom,at?{x:at.x/this.zoom,y:(at.y-this.height)/this.zoom}:null);}
|
|
89
|
+
async command(line) {
|
|
90
|
+
const {name,arg,error}=parseCommand(line);if(error){this.flash(error);return;}
|
|
91
|
+
if(!WAYLAND_COMMANDS.some(c=>c.name===name)){this.flash(`${name} is not used by the Wayland frontend`);return;}
|
|
92
|
+
if(name==='open'){const url=toURL(arg);if(url)await this.navigate(url);else this.flash('Bookmark not found');}
|
|
93
|
+
else if(name==='bookmark'){const match=searchBookmarks(arg)[0];if(match)await this.navigate(match.url);else this.flash('Bookmark not found');}
|
|
94
|
+
else if(name==='help')this.flash(WAYLAND_COMMANDS.map(c=>c.usage).join(' · '));
|
|
95
|
+
else if(name==='find'){const result=await findInPage(this.client,arg);this.flash(result.error||`${result.text}: ${result.total?result.index+1:0}/${result.total}`);}
|
|
96
|
+
else if(name==='zoom'){if(!arg)this.flash(`Zoom ${Math.round(this.zoom*100)}%`);else{const zoom=parseZoom(arg,this.zoom);if(zoom===null)this.flash('Invalid zoom');else{this.zoom=zoom;this.changed();}}}
|
|
97
|
+
else await this.action({links:'hints',copyurl:'copy_url',pasteurl:'paste_url'}[name]||name);
|
|
98
|
+
}
|
|
99
|
+
async action(action) {
|
|
100
|
+
if(action==='quit'){this.quit();return;}
|
|
101
|
+
if(action==='url_bar'){this.open();return;}
|
|
102
|
+
if(action==='command'){this.open(true);return;}
|
|
103
|
+
if(action==='paste_url'){this.open(false,(await this.clipboard.read()).replace(/[\r\n]+/g,' '));return;}
|
|
104
|
+
if(action==='back')await this.context.goBack();
|
|
105
|
+
else if(action==='forward')await this.context.goForward();
|
|
106
|
+
else if(action==='reload')await this.context.reload();
|
|
107
|
+
else if(action==='home')await this.navigate(toURL(loadConfig().homeUrl));
|
|
108
|
+
else if(action==='copy_url'){await this.clipboard.write(this.context.url);this.flash('Copied URL');}
|
|
109
|
+
else if(action==='copy'){
|
|
110
|
+
const text=await this.context.evaluate(`(()=>{const e=document.activeElement;return e&&typeof e.selectionStart==='number'?e.value.slice(e.selectionStart,e.selectionEnd):String(getSelection())})()`);
|
|
111
|
+
if(text)await this.clipboard.write(text);
|
|
112
|
+
}else if(action==='paste')await this.context.type(await this.clipboard.read());
|
|
113
|
+
else if(action==='download')this.flash('Saved: '+await downloadPage(this.client));
|
|
114
|
+
else if(action==='hints'){void this.hints.start(this.client).catch(e=>this.flash(e.message));}
|
|
115
|
+
else if(action==='eruda')this.flash(await toggleEruda(this.client));
|
|
116
|
+
else if(['zoom_in','zoom_out','zoom_reset'].includes(action))this.zoom=parseZoom({zoom_in:'+',zoom_out:'-',zoom_reset:'reset'}[action],this.zoom);
|
|
117
|
+
this.updateURL(this.context.url);this.changed();
|
|
118
|
+
}
|
|
119
|
+
// Existing Casty helpers expect send(); route their small subset directly to
|
|
120
|
+
// the engine. There is no CDP socket or extra remote debugging server here.
|
|
121
|
+
async send(method,p={}) {
|
|
122
|
+
if(method==='Runtime.evaluate')return {result:{value:await this.context.evaluate(p.expression)}};
|
|
123
|
+
if(method==='Input.dispatchMouseEvent'){if(p.type==='mouseReleased')await this.context.click(p.x,p.y);else if(p.type==='mouseMoved')await this.context.move(p.x,p.y);return {};}
|
|
124
|
+
if(method==='Input.insertText'){await this.context.type(p.text);return {};}
|
|
125
|
+
if(method==='Page.getFrameTree'){const r=this.context.resourceContent(this.context.url);return {frameTree:{frame:{id:'main',url:this.context.url,mimeType:r?.contentType||''}}};}
|
|
126
|
+
if(method==='Page.getResourceContent'){const r=this.context.resourceContent(p.url);if(!r)throw Error('Page source unavailable');return {content:Buffer.from(r.body).toString('base64'),base64Encoded:true};}
|
|
127
|
+
throw Error(`Unsupported frontend helper method: ${method}`);
|
|
128
|
+
}
|
|
129
|
+
paragraph(text,color) {
|
|
130
|
+
const key=color+'\n'+text;let p=this.cache.get(key);if(p)return p;
|
|
131
|
+
const {CanvasKit:k,fontCollection,monospaceFamily,fontFamilies}=this.graphics;
|
|
132
|
+
const builder=k.ParagraphBuilder.MakeFromFontCollection(new k.ParagraphStyle({textStyle:{color:k.parseColorString(color),fontSize:16,fontFamilies:[monospaceFamily,...fontFamilies]},maxLines:1}),fontCollection);
|
|
133
|
+
builder.addText(text);p=builder.build();builder.delete();p.layout(100000);
|
|
134
|
+
if(this.cache.size>=256){const first=this.cache.keys().next().value;this.cache.get(first).delete();this.cache.delete(first);}
|
|
135
|
+
this.cache.set(key,p);return p;
|
|
136
|
+
}
|
|
137
|
+
pixels(width) {
|
|
138
|
+
if(width!==this.width){this.width=width;this.dirty=true;this.editor.columns=Math.max(1,Math.floor(width/this.cellWidth));}
|
|
139
|
+
if(!this.dirty&&this.bytes)return this.bytes;
|
|
140
|
+
const {CanvasKit:k}=this.graphics;if(!this.surface||this.surface.width()!==width){this.surface?.delete();this.surface=k.MakeSurface(width,this.height);}
|
|
141
|
+
const canvas=this.surface.getCanvas(),paint=new k.Paint();
|
|
142
|
+
canvas.clear(k.parseColorString(this.editor.editing?'#005f87':'#303030'));
|
|
143
|
+
const cols=this.editor.columns;
|
|
144
|
+
const ansi=this.editor.editing?this.editor._editLine(cols):this.editor._displayLine(cols);
|
|
145
|
+
let inverse=false,x=0;
|
|
146
|
+
for(const part of ansi.split(/(\x1b\[[\d;]*m)/)) {
|
|
147
|
+
if(part.startsWith('\x1b')){if(part==='\x1b[7m')inverse=true;else if(part==='\x1b[27m')inverse=false;continue;}
|
|
148
|
+
for(const char of part){const w=displayWidth(char)*this.cellWidth;
|
|
149
|
+
if(inverse){paint.setColor(k.parseColorString('#f4f8ff'));canvas.drawRect(k.XYWHRect(x,0,w,this.height),paint);}
|
|
150
|
+
if(char.trim()){const p=this.paragraph(char,inverse?'#005f87':'#f4f8ff');canvas.drawParagraph(p,x,(this.height-p.getHeight())/2);}
|
|
151
|
+
x+=w;if(x>=width)break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if(this.editor.editing&&!this.editor.selectionRange&&this.caret){const view=this.editor._editView(cols);paint.setColor(k.parseColorString('#72e0e8'));canvas.drawRect(k.XYWHRect(view.cursorCol*this.cellWidth,6,1,20),paint);}
|
|
155
|
+
paint.delete();this.surface.flush();const image=this.surface.makeImageSnapshot();
|
|
156
|
+
try{this.bytes=Buffer.from(image.readPixels(0,0,{width,height:this.height,colorType:k.ColorType.RGBA_8888,alphaType:k.AlphaType.Unpremul,colorSpace:k.ColorSpace.SRGB}));}finally{image.delete();}
|
|
157
|
+
// Only the small toolbar strip is converted; page BGRA bytes remain untouched.
|
|
158
|
+
for(let i=0;i<this.bytes.length;i+=4){const red=this.bytes[i];this.bytes[i]=this.bytes[i+2];this.bytes[i+2]=red;}
|
|
159
|
+
this.dirty=false;return this.bytes;
|
|
160
|
+
}
|
|
161
|
+
dispose(){clearInterval(this.timer);clearTimeout(this.statusTimer);clearInterval(this.editor._spinTimer);this.editor.cancelEditing();for(const p of this.cache.values())p.delete();this.cache.clear();this.surface?.delete();}
|
|
162
|
+
}
|
package/index.js
CHANGED
|
@@ -43,6 +43,14 @@ const INFORMATION = new Map([
|
|
|
43
43
|
/** Splits the command line into a frontend and the arguments it receives. */
|
|
44
44
|
export function chooseFrontend(argv) {
|
|
45
45
|
const [first, ...rest] = argv;
|
|
46
|
+
|
|
47
|
+
// wayland handle begin: independent native frontend
|
|
48
|
+
if (first === "--wayland") return { frontend: "wayland", args: rest };
|
|
49
|
+
// wayland handle end
|
|
50
|
+
|
|
51
|
+
if (["--quickshell-wayland", "--quickshell-headless", "--quickshell-casty"].includes(first))
|
|
52
|
+
return { frontend: first.slice(2), args: rest };
|
|
53
|
+
|
|
46
54
|
if (first === "--casty") return { frontend: "casty", args: rest };
|
|
47
55
|
if (first === "--headless") return { frontend: "headless", args: rest };
|
|
48
56
|
if (RESERVED_FRONTENDS.has(first)) return { frontend: first.slice(2), args: rest, reserved: true };
|
|
@@ -66,13 +74,17 @@ export function castyCommand(args, which = Bun.which) {
|
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
export function usage(name = packageInfo.name) {
|
|
77
|
+
// wayland handle begin: frontend help
|
|
78
|
+
const waylandHelp = ' --wayland\n Experimental standard Wayland SHM window; --wayland --help for options\n';
|
|
79
|
+
// wayland handle end
|
|
80
|
+
|
|
69
81
|
return `Usage:
|
|
70
82
|
${name} [URL] [CASTY_OPTIONS...] [-- BROWSER_FLAGS...]
|
|
71
83
|
${name} --casty [CASTY_ARGS...]
|
|
72
84
|
${name} --headless [ENGINE_OPTIONS...]
|
|
73
85
|
|
|
74
86
|
Frontends (first argument only):
|
|
75
|
-
--casty
|
|
87
|
+
${typeof waylandHelp === "undefined" ? "" : waylandHelp} --casty
|
|
76
88
|
Browse in the terminal with casty (the default)
|
|
77
89
|
Needs a terminal with the Kitty graphics protocol
|
|
78
90
|
Any first argument not listed here also starts casty,
|
|
@@ -81,6 +93,9 @@ Frontends (first argument only):
|
|
|
81
93
|
--headless
|
|
82
94
|
Start the engine only, as a CDP endpoint
|
|
83
95
|
The engine's options: ${name} --headless --help
|
|
96
|
+
--quickshell-wayland, --quickshell-headless, --quickshell-casty
|
|
97
|
+
Experimental Quickshell CLI (-p FILE_OR_DIRECTORY / -c CONFIG)
|
|
98
|
+
Use -- before backend arguments; each entry supports --help
|
|
84
99
|
--win32, --gtk, --appkit, --qt
|
|
85
100
|
Native windowed frontends (reserved, not implemented yet)
|
|
86
101
|
|
|
@@ -147,6 +162,18 @@ if (import.meta.main) {
|
|
|
147
162
|
await printReadme();
|
|
148
163
|
process.exit(0);
|
|
149
164
|
}
|
|
165
|
+
|
|
166
|
+
if (frontend.startsWith("quickshell-")) {
|
|
167
|
+
const entry = join(import.meta.dir, "lib/qml/qs" + frontend.slice("quickshell-".length));
|
|
168
|
+
process.exit(await run([process.execPath, entry, ...args]) ?? 1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// wayland handle begin: launch the isolated frontend
|
|
172
|
+
if (frontend === "wayland") {
|
|
173
|
+
process.exit(await run([process.execPath, join(import.meta.dir, "frontends/wayland/index.js"), ...args]) ?? 1);
|
|
174
|
+
}
|
|
175
|
+
// wayland handle end
|
|
176
|
+
|
|
150
177
|
const code = frontend === "headless"
|
|
151
178
|
? await run([process.execPath, ENGINE, ...args])
|
|
152
179
|
: await run(castyCommand(args), { ...process.env, CASTY_BROWSER: ENGINE });
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Keep one connection while audio-owning renderer processes are alive. A
|
|
2
|
+
// completed stream does not release its owner: the page may play again.
|
|
3
|
+
export class SharedAudioConnection {
|
|
4
|
+
#connect;
|
|
5
|
+
#pending = null;
|
|
6
|
+
#owners = new Set();
|
|
7
|
+
|
|
8
|
+
constructor(connect) { this.#connect = connect; }
|
|
9
|
+
|
|
10
|
+
acquire(owner) {
|
|
11
|
+
this.#owners.add(owner);
|
|
12
|
+
if (!this.#pending) {
|
|
13
|
+
const pending = Promise.resolve().then(this.#connect).catch(error => {
|
|
14
|
+
if (this.#pending === pending) this.#pending = null;
|
|
15
|
+
throw error;
|
|
16
|
+
});
|
|
17
|
+
this.#pending = pending;
|
|
18
|
+
}
|
|
19
|
+
return this.#pending;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
release(owner) {
|
|
23
|
+
this.#owners.delete(owner);
|
|
24
|
+
if (this.#owners.size || !this.#pending) return;
|
|
25
|
+
const pending = this.#pending;
|
|
26
|
+
this.#pending = null;
|
|
27
|
+
// Also close connections whose authentication is still in flight. A new
|
|
28
|
+
// owner gets a separate connection and cannot inherit this closing one.
|
|
29
|
+
void pending.then(client => client.close()).catch(() => {});
|
|
30
|
+
}
|
|
31
|
+
}
|
package/lib/cdp-server.js
CHANGED
|
@@ -701,7 +701,7 @@ async function dispatch(ctx, state, method, params, sessionId, emit) {
|
|
|
701
701
|
case "Input.dispatchMouseEvent": {
|
|
702
702
|
const target = getSessionTarget(state, sessionId);
|
|
703
703
|
if (params.type === "mouseMoved") {
|
|
704
|
-
await ctx.move?.(params.x, params.y);
|
|
704
|
+
await ctx.move?.(params.x, params.y, {modifiers:params.modifiers ?? 0});
|
|
705
705
|
return {};
|
|
706
706
|
}
|
|
707
707
|
if (params.type === "mouseWheel") {
|
|
@@ -709,9 +709,22 @@ async function dispatch(ctx, state, method, params, sessionId, emit) {
|
|
|
709
709
|
return {};
|
|
710
710
|
}
|
|
711
711
|
|
|
712
|
+
if (ctx.pointerButton && (params.type === 'mousePressed' || params.type === 'mouseReleased')) {
|
|
713
|
+
const navigation = await ctx.pointerButton(params.x,params.y,
|
|
714
|
+
params.button==='right'?273:params.button==='middle'?274:272,
|
|
715
|
+
params.type==='mousePressed'?1:0,
|
|
716
|
+
{clickCount:params.clickCount,modifiers:params.modifiers});
|
|
717
|
+
if (navigation?.url) {
|
|
718
|
+
navigateTarget(state,target,navigation.url,emit,{addHistory:true});
|
|
719
|
+
target.title=navigation.title ?? await getContextTitle(ctx,target);
|
|
720
|
+
target.history[target.historyIndex].title=target.title;
|
|
721
|
+
if(target.screencast)await pushScreencastFrame(ctx,state,target);
|
|
722
|
+
}
|
|
723
|
+
return {};
|
|
724
|
+
}
|
|
712
725
|
if (params.type === "mouseReleased") {
|
|
713
726
|
target.focusedSelector = target.pendingSelector;
|
|
714
|
-
applyClick(target, target.pendingSelector);
|
|
727
|
+
if (!params.button || params.button === "left") applyClick(target, target.pendingSelector);
|
|
715
728
|
const navigation = await ctx.click?.(params.x, params.y, {
|
|
716
729
|
button: params.button,
|
|
717
730
|
clickCount: params.clickCount,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Adapter for jsmdcui's unchanged PowerShell clipboard functions.
|
|
2
|
+
export function runSync(command, options = {}) {
|
|
3
|
+
try {
|
|
4
|
+
const p = Bun.spawnSync(command, {
|
|
5
|
+
stdin: options.stdin == null ? 'ignore' : Buffer.from(options.stdin),
|
|
6
|
+
stdout: options.stdout ?? 'pipe', stderr: 'pipe', timeout: options.timeout ?? 2000,
|
|
7
|
+
});
|
|
8
|
+
return {ok:p.success, stdout:p.stdout?.toString() ?? '', stderr:p.stderr?.toString() ?? ''};
|
|
9
|
+
} catch (error) { return {ok:false, stdout:'', stderr:String(error)}; }
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Functions copied verbatim from jsmdcui/src/platform/clipboard.js (MIT).
|
|
2
|
+
// Keep command spelling, quoting and trailing-newline handling unchanged.
|
|
3
|
+
import { runSync } from "./clipboard-command.js";
|
|
4
|
+
|
|
5
|
+
function powershellClipboard(shell) {
|
|
6
|
+
return {
|
|
7
|
+
name: "powershell",
|
|
8
|
+
// Get-Clipboard -Raw appends \r\n to stdout; strip exactly one trailing line ending.
|
|
9
|
+
read: () => outputOrThrow(runSync([shell, "-NoProfile", "-Command", "Get-Clipboard -Raw"], {})).replace(/\r?\n$/, ""),
|
|
10
|
+
write: (text) => {
|
|
11
|
+
text=(text+'').replaceAll("'","''") ;
|
|
12
|
+
return runSync([shell, "-NoProfile", "-Command", `Set-Clipboard '${text}'`], { stdout: "ignore" }).ok ;
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function outputOrThrow(result) {
|
|
18
|
+
if (!result.ok) throw new Error(result.stderr || result.stdout || "clipboard command failed");
|
|
19
|
+
return result.stdout;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { powershellClipboard };
|
package/lib/clipboard.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import {powershellClipboard} from './clipboard-powershell.js';
|
|
1
2
|
// Host-side text clipboard. No shell interpolation; page bytes go only to stdin.
|
|
2
3
|
export function clipboardCommands(platform = process.platform) {
|
|
3
4
|
return [
|
|
4
5
|
['xclip','-selection','clipboard'],
|
|
5
|
-
...(platform === 'win32' ? [['powershell.exe'
|
|
6
|
-
'[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())']]
|
|
6
|
+
...(platform === 'win32' ? [['powershell.exe']]
|
|
7
7
|
: platform === 'darwin' ? [['pbcopy']] : [['wl-copy'],['termux-clipboard-set']]),
|
|
8
8
|
];
|
|
9
9
|
}
|
|
@@ -13,6 +13,10 @@ export async function writeClipboard(text, {which = Bun.which, spawn = Bun.spawn
|
|
|
13
13
|
const executable=which(command[0]); if(!executable)continue;
|
|
14
14
|
let child,timer;
|
|
15
15
|
try {
|
|
16
|
+
if(command[0]==='powershell.exe') {
|
|
17
|
+
if(powershellClipboard(executable).write(text))return;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
16
20
|
child=spawn([executable,...command.slice(1)],{stdin:Buffer.from(text),stdout:'ignore',stderr:'ignore'});
|
|
17
21
|
timer=setTimeout(()=>child.kill(9),3000);
|
|
18
22
|
if(await child.exited === 0)return;
|
|
@@ -20,3 +24,34 @@ export async function writeClipboard(text, {which = Bun.which, spawn = Bun.spawn
|
|
|
20
24
|
}
|
|
21
25
|
throw Error('No working clipboard command (xclip preferred)');
|
|
22
26
|
}
|
|
27
|
+
|
|
28
|
+
// Read only on an explicit paste request; never poll the system clipboard.
|
|
29
|
+
export async function readClipboard({which = Bun.which, spawn = Bun.spawn, platform = process.platform} = {}) {
|
|
30
|
+
const commands = [['xclip','-selection','clipboard','-o'],
|
|
31
|
+
...(platform === 'win32' ? [['powershell.exe','-NoProfile','-Command','Get-Clipboard -Raw']]
|
|
32
|
+
: platform === 'darwin' ? [['pbpaste']] : [['wl-paste','--no-newline'],['termux-clipboard-get']])];
|
|
33
|
+
for (const command of commands) {
|
|
34
|
+
const executable = which(command[0]); if (!executable) continue;
|
|
35
|
+
let child, timer;
|
|
36
|
+
try {
|
|
37
|
+
if(command[0]==='powershell.exe') {
|
|
38
|
+
const text=powershellClipboard(executable).read();
|
|
39
|
+
if(Buffer.byteLength(text)>1024*1024)throw Error('Clipboard text exceeds limit');
|
|
40
|
+
return text;
|
|
41
|
+
}
|
|
42
|
+
child = spawn([executable,...command.slice(1)], {stdin:'ignore',stdout:'pipe',stderr:'ignore'});
|
|
43
|
+
timer = setTimeout(()=>child.kill(9),3000);
|
|
44
|
+
const chunks=[]; let size=0;
|
|
45
|
+
for await (const chunk of child.stdout) {
|
|
46
|
+
size += chunk.length;
|
|
47
|
+
if (size > 1024*1024) { child.kill(9); throw Error('Clipboard text exceeds limit'); }
|
|
48
|
+
chunks.push(Buffer.from(chunk));
|
|
49
|
+
}
|
|
50
|
+
if (await child.exited === 0) {
|
|
51
|
+
const text=Buffer.concat(chunks).toString();
|
|
52
|
+
return command[0]==='powershell.exe' ? text.replace(/\r?\n$/, '') : text;
|
|
53
|
+
}
|
|
54
|
+
} catch {} finally {clearTimeout(timer);}
|
|
55
|
+
}
|
|
56
|
+
throw Error('No working clipboard read command (xclip preferred)');
|
|
57
|
+
}
|
package/lib/document-types.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// qml handle begin: response classification
|
|
2
|
+
import {isQmlDocument, QML_MIME} from "./qml/format.js";
|
|
3
|
+
// qml handle end
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* What a top-level response looks like in the viewport. HTML is shown as
|
|
3
7
|
* itself; anything else gets a small generated HTML document, as browsers do
|
|
@@ -73,6 +77,13 @@ function previewKind(name, type) {
|
|
|
73
77
|
export function displayDocument({ url, contentType = "", body }) {
|
|
74
78
|
const { type, charset } = parseContentType(contentType);
|
|
75
79
|
const name = fileName(url);
|
|
80
|
+
|
|
81
|
+
// qml handle begin: preserve QML source for the renderer
|
|
82
|
+
if (body.byteLength <= DISPLAY_LIMIT && isQmlDocument(url, contentType)) {
|
|
83
|
+
return {source: decode(body, charset), contentType: QML_MIME};
|
|
84
|
+
}
|
|
85
|
+
// qml handle end
|
|
86
|
+
|
|
76
87
|
if (HTML_TYPES.has(type) || (type === "" && looksLikeHtml(body))) {
|
|
77
88
|
return { source: decode(body, charset), contentType: contentType || "text/html" };
|
|
78
89
|
}
|
package/lib/headless-shell.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// qml handle begin: select a dedicated local application renderer
|
|
2
|
+
import {isQmlDocument} from './qml/format.js';
|
|
3
|
+
// Local QML uses a dedicated host renderer.
|
|
4
|
+
// qml handle end
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Headless shell controller.
|
|
3
8
|
*
|
|
@@ -7,8 +12,8 @@
|
|
|
7
12
|
* runs page content itself.
|
|
8
13
|
*
|
|
9
14
|
* Sandbox flags: `--no-sandbox` is accepted for Chromium compatibility (casty
|
|
10
|
-
* always passes it) but ignored.
|
|
11
|
-
* the renderer sandbox
|
|
15
|
+
* always passes it) but ignored. `--dangerously-allow-host-js` relaxes
|
|
16
|
+
* the renderer sandbox for every page.
|
|
12
17
|
*/
|
|
13
18
|
import { CdpServer } from "./cdp-server.js";
|
|
14
19
|
import { DEFAULT_DOWNLOAD_DIR, isAttachment, saveDownload, suggestedFilename } from "./download.js";
|
|
@@ -80,12 +85,14 @@ export async function runHeadlessShell(args = Bun.argv.slice(2)) {
|
|
|
80
85
|
* anything that needs a renderer waits for it.
|
|
81
86
|
*
|
|
82
87
|
* @param {{ allowHostJs?: boolean, spareRenderer?: boolean, network?: NetworkService, rendererTimeout?: number,
|
|
83
|
-
* waitForRenderer?: boolean, mobile?: boolean, webkit?: boolean
|
|
88
|
+
* waitForRenderer?: boolean, mobile?: boolean, webkit?: boolean,
|
|
89
|
+
* clipboard?: {read: Function, write: Function} }} options
|
|
84
90
|
* WebKit identity defaults on; webkit: false selects Chromium. Mobile takes priority.
|
|
85
91
|
*/
|
|
86
92
|
export async function createBrowser({
|
|
87
93
|
allowHostJs = false, spareRenderer = true, network = new NetworkService(), rendererTimeout = 30_000, mobile = false,
|
|
88
94
|
waitForRenderer = true, webkit = true,
|
|
95
|
+
clipboard = null,
|
|
89
96
|
} = {}) {
|
|
90
97
|
// Explicit presentation profiles fix the identity for requests and scripts.
|
|
91
98
|
const initialUserAgent = mobile ? MOBILE_USER_AGENT : webkit ? WEBKIT_USER_AGENT : DESKTOP_USER_AGENT;
|
|
@@ -158,10 +165,18 @@ export async function createBrowser({
|
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
function createHost() {
|
|
168
|
+
let args = [...(allowHostJs ? [DANGEROUSLY_ALLOW_HOST_JS] : []), ...(mobile ? [MOBILE_FLAG] : [])];
|
|
169
|
+
|
|
170
|
+
// qml handle begin: internal options for a dedicated application renderer
|
|
171
|
+
if (arguments[0]?.qmlHost) args = ["--buninu-qml-host", ...args];
|
|
172
|
+
// qml handle end
|
|
173
|
+
|
|
161
174
|
const host = new RendererHost({
|
|
175
|
+
// Native frontends supply one clipboard for page APIs and input defaults.
|
|
176
|
+
...(clipboard?{clipboardRead:()=>clipboard.read(),clipboardWrite:text=>clipboard.write(text)}:{}),
|
|
162
177
|
network,
|
|
163
178
|
timeout: rendererTimeout,
|
|
164
|
-
args
|
|
179
|
+
args,
|
|
165
180
|
// A replacement renderer starts blank; bring the current page back.
|
|
166
181
|
onRestart: async (invoke) => {
|
|
167
182
|
await replayState(invoke);
|
|
@@ -225,10 +240,22 @@ export async function createBrowser({
|
|
|
225
240
|
|
|
226
241
|
/** The renderer for a URL's origin, swapping processes on an origin change. */
|
|
227
242
|
async function rendererFor(url) {
|
|
228
|
-
|
|
243
|
+
let key = originKey(url);
|
|
244
|
+
let acquireHost = () => {
|
|
245
|
+
const next = spare ?? createHost();
|
|
246
|
+
spare = null;
|
|
247
|
+
return next;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// qml handle begin: never reuse application realms or take an ordinary spare
|
|
251
|
+
if (arguments[1]?.qmlHost) {
|
|
252
|
+
key = Symbol("qml document");
|
|
253
|
+
acquireHost = () => createHost({ qmlHost: true });
|
|
254
|
+
}
|
|
255
|
+
// qml handle end
|
|
256
|
+
|
|
229
257
|
if (current && key === currentKey) return current;
|
|
230
|
-
const next =
|
|
231
|
-
spare = null;
|
|
258
|
+
const next = acquireHost();
|
|
232
259
|
await next.start();
|
|
233
260
|
await replayState((method, ...params) => next.call(method, ...params));
|
|
234
261
|
const previous = current;
|
|
@@ -307,7 +334,18 @@ export async function createBrowser({
|
|
|
307
334
|
writeLog("navigation", `${url}: ${document.error ?? `HTTP ${document.status ?? 200}`}`
|
|
308
335
|
+ `${document.url === url ? "" : ` -> ${document.url}`} in ${Math.round(fetched - started)}ms`);
|
|
309
336
|
if (!invoke) await rendererReady;
|
|
310
|
-
|
|
337
|
+
let selectRenderer = rendererFor;
|
|
338
|
+
|
|
339
|
+
// qml handle begin: host privileges require a local top-level QML document
|
|
340
|
+
const qmlHost = !document.error && new URL(document.url).protocol === "file:"
|
|
341
|
+
&& isQmlDocument(document.url, document.contentType);
|
|
342
|
+
// A restarted renderer may restore a response of a different type.
|
|
343
|
+
const restoreChangedType = invoke && (typeof currentKey === "symbol") !== qmlHost;
|
|
344
|
+
if (restoreChangedType) invoke = null;
|
|
345
|
+
selectRenderer = url => rendererFor(url, { qmlHost });
|
|
346
|
+
// qml handle end
|
|
347
|
+
|
|
348
|
+
const host = invoke ? current : await selectRenderer(document.url);
|
|
311
349
|
let generation;
|
|
312
350
|
try {
|
|
313
351
|
generation = await (invoke ?? ((method, ...params) => host.call(method, ...params)))("loadDocument", document);
|
|
@@ -389,6 +427,7 @@ export async function createBrowser({
|
|
|
389
427
|
}
|
|
390
428
|
|
|
391
429
|
const context = {
|
|
430
|
+
get url() { return currentUrl; },
|
|
392
431
|
navigate,
|
|
393
432
|
get userAgent() { return userAgent ?? initialUserAgent; },
|
|
394
433
|
setDownloadBehavior({ behavior = "allow", downloadPath = DEFAULT_DOWNLOAD_DIR, eventsEnabled = false } = {}) {
|
|
@@ -477,11 +516,14 @@ export async function createBrowser({
|
|
|
477
516
|
lifecycleState() {
|
|
478
517
|
return [...reachedLifecycle(committed.host, committed.generation)];
|
|
479
518
|
},
|
|
480
|
-
move(x, y) {
|
|
481
|
-
return call("move", x / pageScale(), y / pageScale());
|
|
519
|
+
move(x, y, options = {}) {
|
|
520
|
+
return call("move", x / pageScale(), y / pageScale(), options);
|
|
521
|
+
},
|
|
522
|
+
pointerButton(x, y, button, state, options = {}) {
|
|
523
|
+
return this.click(x, y, {...options, button:button===273?'right':button===274?'middle':'left', phase:state?'down':'up'});
|
|
482
524
|
},
|
|
483
|
-
async click(x, y) {
|
|
484
|
-
const clicked = await call("click", x / pageScale(), y / pageScale());
|
|
525
|
+
async click(x, y, options = {}) {
|
|
526
|
+
const clicked = await call("click", x / pageScale(), y / pageScale(), options);
|
|
485
527
|
const href = typeof clicked === "string" ? clicked : clicked?.url;
|
|
486
528
|
if (typeof href !== "string") return null;
|
|
487
529
|
if (clicked.fragmentFrom) {
|