buninu-browser 0.2.4 → 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 +102 -2
- 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 +57 -0
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +78 -13
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +5 -1
- package/lib/paint/frame.js +35 -3
- package/lib/paint/raw-frames.js +47 -0
- 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/compositor-channel.js +103 -0
- package/lib/renderer/compositor.js +25 -33
- package/lib/renderer/flags.js +1 -1
- package/lib/renderer/host.js +128 -92
- package/lib/renderer/main.js +38 -4
- package/lib/renderer/page-realm.js +32 -1
- package/lib/renderer/page-renderer.js +250 -21
- 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 +54 -0
- package/test/fixtures/browser-raw-demo.html +36 -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/fixtures/raw-capture-benchmark.json +76 -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/raw-capture.test.js +152 -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
- package/test/window-resize.test.js +36 -0
- package/tools/bench-raw-capture.js +32 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdtempSync,
|
|
3
|
+
cpSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
export async function withQuickshellFixture(file, callback) {
|
|
13
|
+
const { start } = await import('../../../buninu-land/compositor.js');
|
|
14
|
+
const qsRoot = process.env.BUNINU_QS_ROOT,
|
|
15
|
+
dir = mkdtempSync('/tmp/qs-qml-test-'),
|
|
16
|
+
fixture = join(dir, 'fixture'),
|
|
17
|
+
home = join(dir, 'home'),
|
|
18
|
+
oldRuntime = process.env.XDG_RUNTIME_DIR;
|
|
19
|
+
let land, child;
|
|
20
|
+
try {
|
|
21
|
+
cpSync(
|
|
22
|
+
Bun.fileURLToPath(
|
|
23
|
+
new URL('../fixtures/qml/flea-theme/', import.meta.url),
|
|
24
|
+
),
|
|
25
|
+
fixture,
|
|
26
|
+
{ recursive: true },
|
|
27
|
+
);
|
|
28
|
+
const trim = (path) => {
|
|
29
|
+
for (const e of readdirSync(path, { withFileTypes: true })) {
|
|
30
|
+
const p = join(path, e.name);
|
|
31
|
+
if (e.isDirectory()) trim(p);
|
|
32
|
+
else if (e.name === 'qmldir')
|
|
33
|
+
writeFileSync(
|
|
34
|
+
p,
|
|
35
|
+
readFileSync(p, 'utf8')
|
|
36
|
+
.split('\n')
|
|
37
|
+
.filter((line) => {
|
|
38
|
+
const last = line.trim().split(/\s+/).at(-1);
|
|
39
|
+
return !last.endsWith('.qml') || existsSync(join(path, last));
|
|
40
|
+
})
|
|
41
|
+
.join('\n'),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
trim(fixture);
|
|
46
|
+
const theme = join(home, '.local/state/omarchy/current/theme');
|
|
47
|
+
mkdirSync(theme, { recursive: true });
|
|
48
|
+
writeFileSync(join(theme, 'shell.toml'), '[font]\nbase-size = 14\n');
|
|
49
|
+
writeFileSync(
|
|
50
|
+
join(theme, 'colors.toml'),
|
|
51
|
+
'background="#101315"\nforeground="#eeeeee"\naccent="#70e1e8"\n',
|
|
52
|
+
);
|
|
53
|
+
writeFileSync(join(theme, '../theme.name'), 'demo');
|
|
54
|
+
process.env.XDG_RUNTIME_DIR = dir;
|
|
55
|
+
land = await start(['--remote-debugging-port=0', '--window-size=900,700']);
|
|
56
|
+
const lib = join(qsRoot, 'usr/lib/aarch64-linux-gnu');
|
|
57
|
+
child = Bun.spawn(
|
|
58
|
+
[
|
|
59
|
+
join(lib, 'ld-linux-aarch64.so.1'),
|
|
60
|
+
'--library-path',
|
|
61
|
+
lib + ':' + join(qsRoot, 'usr/lib'),
|
|
62
|
+
join(qsRoot, 'usr/bin/qs'),
|
|
63
|
+
'-p',
|
|
64
|
+
join(fixture, file),
|
|
65
|
+
],
|
|
66
|
+
{
|
|
67
|
+
env: {
|
|
68
|
+
...process.env,
|
|
69
|
+
HOME: home,
|
|
70
|
+
HYPRLAND_INSTANCE_SIGNATURE: '',
|
|
71
|
+
WAYLAND_DISPLAY: 'wayland-buninu',
|
|
72
|
+
QT_PLUGIN_PATH: join(lib, 'qt6/plugins'),
|
|
73
|
+
QML_IMPORT_PATH:
|
|
74
|
+
join(fixture, 'imports') + ':' + join(lib, 'qt6/qml'),
|
|
75
|
+
QT_QPA_PLATFORM: 'wayland',
|
|
76
|
+
QT_WAYLAND_DISABLE_WINDOWDECORATION: '1',
|
|
77
|
+
QT_QUICK_BACKEND: 'software',
|
|
78
|
+
},
|
|
79
|
+
stdout: 'ignore',
|
|
80
|
+
stderr: 'pipe',
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
const logs = new Response(child.stderr).text(),
|
|
84
|
+
title = () => land.compositor.wm.visible[0]?.surface.xdg.toplevel.title;
|
|
85
|
+
const until = async (fn) => {
|
|
86
|
+
for (let i = 0; i < 300; i++) {
|
|
87
|
+
if (await fn()) return;
|
|
88
|
+
if (child.exitCode !== null) throw Error(await logs);
|
|
89
|
+
await Bun.sleep(30);
|
|
90
|
+
}
|
|
91
|
+
throw Error('Quickshell state timed out: ' + title());
|
|
92
|
+
};
|
|
93
|
+
await until(() => title());
|
|
94
|
+
const mouse = async (type, x, y) => {
|
|
95
|
+
const surface = land.compositor.wm.renderSurfaces().find((s) => s.pixels);
|
|
96
|
+
const o = land.compositor.wm.origin(surface);
|
|
97
|
+
land.context.dispatchMouseEvent({
|
|
98
|
+
type,
|
|
99
|
+
x: o.x + x,
|
|
100
|
+
y: o.y + y,
|
|
101
|
+
button: 'left',
|
|
102
|
+
});
|
|
103
|
+
await Bun.sleep(40);
|
|
104
|
+
};
|
|
105
|
+
const click = async (x, y) => {
|
|
106
|
+
for (const type of ['mouseMoved', 'mousePressed', 'mouseReleased'])
|
|
107
|
+
await mouse(type, x, y);
|
|
108
|
+
};
|
|
109
|
+
const key = async (key, code, modifiers = 0) => {
|
|
110
|
+
for (const type of ['keyDown', 'keyUp'])
|
|
111
|
+
land.context.dispatchKeyEvent({ type, key, code, modifiers });
|
|
112
|
+
await Bun.sleep(80);
|
|
113
|
+
};
|
|
114
|
+
await callback({ land, title, until, mouse, click, key });
|
|
115
|
+
} finally {
|
|
116
|
+
if (child) {
|
|
117
|
+
child.kill();
|
|
118
|
+
await child.exited;
|
|
119
|
+
}
|
|
120
|
+
land?.close();
|
|
121
|
+
if (oldRuntime === undefined) delete process.env.XDG_RUNTIME_DIR;
|
|
122
|
+
else process.env.XDG_RUNTIME_DIR = oldRuntime;
|
|
123
|
+
rmSync(dir, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,rmSync} from 'node:fs';
|
|
3
|
+
import {launch} from '../frontends/wayland/index.js';
|
|
4
|
+
test.skipIf(process.env.BUNINU_TEST_WAYLAND!=='1')('HTML Ctrl-C/V, navigator and toolbar share Land clipboard even with host denied',async()=>{
|
|
5
|
+
const {start}=await import('../../buninu-land/compositor.js');
|
|
6
|
+
const dir=mkdtempSync('/tmp/html-clipboard-'),old=process.env.XDG_RUNTIME_DIR;
|
|
7
|
+
let land,app;const copied='Wayland 中文',writes=[];
|
|
8
|
+
const wait=async fn=>{for(let i=0;i<200;i++){if(await fn())return;await Bun.sleep(20);}throw Error('Clipboard did not settle');};
|
|
9
|
+
const server=Bun.serve({hostname:'127.0.0.1',port:0,fetch:()=>new Response(`<input id="field" value="Wayland 中文"><button id="read" style="position:absolute;top:50px;left:0;width:100px;height:30px" onclick="navigator.clipboard.readText().then(t=>globalThis.readResult=t)">Read</button>`,{headers:{'content-type':'text/html; charset=utf-8'}})});
|
|
10
|
+
const url=`http://127.0.0.1:${server.port}/`;
|
|
11
|
+
const run=s=>app.browser.context.evaluate(s);
|
|
12
|
+
const key=async(key,modifiers=2)=>{
|
|
13
|
+
for(const type of ['keyDown','keyUp'])land.context.dispatchKeyEvent({type,key,code:'Key'+key.toUpperCase(),modifiers});
|
|
14
|
+
await Bun.sleep(100);
|
|
15
|
+
};
|
|
16
|
+
try{
|
|
17
|
+
process.env.XDG_RUNTIME_DIR=dir;
|
|
18
|
+
land=await start(['--remote-debugging-port=0','--window-size=640,480']);
|
|
19
|
+
land.compositor.clipboard.host={write:async text=>{writes.push(text);throw Error('Android background denied');},read:async()=>{throw Error('Android background denied');},close(){}};
|
|
20
|
+
app=await launch({url,socket:dir+'/wayland-buninu'});
|
|
21
|
+
await wait(()=>app.client.clipboard.serial>0);
|
|
22
|
+
await run('document.getElementById("field").focus();document.getElementById("field").select()');
|
|
23
|
+
await key('c');await wait(()=>writes.length>0);expect(writes.at(-1)).toBe(copied);
|
|
24
|
+
await key('ArrowRight',0);await key('v');
|
|
25
|
+
await wait(async()=>await run('document.getElementById("field").value')===copied+copied);
|
|
26
|
+
app.close();await app.done;
|
|
27
|
+
await wait(()=>land.compositor.clipboard.selection?.cachedText);
|
|
28
|
+
app=await launch({url,socket:dir+'/wayland-buninu'});
|
|
29
|
+
await wait(()=>app.client.clipboard.selection>0);
|
|
30
|
+
await run('document.getElementById("field").focus();document.getElementById("field").select()');
|
|
31
|
+
await key('Backspace',0);await key('v');
|
|
32
|
+
await wait(async()=>await run('document.getElementById("field").value')===copied);
|
|
33
|
+
await app.browser.context.click(20,65);
|
|
34
|
+
await wait(async()=>await run('globalThis.readResult')===copied);
|
|
35
|
+
// URL bar uses the same selection and the same denied-host fallback.
|
|
36
|
+
app.toolbar.open(true);await key('v');await wait(()=>app.toolbar.editor.text===copied);
|
|
37
|
+
expect(land.compositor.clipboard.textCache.bytes.toString()).toBe(copied);
|
|
38
|
+
// Copy a partial URL-bar selection through the compositor, then insert it.
|
|
39
|
+
await key('ArrowRight',0);await key('ArrowLeft',8);await key('c');
|
|
40
|
+
await wait(()=>writes.at(-1)==='文');
|
|
41
|
+
await key('ArrowRight',0);await key('v');
|
|
42
|
+
await wait(()=>app.toolbar.editor.text===copied+'文');
|
|
43
|
+
// Pasting over a selection replaces it rather than appending twice.
|
|
44
|
+
await key('ArrowLeft',8);await key('v');
|
|
45
|
+
await wait(()=>app.toolbar.editor.text===copied+'文');
|
|
46
|
+
}finally{app?.close();land?.close();server.stop(true);if(old===undefined)delete process.env.XDG_RUNTIME_DIR;else process.env.XDG_RUNTIME_DIR=old;rmSync(dir,{recursive:true,force:true});}
|
|
47
|
+
},30000);
|
package/test/index.test.js
CHANGED
|
@@ -27,3 +27,16 @@ test("casty runs with npx when npx and bun exist, else bunx, else bun x", () =>
|
|
|
27
27
|
expect(castyCommand(["a"], (name) => name === "bun" ? "/bin/bun" : null)).toEqual(["/bin/bun", "x", ...casty, "a"]);
|
|
28
28
|
expect(castyCommand([], () => null)).toEqual([process.argv0, "x", ...casty]);
|
|
29
29
|
});
|
|
30
|
+
|
|
31
|
+
test("explicit Quickshell backends and their help work through the package entry", async () => {
|
|
32
|
+
for (const backend of ["wayland", "headless", "casty"]) {
|
|
33
|
+
const flag = `--quickshell-${backend}`;
|
|
34
|
+
expect(chooseFrontend([flag, "-p", "shell.qml", "--", "--help"]))
|
|
35
|
+
.toEqual({ frontend: flag.slice(2), args: ["-p", "shell.qml", "--", "--help"] });
|
|
36
|
+
const child = Bun.spawn([process.execPath, new URL('../index.js', import.meta.url).pathname, flag, '--help'], { stdout: 'pipe', stderr: 'pipe' });
|
|
37
|
+
const output = await new Response(child.stdout).text();
|
|
38
|
+
expect(await child.exited).toBe(0);
|
|
39
|
+
expect(output).toContain(flag);
|
|
40
|
+
expect(output).toContain('-p, --path');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// bun test/manual-casty-without-qml.js /path/to/stripped/buninu-browser
|
|
2
|
+
// Actual Casty PTY -> CDP -> browser. An isolated xclip/OSC52 store avoids touching
|
|
3
|
+
// the user's clipboard. A minimal Kitty peer acknowledges file/raw support.
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import {mkdtempSync,mkdirSync,writeFileSync,readFileSync,existsSync,unlinkSync} from 'node:fs';
|
|
6
|
+
import {join,resolve} from 'node:path';
|
|
7
|
+
const root=resolve(Bun.argv[2]||join(import.meta.dir,'..'));
|
|
8
|
+
const casty=resolve(import.meta.dir,'../../casty/index.js');
|
|
9
|
+
const dir=mkdtempSync('/tmp/casty-without-qml-');
|
|
10
|
+
const clip=join(dir,'clipboard'),endpointFile=join(dir,'cdp');
|
|
11
|
+
mkdirSync(join(dir,'bin'));mkdirSync(join(dir,'.casty'));
|
|
12
|
+
writeFileSync(join(dir,'.casty/config.json'),JSON.stringify({transport:'auto',format:'auto',media:false}));
|
|
13
|
+
writeFileSync(clip,'initial');
|
|
14
|
+
writeFileSync(join(dir,'bin/xclip'),`#!${process.execPath}\nconst p=process.env.TEST_CLIPBOARD_FILE;if(Bun.argv.includes('-o'))process.stdout.write(await Bun.file(p).text());else await Bun.write(p,await Bun.stdin.text());\n`,{mode:0o755});
|
|
15
|
+
writeFileSync(join(dir,'browser.js'),`import {writeFileSync} from 'node:fs';
|
|
16
|
+
const report=(text)=>{const start=String(text).indexOf('ws://');if(String(text).includes('DevTools listening on ')&&start>=0)writeFileSync(${JSON.stringify(endpointFile)},String(text).slice(start).trim())};
|
|
17
|
+
const error=console.error;console.error=(...args)=>{report(args.join(' '));error(...args)};
|
|
18
|
+
const write=process.stderr.write.bind(process.stderr);process.stderr.write=(text,...args)=>{report(text);return write(text,...args)};
|
|
19
|
+
const {main}=await import(${JSON.stringify(Bun.pathToFileURL(join(root,'buninu-browser.js')).href)});await main();`);
|
|
20
|
+
const html=`<!doctype html><meta charset="utf-8"><title>Casty editing probe</title><style>body{margin:16px;background:#eef4f7;font:18px sans-serif}input,textarea{display:block;width:400px;margin:12px 0;font:20px monospace}textarea{height:100px}button{height:44px;min-width:140px}</style><h1>Casty 中文</h1><input id="field" value="abc中文xyz"><textarea id="multi">abcd\nxy\n12345</textarea><button id="write">API copy</button><button id="read">API paste</button><canvas id="canvas" width="200" height="60"></canvas><script>const c=document.getElementById('canvas').getContext('2d');c.fillStyle='#16a6a0';c.fillRect(0,0,200,60);document.getElementById('write').onclick=async()=>{try{await navigator.clipboard.writeText('navigator 中文');globalThis.writeStatus='ok'}catch(e){globalThis.writeStatus=e.name}};document.getElementById('read').onclick=async()=>{try{globalThis.readResult=await navigator.clipboard.readText()}catch(e){globalThis.readResult=e.name}};</script>`;
|
|
21
|
+
const server=Bun.serve({hostname:'127.0.0.1',port:0,fetch:()=>new Response(html,{headers:{'content-type':'text/html; charset=utf-8'}})});
|
|
22
|
+
let proc,ws,terminalTail='',output='',frames=0,rawFrames=0,lastRaw,oscReads=0;
|
|
23
|
+
const wait=async(fn,label)=>{for(let i=0;i<400;i++){if(await fn())return;await Bun.sleep(25);}throw Error('Timed out: '+label+'\n'+output.slice(-1500))};
|
|
24
|
+
const replies=(t,data)=>{
|
|
25
|
+
const s=Buffer.from(data).toString();output=(output+s).slice(-16000);terminalTail+=s;
|
|
26
|
+
const re=/\x1b_G([\s\S]*?)\x1b\\|\x1b\]52;[^;]*;([^\x07]*)(?:\x07)|\x1b\[14t|\x1b\[\?u/g;let m,last=0;
|
|
27
|
+
while((m=re.exec(terminalTail))){last=re.lastIndex;
|
|
28
|
+
if(m[1]!==undefined){
|
|
29
|
+
const [header,payload='']=m[1].split(';');const fields=Object.fromEntries(header.split(',').map(v=>v.split('=')));
|
|
30
|
+
if(fields.a==='q')t.write(`\x1b_Gi=${fields.i};${fields.f==='1111970369'?'OK Buninu1 bgra':'OK'}\x1b\\`);
|
|
31
|
+
if(fields.a==='T'){
|
|
32
|
+
frames++;
|
|
33
|
+
if(fields.f==='1111970369'&&['f','t'].includes(fields.t)){
|
|
34
|
+
const path=Buffer.from(payload,'base64').toString();
|
|
35
|
+
try{lastRaw=readFileSync(path);assert.equal(lastRaw.length,Number(fields.s)*Number(fields.v)*4);rawFrames++;if(fields.t==='t')unlinkSync(path);}catch(e){output+='\nframe error '+e;}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}else if(m[2]!==undefined){
|
|
39
|
+
if(m[2]==='?'){oscReads++;t.write('\x1b]52;c;'+readFileSync(clip).toString('base64')+'\x07');}
|
|
40
|
+
else writeFileSync(clip,Buffer.from(m[2],'base64'));
|
|
41
|
+
}else if(m[0]==='\x1b[14t')t.write('\x1b[4;640;640t');
|
|
42
|
+
else if(m[0]==='\x1b[?u')t.write('\x1b[?0u');
|
|
43
|
+
}
|
|
44
|
+
if(last)terminalTail=terminalTail.slice(last);if(terminalTail.length>100000)terminalTail=terminalTail.slice(-10000);
|
|
45
|
+
};
|
|
46
|
+
try{
|
|
47
|
+
proc=Bun.spawn([process.execPath,casty,`http://127.0.0.1:${server.port}/`],{env:{...process.env,HOME:dir,PATH:join(dir,'bin')+':'+process.env.PATH,CASTY_BROWSER:join(dir,'browser.js'),TEST_CLIPBOARD_FILE:clip,TERM:'xterm-256color',TMUX:''},terminal:{cols:80,rows:32,data:replies}});
|
|
48
|
+
await wait(()=>existsSync(endpointFile),'CDP startup');
|
|
49
|
+
const endpoint=new URL(readFileSync(endpointFile,'utf8'));
|
|
50
|
+
let pages;await wait(async()=>{pages=await fetch('http://'+endpoint.host+'/json/list').then(r=>r.json());return pages.length>0},'Casty page target');
|
|
51
|
+
ws=new WebSocket(pages[0].webSocketDebuggerUrl);await new Promise((yes,no)=>{ws.onopen=yes;ws.onerror=no});
|
|
52
|
+
let id=0;const requests=new Map();ws.onmessage=e=>{const m=JSON.parse(e.data);const p=requests.get(m.id);if(p){requests.delete(m.id);m.error?p.reject(Error(JSON.stringify(m.error))):p.resolve(m.result)}};
|
|
53
|
+
const call=(method,params={})=>new Promise((resolve,reject)=>{const key=++id;const timer=setTimeout(()=>{requests.delete(key);reject(Error('CDP timeout: '+method+' '+JSON.stringify(params)))},8000);requests.set(key,{resolve:v=>{clearTimeout(timer);resolve(v)},reject:e=>{clearTimeout(timer);reject(e)}});ws.send(JSON.stringify({id:key,method,params}))});
|
|
54
|
+
const run=async expression=>{const r=await call('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw Error(JSON.stringify(r.exceptionDetails));return r.result?.value};
|
|
55
|
+
const key=async bytes=>{console.log('key',JSON.stringify(bytes));proc.terminal.write(bytes);await Bun.sleep(150)};
|
|
56
|
+
const click=async id=>{const r=await run("document.getElementById("+JSON.stringify(id)+").getBoundingClientRect().toJSON()");const col=Math.floor((r.x+r.width/2)/8)+1,row=Math.floor((r.y+r.height/2)/20)+2;await key('\x1b[<0;'+col+';'+row+'M');await key('\x1b[<0;'+col+';'+row+'m')};
|
|
57
|
+
const state=()=>run(`(()=>{const e=document.getElementById('field');return [e.value,e.selectionStart,e.selectionEnd]})()`);
|
|
58
|
+
await wait(async()=>await run("!!document.getElementById('field')"),'page loaded');
|
|
59
|
+
await wait(()=>rawFrames>0,'raw Kitty frame');console.log('page and raw frames ready');
|
|
60
|
+
await run("document.getElementById('field').focus()");
|
|
61
|
+
await key('\x01');assert.deepEqual((await state()).slice(1),[0,8]);
|
|
62
|
+
await key('\x1b[C');assert.deepEqual((await state()).slice(1),[8,8]);
|
|
63
|
+
await key('\x1b[1;2D');assert.deepEqual((await state()).slice(1),[7,8]);
|
|
64
|
+
await key('\x03');await wait(()=>readFileSync(clip,'utf8')==='z','Ctrl-C via browser xclip');
|
|
65
|
+
writeFileSync(clip,'貼上');await key('\x16');await wait(async()=>(await state())[0]==='abc中文xy貼上','Casty OSC52 paste');
|
|
66
|
+
assert.ok(oscReads>0);
|
|
67
|
+
await key('\x1b[1;2D');await key('\x18');await wait(async()=>(await state())[0]==='abc中文xy貼','Ctrl-X');assert.equal(readFileSync(clip,'utf8'),'上');
|
|
68
|
+
await run("document.getElementById('multi').focus();document.getElementById('multi').setSelectionRange(3,3)");
|
|
69
|
+
await key('\x1b[1;2B');assert.deepEqual(await run("[document.getElementById('multi').selectionStart,document.getElementById('multi').selectionEnd]"),[3,7]);
|
|
70
|
+
await click('write');
|
|
71
|
+
await wait(async()=>await run('globalThis.writeStatus')==='ok','navigator.writeText');assert.equal(readFileSync(clip,'utf8'),'navigator 中文');
|
|
72
|
+
await click('read');
|
|
73
|
+
await wait(async()=>await run('globalThis.readResult')==='navigator 中文','navigator.readText');
|
|
74
|
+
assert.equal(await run("navigator.clipboard.readText().then(()=> 'allowed',e=>e.name)"),'NotAllowedError');
|
|
75
|
+
await run("document.getElementById('field').focus();document.getElementById('field').setSelectionRange(1,1)");
|
|
76
|
+
await Bun.sleep(150);const a=await call('Page.captureScreenshot',{format:'png'});
|
|
77
|
+
await Bun.sleep(550);const b=await call('Page.captureScreenshot',{format:'png'});
|
|
78
|
+
assert.notEqual(a.data,b.data,'caret blink must change pixels');
|
|
79
|
+
writeFileSync(join(dir,'casty-page.png'),Buffer.from(a.data,'base64'));
|
|
80
|
+
const n=rawFrames;await run("document.body.style.background='#ffccdd'");await wait(()=>rawFrames>n,'updated terminal frame');
|
|
81
|
+
assert.ok(lastRaw.some(v=>v!==0));
|
|
82
|
+
console.log(JSON.stringify({root,dir,frames,rawFrames,oscReads,copyPaste:true,selection:true,caretMovement:true,caretBlink:true,navigatorReadWrite:true,rendering:true}));
|
|
83
|
+
await key('\x11');await Promise.race([proc.exited,Bun.sleep(2000)]);
|
|
84
|
+
}finally{
|
|
85
|
+
ws?.close();if(proc&&proc.exitCode===null)proc.kill();proc?.terminal?.close();server.stop(true);
|
|
86
|
+
writeFileSync(join(dir,'terminal.log'),output);
|
|
87
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Run untouched Flea source through the browser's Wayland frontend, with an
|
|
2
|
+
// isolated HOME/XDG state. Set FLEA_BIN to a real installed backend to exercise
|
|
3
|
+
// filesystem operations. This script does not provide backend stubs.
|
|
4
|
+
import { resolve, join } from 'node:path';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { launch } from '../frontends/wayland/index.js';
|
|
7
|
+
import { fleaSandbox } from './helpers/flea-sandbox.js';
|
|
8
|
+
if (process.argv.some((arg) => arg === '-h' || arg === '--help')) {
|
|
9
|
+
console.log(`Usage: bun test/manual-flea-application.js
|
|
10
|
+
|
|
11
|
+
Uses the current WAYLAND_DISPLAY / XDG_RUNTIME_DIR.
|
|
12
|
+
BUNINU_FLEA_ROOT Flea checkout (default ../flea)
|
|
13
|
+
BUNINU_OMARCHY_ROOT Omarchy checkout with shell/Commons and shell/Ui
|
|
14
|
+
(default ../omarchy-qml-reference)
|
|
15
|
+
FLEA_BIN Optional real Flea backend executable
|
|
16
|
+
|
|
17
|
+
Without FLEA_BIN, Flea shows its real backend-unavailable state. Settings,
|
|
18
|
+
path entry, keyboard help and the network form can still be exercised.
|
|
19
|
+
All application state is temporary and removed on exit.
|
|
20
|
+
No dependencies are installed by this script.`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const flea = resolve(process.env.BUNINU_FLEA_ROOT || '../flea'),
|
|
24
|
+
omarchy = resolve(
|
|
25
|
+
process.env.BUNINU_OMARCHY_ROOT || '../omarchy-qml-reference',
|
|
26
|
+
);
|
|
27
|
+
for (const path of [
|
|
28
|
+
join(flea, 'ui/boot/shell.qml'),
|
|
29
|
+
join(omarchy, 'shell/Commons/qmldir'),
|
|
30
|
+
join(omarchy, 'shell/Ui/qmldir'),
|
|
31
|
+
])
|
|
32
|
+
if (!existsSync(path)) throw Error('Missing source checkout: ' + path);
|
|
33
|
+
const sandbox = fleaSandbox(flea, omarchy);
|
|
34
|
+
let app;
|
|
35
|
+
try {
|
|
36
|
+
app = await launch({
|
|
37
|
+
url: Bun.pathToFileURL(join(flea, 'ui/boot/shell.qml')).href,
|
|
38
|
+
width: 1000,
|
|
39
|
+
height: 720,
|
|
40
|
+
});
|
|
41
|
+
console.log('Unmodified Flea QML; isolated state: ' + sandbox.dir);
|
|
42
|
+
console.log('Backend: ' + sandbox.values.FLEA_BIN);
|
|
43
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'])
|
|
44
|
+
process.on(signal, () => app.close());
|
|
45
|
+
await app.done;
|
|
46
|
+
} finally {
|
|
47
|
+
app?.close();
|
|
48
|
+
sandbox.close();
|
|
49
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Run an installed, unmodified Flea executable through our opt-in qs wrapper.
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { existsSync, mkdirSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { fleaSandbox } from './helpers/flea-sandbox.js';
|
|
5
|
+
if (process.argv.some((arg) => ['-h', '--help'].includes(arg))) {
|
|
6
|
+
console.log(
|
|
7
|
+
`Usage: FLEA_BIN=/path/to/flea BUNINU_FLEA_ROOT=/path/to/share/flea bun test/manual-flea-binary.js\n\nUses the current Wayland socket. BUNINU_OMARCHY_ROOT defaults to ../omarchy-qml-reference.\nLaunches the actual binary with --gui; a temporary qs symlink selects lib/qml/qswayland through PATH.\nCreates an isolated HOME/XDG state with sample files; removes it on exit.\nNo dependencies are installed. The native Quickshell executable is not replaced.`,
|
|
8
|
+
);
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
const binary = process.env.FLEA_BIN && resolve(process.env.FLEA_BIN);
|
|
12
|
+
const root = resolve(process.env.BUNINU_FLEA_ROOT || '../flea');
|
|
13
|
+
const omarchy = resolve(
|
|
14
|
+
process.env.BUNINU_OMARCHY_ROOT || '../omarchy-qml-reference',
|
|
15
|
+
);
|
|
16
|
+
for (const path of [
|
|
17
|
+
binary,
|
|
18
|
+
join(root, 'ui/boot/shell.qml'),
|
|
19
|
+
join(omarchy, 'shell/Commons/qmldir'),
|
|
20
|
+
join(omarchy, 'shell/Ui/qmldir'),
|
|
21
|
+
])
|
|
22
|
+
if (!path || !existsSync(path))
|
|
23
|
+
throw Error('Missing binary or QML imports: ' + path);
|
|
24
|
+
const sandbox = fleaSandbox(root, omarchy);
|
|
25
|
+
const wrapperDir = join(sandbox.dir, 'bin');
|
|
26
|
+
mkdirSync(wrapperDir);
|
|
27
|
+
symlinkSync(resolve('lib/qml/qswayland'), join(wrapperDir, 'qs'));
|
|
28
|
+
mkdirSync(join(sandbox.values.HOME, 'Documents'));
|
|
29
|
+
writeFileSync(
|
|
30
|
+
join(sandbox.values.HOME, 'hello.txt'),
|
|
31
|
+
'Official Flea backend, Buninu QML frontend.\n',
|
|
32
|
+
);
|
|
33
|
+
writeFileSync(
|
|
34
|
+
join(sandbox.values.HOME, 'Documents/inside.txt'),
|
|
35
|
+
'Directory navigation test.\n',
|
|
36
|
+
);
|
|
37
|
+
let child;
|
|
38
|
+
const stop = () => child?.kill();
|
|
39
|
+
try {
|
|
40
|
+
child = Bun.spawn([binary, '--gui', sandbox.values.HOME], {
|
|
41
|
+
env: {
|
|
42
|
+
...process.env,
|
|
43
|
+
PATH: wrapperDir + ':' + process.env.PATH,
|
|
44
|
+
FLEA_UI: join(root, 'ui'),
|
|
45
|
+
QSG_RHI_BACKEND: 'opengl',
|
|
46
|
+
},
|
|
47
|
+
stdin: 'inherit',
|
|
48
|
+
stdout: 'inherit',
|
|
49
|
+
stderr: 'inherit',
|
|
50
|
+
});
|
|
51
|
+
console.log(
|
|
52
|
+
'Official Flea -> qs (temporary symlink) -> Buninu qswayland. Temporary HOME: ' + sandbox.values.HOME,
|
|
53
|
+
);
|
|
54
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'])
|
|
55
|
+
process.on(signal, stop);
|
|
56
|
+
process.exitCode = await child.exited;
|
|
57
|
+
} finally {
|
|
58
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'])
|
|
59
|
+
process.off(signal, stop);
|
|
60
|
+
child?.kill();
|
|
61
|
+
sandbox.close();
|
|
62
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Manual, opt-in Wayland demo. Uses real QML imports, not injected JS globals.
|
|
2
|
+
import {launch} from '../frontends/wayland/index.js';
|
|
3
|
+
const url=new URL('./fixtures/qml/flea/demo.qml',import.meta.url).href;
|
|
4
|
+
const app=await launch({url,width:640,height:400});
|
|
5
|
+
try {
|
|
6
|
+
console.log('Flea EscapeHint: real component imports and a local test Theme.qml singleton (not full Flea).');
|
|
7
|
+
for(const signal of ['SIGINT','SIGTERM','SIGHUP'])process.on(signal,app.close);
|
|
8
|
+
await app.done;
|
|
9
|
+
}finally{app.close();}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Real upstream Theme, with isolated sample user data. No Flea/Commons stubs.
|
|
2
|
+
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { launch } from '../frontends/wayland/index.js';
|
|
6
|
+
const fixture = new URL('./fixtures/qml/flea-theme/', import.meta.url);
|
|
7
|
+
const home = mkdtempSync(join(tmpdir(), 'flea-settings-demo-'));
|
|
8
|
+
const theme = join(home, '.local/state/omarchy/current/theme');
|
|
9
|
+
mkdirSync(theme, { recursive: true });
|
|
10
|
+
await Bun.write(
|
|
11
|
+
join(theme, 'colors.toml'),
|
|
12
|
+
'background = "#101315"\nforeground = "#eeeeee"\naccent = "#70e1e8"\nred = "#ff3333"\n',
|
|
13
|
+
);
|
|
14
|
+
await Bun.write(join(theme, 'shell.toml'), '[font]\nbase-size = 14\n');
|
|
15
|
+
await Bun.write(join(theme, '../theme.name'), 'demo');
|
|
16
|
+
const oldHome = process.env.HOME,
|
|
17
|
+
oldImports = process.env.QML_IMPORT_PATH;
|
|
18
|
+
process.env.HOME = home;
|
|
19
|
+
process.env.QML_IMPORT_PATH = Bun.fileURLToPath(new URL('imports/', fixture));
|
|
20
|
+
let app;
|
|
21
|
+
try {
|
|
22
|
+
app = await launch({
|
|
23
|
+
url: new URL('settings.qml', fixture).href,
|
|
24
|
+
width: 800,
|
|
25
|
+
height: 420,
|
|
26
|
+
});
|
|
27
|
+
console.log(
|
|
28
|
+
'Real Flea SettingsRuler + Theme; isolated sample data: ' + home,
|
|
29
|
+
);
|
|
30
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'])
|
|
31
|
+
process.on(signal, () => app.close());
|
|
32
|
+
await app.done;
|
|
33
|
+
} finally {
|
|
34
|
+
app?.close();
|
|
35
|
+
if (oldHome === undefined) delete process.env.HOME;
|
|
36
|
+
else process.env.HOME = oldHome;
|
|
37
|
+
if (oldImports === undefined) delete process.env.QML_IMPORT_PATH;
|
|
38
|
+
else process.env.QML_IMPORT_PATH = oldImports;
|
|
39
|
+
rmSync(home, { recursive: true, force: true });
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Real upstream Theme, with isolated sample user data. No Flea/Commons stubs.
|
|
2
|
+
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { launch } from '../frontends/wayland/index.js';
|
|
6
|
+
const fixture = new URL('./fixtures/qml/flea-theme/', import.meta.url);
|
|
7
|
+
const home = mkdtempSync(join(tmpdir(), 'flea-theme-demo-'));
|
|
8
|
+
const theme = join(home, '.local/state/omarchy/current/theme');
|
|
9
|
+
mkdirSync(theme, { recursive: true });
|
|
10
|
+
await Bun.write(
|
|
11
|
+
join(theme, 'colors.toml'),
|
|
12
|
+
'background = "#101315"\nforeground = "#eeeeee"\naccent = "#70e1e8"\nred = "#ff3333"\n',
|
|
13
|
+
);
|
|
14
|
+
await Bun.write(join(theme, 'shell.toml'), '[font]\nbase-size = 14\n');
|
|
15
|
+
await Bun.write(join(theme, '../theme.name'), 'demo');
|
|
16
|
+
const oldHome = process.env.HOME,
|
|
17
|
+
oldImports = process.env.QML_IMPORT_PATH;
|
|
18
|
+
process.env.HOME = home;
|
|
19
|
+
process.env.QML_IMPORT_PATH = Bun.fileURLToPath(new URL('imports/', fixture));
|
|
20
|
+
let app;
|
|
21
|
+
try {
|
|
22
|
+
app = await launch({
|
|
23
|
+
url: new URL('demo.qml', fixture).href,
|
|
24
|
+
width: 800,
|
|
25
|
+
height: 900,
|
|
26
|
+
});
|
|
27
|
+
console.log(
|
|
28
|
+
'Real Flea Theme + Omarchy Commons; isolated sample data: ' + home,
|
|
29
|
+
);
|
|
30
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'])
|
|
31
|
+
process.on(signal, () => app.close());
|
|
32
|
+
await app.done;
|
|
33
|
+
} finally {
|
|
34
|
+
app?.close();
|
|
35
|
+
if (oldHome === undefined) delete process.env.HOME;
|
|
36
|
+
else process.env.HOME = oldHome;
|
|
37
|
+
if (oldImports === undefined) delete process.env.QML_IMPORT_PATH;
|
|
38
|
+
else process.env.QML_IMPORT_PATH = oldImports;
|
|
39
|
+
rmSync(home, { recursive: true, force: true });
|
|
40
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Run explicitly: bun test/manual-qml-removal.js
|
|
2
|
+
// Test the ordinary browser after physically removing every QML integration.
|
|
3
|
+
import { cpSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const source = Bun.fileURLToPath(new URL('../', import.meta.url));
|
|
8
|
+
const copy = mkdtempSync(join(tmpdir(), 'browser-without-qml-'));
|
|
9
|
+
const files = [
|
|
10
|
+
'frontends/wayland/index.js',
|
|
11
|
+
'lib/document-types.js',
|
|
12
|
+
'lib/headless-shell.js',
|
|
13
|
+
'lib/renderer/page-renderer.js',
|
|
14
|
+
'lib/renderer/canvas-2d.js',
|
|
15
|
+
'lib/render-tree/index.js',
|
|
16
|
+
'lib/renderer/host.js',
|
|
17
|
+
'lib/renderer/main.js',
|
|
18
|
+
];
|
|
19
|
+
try {
|
|
20
|
+
for (const entry of readdirSync(source, { withFileTypes: true })) {
|
|
21
|
+
if (entry.name.startsWith('.')) continue;
|
|
22
|
+
const from = join(source, entry.name), to = join(copy, entry.name);
|
|
23
|
+
if (entry.isDirectory() && !['lib', 'test', 'frontends'].includes(entry.name)) symlinkSync(from, to, 'dir');
|
|
24
|
+
else cpSync(from, to, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
let blocks = 0;
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const path = join(copy, file);
|
|
29
|
+
const stripped = readFileSync(path, 'utf8').replace(
|
|
30
|
+
/^[ \t]*\/\/ qml handle begin:[^\n]*\n[\s\S]*?^[ \t]*\/\/ qml handle end[^\n]*(?:\n|$)/gm,
|
|
31
|
+
() => { blocks++; return ''; },
|
|
32
|
+
);
|
|
33
|
+
if (/qml/i.test(stripped)) throw Error(`QML reference remains in ${file}`);
|
|
34
|
+
writeFileSync(path, stripped);
|
|
35
|
+
}
|
|
36
|
+
rmSync(join(copy, 'lib/qml'), { recursive: true });
|
|
37
|
+
console.log(`Removed ${blocks} integration blocks and lib/qml in ${copy}`);
|
|
38
|
+
const child = Bun.spawn([
|
|
39
|
+
process.execPath, 'test',
|
|
40
|
+
'test/renderer-host.test.js', 'test/navigation.test.js', 'test/headless-shell.test.js',
|
|
41
|
+
], { cwd: copy, stdout: 'inherit', stderr: 'inherit' });
|
|
42
|
+
const code = await child.exited;
|
|
43
|
+
if (code) throw Error(`Browser regression tests exited ${code}`);
|
|
44
|
+
} finally {
|
|
45
|
+
rmSync(copy, { recursive: true, force: true });
|
|
46
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Run from the browser checkout: bun test/manual-without-qml.js
|
|
2
|
+
// Retains an isolated copy, logs and screenshots for review. No dependencies installed.
|
|
3
|
+
import {cpSync,existsSync,mkdtempSync,readFileSync,readdirSync,rmSync,symlinkSync,writeFileSync} from 'node:fs';
|
|
4
|
+
import {join,relative,resolve} from 'node:path';
|
|
5
|
+
import {tmpdir} from 'node:os';
|
|
6
|
+
const original=resolve(import.meta.dir,'..');
|
|
7
|
+
const dir=mkdtempSync(join(tmpdir(),'browser-without-qml-'));
|
|
8
|
+
const copy=join(dir,'buninu-browser');
|
|
9
|
+
cpSync(original,copy,{recursive:true,filter:path=>!['.git','node_modules'].includes(relative(original,path).split('/')[0])});
|
|
10
|
+
let blocks=0,changed=0;
|
|
11
|
+
function strip(dir){
|
|
12
|
+
for(const entry of readdirSync(dir,{withFileTypes:true})){
|
|
13
|
+
const path=join(dir,entry.name);
|
|
14
|
+
if(entry.isDirectory()){strip(path);continue;}
|
|
15
|
+
if(!entry.isFile()||!entry.name.endsWith('.js'))continue;
|
|
16
|
+
const source=readFileSync(path,'utf8');let depth=0,count=0;const lines=[];
|
|
17
|
+
for(const line of source.split('\n')){
|
|
18
|
+
if(/^\s*\/\/ qml handle begin\b/.test(line)){depth++;count++;continue;}
|
|
19
|
+
if(/^\s*\/\/ qml handle end\b/.test(line)){if(--depth<0)throw Error('Unmatched end: '+path);continue;}
|
|
20
|
+
if(!depth)lines.push(line);
|
|
21
|
+
}
|
|
22
|
+
if(depth)throw Error('Unclosed handle: '+path);
|
|
23
|
+
if(count){writeFileSync(path,lines.join('\n'));blocks+=count;changed++;}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
strip(copy);
|
|
27
|
+
rmSync(join(copy,'lib/qml'),{recursive:true});
|
|
28
|
+
if(existsSync(join(copy,'lib/qml')))throw Error('QML directory still exists');
|
|
29
|
+
// Only the test compositor is external; every browser module comes from the copy.
|
|
30
|
+
symlinkSync(resolve(original,'../buninu-land'),join(dir,'buninu-land'),'dir');
|
|
31
|
+
console.log(JSON.stringify({dir,copy,removedBlocks:blocks,changedFiles:changed,qmlDirectoryRemoved:true}));
|
|
32
|
+
async function command(args,cwd,log){
|
|
33
|
+
const out=Bun.file(join(dir,log));
|
|
34
|
+
const p=Bun.spawn([process.execPath,...args],{cwd,env:{...process.env,BUNINU_TEST_WAYLAND:'1'},stdout:out,stderr:out});
|
|
35
|
+
const code=await p.exited;console.log(log,code);if(code!==0)throw Error('Failed: '+log);
|
|
36
|
+
}
|
|
37
|
+
await command(['test','test/text-input.test.js','test/text-input-pointer.test.js','test/text-input-scroll.test.js','test/clipboard.test.js','test/html-clipboard-wayland.test.js','test/wayland-urlbar.test.js','test/wayland-frontend.test.js'],copy,'tests.log');
|
|
38
|
+
// Deterministic rendered fixture: compare full PNG bytes with and without QML.
|
|
39
|
+
const fixture=`<!doctype html><meta charset="utf-8"><title>HTML rendering without QML</title><style>body{margin:24px;background:#eef4f7;color:#183440;font:18px sans-serif}input,textarea{display:block;box-sizing:border-box;width:440px;margin:12px 0;padding:8px;border:1px solid #65899b;font:18px monospace}textarea{height:140px}canvas{border:1px solid #65899b}</style><h1>一般網頁 · HTML</h1><p>中文排版、選取、游標與 Canvas</p><input id="field" value="abc 中文 😀 xyz"><textarea>第一行:Hello\n第二行:選取與貼上\n第三行:12345</textarea><canvas id="picture" width="220" height="70"></canvas><script>const c=document.getElementById('picture').getContext('2d');c.fillStyle='#19a7a0';c.fillRect(0,0,220,70);c.fillStyle='white';c.font='20px sans-serif';c.fillText('Canvas 正常',12,42);const f=document.getElementById('field');f.focus();f.setSelectionRange(4,6);</script>`;
|
|
40
|
+
const fixturePath=join(dir,'fixture.html');writeFileSync(fixturePath,fixture);
|
|
41
|
+
await command(['index.js','--headless','--window-size=640,480','--screenshot='+join(dir,'original.png'),Bun.pathToFileURL(fixturePath).href],original,'original-render.log');
|
|
42
|
+
await command(['index.js','--headless','--window-size=640,480','--screenshot='+join(dir,'without-qml.png'),Bun.pathToFileURL(fixturePath).href],copy,'without-qml-render.log');
|
|
43
|
+
const before=readFileSync(join(dir,'original.png')),after=readFileSync(join(dir,'without-qml.png'));
|
|
44
|
+
if(!before.equals(after))throw Error('Rendering differs: compare screenshots in '+dir);
|
|
45
|
+
console.log('PASS: HTML tests and byte-identical rendered PNG; artifacts retained at '+dir);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
for (const mode of ['local', 'html', 'remote']) test(`DOM canvas snapshots: ${mode}`, async () => {
|
|
4
|
+
const child = Bun.spawn([process.execPath, new URL('./fixtures/qml-canvas-dom.js', import.meta.url).pathname, mode],
|
|
5
|
+
{ stdout: 'pipe', stderr: 'pipe' });
|
|
6
|
+
const [exit, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]);
|
|
7
|
+
expect({ exit, stdout, stderr }).toEqual({ exit: 0, stdout: '', stderr: '' });
|
|
8
|
+
}, 30000);
|
|
9
|
+
|
|
10
|
+
// Native ownership must be released on both painting and argument failures.
|
|
11
|
+
import { captureQmlDomImage, drawQmlDomImage } from '../lib/qml/canvas-dom.js';
|
|
12
|
+
test('DOM snapshot surfaces and images are released on success and exceptions', () => {
|
|
13
|
+
const document = {};
|
|
14
|
+
const element = { nodeType: 1, ownerDocument: document, isConnected: true, contains: target => target === element };
|
|
15
|
+
const root = { id: 1, domNode: element, children: [], style: {} };
|
|
16
|
+
let disposed = 0, deleted = 0, drawn = 0;
|
|
17
|
+
const options = {
|
|
18
|
+
document, tree: { root, nodesById: new Map([[1, root]]) },
|
|
19
|
+
layout: { boxes: [{ nodeId: 1, x: 0, y: 0, width: 10, height: 10, style: {} }], fragments: [], width: 10, height: 10 },
|
|
20
|
+
resources: new Map(), isOriginClean: () => true, paint: () => {},
|
|
21
|
+
kit: { TRANSPARENT: 0, MakeSurface: () => ({ getCanvas: () => ({ clear() {} }), flush() {},
|
|
22
|
+
makeImageSnapshot: () => ({ delete() { deleted++; } }), dispose() { disposed++; } }) },
|
|
23
|
+
};
|
|
24
|
+
const first = captureQmlDomImage(element, options);
|
|
25
|
+
expect(disposed).toBe(1);expect(deleted).toBe(0);
|
|
26
|
+
expect(drawQmlDomImage(first, {}, { drawImage() { drawn++; } }, [element, 0, 0])).toBe(true);
|
|
27
|
+
expect(drawn).toBe(1);expect(deleted).toBe(1);
|
|
28
|
+
expect(drawQmlDomImage(first, {}, {}, [])).toBe(false);
|
|
29
|
+
const second = captureQmlDomImage(element, options);
|
|
30
|
+
expect(() => drawQmlDomImage(second, {}, { drawImage() { throw Error('invalid arguments'); } }, [])).toThrow('invalid arguments');
|
|
31
|
+
expect(deleted).toBe(2);
|
|
32
|
+
const third = captureQmlDomImage(element, options);
|
|
33
|
+
expect(() => drawQmlDomImage(third, element, {}, [])).toThrow('contains the destination');
|
|
34
|
+
expect(deleted).toBe(3);
|
|
35
|
+
expect(() => captureQmlDomImage(element, { ...options, paint() { throw Error('paint failed'); } })).toThrow('paint failed');
|
|
36
|
+
expect(disposed).toBe(4);expect(deleted).toBe(3);
|
|
37
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {attachQmlClipboard} from '../lib/qml/wayland-clipboard.js';
|
|
3
|
+
test('QML clipboard endpoint is lazy, reused and closed; HTML navigation needs none',async()=>{
|
|
4
|
+
let finish;const done=new Promise(r=>finish=r),scripts=[],visits=[];
|
|
5
|
+
const navigate=async url=>visits.push(url);
|
|
6
|
+
const browser={context:{navigate,addInitScript:async s=>scripts.push(s)}};
|
|
7
|
+
const client={clipboard:{read:async()=> 'text',write:async()=>{}}};
|
|
8
|
+
try{
|
|
9
|
+
await attachQmlClipboard(client,browser,done);
|
|
10
|
+
await browser.context.navigate('about:blank');
|
|
11
|
+
await browser.context.navigate('https://example.test/');
|
|
12
|
+
expect(scripts.length).toBe(0);
|
|
13
|
+
await browser.context.navigate('file:///tmp/app.qml');
|
|
14
|
+
expect(scripts.length).toBe(1);
|
|
15
|
+
await browser.context.navigate('file:///tmp/other.qml');
|
|
16
|
+
expect(scripts.length).toBe(1);
|
|
17
|
+
const endpoint=JSON.parse(scripts[0].match(/setClipboardEndpoint\((.*)\);/)[1]);
|
|
18
|
+
const response=await fetch(endpoint,{method:'POST',body:JSON.stringify({operation:'read'})});
|
|
19
|
+
expect(await response.json()).toEqual({text:'text'});
|
|
20
|
+
expect(visits.length).toBe(4);
|
|
21
|
+
finish();await Bun.sleep(5);
|
|
22
|
+
expect(browser.context.navigate).toBe(navigate);
|
|
23
|
+
await expect(fetch(endpoint,{method:'POST',body:'{}'})).rejects.toThrow();
|
|
24
|
+
}finally{finish();}
|
|
25
|
+
});
|