buninu-browser 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/jsmdcui.txt +22 -0
- package/README.md +19 -4
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +123 -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 +116 -0
- package/frontends/wayland/input.js +56 -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 +158 -0
- package/index.js +28 -1
- 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 +47 -8
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +5 -1
- package/lib/qml/README.md +971 -0
- package/lib/qml/builtins.js +700 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/document.js +1176 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +324 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +517 -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 +159 -0
- package/lib/qml/runtime.js +3381 -0
- package/lib/qml/shapes.js +243 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +123 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +11 -0
- package/lib/renderer/canvas-2d.js +13 -0
- package/lib/renderer/host.js +31 -6
- package/lib/renderer/main.js +19 -6
- package/lib/renderer/page-realm.js +16 -1
- package/lib/renderer/page-renderer.js +230 -13
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +2 -2
- package/package.json +1 -1
- package/test/clipboard.test.js +19 -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/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-media-audio.js +39 -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-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-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/qml-canvas-dom.test.js +37 -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 +73 -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-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-media.test.js +63 -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-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-worker.test.js +69 -0
- package/test/qml.test.js +83 -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/wayland-frontend.test.js +83 -0
- package/test/wayland-urlbar.test.js +38 -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,158 @@
|
|
|
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) {
|
|
71
|
+
if(button!==272)return;
|
|
72
|
+
if(y<this.height||this.editor.dragging) {
|
|
73
|
+
const col=Math.max(1,Math.floor(x/this.cellWidth)+1);
|
|
74
|
+
if(state) {
|
|
75
|
+
if(!this.editor.editing){this.open(col<=3);if(col>3)this.editor.armDrag(col);}
|
|
76
|
+
else this.editor.pointerDown(col);
|
|
77
|
+
}else this.editor.pointerUp();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if(state&&this.editor.editing)this.editor.cancelEditing();
|
|
81
|
+
if(!state)return this.context.click(x/this.zoom,(y-this.height)/this.zoom);
|
|
82
|
+
}
|
|
83
|
+
move(x,y){if(this.editor.dragging){this.editor.pointerMove(Math.max(1,Math.floor(x/this.cellWidth)+1));return;}if(y>=this.height)return this.context.move(x/this.zoom,(y-this.height)/this.zoom);}
|
|
84
|
+
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);}
|
|
85
|
+
async command(line) {
|
|
86
|
+
const {name,arg,error}=parseCommand(line);if(error){this.flash(error);return;}
|
|
87
|
+
if(!WAYLAND_COMMANDS.some(c=>c.name===name)){this.flash(`${name} is not used by the Wayland frontend`);return;}
|
|
88
|
+
if(name==='open'){const url=toURL(arg);if(url)await this.navigate(url);else this.flash('Bookmark not found');}
|
|
89
|
+
else if(name==='bookmark'){const match=searchBookmarks(arg)[0];if(match)await this.navigate(match.url);else this.flash('Bookmark not found');}
|
|
90
|
+
else if(name==='help')this.flash(WAYLAND_COMMANDS.map(c=>c.usage).join(' · '));
|
|
91
|
+
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}`);}
|
|
92
|
+
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();}}}
|
|
93
|
+
else await this.action({links:'hints',copyurl:'copy_url',pasteurl:'paste_url'}[name]||name);
|
|
94
|
+
}
|
|
95
|
+
async action(action) {
|
|
96
|
+
if(action==='quit'){this.quit();return;}
|
|
97
|
+
if(action==='url_bar'){this.open();return;}
|
|
98
|
+
if(action==='command'){this.open(true);return;}
|
|
99
|
+
if(action==='paste_url'){this.open(false,(await this.clipboard.read()).replace(/[\r\n]+/g,' '));return;}
|
|
100
|
+
if(action==='back')await this.context.goBack();
|
|
101
|
+
else if(action==='forward')await this.context.goForward();
|
|
102
|
+
else if(action==='reload')await this.context.reload();
|
|
103
|
+
else if(action==='home')await this.navigate(toURL(loadConfig().homeUrl));
|
|
104
|
+
else if(action==='copy_url'){await this.clipboard.write(this.context.url);this.flash('Copied URL');}
|
|
105
|
+
else if(action==='copy'){
|
|
106
|
+
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())})()`);
|
|
107
|
+
if(text)await this.clipboard.write(text);
|
|
108
|
+
}else if(action==='paste')await this.context.type(await this.clipboard.read());
|
|
109
|
+
else if(action==='download')this.flash('Saved: '+await downloadPage(this.client));
|
|
110
|
+
else if(action==='hints'){void this.hints.start(this.client).catch(e=>this.flash(e.message));}
|
|
111
|
+
else if(action==='eruda')this.flash(await toggleEruda(this.client));
|
|
112
|
+
else if(['zoom_in','zoom_out','zoom_reset'].includes(action))this.zoom=parseZoom({zoom_in:'+',zoom_out:'-',zoom_reset:'reset'}[action],this.zoom);
|
|
113
|
+
this.updateURL(this.context.url);this.changed();
|
|
114
|
+
}
|
|
115
|
+
// Existing Casty helpers expect send(); route their small subset directly to
|
|
116
|
+
// the engine. There is no CDP socket or extra remote debugging server here.
|
|
117
|
+
async send(method,p={}) {
|
|
118
|
+
if(method==='Runtime.evaluate')return {result:{value:await this.context.evaluate(p.expression)}};
|
|
119
|
+
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 {};}
|
|
120
|
+
if(method==='Input.insertText'){await this.context.type(p.text);return {};}
|
|
121
|
+
if(method==='Page.getFrameTree'){const r=this.context.resourceContent(this.context.url);return {frameTree:{frame:{id:'main',url:this.context.url,mimeType:r?.contentType||''}}};}
|
|
122
|
+
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};}
|
|
123
|
+
throw Error(`Unsupported frontend helper method: ${method}`);
|
|
124
|
+
}
|
|
125
|
+
paragraph(text,color) {
|
|
126
|
+
const key=color+'\n'+text;let p=this.cache.get(key);if(p)return p;
|
|
127
|
+
const {CanvasKit:k,fontCollection,monospaceFamily,fontFamilies}=this.graphics;
|
|
128
|
+
const builder=k.ParagraphBuilder.MakeFromFontCollection(new k.ParagraphStyle({textStyle:{color:k.parseColorString(color),fontSize:16,fontFamilies:[monospaceFamily,...fontFamilies]},maxLines:1}),fontCollection);
|
|
129
|
+
builder.addText(text);p=builder.build();builder.delete();p.layout(100000);
|
|
130
|
+
if(this.cache.size>=256){const first=this.cache.keys().next().value;this.cache.get(first).delete();this.cache.delete(first);}
|
|
131
|
+
this.cache.set(key,p);return p;
|
|
132
|
+
}
|
|
133
|
+
pixels(width) {
|
|
134
|
+
if(width!==this.width){this.width=width;this.dirty=true;this.editor.columns=Math.max(1,Math.floor(width/this.cellWidth));}
|
|
135
|
+
if(!this.dirty&&this.bytes)return this.bytes;
|
|
136
|
+
const {CanvasKit:k}=this.graphics;if(!this.surface||this.surface.width()!==width){this.surface?.delete();this.surface=k.MakeSurface(width,this.height);}
|
|
137
|
+
const canvas=this.surface.getCanvas(),paint=new k.Paint();
|
|
138
|
+
canvas.clear(k.parseColorString(this.editor.editing?'#005f87':'#303030'));
|
|
139
|
+
const cols=this.editor.columns;
|
|
140
|
+
const ansi=this.editor.editing?this.editor._editLine(cols):this.editor._displayLine(cols);
|
|
141
|
+
let inverse=false,x=0;
|
|
142
|
+
for(const part of ansi.split(/(\x1b\[[\d;]*m)/)) {
|
|
143
|
+
if(part.startsWith('\x1b')){if(part==='\x1b[7m')inverse=true;else if(part==='\x1b[27m')inverse=false;continue;}
|
|
144
|
+
for(const char of part){const w=displayWidth(char)*this.cellWidth;
|
|
145
|
+
if(inverse){paint.setColor(k.parseColorString('#f4f8ff'));canvas.drawRect(k.XYWHRect(x,0,w,this.height),paint);}
|
|
146
|
+
if(char.trim()){const p=this.paragraph(char,inverse?'#005f87':'#f4f8ff');canvas.drawParagraph(p,x,(this.height-p.getHeight())/2);}
|
|
147
|
+
x+=w;if(x>=width)break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
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);}
|
|
151
|
+
paint.delete();this.surface.flush();const image=this.surface.makeImageSnapshot();
|
|
152
|
+
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();}
|
|
153
|
+
// Only the small toolbar strip is converted; page BGRA bytes remain untouched.
|
|
154
|
+
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;}
|
|
155
|
+
this.dirty=false;return this.bytes;
|
|
156
|
+
}
|
|
157
|
+
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();}
|
|
158
|
+
}
|
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,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 } = {}) {
|
|
@@ -236,9 +236,9 @@ function blockChildren(node, flags) {
|
|
|
236
236
|
children.push(buildBox(child, inheritedFlags(child, childFlags)));
|
|
237
237
|
for (const open of openInlines) items.push({ kind: "open", node: open, style: open.style ?? null });
|
|
238
238
|
} else if (child.type === "image") {
|
|
239
|
-
items.push({ kind: "image", node: child });
|
|
239
|
+
items.push({ kind: "image", node: child, wrap:childFlags.wrap });
|
|
240
240
|
} else if (isAtomic(child)) {
|
|
241
|
-
items.push({ kind: "atomic", node: child, box: buildBox(child, inheritedFlags(child, childFlags)) });
|
|
241
|
+
items.push({ kind: "atomic", node: child, wrap:childFlags.wrap, box: buildBox(child, inheritedFlags(child, childFlags)) });
|
|
242
242
|
} else {
|
|
243
243
|
items.push({ kind: "open", node: child, style: child.style ?? null });
|
|
244
244
|
openInlines.push(child);
|
|
@@ -2117,11 +2117,13 @@ function buildParagraph(ctx, ifc) {
|
|
|
2117
2117
|
let value = String(item.text).replace(/\r\n?/g, "\n");
|
|
2118
2118
|
if (item.preserve) {
|
|
2119
2119
|
const pieces = value.split("\n");
|
|
2120
|
+
let sourceStart=0;
|
|
2120
2121
|
pieces.forEach((piece, index) => {
|
|
2121
2122
|
if (index > 0) forced.set(text.length, (forced.get(text.length) ?? 0) + 1);
|
|
2122
2123
|
if (piece) {
|
|
2123
|
-
pushRun({ kind: "text", nodeId: item.node.id, style: item.style, collapsible: false, wrap: item.wrap }, piece);
|
|
2124
|
+
pushRun({ kind: "text", nodeId: item.node.id, style: item.style, collapsible: false, wrap: item.wrap, sourceStart }, piece);
|
|
2124
2125
|
}
|
|
2126
|
+
sourceStart+=piece.length+1;
|
|
2125
2127
|
});
|
|
2126
2128
|
afterSpace = false;
|
|
2127
2129
|
} else {
|
|
@@ -2137,7 +2139,7 @@ function buildParagraph(ctx, ifc) {
|
|
|
2137
2139
|
forced.set(text.length, (forced.get(text.length) ?? 0) + 1);
|
|
2138
2140
|
afterSpace = true;
|
|
2139
2141
|
} else if (item.kind === "image" || item.kind === "atomic") {
|
|
2140
|
-
pushRun({ kind: item.kind, nodeId: item.node.id, style: item.node.style ?? null, node: item.node, box: item.box, wrap:
|
|
2142
|
+
pushRun({ kind: item.kind, nodeId: item.node.id, style: item.node.style ?? null, node: item.node, box: item.box, wrap: item.wrap!==false }, "");
|
|
2141
2143
|
afterSpace = false;
|
|
2142
2144
|
}
|
|
2143
2145
|
}
|
|
@@ -2158,7 +2160,7 @@ function buildParagraph(ctx, ifc) {
|
|
|
2158
2160
|
const end = index + segment.length;
|
|
2159
2161
|
if (!run) continue;
|
|
2160
2162
|
if (run.collapsible && run.wrap && segment === " ") opportunities.add(end);
|
|
2161
|
-
else if (run.kind === "image" || run.kind === "atomic") {
|
|
2163
|
+
else if (run.wrap && (run.kind === "image" || run.kind === "atomic")) {
|
|
2162
2164
|
opportunities.add(index);
|
|
2163
2165
|
opportunities.add(end);
|
|
2164
2166
|
} else if (run.wrap && run.kind === "text" && (Bun.stringWidth(segment) > 1 || (!run.collapsible && segment === " "))) {
|
|
@@ -2404,7 +2406,7 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2404
2406
|
ascent = Math.max(ascent, atomAscent);
|
|
2405
2407
|
descent = Math.max(descent, atom.marginTop + atom.height + atom.marginBottom - atomAscent);
|
|
2406
2408
|
} else {
|
|
2407
|
-
segment.metrics = textMetrics(ctx, run.style);
|
|
2409
|
+
segment.metrics = textMetrics(ctx, run.style, paragraph.text.slice(segment.start,segment.end));
|
|
2408
2410
|
ascent = Math.max(ascent, segment.metrics.ascent);
|
|
2409
2411
|
descent = Math.max(descent, segment.metrics.descent);
|
|
2410
2412
|
}
|
|
@@ -2461,6 +2463,7 @@ function placeLine(ctx, ifc, paragraph, line, x, width, y, atomFor) {
|
|
|
2461
2463
|
const common = {
|
|
2462
2464
|
nodeId: run.nodeId,
|
|
2463
2465
|
text: run.kind === "text" ? paragraph.text.slice(segment.start, segment.end) : "",
|
|
2466
|
+
sourceStart:(run.sourceStart??0)+segment.start-run.start,
|
|
2464
2467
|
start: segment.start - line.start,
|
|
2465
2468
|
end: segment.end - line.start,
|
|
2466
2469
|
style: run.style ?? style,
|
|
@@ -2598,8 +2601,8 @@ function measureAtomic(ctx, box, availableWidth) {
|
|
|
2598
2601
|
};
|
|
2599
2602
|
}
|
|
2600
2603
|
|
|
2601
|
-
function textMetrics(ctx, style) {
|
|
2602
|
-
const font = ctx.metrics(style);
|
|
2604
|
+
function textMetrics(ctx, style, text = null) {
|
|
2605
|
+
const font = ctx.metrics(style,text);
|
|
2603
2606
|
// `line-height: normal` (null) uses the font's own content height.
|
|
2604
2607
|
const lineHeight = ctx.lineHeight ?? (style ? style.lineHeight ?? font.height : ctx.fallbackLineHeight);
|
|
2605
2608
|
const halfLeading = (lineHeight - font.height) / 2;
|
|
@@ -2668,7 +2671,12 @@ function cachedMetrics(fontMetrics) {
|
|
|
2668
2671
|
}
|
|
2669
2672
|
return metrics;
|
|
2670
2673
|
};
|
|
2671
|
-
return (style) => {
|
|
2674
|
+
return (style,text=null) => {
|
|
2675
|
+
// Fallback fonts can be taller than the Latin strut. Use the same glyphs
|
|
2676
|
+
// measured for this run rather than assuming every character uses "Hg".
|
|
2677
|
+
if(text && /[^\x00-\x7f]/.test(text) && typeof fontMetrics==='function') {
|
|
2678
|
+
const measured=fontMetrics(style,text);if(measured?.height>0)return measured;
|
|
2679
|
+
}
|
|
2672
2680
|
if (style === null || typeof style !== "object") return metricsFor(style);
|
|
2673
2681
|
let metrics = byStyle.get(style);
|
|
2674
2682
|
if (!metrics) byStyle.set(style, metrics = metricsFor(style));
|