buninu-browser 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/jsmdcui.txt +22 -0
- package/README.md +40 -4
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +152 -0
- package/frontends/wayland/casty/LICENSE +21 -0
- package/frontends/wayland/casty/bookmarks.js +32 -0
- package/frontends/wayland/casty/click.js +96 -0
- package/frontends/wayland/casty/commands.js +61 -0
- package/frontends/wayland/casty/config.js +45 -0
- package/frontends/wayland/casty/download-directory.js +28 -0
- package/frontends/wayland/casty/download.js +49 -0
- package/frontends/wayland/casty/hints.js +334 -0
- package/frontends/wayland/casty/keys.js +56 -0
- package/frontends/wayland/casty/page-commands.js +80 -0
- package/frontends/wayland/casty/prompt-width.js +148 -0
- package/frontends/wayland/casty/urlbar.js +606 -0
- package/frontends/wayland/client.js +88 -0
- package/frontends/wayland/clipboard.js +80 -0
- package/frontends/wayland/index.js +130 -0
- package/frontends/wayland/input.js +75 -0
- package/frontends/wayland/protocol.js +55 -0
- package/frontends/wayland/protocols/wayland.xml +3248 -0
- package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
- package/frontends/wayland/transport.js +120 -0
- package/frontends/wayland/urlbar.js +162 -0
- package/index.js +28 -1
- package/lib/audio/shared-connection.js +31 -0
- package/lib/cdp-server.js +15 -2
- package/lib/clipboard-command.js +10 -0
- package/lib/clipboard-powershell.js +22 -0
- package/lib/clipboard.js +37 -2
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +54 -12
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +6 -1
- package/lib/qml/README.md +1427 -0
- package/lib/qml/builtins.js +771 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/canvas.js +76 -0
- package/lib/qml/document.js +1199 -0
- package/lib/qml/fonts.js +69 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +352 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +521 -0
- package/lib/qml/media-renderer.js +49 -0
- package/lib/qml/media.js +86 -0
- package/lib/qml/models.js +622 -0
- package/lib/qml/parser.js +381 -0
- package/lib/qml/qs +5 -0
- package/lib/qml/qscasty +3 -0
- package/lib/qml/qsheadless +3 -0
- package/lib/qml/qswayland +3 -0
- package/lib/qml/reactive.js +182 -0
- package/lib/qml/runtime.js +3603 -0
- package/lib/qml/shader/fragment.js +83 -0
- package/lib/qml/shader/qsb.js +27 -0
- package/lib/qml/shader/renderer.js +39 -0
- package/lib/qml/shader/runtime.js +61 -0
- package/lib/qml/shapes.js +251 -0
- package/lib/qml/shortcuts.js +65 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +126 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/window-constraints.js +16 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +16 -1
- package/lib/renderer/canvas-2d.js +29 -0
- package/lib/renderer/document-selection.js +19 -0
- package/lib/renderer/host.js +45 -17
- package/lib/renderer/main.js +21 -6
- package/lib/renderer/page-realm.js +20 -10
- package/lib/renderer/page-renderer.js +497 -42
- package/lib/renderer/pointer-move.js +5 -3
- package/lib/renderer/render-interval.js +9 -0
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +10 -4
- package/package.json +1 -1
- package/test/audio-connection-lifetime.test.js +46 -0
- package/test/canvas-paint.test.js +8 -0
- package/test/clipboard.test.js +19 -0
- package/test/computed-style.test.js +12 -0
- package/test/document-selection.test.js +27 -0
- package/test/double-click.test.js +85 -0
- package/test/fixtures/canvas-damage.js +40 -0
- package/test/fixtures/fake-renderer.js +1 -0
- package/test/fixtures/flea-helpers/Dockerfile +36 -0
- package/test/fixtures/flea-helpers/bat.mjs +43 -0
- package/test/fixtures/flea-helpers/bin/bat +3 -0
- package/test/fixtures/flea-helpers/bin/bwrap +10 -0
- package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
- package/test/fixtures/flea-helpers/bin/glow +3 -0
- package/test/fixtures/flea-helpers/bin/qs +3 -0
- package/test/fixtures/flea-helpers/bin/unsupported +3 -0
- package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
- package/test/fixtures/flea-helpers/check-media.mjs +29 -0
- package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
- package/test/fixtures/flea-helpers/check.mjs +36 -0
- package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
- package/test/fixtures/flea-helpers/source-mime.xml +28 -0
- package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
- package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
- package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
- package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
- package/test/fixtures/omastorm-helpers/check.mjs +56 -0
- package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
- package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
- package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
- package/test/fixtures/pointer-paint-batching.js +23 -0
- package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea/LICENSE +21 -0
- package/test/fixtures/qml/flea/README.md +25 -0
- package/test/fixtures/qml/flea/Theme.qml +8 -0
- package/test/fixtures/qml/flea/demo.qml +24 -0
- package/test/fixtures/qml/flea/qmldir +3 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
- package/test/fixtures/qml/flea-theme/README.md +70 -0
- package/test/fixtures/qml/flea-theme/button.qml +17 -0
- package/test/fixtures/qml/flea-theme/demo.qml +27 -0
- package/test/fixtures/qml/flea-theme/field.qml +21 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
- package/test/fixtures/qml/flea-theme/segment.qml +17 -0
- package/test/fixtures/qml/flea-theme/settings.qml +58 -0
- package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
- package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
- package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
- package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
- package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
- package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
- package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
- package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
- package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
- package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
- package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
- package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
- package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
- package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
- package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
- package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
- package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
- package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
- package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
- package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
- package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
- package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
- package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
- package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
- package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
- package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
- package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
- package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
- package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
- package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
- package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
- package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
- package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
- package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
- package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
- package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
- package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
- package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
- package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
- package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
- package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
- package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
- package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
- package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
- package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
- package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
- package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
- package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
- package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
- package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
- package/test/fixtures/qml/hello.qml +26 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
- package/test/fixtures/qml/imports/components/Badge.qml +15 -0
- package/test/fixtures/qml/imports/logic/base.js +2 -0
- package/test/fixtures/qml/imports/logic/counter.js +4 -0
- package/test/fixtures/qml/imports/logic/math.js +3 -0
- package/test/fixtures/qml/imports/logic/shared.js +3 -0
- package/test/fixtures/qml/imports/main.qml +21 -0
- package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
- package/test/fixtures/qml/imports/theme/qmldir +2 -0
- package/test/fixtures/qml/multiply.qml +61 -0
- package/test/fixtures/qml-canvas-dom.js +90 -0
- package/test/fixtures/qml-fonts/main.qml +10 -0
- package/test/fixtures/qml-locale/check.mjs +6 -0
- package/test/fixtures/qml-locale/main.qml +8 -0
- package/test/fixtures/qml-media-audio.js +39 -0
- package/test/fixtures/qml-shader/grab.qml +14 -0
- package/test/fixtures/qml-shader/gradient.frag +5 -0
- package/test/fixtures/qml-shader/main.qml +14 -0
- package/test/fixtures/qml-shader/texture-grab.qml +25 -0
- package/test/fixtures/qml-shader/texture-size.frag +5 -0
- package/test/fixtures/qml-shader-check.mjs +24 -0
- package/test/fixtures/title-no-paint.js +20 -0
- package/test/helpers/flea-sandbox.js +48 -0
- package/test/helpers/quickshell-fixture.js +125 -0
- package/test/html-clipboard-wayland.test.js +47 -0
- package/test/index.test.js +13 -0
- package/test/manual-casty-without-qml.js +87 -0
- package/test/manual-double-click.html +21 -0
- package/test/manual-flea-application.js +49 -0
- package/test/manual-flea-binary.js +62 -0
- package/test/manual-flea-qml.js +9 -0
- package/test/manual-flea-settings.js +40 -0
- package/test/manual-flea-theme.js +40 -0
- package/test/manual-mouse-buttons.html +18 -0
- package/test/manual-omastorm-engine.mjs +19 -0
- package/test/manual-omastorm-shaders.mjs +28 -0
- package/test/manual-omastorm-software.mjs +25 -0
- package/test/manual-pointer-scroll.html +43 -0
- package/test/manual-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/pointer-drag.test.js +104 -0
- package/test/qml-canvas-dom.test.js +37 -0
- package/test/qml-canvas.test.js +21 -0
- package/test/qml-clipboard-bridge.test.js +25 -0
- package/test/qml-clipboard-wayland.test.js +40 -0
- package/test/qml-connections-timing.test.js +95 -0
- package/test/qml-controls-window.test.js +182 -0
- package/test/qml-dynamic.test.js +401 -0
- package/test/qml-flea-app.test.js +146 -0
- package/test/qml-flea-backend.test.js +78 -0
- package/test/qml-flea-columns-backend.test.js +38 -0
- package/test/qml-flea-compile.test.js +78 -0
- package/test/qml-flea-launcher.test.js +88 -0
- package/test/qml-flea-markdown-backend.test.js +31 -0
- package/test/qml-flea-media.test.js +28 -0
- package/test/qml-flea-navigation-backend.test.js +35 -0
- package/test/qml-flea-navigation-timing.test.js +147 -0
- package/test/qml-flea-preview-backend.test.js +53 -0
- package/test/qml-flea-previews.test.js +163 -0
- package/test/qml-flea-rows.test.js +101 -0
- package/test/qml-flea-textures.test.js +59 -0
- package/test/qml-flea-view-switch.test.js +33 -0
- package/test/qml-flea-wayland.test.js +68 -0
- package/test/qml-flea.test.js +34 -0
- package/test/qml-fonts.test.js +31 -0
- package/test/qml-grid-multiline.test.js +48 -0
- package/test/qml-host.test.js +238 -0
- package/test/qml-imports.test.js +102 -0
- package/test/qml-inherited-handlers.test.js +51 -0
- package/test/qml-input-selection.test.js +66 -0
- package/test/qml-interactions.test.js +309 -0
- package/test/qml-language.test.js +167 -0
- package/test/qml-launcher.test.js +54 -0
- package/test/qml-layout-text.test.js +205 -0
- package/test/qml-locale.test.js +45 -0
- package/test/qml-media.test.js +63 -0
- package/test/qml-nonvisual-visible.test.js +19 -0
- package/test/qml-persisted-delegates.test.js +45 -0
- package/test/qml-quickshell-controls.test.js +65 -0
- package/test/qml-quickshell-settings.test.js +62 -0
- package/test/qml-repeater.test.js +72 -0
- package/test/qml-shader-runtime.test.js +6 -0
- package/test/qml-shortcuts.test.js +39 -0
- package/test/qml-socket.test.js +60 -0
- package/test/qml-software-shader.test.js +55 -0
- package/test/qml-style-updates.test.js +124 -0
- package/test/qml-textures.test.js +66 -0
- package/test/qml-views.test.js +332 -0
- package/test/qml-window-update.test.js +36 -0
- package/test/qml-worker.test.js +69 -0
- package/test/qml.test.js +83 -0
- package/test/render-interval.test.js +33 -0
- package/test/renderer-host.test.js +11 -0
- package/test/text-input-pointer.test.js +31 -0
- package/test/text-input-scroll.test.js +29 -0
- package/test/text-input.test.js +47 -0
- package/test/title-no-paint.test.js +6 -0
- package/test/wayland-frontend.test.js +83 -0
- package/test/wayland-scroll.test.js +40 -0
- package/test/wayland-urlbar.test.js +61 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {strict as assert} from 'node:assert';
|
|
2
|
+
import {createPageRenderer} from '../../lib/renderer/page-renderer.js';
|
|
3
|
+
const traces=[];
|
|
4
|
+
const r=await createPageRenderer({trace:s=>traces.push(s)});
|
|
5
|
+
try{
|
|
6
|
+
r.setScripting('host');
|
|
7
|
+
await r.loadDocument({url:'https://title.test/',contentType:'text/html',source:`<title>Initial</title><p>Before</p><script>globalThis.changeTitle=value=>{document.title=value;document.querySelector('p').textContent='Changed '+value};</script>`});
|
|
8
|
+
await r.whenLoaded();
|
|
9
|
+
assert.equal(r.title(),'Initial');
|
|
10
|
+
// Host scripting allows the fixture to mutate the DOM without evaluate()'s
|
|
11
|
+
// explicit refresh. No onFrame callback: pending work stays pending.
|
|
12
|
+
for(const title of ['Updated','']){
|
|
13
|
+
globalThis.changeTitle(title);
|
|
14
|
+
traces.length=0;
|
|
15
|
+
assert.equal(r.title(),title);
|
|
16
|
+
assert.equal(traces.length,0,'title reads must not rebuild or rasterize');
|
|
17
|
+
await r.screenshot();
|
|
18
|
+
assert.ok(traces.some(s=>s.includes('restyle and relayout')),'DOM mutations remain pending until rendering is requested');
|
|
19
|
+
}
|
|
20
|
+
}finally{delete globalThis.changeTitle;r.dispose()}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, symlinkSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
// Application tests own every XDG directory. Backend replies are never replaced.
|
|
4
|
+
export function fleaSandbox(flea, omarchy) {
|
|
5
|
+
const dir = mkdtempSync('/tmp/qml-flea-'),
|
|
6
|
+
saved = {};
|
|
7
|
+
const values = {
|
|
8
|
+
HOME: join(dir, 'home'),
|
|
9
|
+
XDG_CONFIG_HOME: join(dir, 'config'),
|
|
10
|
+
XDG_STATE_HOME: join(dir, 'state'),
|
|
11
|
+
XDG_DATA_HOME: join(dir, 'data'),
|
|
12
|
+
XDG_CACHE_HOME: join(dir, 'cache'),
|
|
13
|
+
QML_IMPORT_PATH: join(dir, 'imports'),
|
|
14
|
+
HYPRLAND_INSTANCE_SIGNATURE: '',
|
|
15
|
+
FLEA_BIN: process.env.FLEA_BIN || join(dir, 'no-flea-backend'),
|
|
16
|
+
};
|
|
17
|
+
for (const [key, value] of Object.entries(values)) {
|
|
18
|
+
saved[key] = process.env[key];
|
|
19
|
+
process.env[key] = value;
|
|
20
|
+
}
|
|
21
|
+
for (const key of [
|
|
22
|
+
'HOME',
|
|
23
|
+
'XDG_CONFIG_HOME',
|
|
24
|
+
'XDG_STATE_HOME',
|
|
25
|
+
'XDG_DATA_HOME',
|
|
26
|
+
'XDG_CACHE_HOME',
|
|
27
|
+
])
|
|
28
|
+
mkdirSync(values[key], { recursive: true });
|
|
29
|
+
mkdirSync(join(dir, 'imports/qs'), { recursive: true });
|
|
30
|
+
for (const name of ['Commons', 'Ui'])
|
|
31
|
+
symlinkSync(
|
|
32
|
+
resolve(omarchy, 'shell', name),
|
|
33
|
+
join(dir, 'imports/qs', name),
|
|
34
|
+
'dir',
|
|
35
|
+
);
|
|
36
|
+
return {
|
|
37
|
+
dir,
|
|
38
|
+
flea: resolve(flea),
|
|
39
|
+
values,
|
|
40
|
+
close() {
|
|
41
|
+
for (const [key, value] of Object.entries(saved)) {
|
|
42
|
+
if (value === undefined) delete process.env[key];
|
|
43
|
+
else process.env[key] = value;
|
|
44
|
+
}
|
|
45
|
+
rmSync(dir, { recursive: true, force: true });
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -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,21 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<meta charset="utf-8">
|
|
3
|
+
<title>雙擊測試 · ondblclick</title>
|
|
4
|
+
<style>
|
|
5
|
+
body{margin:0;padding:28px;background:#101c2c;color:#e9f2ff;font-family:sans-serif}
|
|
6
|
+
h1{font-size:26px}p{line-height:1.6;color:#bed0e8}
|
|
7
|
+
#target{display:block;width:100%;height:180px;background:#204763;color:white;border:2px solid #5ad5df;border-radius:12px;font-size:26px}
|
|
8
|
+
#result{padding:18px;font-size:22px;color:#71edcf}
|
|
9
|
+
#stats{padding:14px;background:#192b40;line-height:1.8}
|
|
10
|
+
</style>
|
|
11
|
+
<h1>雙擊才能開啟</h1>
|
|
12
|
+
<p>在下面區域快速點兩下。單擊只更新紀錄,雙擊才會切換開啟/關閉狀態。</p>
|
|
13
|
+
<button id="target" onclick="recordClick(event)" ondblclick="toggleDoor(event)">🔒 雙擊開啟</button>
|
|
14
|
+
<div id="result">等待 dblclick…</div>
|
|
15
|
+
<div id="stats">click:0 dblclick:0 最近 detail:0</div>
|
|
16
|
+
<script>
|
|
17
|
+
let clicks=0,doubles=0,opened=false;
|
|
18
|
+
function stats(detail){document.getElementById('stats').textContent='click:'+clicks+' dblclick:'+doubles+' 最近 detail:'+detail}
|
|
19
|
+
function recordClick(e){clicks++;stats(e.detail)}
|
|
20
|
+
function toggleDoor(e){doubles++;opened=!opened;document.getElementById('target').textContent=opened?'🔓 已開啟,雙擊關閉':'🔒 已關閉,雙擊開啟';document.getElementById('result').textContent='收到 ondblclick!第 '+doubles+' 次';stats(e.detail)}
|
|
21
|
+
</script>
|
|
@@ -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,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-Hant"><meta charset="utf-8"><title>滑鼠按鍵測試</title>
|
|
3
|
+
<style>
|
|
4
|
+
body{margin:0;padding:28px;background:#101a28;color:#e1f2ff;font-family:sans-serif}
|
|
5
|
+
h1{font-size:28px}p{line-height:1.6}button{display:block;width:100%;height:220px;background:#146d83;color:white;border:3px solid #62dbe9;border-radius:18px;font-size:34px;font-weight:bold}
|
|
6
|
+
#result{margin-top:24px;font-size:27px;color:#70efce}#counts{font-size:22px}pre{white-space:pre-wrap;background:#19283b;padding:16px;font-size:17px}
|
|
7
|
+
</style>
|
|
8
|
+
<h1>左鍵・右鍵・中鍵測試</h1><p>直接點按測左鍵。長按圖片,選 Right click 測右鍵。右鍵不應增加左鍵次數。</p>
|
|
9
|
+
<button id="pad">在這裡點擊 / 長按</button><div id="result">等待按鍵…</div><p id="counts">左鍵 0 | 右鍵 0 | 中鍵 0</p><pre id="events">事件記錄</pre>
|
|
10
|
+
<script>
|
|
11
|
+
const pad=document.getElementById('pad'),result=document.getElementById('result'),counts=document.getElementById('counts'),log=document.getElementById('events');
|
|
12
|
+
const totals=[0,0,0],names=['左鍵','中鍵','右鍵'];let rows=[];
|
|
13
|
+
for(const type of ['mousedown','contextmenu','mouseup','click','auxclick'])pad.addEventListener(type,e=>{
|
|
14
|
+
if(type==='contextmenu')e.preventDefault();
|
|
15
|
+
rows.unshift(type+' button='+e.button+' buttons='+e.buttons);rows=rows.slice(0,10);log.textContent=rows.join('\n');
|
|
16
|
+
if(type==='click'||type==='auxclick'){totals[e.button]++;result.textContent='收到:'+names[e.button];counts.textContent='左鍵 '+totals[0]+' | 右鍵 '+totals[2]+' | 中鍵 '+totals[1];}
|
|
17
|
+
});
|
|
18
|
+
</script></html>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Requires an already-running official engine; never starts or installs one.
|
|
2
|
+
import {mkdtemp,rm} from 'node:fs/promises';
|
|
3
|
+
import {resolve} from 'node:path';
|
|
4
|
+
import {pathToFileURL} from 'node:url';
|
|
5
|
+
import {createBrowser} from '../lib/headless-shell.js';
|
|
6
|
+
const root=resolve(process.argv[2] || '../omastorm');
|
|
7
|
+
const dir=await mkdtemp('/tmp/omastorm-engine-probe-');
|
|
8
|
+
await Bun.write(dir+'/probe.qml',`import QtQuick
|
|
9
|
+
import ${JSON.stringify(pathToFileURL(root+'/ui').href)} as Storm
|
|
10
|
+
Item {Storm.Engine {id:engine} Component.onCompleted:globalThis.engineProbe=engine}`);
|
|
11
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
12
|
+
try {
|
|
13
|
+
await browser.context.navigate(pathToFileURL(dir+'/probe.qml').href);
|
|
14
|
+
for(let i=0;i<100;i++){
|
|
15
|
+
const value=await browser.context.evaluate('globalThis.engineProbe ? {error:engineProbe.error, sites:engineProbe.sites.length,state:engineProbe.state?.type,frame:engineProbe.state?.frame?.kind} : {text:document.body.textContent.slice(0,1000)}');
|
|
16
|
+
if(value?.state || value?.error || value?.text){console.log(JSON.stringify(value));break;}
|
|
17
|
+
await Bun.sleep(50);
|
|
18
|
+
}
|
|
19
|
+
}finally{browser.close();await rm(dir,{recursive:true,force:true});}
|