flea-qml 0.0.0-stage → 0.2.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/LICENSE +22 -0
- package/README.md +511 -2
- package/THIRD-PARTY-NOTICES.md +29 -0
- package/demos/App.java +5 -0
- package/demos/Dockerfile +4 -0
- package/demos/Main.kt +3 -0
- package/demos/Program.cs +1 -0
- package/demos/README.md +12 -0
- package/demos/app.dart +3 -0
- package/demos/app.deb +0 -0
- package/demos/app.js +1 -0
- package/demos/backup.tar +0 -0
- package/demos/budget.xlsx +0 -0
- package/demos/build.sh +2 -0
- package/demos/bundle.zip +0 -0
- package/demos/clip.mp4 +0 -0
- package/demos/data.sqlite +0 -0
- package/demos/disk.iso +0 -0
- package/demos/engine.cpp +5 -0
- package/demos/firmware.bin +0 -0
- package/demos/flake.nix +4 -0
- package/demos/folder/inside.txt +1 -0
- package/demos/font.ttf +0 -0
- package/demos/index.html +5 -0
- package/demos/index.php +2 -0
- package/demos/init.lua +1 -0
- package/demos/letter.docx +0 -0
- package/demos/lib.rs +3 -0
- package/demos/main.c +6 -0
- package/demos/main.h +3 -0
- package/demos/math-and-diagrams.md +200 -0
- package/demos/notes.tar.gz +0 -0
- package/demos/notes.txt +3 -0
- package/demos/photo.png +0 -0
- package/demos/program +2 -0
- package/demos/report.pdf +32 -0
- package/demos/script.py +1 -0
- package/demos/server.go +7 -0
- package/demos/settings.json +4 -0
- package/demos/slides.pptx +0 -0
- package/demos/song.mp3 +0 -0
- package/demos/style.css +4 -0
- package/demos/tool.rb +1 -0
- package/demos/types.ts +2 -0
- package/docs/pele.md +23 -0
- package/docs/protocol.md +3420 -0
- package/docs/temml.md +63 -0
- package/index.js +107 -0
- package/package.json +34 -4
- package/src/archive.js +407 -0
- package/src/archiveops.js +728 -0
- package/src/backend.js +369 -0
- package/src/bulkrename.js +95 -0
- package/src/clip-host.js +76 -0
- package/src/clip.js +462 -0
- package/src/code.js +168 -0
- package/src/compare.js +74 -0
- package/src/create.js +128 -0
- package/src/dirsize.js +147 -0
- package/src/figure-svg.js +530 -0
- package/src/figure-thread.js +23 -0
- package/src/figure.js +132 -0
- package/src/find.js +55 -0
- package/src/flea.js +39 -0
- package/src/fsinfo.js +39 -0
- package/src/highlight.js +141 -0
- package/src/html.js +52 -0
- package/src/kinds.js +89 -0
- package/src/listing.js +130 -0
- package/src/markdown-html.js +317 -0
- package/src/mathml.js +186 -0
- package/src/menu.js +180 -0
- package/src/meta.js +135 -0
- package/src/office.js +810 -0
- package/src/order.js +100 -0
- package/src/peek.js +49 -0
- package/src/pele-wrapper.js +695 -0
- package/src/permissions.js +180 -0
- package/src/places.js +313 -0
- package/src/preview-language.js +10 -0
- package/src/terminal-server.js +95 -0
- package/src/terminal-shell.js +26 -0
- package/src/transfer.js +669 -0
- package/src/trash-record.js +41 -0
- package/src/trash.js +210 -0
- package/src/trashbrowse.js +327 -0
- package/src/uistate.js +393 -0
- package/src/uistore.js +168 -0
- package/src/vendor/LICENSES/Apache-2.0.txt +202 -0
- package/src/vendor/LICENSES/MIT-katex-2015.txt +27 -0
- package/src/vendor/LICENSES/MIT-katex.txt +21 -0
- package/src/vendor/LICENSES/MIT-mermaid.txt +21 -0
- package/src/vendor/LICENSES/MIT-pele.txt +40 -0
- package/src/vendor/LICENSES/MIT-perseus.txt +18 -0
- package/src/vendor/LICENSES/MIT-temml.txt +21 -0
- package/src/vendor/pele.js +145 -0
- package/src/vendor/temml.mjs +14708 -0
- package/src/watch.js +98 -0
- package/src/wayland/client.js +109 -0
- package/src/wayland/protocol.js +55 -0
- package/src/wayland/protocols/ext-data-control-v1.xml +276 -0
- package/src/wayland/protocols/wayland.xml +3248 -0
- package/src/wayland/protocols/wlr-data-control-unstable-v1.xml +278 -0
- package/src/wayland/resource-io.js +6 -0
- package/src/wayland/socket-discovery.js +26 -0
- package/src/wayland/transport-posix.js +120 -0
- package/src/wayland/transport-win32.js +11 -0
- package/src/wayland/transport.js +3 -0
- package/src/wayland/win32-resources.js +80 -0
- package/src/wayland/win32-wire.js +62 -0
- package/test/archive.test.js +80 -0
- package/test/bench-markdown-scroll.mjs +190 -0
- package/test/clip-host.test.js +113 -0
- package/test/code-preview.test.js +35 -0
- package/test/compare-ui-paths.test.js +24 -0
- package/test/compare.test.js +54 -0
- package/test/delete-trash.test.js +67 -0
- package/test/dirsize.test.js +138 -0
- package/test/file-ops.test.js +307 -0
- package/test/file-url.test.js +24 -0
- package/test/find-contents.test.js +161 -0
- package/test/find-input.test.js +66 -0
- package/test/fixtures/pele-parity.json +1006 -0
- package/test/highlight-launcher.test.js +35 -0
- package/test/html-preview.test.js +22 -0
- package/test/input.test.js +15 -0
- package/test/keymap.test.js +26 -0
- package/test/language-icons.test.js +36 -0
- package/test/launcher.test.js +51 -0
- package/test/manual-archive-here-ui.mjs +47 -0
- package/test/manual-code-scroll.mjs +28 -0
- package/test/manual-column-scroll.mjs +24 -0
- package/test/manual-compare-ui.mjs +20 -0
- package/test/manual-find-status.mjs +37 -0
- package/test/manual-folder-focus.mjs +63 -0
- package/test/manual-html-security.mjs +23 -0
- package/test/manual-html-ui.mjs +30 -0
- package/test/manual-syntax-preview.mjs +26 -0
- package/test/manual-terminal-lifecycle.mjs +42 -0
- package/test/manual-terminal-navigation.mjs +48 -0
- package/test/manual-terminal-ui.mjs +47 -0
- package/test/manual-trash-ui.mjs +86 -0
- package/test/markdown-diagrams.test.js +93 -0
- package/test/markdown-html.test.js +105 -0
- package/test/mathml.test.js +92 -0
- package/test/menu-transfer.test.js +57 -0
- package/test/office-preview.test.js +154 -0
- package/test/pele-parity.test.js +28 -0
- package/test/places.test.js +134 -0
- package/test/preview-keys.test.js +60 -0
- package/test/preview-language.test.js +22 -0
- package/test/status-filename.test.js +20 -0
- package/test/transfer-replace-merge.test.js +187 -0
- package/test/trash-address.test.js +23 -0
- package/test/trash-view.test.js +37 -0
- package/test/trashbrowse.test.js +135 -0
- package/ui/AboutFacts.qml +78 -0
- package/ui/ArchiveCards.qml +95 -0
- package/ui/ArchiveCompressDialog.qml +378 -0
- package/ui/ArchiveHereConfirm.qml +127 -0
- package/ui/ArchivePasswordDialog.qml +257 -0
- package/ui/Backend.qml +775 -0
- package/ui/BulkOldNames.qml +47 -0
- package/ui/BulkRenameDialog.qml +380 -0
- package/ui/CardFrame.qml +67 -0
- package/ui/CardScroll.qml +79 -0
- package/ui/CheckBox.qml +73 -0
- package/ui/ChromeBar.qml +360 -0
- package/ui/ChromeButton.qml +131 -0
- package/ui/ChromeModes.qml +43 -0
- package/ui/CodeLines.qml +256 -0
- package/ui/CodeRefusal.qml +52 -0
- package/ui/CollideConfirm.qml +147 -0
- package/ui/CollideHost.qml +167 -0
- package/ui/ColumnPane.qml +525 -0
- package/ui/ColumnRow.qml +380 -0
- package/ui/ColumnsArea.qml +594 -0
- package/ui/ColumnsLanding.qml +147 -0
- package/ui/CompareView.qml +321 -0
- package/ui/ContextMenu.qml +743 -0
- package/ui/ConvertDialog.qml +456 -0
- package/ui/CountedWorker.qml +7 -0
- package/ui/Crumb.qml +64 -0
- package/ui/CrumbDrop.qml +25 -0
- package/ui/CsvTable.qml +394 -0
- package/ui/CursorMark.qml +41 -0
- package/ui/DefaultClaim.qml +130 -0
- package/ui/DeviceLock.qml +47 -0
- package/ui/DeviceMounts.qml +540 -0
- package/ui/DialogButton.qml +150 -0
- package/ui/DialogField.qml +147 -0
- package/ui/DialogHeader.qml +31 -0
- package/ui/DialogScrim.qml +20 -0
- package/ui/DialogTitle.qml +37 -0
- package/ui/Divider.qml +9 -0
- package/ui/DropInto.qml +126 -0
- package/ui/DropTarget.qml +35 -0
- package/ui/DropboxMark.qml +60 -0
- package/ui/EmptyState.qml +150 -0
- package/ui/EscapeHint.qml +11 -0
- package/ui/FactsTable.qml +54 -0
- package/ui/FastScrollHandler.qml +476 -0
- package/ui/FatNamesConfirm.qml +157 -0
- package/ui/Favourites.qml +143 -0
- package/ui/FigureService.qml +120 -0
- package/ui/FileDrag.qml +267 -0
- package/ui/FilterStrip.qml +133 -0
- package/ui/FitMetrics.qml +22 -0
- package/ui/FlashDriveRows.qml +137 -0
- package/ui/FlashHost.qml +72 -0
- package/ui/FlashImageRow.qml +104 -0
- package/ui/FlashRunner.qml +119 -0
- package/ui/FlashStepRow.qml +172 -0
- package/ui/FlashStrip.qml +38 -0
- package/ui/FlashView.qml +397 -0
- package/ui/FleaMark.qml +88 -0
- package/ui/FlyoutFade.qml +23 -0
- package/ui/FontPane.qml +170 -0
- package/ui/Glass.qml +114 -0
- package/ui/GlassTagger.qml +59 -0
- package/ui/Glyph.qml +64 -0
- package/ui/GlyphSolid.qml +22 -0
- package/ui/GridArea.qml +664 -0
- package/ui/GridTile.qml +325 -0
- package/ui/GvfsBridge.qml +164 -0
- package/ui/Header.qml +433 -0
- package/ui/HoverTimers.qml +39 -0
- package/ui/HtmlClip.qml +30 -0
- package/ui/HtmlPane.qml +63 -0
- package/ui/HyprlandMark.qml +45 -0
- package/ui/Ipc.qml +1005 -0
- package/ui/IpcControls.qml +7 -0
- package/ui/IpcMenuState.qml +48 -0
- package/ui/IpcPreviewState.qml +185 -0
- package/ui/JumpPath.qml +51 -0
- package/ui/KeymapSheet.qml +561 -0
- package/ui/KeymapSheetResult.qml +89 -0
- package/ui/KindStack.qml +40 -0
- package/ui/LangMark.qml +51 -0
- package/ui/LangMarkLoader.qml +26 -0
- package/ui/List.qml +480 -0
- package/ui/LoadingState.qml +54 -0
- package/ui/LocalSend.qml +56 -0
- package/ui/LocalSendMark.qml +73 -0
- package/ui/MarkdownBlockView.qml +311 -0
- package/ui/MarkdownBlocks.qml +35 -0
- package/ui/MarkdownFigure.qml +254 -0
- package/ui/MarkdownImages.qml +72 -0
- package/ui/MarkdownList.qml +81 -0
- package/ui/MarkdownMathsText.qml +97 -0
- package/ui/MarkdownPane.qml +83 -0
- package/ui/MarkdownPictures.qml +41 -0
- package/ui/MarkdownQuote.qml +52 -0
- package/ui/MarkdownRemote.qml +82 -0
- package/ui/MarkdownSource.qml +155 -0
- package/ui/MarkdownTable.qml +222 -0
- package/ui/MarkdownTableScroll.qml +64 -0
- package/ui/MarkdownTableWheel.qml +126 -0
- package/ui/MarkdownText.qml +87 -0
- package/ui/MarkdownWorker.js +132 -0
- package/ui/MatchText.qml +99 -0
- package/ui/MediaSession.qml +38 -0
- package/ui/MediaSound.qml +26 -0
- package/ui/MediaStrip.qml +191 -0
- package/ui/MediaTrack.qml +36 -0
- package/ui/MediaVolume.qml +54 -0
- package/ui/MenuActionDialog.qml +396 -0
- package/ui/MenuEdgeFade.qml +24 -0
- package/ui/MenuHost.qml +227 -0
- package/ui/MenuRow.qml +351 -0
- package/ui/MountListing.qml +100 -0
- package/ui/NetworkDialog.qml +435 -0
- package/ui/NetworkForm.qml +426 -0
- package/ui/NetworkMounts.qml +913 -0
- package/ui/NetworkPlaces.qml +127 -0
- package/ui/OpenWithDialog.qml +613 -0
- package/ui/Opener.qml +95 -0
- package/ui/Pane.qml +1320 -0
- package/ui/PaneMenuActions.qml +527 -0
- package/ui/PanePath.qml +90 -0
- package/ui/PaneRail.qml +268 -0
- package/ui/PaneStates.qml +88 -0
- package/ui/PaneSwap.qml +339 -0
- package/ui/PaneWire.qml +601 -0
- package/ui/ParkedWait.qml +9 -0
- package/ui/PathEntry.qml +186 -0
- package/ui/PathJump.qml +400 -0
- package/ui/PdfViewer.qml +297 -0
- package/ui/PermissionsDialog.qml +658 -0
- package/ui/PermissionsHeadings.qml +31 -0
- package/ui/PhoneMounts.qml +67 -0
- package/ui/PickerAnswers.qml +75 -0
- package/ui/PickerBox.qml +26 -0
- package/ui/PickerChrome.qml +295 -0
- package/ui/PickerFilter.qml +97 -0
- package/ui/PickerGrid.qml +308 -0
- package/ui/PickerHeader.qml +28 -0
- package/ui/PickerLifecycle.qml +27 -0
- package/ui/PickerList.qml +218 -0
- package/ui/PickerListing.qml +149 -0
- package/ui/PickerPathBar.qml +127 -0
- package/ui/PickerPlaces.qml +189 -0
- package/ui/PickerRecent.qml +92 -0
- package/ui/PickerSave.qml +101 -0
- package/ui/PickerSelection.qml +155 -0
- package/ui/PickerWindow.qml +846 -0
- package/ui/PowerSectorsReader.qml +10 -0
- package/ui/PrebuildDriver.qml +146 -0
- package/ui/Preview.qml +1005 -0
- package/ui/PreviewAnimation.qml +24 -0
- package/ui/PreviewArchive.qml +91 -0
- package/ui/PreviewColumn.qml +553 -0
- package/ui/PreviewEdge.qml +63 -0
- package/ui/PreviewFacts.qml +30 -0
- package/ui/PreviewFactsLine.qml +34 -0
- package/ui/PreviewHtml.qml +233 -0
- package/ui/PreviewImage.qml +253 -0
- package/ui/PreviewImagePan.qml +13 -0
- package/ui/PreviewImageSharp.qml +32 -0
- package/ui/PreviewLines.qml +106 -0
- package/ui/PreviewMarkdown.qml +706 -0
- package/ui/PreviewMedia.qml +250 -0
- package/ui/PreviewPdf.qml +482 -0
- package/ui/PreviewScroll.qml +201 -0
- package/ui/PreviewSwap.qml +335 -0
- package/ui/PreviewText.qml +149 -0
- package/ui/ProtocolChip.qml +18 -0
- package/ui/QuickLookArchive.qml +41 -0
- package/ui/QuickLookBar.qml +103 -0
- package/ui/QuickLookPrepare.qml +355 -0
- package/ui/QuickLookSwap.qml +10 -0
- package/ui/QuickLookWarm.qml +65 -0
- package/ui/RailDrop.qml +155 -0
- package/ui/RailHeading.qml +19 -0
- package/ui/RailPlaces.qml +38 -0
- package/ui/RailTrashRow.qml +67 -0
- package/ui/RenameField.qml +353 -0
- package/ui/RendererRetry.qml +11 -0
- package/ui/ResizeHandle.qml +52 -0
- package/ui/Reveal.qml +54 -0
- package/ui/Row.qml +476 -0
- package/ui/RowDrag.qml +213 -0
- package/ui/RowDropTarget.qml +27 -0
- package/ui/RunCard.qml +286 -0
- package/ui/Scripts.qml +65 -0
- package/ui/SearchStrip.qml +232 -0
- package/ui/SelectionBand.qml +140 -0
- package/ui/SelectionPreview.qml +370 -0
- package/ui/SettingsFavourite.qml +122 -0
- package/ui/SettingsGroup.qml +96 -0
- package/ui/SettingsHint.qml +12 -0
- package/ui/SettingsMetrics.qml +29 -0
- package/ui/SettingsPane.qml +178 -0
- package/ui/SettingsPanel.qml +547 -0
- package/ui/SettingsRail.qml +128 -0
- package/ui/SettingsRow.qml +409 -0
- package/ui/SettingsRuler.qml +85 -0
- package/ui/SettingsSegment.qml +85 -0
- package/ui/ShareBrowser.qml +113 -0
- package/ui/ShareLink.qml +67 -0
- package/ui/ShelfPins.qml +101 -0
- package/ui/Sidebar.qml +728 -0
- package/ui/SidebarRow.qml +358 -0
- package/ui/Spinner.qml +60 -0
- package/ui/StableModel.qml +89 -0
- package/ui/StateMessage.qml +84 -0
- package/ui/StatusBar.qml +512 -0
- package/ui/StatusFileName.qml +27 -0
- package/ui/TabBar.qml +673 -0
- package/ui/TabDragGeometry.qml +60 -0
- package/ui/TagMenuMark.qml +52 -0
- package/ui/TagRailSection.qml +53 -0
- package/ui/TagSlot.qml +40 -0
- package/ui/TagState.qml +150 -0
- package/ui/TagSwatch.qml +28 -0
- package/ui/Taildrop.qml +86 -0
- package/ui/TailscaleMark.qml +50 -0
- package/ui/TerminalService.qml +69 -0
- package/ui/TerminalTab.qml +48 -0
- package/ui/TerminalTabs.qml +27 -0
- package/ui/Theme.qml +460 -0
- package/ui/ThemeColors.qml +43 -0
- package/ui/TransferCard.qml +239 -0
- package/ui/TrashConfirm.qml +129 -0
- package/ui/TrashDropHandler.qml +11 -0
- package/ui/TrashHost.qml +202 -0
- package/ui/TrashMonitor.qml +30 -0
- package/ui/TrashToolbar.qml +19 -0
- package/ui/TrashView.qml +492 -0
- package/ui/UnlockDialog.qml +450 -0
- package/ui/UnlockHost.qml +206 -0
- package/ui/UpdateCheck.qml +97 -0
- package/ui/UserDirs.qml +25 -0
- package/ui/ViewState.qml +565 -0
- package/ui/ViewportScrollBar.qml +195 -0
- package/ui/ViewportScrollBars.qml +25 -0
- package/ui/WebPreview.qml +33 -0
- package/ui/WebPreviewView.qml +26 -0
- package/ui/WindowBody.qml +829 -0
- package/ui/WindowDrag.qml +35 -0
- package/ui/boot/Commons/AudioNodes.qml +50 -0
- package/ui/boot/Commons/AudioNodesModel.js +96 -0
- package/ui/boot/Commons/Border.qml +243 -0
- package/ui/boot/Commons/BorderGeometry.js +373 -0
- package/ui/boot/Commons/Color.qml +258 -0
- package/ui/boot/Commons/I18n.qml +118 -0
- package/ui/boot/Commons/InputDevices.qml +112 -0
- package/ui/boot/Commons/InputMethodState.qml +38 -0
- package/ui/boot/Commons/IpcRegistry.qml +77 -0
- package/ui/boot/Commons/Numbers.js +19 -0
- package/ui/boot/Commons/Plural.js +91 -0
- package/ui/boot/Commons/ShellIpc.qml +12 -0
- package/ui/boot/Commons/Style.qml +561 -0
- package/ui/boot/Commons/Translations.qml +100 -0
- package/ui/boot/Commons/UntypedInput.qml +152 -0
- package/ui/boot/Commons/Util.qml +188 -0
- package/ui/boot/Commons/qmldir +13 -0
- package/ui/boot/LICENSE-omarchy +20 -0
- package/ui/boot/Ui/BackgroundMedia.qml +49 -0
- package/ui/boot/Ui/BarIconButton.qml +106 -0
- package/ui/boot/Ui/BarIconContent.qml +38 -0
- package/ui/boot/Ui/BarIndicator.qml +53 -0
- package/ui/boot/Ui/BarWidget.qml +50 -0
- package/ui/boot/Ui/BorderOverlay.qml +54 -0
- package/ui/boot/Ui/BorderSurface.qml +40 -0
- package/ui/boot/Ui/Button.qml +224 -0
- package/ui/boot/Ui/ButtonGroup.qml +131 -0
- package/ui/boot/Ui/ConfirmDialog.qml +134 -0
- package/ui/boot/Ui/CursorSurface.qml +42 -0
- package/ui/boot/Ui/Dropdown.qml +245 -0
- package/ui/boot/Ui/FilterInput.qml +26 -0
- package/ui/boot/Ui/IconContentBounds.qml +141 -0
- package/ui/boot/Ui/KeyboardPanel.qml +453 -0
- package/ui/boot/Ui/MultiSelect.qml +624 -0
- package/ui/boot/Ui/NumberField.qml +86 -0
- package/ui/boot/Ui/OpticalGlyph.qml +68 -0
- package/ui/boot/Ui/OverlayWindow.qml +102 -0
- package/ui/boot/Ui/Panel.qml +60 -0
- package/ui/boot/Ui/PanelActionButton.qml +105 -0
- package/ui/boot/Ui/PanelController.qml +16 -0
- package/ui/boot/Ui/PanelHero.qml +112 -0
- package/ui/boot/Ui/PanelKeyCatcher.qml +100 -0
- package/ui/boot/Ui/PanelSectionHeader.qml +31 -0
- package/ui/boot/Ui/PanelSeparator.qml +19 -0
- package/ui/boot/Ui/PanelSlider.qml +156 -0
- package/ui/boot/Ui/PanelToolTip.qml +50 -0
- package/ui/boot/Ui/PluginBarApi.qml +89 -0
- package/ui/boot/Ui/PointerMoveGate.qml +54 -0
- package/ui/boot/Ui/PopupCard.qml +181 -0
- package/ui/boot/Ui/ScreenMoveRemap.qml +43 -0
- package/ui/boot/Ui/SearchableDropdown.qml +354 -0
- package/ui/boot/Ui/SizedImage.qml +125 -0
- package/ui/boot/Ui/SpeedTestOverlay.qml +412 -0
- package/ui/boot/Ui/TextField.qml +58 -0
- package/ui/boot/Ui/Toggle.qml +118 -0
- package/ui/boot/Ui/ToggleSwitch.qml +115 -0
- package/ui/boot/Ui/WidgetButton.qml +172 -0
- package/ui/boot/Ui/qmldir +41 -0
- package/ui/boot/fleatab.qml +50 -0
- package/ui/boot/picker.qml +39 -0
- package/ui/boot/shell.qml +131 -0
- package/ui/boot/tabtearoff.qml +51 -0
- package/ui/js/Anchor.js +379 -0
- package/ui/js/AnchorHold.js +162 -0
- package/ui/js/Archive.js +157 -0
- package/ui/js/ArchiveCard.js +44 -0
- package/ui/js/ArchiveOps.js +44 -0
- package/ui/js/BackReturn.js +118 -0
- package/ui/js/BulkRename.js +100 -0
- package/ui/js/Buttons.js +54 -0
- package/ui/js/ChromeKeys.js +116 -0
- package/ui/js/ClipMarks.js +73 -0
- package/ui/js/ClipSelection.js +67 -0
- package/ui/js/Clipboard.js +179 -0
- package/ui/js/Cloud.js +97 -0
- package/ui/js/CodeRuns.js +72 -0
- package/ui/js/Collide.js +212 -0
- package/ui/js/ColumnFit.js +28 -0
- package/ui/js/ColumnMenu.js +111 -0
- package/ui/js/Columns.js +298 -0
- package/ui/js/ColumnsCarry.js +103 -0
- package/ui/js/Compare.js +74 -0
- package/ui/js/Contrast.js +157 -0
- package/ui/js/ControlReader.js +42 -0
- package/ui/js/Convert.js +52 -0
- package/ui/js/CopyAs.js +52 -0
- package/ui/js/Crumbs.js +178 -0
- package/ui/js/Csv.js +231 -0
- package/ui/js/Density.js +44 -0
- package/ui/js/Devices.js +440 -0
- package/ui/js/DirSizes.js +180 -0
- package/ui/js/Drag.js +360 -0
- package/ui/js/DragOut.js +166 -0
- package/ui/js/DropDev.js +52 -0
- package/ui/js/Dropbox.js +41 -0
- package/ui/js/Eject.js +217 -0
- package/ui/js/Errors.js +188 -0
- package/ui/js/ExtThumbs.js +115 -0
- package/ui/js/Facts.js +402 -0
- package/ui/js/FigureDate.js +14 -0
- package/ui/js/Filter.js +285 -0
- package/ui/js/Find.js +46 -0
- package/ui/js/Flash.js +184 -0
- package/ui/js/FlashCard.js +79 -0
- package/ui/js/FlashEndings.js +125 -0
- package/ui/js/FlashPane.js +116 -0
- package/ui/js/FleaBin.js +12 -0
- package/ui/js/Focus.js +504 -0
- package/ui/js/FolderSorts.js +108 -0
- package/ui/js/FolderViews.js +44 -0
- package/ui/js/Format.js +314 -0
- package/ui/js/Fuzzy.js +141 -0
- package/ui/js/GlassTag.js +72 -0
- package/ui/js/Grid.js +33 -0
- package/ui/js/GridGeometry.js +90 -0
- package/ui/js/GridNames.js +153 -0
- package/ui/js/GvfsBridge.js +200 -0
- package/ui/js/HostClipboard.js +29 -0
- package/ui/js/Icons.js +218 -0
- package/ui/js/Input.js +50 -0
- package/ui/js/Jump.js +200 -0
- package/ui/js/KeyName.js +64 -0
- package/ui/js/Keymap.js +401 -0
- package/ui/js/Kinds.js +164 -0
- package/ui/js/LICENSES/ISC-lucide.txt +43 -0
- package/ui/js/LangMarkPaths.js +47 -0
- package/ui/js/LangMarks.js +120 -0
- package/ui/js/Lists.js +11 -0
- package/ui/js/LocalSend.js +39 -0
- package/ui/js/LockedMenu.js +63 -0
- package/ui/js/MakeDefault.js +186 -0
- package/ui/js/MarkBox.js +7 -0
- package/ui/js/Markdown.js +119 -0
- package/ui/js/MarkdownLists.js +86 -0
- package/ui/js/MarkdownMaths.js +58 -0
- package/ui/js/MarkdownPictures.js +32 -0
- package/ui/js/MarkdownPrepared.js +139 -0
- package/ui/js/MarkdownTableFit.js +34 -0
- package/ui/js/Marks.js +148 -0
- package/ui/js/Marquee.js +96 -0
- package/ui/js/Match.js +93 -0
- package/ui/js/Material.js +109 -0
- package/ui/js/MdBlocks.js +437 -0
- package/ui/js/MdBreak.js +71 -0
- package/ui/js/MdChunks.js +70 -0
- package/ui/js/MdContainer.js +137 -0
- package/ui/js/MdDocument.js +337 -0
- package/ui/js/MdEmph.js +97 -0
- package/ui/js/MdEntity.js +108 -0
- package/ui/js/MdEntityTable.js +223 -0
- package/ui/js/MdEscape.js +59 -0
- package/ui/js/MdFront.js +37 -0
- package/ui/js/MdHold.js +32 -0
- package/ui/js/MdHtml.js +307 -0
- package/ui/js/MdHtmlBlock.js +290 -0
- package/ui/js/MdHtmlHeading.js +53 -0
- package/ui/js/MdHtmlImage.js +179 -0
- package/ui/js/MdInline.js +154 -0
- package/ui/js/MdItems.js +212 -0
- package/ui/js/MdLeaf.js +309 -0
- package/ui/js/MdLink.js +196 -0
- package/ui/js/MdMath.js +67 -0
- package/ui/js/MdParagraphs.js +138 -0
- package/ui/js/MdRefs.js +232 -0
- package/ui/js/MdResolve.js +226 -0
- package/ui/js/MdRun.js +300 -0
- package/ui/js/MdUrl.js +226 -0
- package/ui/js/Menu.js +514 -0
- package/ui/js/MenuContext.js +85 -0
- package/ui/js/MenuFit.js +13 -0
- package/ui/js/MenuRefresh.js +74 -0
- package/ui/js/MenuWheel.js +35 -0
- package/ui/js/Messages.js +186 -0
- package/ui/js/Motion.js +16 -0
- package/ui/js/Mounts.js +320 -0
- package/ui/js/MouseNav.js +13 -0
- package/ui/js/Nav.js +329 -0
- package/ui/js/NetFs.js +26 -0
- package/ui/js/OpenWith.js +83 -0
- package/ui/js/Ops.js +585 -0
- package/ui/js/Palette.js +33 -0
- package/ui/js/PathBar.js +260 -0
- package/ui/js/Permissions.js +179 -0
- package/ui/js/Phones.js +112 -0
- package/ui/js/Picker.js +353 -0
- package/ui/js/PlaceMenu.js +96 -0
- package/ui/js/Places.js +265 -0
- package/ui/js/PreviewAsk.js +21 -0
- package/ui/js/PreviewKeys.js +155 -0
- package/ui/js/PreviewSettle.js +29 -0
- package/ui/js/PreviewSwap.js +101 -0
- package/ui/js/PropertyFacts.js +144 -0
- package/ui/js/Protocols.js +296 -0
- package/ui/js/QuickLookKinds.js +27 -0
- package/ui/js/QuickLookRect.js +24 -0
- package/ui/js/RailDrop.js +51 -0
- package/ui/js/RailKeys.js +87 -0
- package/ui/js/RailMenu.js +133 -0
- package/ui/js/Recent.js +135 -0
- package/ui/js/RecentMode.js +156 -0
- package/ui/js/Reload.js +60 -0
- package/ui/js/Renderer.js +9 -0
- package/ui/js/RunFile.js +143 -0
- package/ui/js/Scripts.js +28 -0
- package/ui/js/Scroll.js +304 -0
- package/ui/js/ScrollOff.js +69 -0
- package/ui/js/Search.js +478 -0
- package/ui/js/Selection.js +97 -0
- package/ui/js/Settings.js +476 -0
- package/ui/js/SettingsAbout.js +97 -0
- package/ui/js/SettingsShelf.js +52 -0
- package/ui/js/SheetKeys.js +42 -0
- package/ui/js/SheetQuery.js +281 -0
- package/ui/js/ShelfPile.js +27 -0
- package/ui/js/SlowClick.js +53 -0
- package/ui/js/SlowOp.js +6 -0
- package/ui/js/Sort.js +134 -0
- package/ui/js/Startup.js +63 -0
- package/ui/js/StateWords.js +17 -0
- package/ui/js/Status.js +238 -0
- package/ui/js/Swap.js +166 -0
- package/ui/js/TabMove.js +39 -0
- package/ui/js/Tabs.js +751 -0
- package/ui/js/Tags.js +134 -0
- package/ui/js/Taildrop.js +128 -0
- package/ui/js/Tap.js +180 -0
- package/ui/js/TextSize.js +107 -0
- package/ui/js/Thumbs.js +158 -0
- package/ui/js/Transfer.js +172 -0
- package/ui/js/Trash.js +84 -0
- package/ui/js/TrashDates.js +47 -0
- package/ui/js/TrashDrop.js +64 -0
- package/ui/js/UiState.js +438 -0
- package/ui/js/Unlock.js +152 -0
- package/ui/js/Update.js +130 -0
- package/ui/js/UserDirs.js +56 -0
- package/ui/qmldir +230 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {test,expect,afterAll} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,mkdirSync,copyFileSync,writeFileSync,rmSync} from 'node:fs';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
const root=mkdtempSync(join(tmpdir(),'flea-highlight-'));
|
|
6
|
+
afterAll(()=>rmSync(root,{recursive:true,force:true}));
|
|
7
|
+
let serial=0;
|
|
8
|
+
async function run(name,{failure=false,sibling=false,filetype=null}={}) {
|
|
9
|
+
const dir=join(root,String(++serial)),src=join(dir,'flea','src'),bin=join(dir,'commands');
|
|
10
|
+
mkdirSync(src,{recursive:true});mkdirSync(bin);
|
|
11
|
+
// Isolate discovery from the real sibling checkout without changing it or using links.
|
|
12
|
+
copyFileSync(new URL('../src/highlight.js',import.meta.url),join(src,'highlight.js'));
|
|
13
|
+
const path=join(dir,'sample with spaces.js');writeFileSync(path,'const n = 1;');
|
|
14
|
+
const args=['--cat','--edit',...(filetype?['-filetype',filetype]:[]),path];
|
|
15
|
+
const body=`if (JSON.stringify(process.argv.slice(2)) !== JSON.stringify(${JSON.stringify(args)})) process.exit(9);\nprocess.stdout.write('\\x1b[31mconst\\x1b[0m n = 1;\\n');\n`;
|
|
16
|
+
writeFileSync(join(bin,name),`#!${process.execPath}\n${failure?'process.exit(7);':body}`,{mode:0o755});
|
|
17
|
+
if(sibling){const target=join(dir,'jsmdcui','src');mkdirSync(target,{recursive:true});writeFileSync(join(target,'index.js'),body);}
|
|
18
|
+
const script=join(src,'probe.js');
|
|
19
|
+
writeFileSync(script,`import {highlightFile,jsmdcui} from './highlight.js'; console.log(JSON.stringify({command:jsmdcui(),lines:await highlightFile(${JSON.stringify(path)},undefined,${JSON.stringify(filetype)})}));`);
|
|
20
|
+
const proc=Bun.spawn([process.execPath,script],{env:{...process.env,PATH:bin},stdout:'pipe',stderr:'pipe'});
|
|
21
|
+
const [text,err,code]=await Promise.all([new Response(proc.stdout).text(),new Response(proc.stderr).text(),proc.exited]);
|
|
22
|
+
expect({code,err}).toEqual({code:0,err:''});
|
|
23
|
+
return {result:JSON.parse(text),bin};
|
|
24
|
+
}
|
|
25
|
+
for(const name of ['jmi','jsmdcui'])test(`executes PATH ${name} launcher without guessing its install layout`,async()=>{
|
|
26
|
+
const {result,bin}=await run(name);expect(result.command).toEqual([join(bin,name)]);expect(result.lines[0]).toEqual([1,'const',0,' n = 1;']);
|
|
27
|
+
});
|
|
28
|
+
test('failed launcher preserves plain-text fallback',async()=>{expect((await run('jmi',{failure:true})).result.lines).toBeNull();});
|
|
29
|
+
test('sibling checkout still runs through Bun before PATH launchers',async()=>{
|
|
30
|
+
const {result}=await run('jmi',{failure:true,sibling:true});expect(result.command.length).toBe(2);expect(result.command[1]).toEndWith('/jsmdcui/src/index.js');expect(result.lines[0][0]).toBe(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('passes a language override as the single-dash jsmdcui setting',async()=>{
|
|
34
|
+
expect((await run('jmi',{filetype:'v'})).result.lines[0][0]).toBe(1);
|
|
35
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {previewHtml,html,HTML_LIMIT} from '../src/html.js';
|
|
3
|
+
import {mkdtemp,writeFile,rm} from 'node:fs/promises';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import {join} from 'node:path';
|
|
6
|
+
test('HTML is rebuilt with fixed tags, no source attributes, nested divs and literal entities',()=>{
|
|
7
|
+
expect(previewHtml('<div id=x><div onclick="evil()">A & B</div></div>')).toBe('<div><div>A & B</div></div>');
|
|
8
|
+
expect(previewHtml('<textarea><img src=x onerror="evil()"></textarea>')).toBe('<img src=x onerror="evil()">');
|
|
9
|
+
expect(previewHtml('<script>globalThis.__htmlProbe++</script><style>bad</style><p>OK</p>')).toBe('<p>OK</p>');
|
|
10
|
+
expect(previewHtml('<svg><foreignObject><b>hidden</b></foreignObject></svg><p>OK</p>')).toBe('<p>OK</p>');
|
|
11
|
+
expect(()=>previewHtml('x'.repeat(HTML_LIMIT+1))).toThrow();
|
|
12
|
+
});
|
|
13
|
+
test('HTML file replies are bounded and cancellation produces no result',async()=>{
|
|
14
|
+
const dir=await mkdtemp(join(tmpdir(),'flea-html-'));
|
|
15
|
+
try{
|
|
16
|
+
const path=join(dir,'test.html');await writeFile(path,'<h1 onmouseover="evil()">Hello</h1>');
|
|
17
|
+
expect(await html({path,token:4,slot:'look'})).toMatchObject({state:'ready',html:'<h1>Hello</h1>',token:4,slot:'look'});
|
|
18
|
+
expect(await html({path,cancel:true})).toBe(null);
|
|
19
|
+
expect((await html({path:dir})).state).toBe('failed');
|
|
20
|
+
await writeFile(path,'x'.repeat(HTML_LIMIT+1));expect((await html({path})).state).toBe('failed');
|
|
21
|
+
}finally{await rm(dir,{recursive:true,force:true})}
|
|
22
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
const source=(await Bun.file(new URL('../ui/js/Input.js',import.meta.url)).text()).replace(/^\.pragma library\s*/,'');
|
|
3
|
+
const warnings=[];
|
|
4
|
+
const {pasteInto}=new Function('console',source+'; return {pasteInto};')({warn: e=>warnings.push(e)});
|
|
5
|
+
test('paste synchronous and asynchronous clipboard text',async()=>{
|
|
6
|
+
const values=[];pasteInto({clipboardText:()=> 'hello\nother'},v=>values.push(v));
|
|
7
|
+
pasteInto({clipboardText:()=>Promise.resolve('中文😀')},v=>values.push(v));
|
|
8
|
+
await Bun.sleep(0);expect(values).toEqual(['hello','中文😀']);
|
|
9
|
+
});
|
|
10
|
+
test('clipboard exceptions and rejections are caught',async()=>{
|
|
11
|
+
warnings.length=0;
|
|
12
|
+
pasteInto({clipboardText:()=>{throw Error('sync')}},()=>{});
|
|
13
|
+
pasteInto({clipboardText:()=>Promise.reject(Error('async'))},()=>{});
|
|
14
|
+
await Bun.sleep(0);expect(warnings.length).toBe(2);
|
|
15
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// flea-qml's own changes to the generated key table (ui/js/Keymap.js), checked in every preset.
|
|
4
|
+
const source = (await Bun.file(new URL("../ui/js/Keymap.js", import.meta.url)).text()).replace(/^\.(?:pragma|import).*$/gm, "");
|
|
5
|
+
let next = 1000;
|
|
6
|
+
const named = { ControlModifier: 0x04000000, AltModifier: 0x08000000, ShiftModifier: 0x02000000, MetaModifier: 0x10000000,
|
|
7
|
+
Key_Return: 16777220, Key_Enter: 16777221, Key_D: 68, Key_I: 73, Key_Equal: 0x3d };
|
|
8
|
+
const Qt = new Proxy(named, { get: (t, k) => (k in t ? t[k] : (t[k] = next++)) });
|
|
9
|
+
const { lookupFor } = new Function("Qt", source + ";return {lookupFor};")(Qt);
|
|
10
|
+
const PRESETS = ["default", "vim", "mac", "windows"];
|
|
11
|
+
|
|
12
|
+
test("Alt+Enter opens Properties in every preset, as Windows, KDE and GNOME do", () => {
|
|
13
|
+
for (const preset of PRESETS) {
|
|
14
|
+
expect(lookupFor(preset, named.Key_Return, "", named.AltModifier)).toBe("properties");
|
|
15
|
+
expect(lookupFor(preset, named.Key_Enter, "", named.AltModifier)).toBe("properties");
|
|
16
|
+
}
|
|
17
|
+
expect(lookupFor("mac", named.Key_I, "", named.MetaModifier)).toBe("properties");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("= and Alt+= measure folders in every preset; Ctrl+= stays the text size", () => {
|
|
21
|
+
for (const preset of PRESETS) {
|
|
22
|
+
expect(lookupFor(preset, named.Key_Equal, "=", 0)).toBe("measureDirs");
|
|
23
|
+
expect(lookupFor(preset, named.Key_Equal, "", named.AltModifier)).toBe("measureAllDirs");
|
|
24
|
+
expect(lookupFor(preset, named.Key_Equal, "", named.ControlModifier)).toBe("textSizeUp");
|
|
25
|
+
}
|
|
26
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
async function load(name,exports,deps={}) {
|
|
3
|
+
const source=(await Bun.file(new URL('../ui/js/'+name+'.js',import.meta.url)).text()).replace(/^\.(?:pragma|import).*$/gm,'');
|
|
4
|
+
return new Function(...Object.keys(deps),source+';return {'+exports+'};')(...Object.values(deps));
|
|
5
|
+
}
|
|
6
|
+
const Icons=await load('Icons','isEmoji');
|
|
7
|
+
const Format=await load('Format','isSymlink');
|
|
8
|
+
|
|
9
|
+
test('emoji and directory rows never load language components, even during Find',async()=>{
|
|
10
|
+
const Lang=await load('LangMarks','slugForRow,syncMark',{Icons,Format,Qt:{createComponent(){throw Error('unnecessary component load')}}});
|
|
11
|
+
for(const row of [{n:'main.js',i:'📄',p:33188},{n:'code.py',i:'🐍',p:33188},{n:'folder.rs',d:true}]) {
|
|
12
|
+
const owner={langSlug:Lang.slugForRow(row),thumbDrawn:false,langLoader:null};
|
|
13
|
+
expect(owner.langSlug).toBe('');
|
|
14
|
+
Lang.syncMark(owner,{parent:{}},'LangMarkLoader.qml',1);
|
|
15
|
+
expect(owner.langLoader).toBeNull();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('visible language logos load once and release when replaced by emoji or thumbnails',async()=>{
|
|
20
|
+
let loads=0,released=0,componentReleased=0;
|
|
21
|
+
const Lang=await load('LangMarks','slugForRow,syncMark',{Icons,Format,Qt:{createComponent(){loads++;return {
|
|
22
|
+
status:1,createObject(){return {active:true,destroy(){released++;}}},destroy(){componentReleased++;}
|
|
23
|
+
};}}});
|
|
24
|
+
const owner={langSlug:Lang.slugForRow({n:'main.js',i:'file',p:33188}),thumbDrawn:false,langLoader:null};
|
|
25
|
+
const glyph={parent:{}};
|
|
26
|
+
expect(owner.langSlug).toBe('javascript');
|
|
27
|
+
Lang.syncMark(owner,glyph,'LangMarkLoader.qml',1);
|
|
28
|
+
Lang.syncMark(owner,glyph,'LangMarkLoader.qml',1);
|
|
29
|
+
expect(loads).toBe(1);expect(componentReleased).toBe(1);
|
|
30
|
+
owner.langSlug=Lang.slugForRow({n:'main.js',i:'📄',p:33188});
|
|
31
|
+
Lang.syncMark(owner,glyph,'LangMarkLoader.qml',1);
|
|
32
|
+
expect(released).toBe(1);expect(owner.langLoader).toBeNull();
|
|
33
|
+
owner.langSlug='javascript';owner.thumbDrawn=true;
|
|
34
|
+
Lang.syncMark(owner,glyph,'LangMarkLoader.qml',1);
|
|
35
|
+
expect(loads).toBe(1);
|
|
36
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {EventEmitter} from 'node:events';
|
|
3
|
+
import {browserPackage,browserCommand,selectFrontend,detectKitty,entryURL} from '../index.js';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
import info from '../package.json';
|
|
6
|
+
import {isAbsolute} from 'node:path';
|
|
7
|
+
|
|
8
|
+
test('launcher follows browser package manager order',()=>{
|
|
9
|
+
const which = names => name => names.includes(name) ? '/bin/'+name : null;
|
|
10
|
+
expect(browserCommand(['file:///a%20b.qml'],which(['npx','bun','bunx']))).toEqual(['/bin/npx','--loglevel=error','-p',browserPackage(),'--','buninu-browser','file:///a%20b.qml']);
|
|
11
|
+
expect(browserCommand([],which(['npx','bunx']))).toEqual(['/bin/bunx',browserPackage()]);
|
|
12
|
+
expect(browserCommand([],which(['bun']))).toEqual(['/bin/bun','x',browserPackage()]);
|
|
13
|
+
expect(browserCommand([],which([]),'/running/bun')).toEqual(['/running/bun','x',browserPackage()]);
|
|
14
|
+
expect(isAbsolute(fileURLToPath(entryURL))).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
test('explicit frontends override detection on Windows and Linux',()=>{
|
|
17
|
+
expect(selectFrontend([],true,'win32').frontend).toBe('--casty');
|
|
18
|
+
expect(selectFrontend([],false,'win32').frontend).toBe('--win32');
|
|
19
|
+
for(const platform of ['linux','android']) expect(selectFrontend([],false,platform).frontend).toBe('--wayland');
|
|
20
|
+
for(const flag of ['--casty','--wayland','--win32','--headless']) expect(selectFrontend([flag,'--help'],true,'win32')).toEqual({frontend:flag,args:['--help']});
|
|
21
|
+
expect(selectFrontend(['--future-frontend','arg'],false)).toEqual({frontend:'--future-frontend',args:['arg']});
|
|
22
|
+
});
|
|
23
|
+
function terminal(reply) {
|
|
24
|
+
const input=new EventEmitter(),output=new EventEmitter();
|
|
25
|
+
input.isTTY=output.isTTY=true;input.isRaw=false;input.paused=true;
|
|
26
|
+
input.isPaused=()=>input.paused;input.pause=()=>{input.paused=true;};input.resume=()=>{input.paused=false;};input.setRawMode=v=>{input.isRaw=v;};input.unshift=()=>{};
|
|
27
|
+
output.write=query=>{const id=/i=(\d+)/.exec(query)[1];if(reply!==null)queueMicrotask(()=>{input.emit('data',Buffer.from(`\x1b_Gi=${id};${reply}\x1b`));input.emit('data',Buffer.from('\\'));});};
|
|
28
|
+
return {input,output};
|
|
29
|
+
}
|
|
30
|
+
test('graphics query handles fragmented replies and restores terminal on timeout',async()=>{
|
|
31
|
+
for(const [reply,expected] of [['OK',true],['OK Buninu1 bgra',true],['EINVAL',false],[null,false]]) {
|
|
32
|
+
const {input,output}=terminal(reply);
|
|
33
|
+
expect(await detectKitty(input,output,5)).toBe(expected);
|
|
34
|
+
expect(input.isRaw).toBe(false);expect(input.paused).toBe(true);expect(input.listenerCount('data')).toBe(0);
|
|
35
|
+
}
|
|
36
|
+
expect(await detectKitty({isTTY:false},{isTTY:true})).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('browser package comes from the script without shell execution',()=>{
|
|
40
|
+
expect(browserPackage()).toBe(info.scripts.browser.split(/\s+/)[1]);
|
|
41
|
+
expect(browserPackage('bunx buninu-browser@9.8.7')).toBe('buninu-browser@9.8.7');
|
|
42
|
+
expect(()=>browserPackage('bunx buninu-browser@1; echo bad')).toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('version flags report the package and pinned browser versions', async()=>{
|
|
46
|
+
for (const flag of ['--version','-V']) {
|
|
47
|
+
const child = Bun.spawn([process.execPath, fileURLToPath(new URL('../index.js', import.meta.url)), flag], {stdout:'pipe',stderr:'pipe'});
|
|
48
|
+
expect(await new Response(child.stdout).text()).toBe(`flea-qml ${info.version}\n${browserPackage()} (pinned)\n`);
|
|
49
|
+
expect(await child.exited).toBe(0);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// bun test/manual-archive-here-ui.mjs (requires the sibling buninu-browser checkout).
|
|
2
|
+
import {mkdtempSync,mkdirSync,rmSync,existsSync} from 'node:fs';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
const repo=Bun.fileURLToPath(new URL('../',import.meta.url));
|
|
7
|
+
const dir=mkdtempSync(join(tmpdir(),'flea-terminal-ui-'));
|
|
8
|
+
process.env.XDG_DATA_HOME=join(dir,'data');process.env.XDG_CONFIG_HOME=join(dir,'config');process.env.XDG_CACHE_HOME=join(dir,'cache');process.env.FLEA_PATH=dir;process.env.BUNINU_BUN=process.execPath;
|
|
9
|
+
for(const p of ['data','config','cache'])mkdirSync(join(dir,p));
|
|
10
|
+
await Bun.write(join(dir,'notes.txt'),'line of preview text\n'.repeat(200));
|
|
11
|
+
const {createBrowser}=await import('../../buninu-browser/lib/headless-shell.js');
|
|
12
|
+
const file=join(repo,'ui/boot/.terminal-check-'+process.pid+'.qml');
|
|
13
|
+
await Bun.write(file,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nimport "../js/Tabs.js" as Tabs\nFloatingWindow { id: win; width:900; height:600; Loader {anchors.fill:parent; source:"../WindowBody.qml";onLoaded:{item.host=win;globalThis.testTabs=Tabs;globalThis.testBody=item;globalThis.terminalService=Flea.TerminalService}} }`);
|
|
14
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
15
|
+
const run=s=>browser.context.evaluate(s);
|
|
16
|
+
const until=async(s)=>{for(let i=0;i<200;i++){if(await run(s))return;await Bun.sleep(100);}throw Error('UI timeout '+s+' '+await run('document.body.innerText.slice(-2500)'));};
|
|
17
|
+
try {
|
|
18
|
+
const {archive}=await import('../src/archiveops.js');
|
|
19
|
+
const source=join(dir,'source');mkdirSync(source);
|
|
20
|
+
for(let i=0;i<8;i++)await Bun.write(join(source,`file${i}.txt`),'test');
|
|
21
|
+
await new Promise((resolve,reject)=>archive({op:'compress',paths:[source],dest:join(dir,'sample.zip'),format:'zip'},m=>{if(m.t==='archivedone')m.ok?resolve():reject(Error(m.err))}));
|
|
22
|
+
await browser.context.navigate(Bun.pathToFileURL(file).href);
|
|
23
|
+
await until('!!globalThis.testBody && !testBody.currentPane.listInFlight');
|
|
24
|
+
await run('for(let i=0;i<testBody.currentPane.total;i++)if(testBody.currentPane.rowFor(i)?.n==="sample.zip")testBody.currentPane.selectOnly(i,0)');
|
|
25
|
+
await until('testBody.currentPane.previewColumnItem?.meta?.topEntries===1');
|
|
26
|
+
assert.equal(await run('testBody.currentPane.previewColumnItem.meta.entries'),9);
|
|
27
|
+
console.log('PASS right preview carries 9 members / 1 top-level item');
|
|
28
|
+
await run('testBody.currentPane.act("preview")');
|
|
29
|
+
await until('testBody.currentPane.quickLook().archiveMeta?.topEntries===1');
|
|
30
|
+
assert.equal(await run('testBody.currentPane.quickLook().archiveMeta.entries'),9);
|
|
31
|
+
console.log('PASS Quick Look carries 9 members / 1 top-level item');
|
|
32
|
+
const button=await run('(()=>{const e=[...document.querySelectorAll("[data-qml-type=Text]")].filter(e=>e.textContent==="Extract here"&&e.getBoundingClientRect().width>0).at(-1);const r=e.getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2}})()');
|
|
33
|
+
await browser.context.click(button.x,button.y);
|
|
34
|
+
for(let i=0;i<100&&!existsSync(join(dir,'source copy'));i++)await Bun.sleep(100);
|
|
35
|
+
assert(existsSync(join(dir,'source copy')),'Extract here should run without confirmation');
|
|
36
|
+
console.log('PASS clicking Quick Look Extract here extracts without a confirmation dialog');
|
|
37
|
+
await run('testBody.currentPane.quickLook().close()');
|
|
38
|
+
await until('!testBody.currentPane.listInFlight');
|
|
39
|
+
await run('for(let i=0;i<testBody.currentPane.total;i++)if(testBody.currentPane.rowFor(i)?.n==="sample.zip")testBody.currentPane.selectOnly(i,0)');
|
|
40
|
+
await until('testBody.currentPane.previewColumnItem?.meta?.topEntries===1');
|
|
41
|
+
const right=await run('(()=>{const e=[...document.querySelectorAll("[data-qml-type=Text]")].find(e=>e.textContent==="Extract here"&&e.getBoundingClientRect().width>0);const r=e.getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2}})()');
|
|
42
|
+
await browser.context.click(right.x,right.y);
|
|
43
|
+
for(let i=0;i<100&&!existsSync(join(dir,'source copy 2'));i++)await Bun.sleep(100);
|
|
44
|
+
assert(existsSync(join(dir,'source copy 2')),'right preview Extract here should run without confirmation');
|
|
45
|
+
console.log('PASS clicking right preview Extract here extracts without confirmation');
|
|
46
|
+
|
|
47
|
+
}finally{await browser.close();rmSync(file,{force:true});rmSync(dir,{recursive:true,force:true});}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// bun test/manual-code-scroll.mjs (requires sibling buninu-browser).
|
|
2
|
+
import {rm} from 'node:fs/promises';
|
|
3
|
+
import {createBrowser} from '../../buninu-browser/lib/headless-shell.js';
|
|
4
|
+
const repo=Bun.fileURLToPath(new URL('../',import.meta.url));
|
|
5
|
+
const qml=repo+'ui/boot/.scroll-check-'+process.pid+'.qml';
|
|
6
|
+
const text='x'.repeat(512)+'/system/bin/linker64\n'+Array.from({length:150},(_,i)=>'line '+i).join('\n');
|
|
7
|
+
await Bun.write(qml,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nFloatingWindow {width:500;height:300;color:"#171b22";Flea.CodeLines {id:code;anchors.fill:parent;fromText:true;text:${JSON.stringify(text)};active:true;scrolls:true;Component.onCompleted:globalThis.code=code;Flea.FastScrollHandler {flickable:code.viewport}}}`);
|
|
8
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
9
|
+
try{
|
|
10
|
+
await browser.context.navigate(Bun.pathToFileURL(qml).href);
|
|
11
|
+
await Bun.sleep(300);
|
|
12
|
+
const run=s=>browser.context.evaluate(s);
|
|
13
|
+
const before=await run('({w:code.viewport.width,cw:code.viewport.contentWidth,h:code.viewport.height,ch:code.viewport.contentHeight})');
|
|
14
|
+
if(before.cw<=before.w||before.ch<=before.h)throw Error(JSON.stringify(before));
|
|
15
|
+
await run('document.elementFromPoint(200,100).dispatchEvent(new WheelEvent("wheel",{bubbles:true,cancelable:true,clientX:200,clientY:100,deltaX:200,deltaY:0}))');
|
|
16
|
+
await Bun.sleep(100);
|
|
17
|
+
if(!(await run('code.viewport.contentX > 0')))throw Error('Horizontal wheel failed');
|
|
18
|
+
await run('document.elementFromPoint(200,100).dispatchEvent(new WheelEvent("wheel",{bubbles:true,cancelable:true,clientX:200,clientY:100,deltaX:0,deltaY:150}))');
|
|
19
|
+
await Bun.sleep(100);
|
|
20
|
+
if(!(await run('code.viewport.contentY > 0')))throw Error('Vertical wheel failed');
|
|
21
|
+
await run('code.viewport.contentY=0;code.viewport.contentX=code.viewport.contentWidth-code.viewport.width');
|
|
22
|
+
await Bun.write('/tmp/flea-code-scroll.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
|
|
23
|
+
await run('code.wrap=true');await Bun.sleep(100);
|
|
24
|
+
if(!await run('code.viewport.contentX===0 && code.viewport.contentWidth===code.viewport.width'))throw Error('Wrap did not reset horizontal lane');
|
|
25
|
+
await run('code.wrap=false;code.text="short"');await Bun.sleep(100);
|
|
26
|
+
if(!await run('code.viewport.contentX===0 && code.viewport.contentWidth===code.viewport.width'))throw Error('New file did not reset width');
|
|
27
|
+
console.log('PASS horizontal/vertical wheel, wrap and new file reset',before);
|
|
28
|
+
}finally{await browser.close();await rm(qml,{force:true})}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// bun test/manual-column-scroll.mjs: complete Flea preview column, not bare CodeLines.
|
|
2
|
+
import {mkdtempSync,mkdirSync,rmSync} from 'node:fs';
|
|
3
|
+
import {join} from 'node:path';import {tmpdir} from 'node:os';
|
|
4
|
+
const repo=Bun.fileURLToPath(new URL('../',import.meta.url));const dir=mkdtempSync(join(tmpdir(),'flea-column-scroll-'));
|
|
5
|
+
for(const [key,sub] of [['HOME','home'],['XDG_DATA_HOME','data'],['XDG_CONFIG_HOME','config'],['XDG_CACHE_HOME','cache']]){process.env[key]=join(dir,sub);mkdirSync(process.env[key])}
|
|
6
|
+
process.env.FLEA_PATH=dir;process.env.BUNINU_BUN=process.execPath;
|
|
7
|
+
await Bun.write(join(dir,'long.txt'),'x'.repeat(512)+'/system/bin/linker64\n'+Array.from({length:200},(_,i)=>'line '+i).join('\n'));
|
|
8
|
+
const {createBrowser}=await import('../../buninu-browser/lib/headless-shell.js');
|
|
9
|
+
const qml=join(repo,'ui/boot/.column-scroll-'+process.pid+'.qml');
|
|
10
|
+
await Bun.write(qml,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nFloatingWindow {id:win;width:900;height:600;Loader{anchors.fill:parent;Component.onCompleted:setSource("${Bun.pathToFileURL(join(repo,'ui/WindowBody.qml')).href}");onLoaded:{item.host=win;globalThis.body=item}}}`);
|
|
11
|
+
const browser=await createBrowser({spareRenderer:false});const run=s=>browser.context.evaluate(s);
|
|
12
|
+
const wait=async s=>{for(let i=0;i<150;i++){if(await run(s))return;await Bun.sleep(100)}throw Error('Timeout '+s)};
|
|
13
|
+
try{
|
|
14
|
+
await browser.context.navigate(Bun.pathToFileURL(qml).href);
|
|
15
|
+
await wait('!!globalThis.body && !body.currentPane.listInFlight');
|
|
16
|
+
await run('body.currentPane.viewMode="columns"');await Bun.sleep(500);await run('body.currentPane.setCursor(body.currentPane.rows.findIndex(r=>r.n==="long.txt"))');
|
|
17
|
+
await wait('!!body.currentPane.columnsArea?.previewColumn?.linesItem?.code?.firstScreen');
|
|
18
|
+
await run('globalThis.col=body.currentPane.columnsArea.previewColumn;globalThis.v=col.linesItem.viewport');
|
|
19
|
+
const rect=await run('(()=>{const r=__qmlRuntime.element(col.frameItem).getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2}})()');
|
|
20
|
+
const wheel=async(dx,dy)=>{await run(`document.elementFromPoint(${rect.x},${rect.y}).dispatchEvent(new WheelEvent("wheel",{bubbles:true,cancelable:true,clientX:${rect.x},clientY:${rect.y},deltaX:${dx},deltaY:${dy}}))`);await Bun.sleep(200)};
|
|
21
|
+
await wheel(180,0);if(!await run('v.contentX>0'))throw Error('Column horizontal wheel failed');
|
|
22
|
+
await wheel(0,100);await wait('v.contentY>0');
|
|
23
|
+
console.log('PASS complete preview column horizontal and vertical wheel',await run('({x:v.contentX,y:v.contentY,cw:v.contentWidth,ch:v.contentHeight})'));
|
|
24
|
+
}finally{await browser.close();rmSync(qml,{force:true});rmSync(dir,{recursive:true,force:true})}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Actual JS backend -> bunmsh -cc builtin diff -> QML CompareView.
|
|
2
|
+
import {mkdtemp,rm} from 'node:fs/promises';import {join} from 'node:path';import {tmpdir} from 'node:os';
|
|
3
|
+
import {createBrowser} from '../../buninu-browser/lib/headless-shell.js';
|
|
4
|
+
const dir=await mkdtemp(join(tmpdir(),'flea-compare-ui-'));
|
|
5
|
+
const repo=Bun.fileURLToPath(new URL('../',import.meta.url));
|
|
6
|
+
const qml=join(repo,'ui/boot/.compare-'+process.pid+'.qml');
|
|
7
|
+
const a=join(dir,'before.txt'),b=join(dir,'after.txt');
|
|
8
|
+
process.env.BUNINU_BUN=process.execPath;
|
|
9
|
+
await Bun.write(a,'first\nold\nlast\n');await Bun.write(b,'first\nnew\nlast\n');
|
|
10
|
+
await Bun.write(qml,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nFloatingWindow{width:640;height:480;color:"#171b22";Flea.Backend{id:backend} Flea.CompareView{id:view;anchors.fill:parent;backend:backend;active:true;pathA:${JSON.stringify(a)};pathB:${JSON.stringify(b)};Component.onCompleted:globalThis.comparison=view}}`);
|
|
11
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
12
|
+
try{
|
|
13
|
+
await browser.context.navigate(Bun.pathToFileURL(qml).href);
|
|
14
|
+
let state;for(let i=0;i<100;i++){state=await browser.context.evaluate('globalThis.comparison?.answer');if(state)break;await Bun.sleep(100)}
|
|
15
|
+
if(state!=='diff')throw Error('Compare answer: '+state);
|
|
16
|
+
const kinds=await browser.context.evaluate('comparison.rows.map(r=>r.kind)');
|
|
17
|
+
if(!kinds.includes('removed')||!kinds.includes('added'))throw Error(JSON.stringify(kinds));
|
|
18
|
+
await Bun.write('/tmp/flea-compare.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
|
|
19
|
+
console.log('PASS real backend and QML CompareView',kinds);
|
|
20
|
+
}finally{await browser.close();await rm(qml,{force:true});await rm(dir,{recursive:true,force:true})}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Exercise the real status bar bindings, including completed Find and ordinary lists.
|
|
2
|
+
import {rm} from 'node:fs/promises';
|
|
3
|
+
import {createBrowser} from '../../buninu-browser/lib/headless-shell.js';
|
|
4
|
+
const qml=new URL('../ui/boot/.find-status-'+process.pid+'.qml',import.meta.url);
|
|
5
|
+
process.env.BUNINU_BUN=process.execPath;
|
|
6
|
+
await Bun.write(qml,`import QtQuick
|
|
7
|
+
import Quickshell
|
|
8
|
+
import ".." as Flea
|
|
9
|
+
FloatingWindow {
|
|
10
|
+
width: 1000; height: 100; color: "#171b22"
|
|
11
|
+
Flea.StatusBar {
|
|
12
|
+
id: bar; width: parent.width
|
|
13
|
+
listingState: "ready"; total: 2
|
|
14
|
+
searchLine: "2 found in 0.4 s"; searchRunning: false
|
|
15
|
+
pane: ({
|
|
16
|
+
selectionVersion: 1, selection: {count: function(){return 0}, indices: function(){return []}},
|
|
17
|
+
cursorIndex: 0, shown: null, shownTotal: 2, rows: [{n:"sub/完整的 long filename.txt",s:123}],
|
|
18
|
+
cursorRow: {n:"sub/完整的 long filename.txt",s:123},
|
|
19
|
+
rowFor: function(i){return this.rows[i]}
|
|
20
|
+
})
|
|
21
|
+
Component.onCompleted: globalThis.bar = bar
|
|
22
|
+
}
|
|
23
|
+
}`);
|
|
24
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
25
|
+
function assert(ok,message){if(!ok)throw Error(message)}
|
|
26
|
+
try{
|
|
27
|
+
await browser.context.navigate(Bun.pathToFileURL(Bun.fileURLToPath(qml)).href);
|
|
28
|
+
for(let i=0;i<100;i++){if(await browser.context.evaluate('!!globalThis.bar?.fileNameItem'))break;await Bun.sleep(50)}
|
|
29
|
+
let state=await browser.context.evaluate('({shown:bar.showFileName,label:bar.fileLabel,count:bar.countText(),visible:bar.fileNameItem?.visible})');
|
|
30
|
+
assert(state.shown&&state.visible&&state.label.includes('123')&&state.label.endsWith('完整的 long filename.txt'),JSON.stringify(state));
|
|
31
|
+
assert(state.count==='2 found in 0.4 s',JSON.stringify(state));
|
|
32
|
+
await browser.context.evaluate('bar.searchRunning=true');
|
|
33
|
+
assert(await browser.context.evaluate('!bar.showFileName && bar.centreText()===bar.searchLine'),'live progress lost');
|
|
34
|
+
await browser.context.evaluate('bar.searchRunning=false; bar.searchLine=""');
|
|
35
|
+
assert(await browser.context.evaluate('bar.showFileName && bar.countText()==="2 items"'),'ordinary listing regressed');
|
|
36
|
+
console.log('PASS Find filename, file size, completed summary, running progress and ordinary listing');
|
|
37
|
+
}finally{await browser.close();await rm(qml,{force:true})}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// bun test/manual-terminal-ui.mjs (requires the sibling buninu-browser checkout).
|
|
2
|
+
import {mkdtempSync,mkdirSync,rmSync} from 'node:fs';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
const repo=Bun.fileURLToPath(new URL('../',import.meta.url));
|
|
7
|
+
const dir=mkdtempSync(join(tmpdir(),'flea-terminal-ui-'));
|
|
8
|
+
process.env.XDG_DATA_HOME=join(dir,'data');process.env.XDG_CONFIG_HOME=join(dir,'config');process.env.XDG_CACHE_HOME=join(dir,'cache');process.env.FLEA_PATH=dir;process.env.BUNINU_BUN=process.execPath;
|
|
9
|
+
for(const p of ['data','config','cache'])mkdirSync(join(dir,p));
|
|
10
|
+
await Bun.write(join(dir,'notes.txt'),'line of preview text\n'.repeat(200));
|
|
11
|
+
const {createBrowser}=await import('../../buninu-browser/lib/headless-shell.js');
|
|
12
|
+
const file=join(repo,'ui/boot/.terminal-check-'+process.pid+'.qml');
|
|
13
|
+
await Bun.write(file,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nimport "../js/Tabs.js" as Tabs\nFloatingWindow { id: win; width:900; height:600; Loader {anchors.fill:parent; source:"../WindowBody.qml";onLoaded:{item.host=win;globalThis.testTabs=Tabs;globalThis.testBody=item;globalThis.terminalService=Flea.TerminalService}} }`);
|
|
14
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
15
|
+
const run=s=>browser.context.evaluate(s);
|
|
16
|
+
const until=async(s)=>{for(let i=0;i<200;i++){if(await run(s))return;await Bun.sleep(100);}throw Error('UI timeout '+s+' '+await run('document.body.innerText.slice(-2500)'));};
|
|
17
|
+
try {
|
|
18
|
+
await browser.context.navigate(Bun.pathToFileURL(file).href);
|
|
19
|
+
await until('!!globalThis.testBody && !testBody.currentPane.listInFlight');
|
|
20
|
+
|
|
21
|
+
await run('testBody.currentPane.act("newFolder")');
|
|
22
|
+
await until('testBody.currentPane.renamingIndex>=0 && document.activeElement.tagName==="INPUT"');
|
|
23
|
+
await browser.context.type('Created Folder');
|
|
24
|
+
await browser.context.press('Enter');
|
|
25
|
+
await until('testBody.currentPane.renamingIndex<0 && !testBody.currentPane.listInFlight');
|
|
26
|
+
await until('testBody.currentPane.activeFocus');
|
|
27
|
+
await Bun.sleep(300);
|
|
28
|
+
const before=await run('testBody.currentPane.cursorIndex');
|
|
29
|
+
await browser.context.press(before>0?'ArrowUp':'ArrowDown');
|
|
30
|
+
assert.equal(await run('testBody.currentPane.cursorIndex'),before>0?before-1:before+1);
|
|
31
|
+
console.log('PASS mkdir + rename + ArrowDown');
|
|
32
|
+
await run('document.activeElement.blur()');
|
|
33
|
+
const pos=await run('(()=>{const r=__qmlRuntime.element(testBody.currentPane.visibleItemFor(Array.from({length:testBody.currentPane.total},(_,i)=>i).find(i=>testBody.currentPane.rowFor(i)?.n==="notes.txt"))).getBoundingClientRect();return {x:r.x+40,y:r.y+r.height/2}})()');
|
|
34
|
+
await browser.context.click(pos.x,pos.y);
|
|
35
|
+
await until('testBody.currentPane.activeFocus');
|
|
36
|
+
const clicked=await run('testBody.currentPane.cursorIndex');
|
|
37
|
+
await browser.context.press('ArrowUp');
|
|
38
|
+
assert.equal(await run('testBody.currentPane.cursorIndex'),clicked-1);
|
|
39
|
+
console.log('PASS clicking row recovers list focus from BODY');
|
|
40
|
+
// Empty list space also takes focus, independently of a row TapHandler.
|
|
41
|
+
const point=async expr=>run(`(()=>{const r=__qmlRuntime.element(${expr}).getBoundingClientRect();return {x:r.x+r.width/2,y:r.bottom-15}})()`);
|
|
42
|
+
await run('document.activeElement.blur()');
|
|
43
|
+
const ground=await point('testBody.currentPane.listArea');
|
|
44
|
+
await browser.context.click(ground.x,ground.y);
|
|
45
|
+
await until('testBody.currentPane.listArea.activeFocus');
|
|
46
|
+
console.log('PASS empty listing ground takes focus');
|
|
47
|
+
await run('document.activeElement.blur()');
|
|
48
|
+
const rail=await point('testBody.currentPane.sidebar');
|
|
49
|
+
await browser.context.click(rail.x,rail.y);
|
|
50
|
+
await until('testBody.currentPane.sidebar.activeFocus');
|
|
51
|
+
const railIndex=await run('testBody.currentPane.sidebar.cursorIndex');
|
|
52
|
+
await browser.context.press('ArrowDown');
|
|
53
|
+
assert.equal(await run('testBody.currentPane.sidebar.cursorIndex'),railIndex+1);
|
|
54
|
+
console.log('PASS Places empty ground takes focus and arrow keys');
|
|
55
|
+
await run('testBody.currentPane.focusView="list";for(let i=0;i<testBody.currentPane.total;i++)if(testBody.currentPane.rowFor(i)?.n==="notes.txt")testBody.currentPane.selectOnly(i,0)');
|
|
56
|
+
await until('!!testBody.currentPane.previewColumnItem?.visible');
|
|
57
|
+
const preview=await point('testBody.currentPane.previewColumnItem');
|
|
58
|
+
await run('document.activeElement.blur()');
|
|
59
|
+
await browser.context.click(preview.x,preview.y);
|
|
60
|
+
await until('testBody.currentPane.previewColumnItem.activeFocus');
|
|
61
|
+
console.log('PASS preview ground takes keyboard focus');
|
|
62
|
+
|
|
63
|
+
}finally{await browser.close();rmSync(file,{force:true});rmSync(dir,{recursive:true,force:true});}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// bun test/manual-html-security.mjs; uses sibling browser, with scripts enabled.
|
|
2
|
+
// The attack only increments a harmless global counter. Never run raw fixtures.
|
|
3
|
+
import {previewHtml,HTML_TAGS} from '../src/html.js';
|
|
4
|
+
import {createBrowser} from '../../buninu-browser/lib/headless-shell.js';
|
|
5
|
+
const probe='globalThis.__htmlProbe=(globalThis.__htmlProbe||0)+1';
|
|
6
|
+
const payloads=[`<img src=x onerror="${probe}">`,`<script>${probe}</script>`,`<svg onload="${probe}"></svg>`,`<iframe srcdoc="<script>${probe}</script>"></iframe>`,`<p onclick="${probe}">click</p>`,'<img src=x onerror=alert(1)>','<!--><img src=x onerror="'+probe+'">'];
|
|
7
|
+
const wrappers=['div','textarea','plaintext','xmp','title','noscript','select','option','table','svg','math','template','script','style','iframe'];
|
|
8
|
+
const inputs=[];
|
|
9
|
+
for(const a of wrappers)for(const b of wrappers)for(const p of payloads)inputs.push(`<${a}><${b}>${p}</${a}></${b}><p>tail</p>`);
|
|
10
|
+
inputs.push('<math><mtext><table><mglyph><style><!--</style><img title="--><img src=x onerror='+probe+'>">');
|
|
11
|
+
const outputs=inputs.map(previewHtml);
|
|
12
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
13
|
+
try{
|
|
14
|
+
await browser.context.navigate('about:blank');
|
|
15
|
+
// First prove the harmless probe works in this execution context.
|
|
16
|
+
await browser.context.evaluate(probe);
|
|
17
|
+
if(await browser.context.evaluate('globalThis.__htmlProbe')!==1)throw Error('Probe did not execute');
|
|
18
|
+
await browser.context.evaluate(`globalThis.__htmlProbe=0;globalThis.__bad=[];const allowed=new Set(${JSON.stringify([...HTML_TAGS])});for(const html of ${JSON.stringify(outputs)}){const host=document.createElement('div');host.innerHTML=html;document.body.appendChild(host);for(const el of host.querySelectorAll('*')){if(!allowed.has(el.localName)||el.attributes.length||el.namespaceURI!=='http://www.w3.org/1999/xhtml')__bad.push(el.outerHTML);el.dispatchEvent(new Event('click'));el.dispatchEvent(new Event('error'));el.dispatchEvent(new Event('load'));}host.remove();}`);
|
|
19
|
+
await Bun.sleep(100);
|
|
20
|
+
const result=await browser.context.evaluate('({executed:__htmlProbe,bad:__bad})');
|
|
21
|
+
if(result.executed||result.bad.length)throw Error(JSON.stringify(result));
|
|
22
|
+
console.log('PASS',outputs.length,'adversarial fragments; no illegal elements/attributes or script execution');
|
|
23
|
+
}finally{await browser.close()}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// bun test/manual-html-ui.mjs; real backend + QML rich-text preview, no external resources.
|
|
2
|
+
import {mkdtemp,rm} from 'node:fs/promises';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import {createBrowser} from '../../buninu-browser/lib/headless-shell.js';
|
|
6
|
+
const repo=Bun.fileURLToPath(new URL('../',import.meta.url));
|
|
7
|
+
const dir=await mkdtemp(join(tmpdir(),'flea-html-ui-'));
|
|
8
|
+
const qml=join(repo,'ui/boot/.html-check-'+process.pid+'.qml');
|
|
9
|
+
const path=join(dir,'preview.html');
|
|
10
|
+
process.env.BUNINU_BUN=process.execPath;
|
|
11
|
+
await Bun.write(path,'<h1 onclick="globalThis.__htmlProbe=1">HTML preview</h1><div><b>Bold</b> & <i>italic</i><div>Nested div</div></div><textarea><img src=x onerror="globalThis.__htmlProbe=1"></textarea>'+ '<p>Scrollable text</p>'.repeat(80));
|
|
12
|
+
await Bun.write(qml,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nFloatingWindow {width:640;height:480;color:"#171b22";Flea.Backend{id:backend} Loader {anchors.fill:parent;Component.onCompleted:setSource("${Bun.pathToFileURL(join(repo,'ui/PreviewHtml.qml')).href}");onLoaded:{item.backend=backend;item.path=${JSON.stringify(path)};item.active=true;globalThis.preview=item}}}`);
|
|
13
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
14
|
+
try{
|
|
15
|
+
await browser.context.navigate(Bun.pathToFileURL(qml).href);
|
|
16
|
+
let ready=false;
|
|
17
|
+
for(let i=0;i<150;i++){ready=await browser.context.evaluate('!!globalThis.preview && preview.contentReady');if(ready)break;await Bun.sleep(100)}
|
|
18
|
+
if(!ready)throw Error('Preview never ready: '+await browser.context.evaluate('document.body.innerText'));
|
|
19
|
+
const result=await browser.context.evaluate('({text:document.body.innerText,probe:globalThis.__htmlProbe||0,height:preview.viewport.contentHeight,viewport:preview.viewport.height})');
|
|
20
|
+
if(!result.text.includes('Nested div')||result.probe||result.height<=result.viewport)throw Error(JSON.stringify(result));
|
|
21
|
+
await browser.context.evaluate('preview.scrollBy(5)');
|
|
22
|
+
if(!(await browser.context.evaluate('preview.scrollY > 0')))throw Error('Scroll failed');
|
|
23
|
+
await browser.context.evaluate('preview.viewport.contentY=0');
|
|
24
|
+
await Bun.write('/tmp/flea-html-preview.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
|
|
25
|
+
await browser.context.evaluate('preview.width=240');
|
|
26
|
+
const boxes=await browser.context.evaluate(`(()=>{const e=document.querySelector('h1'),p=document.querySelector('p');return {heading:e.getBoundingClientRect(),paragraph:p.getBoundingClientRect(),content:preview.viewport.contentHeight}})()`);
|
|
27
|
+
if(boxes.paragraph.top<boxes.heading.bottom)throw Error('Heading overlaps paragraph: '+JSON.stringify(boxes));
|
|
28
|
+
await Bun.write('/tmp/flea-html-preview-narrow.png',Buffer.from(await browser.context.screenshot({format:'png'}),'base64'));
|
|
29
|
+
console.log('PASS backend HTML, nested rich text, literal attack text and scrolling',result.height,result.viewport);
|
|
30
|
+
}finally{await browser.close();await rm(qml,{force:true});await rm(dir,{recursive:true,force:true})}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Run: bun test/manual-syntax-preview.mjs [path/to/jsmdcui]
|
|
2
|
+
// Uses that checkout's runtime/syntax definitions and syntax-parity samples.
|
|
3
|
+
import {resolve,join} from 'node:path';
|
|
4
|
+
const base=resolve(process.argv[2] || new URL('../../jsmdcui',import.meta.url).pathname);
|
|
5
|
+
const {SyntaxDefinition,parseHeaderYaml,detectSyntax}=await import(Bun.pathToFileURL(join(base,'src/highlight/parser.js')).href);
|
|
6
|
+
import {code} from '../src/code.js';
|
|
7
|
+
import {runsOf} from '../src/highlight.js';
|
|
8
|
+
import {tmpdir} from 'node:os';
|
|
9
|
+
import {mkdtempSync,writeFileSync,rmSync} from 'node:fs';
|
|
10
|
+
const defs=[];
|
|
11
|
+
for(const file of new Bun.Glob('*.yaml').scanSync(base+'/runtime/syntax')) {const text=await Bun.file(base+'/runtime/syntax/'+file).text(),src=Bun.YAML.parse(text);defs.push({file,def:new SyntaxDefinition(parseHeaderYaml(src),src,text)});}
|
|
12
|
+
const dir=mkdtempSync(join(tmpdir(),'flea-syntax-'));const results=[];
|
|
13
|
+
const words=new Set(['Dockerfile','Containerfile','.bashrc','Makefile','CMakeLists.txt','README.md','Cargo.lock','sample.dats','sample.f90','sample.ebuild','sample.eclass','gitconfig','git-rebase-todo','go.mod','meson.build','sample.mm','sample.pl','sample.ps1','Xresources','sample.yml','sample.zig','sample.zc']);
|
|
14
|
+
for(const {def} of defs) for(const w of (def.header.filename||'').match(/[A-Za-z0-9_-]+/g)||[]) {words.add(w);words.add('.'+w);words.add('sample.'+w);words.add(w+'.conf');}
|
|
15
|
+
let index=0;
|
|
16
|
+
try {await Promise.all(Array.from({length:4},async()=>{for(;;){const i=index++;if(i>=defs.length)return;const {file,def}=defs[i];if(/^html[45]?$/.test(def.filetype))continue;
|
|
17
|
+
const sample=Bun.file(base+'/tests/syntax-parity/samples/'+file.replace(/yaml$/,'txt'));if(!await sample.exists()){results.push({file,error:'missing sample'});continue;}
|
|
18
|
+
const text=await sample.text();
|
|
19
|
+
const child=Bun.spawn([process.execPath,join(base,'src/index.js'),'--cat','--edit','-filetype',def.filetype,sample.name],{stdout:'pipe',stderr:'pipe'});
|
|
20
|
+
const [ansi,stderr,exit]=await Promise.all([new Response(child.stdout).text(),new Response(child.stderr).text(),child.exited]);
|
|
21
|
+
const explicitColour=exit===0 && ansi.split('\n').some(l=>runsOf(l).some((v,i)=>i%2===0&&v!==0));
|
|
22
|
+
const path=[...words].find(p=>def.header.matchFileName(p)&&detectSyntax(defs.map(x=>x.def),{path:p,firstLine:text.split('\n')[0],lines:text.split('\n').slice(0,50)})?.filetype===def.filetype);
|
|
23
|
+
if(!path){results.push({file,explicitColour,error:'no automatic filename witness',pattern:def.header.filename});continue;}
|
|
24
|
+
const target=dir+'/'+i;await Bun.write(target+'/'+path,text);const r=await code({path:target+'/'+path});results.push({file,explicitColour,path,state:r.state,lines:r.total});
|
|
25
|
+
}}));}finally{rmSync(dir,{recursive:true,force:true});}
|
|
26
|
+
results.sort((a,b)=>a.file.localeCompare(b.file));await Bun.write(join(tmpdir(),'flea-syntax-results.json'),JSON.stringify(results,null,2));console.log(JSON.stringify({total:results.length,explicitColour:results.filter(r=>r.explicitColour).length,styled:results.filter(r=>r.state==='styled').length,other:results.filter(r=>r.state!=='styled')},null,2));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// bun test/manual-terminal-ui.mjs (requires the sibling buninu-browser checkout).
|
|
2
|
+
import {mkdtempSync,mkdirSync,rmSync} from 'node:fs';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
const repo=Bun.fileURLToPath(new URL('../',import.meta.url));
|
|
7
|
+
const dir=mkdtempSync(join(tmpdir(),'flea-terminal-ui-'));
|
|
8
|
+
process.env.XDG_DATA_HOME=join(dir,'data');process.env.XDG_CONFIG_HOME=join(dir,'config');process.env.XDG_CACHE_HOME=join(dir,'cache');process.env.FLEA_PATH=dir;process.env.BUNINU_BUN=process.execPath;
|
|
9
|
+
for(const p of ['data','config','cache'])mkdirSync(join(dir,p));
|
|
10
|
+
const {createBrowser}=await import('../../buninu-browser/lib/headless-shell.js');
|
|
11
|
+
const file=join(repo,'ui/boot/.terminal-check-'+process.pid+'.qml');
|
|
12
|
+
await Bun.write(file,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nFloatingWindow { id: win; width:900; height:600; Loader {anchors.fill:parent; source:"../WindowBody.qml";onLoaded:{item.host=win;globalThis.testBody=item;globalThis.terminalService=Flea.TerminalService}} }`);
|
|
13
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
14
|
+
const run=s=>browser.context.evaluate(s);
|
|
15
|
+
const until=async(s)=>{for(let i=0;i<200;i++){if(await run(s))return;await Bun.sleep(100);}throw Error('UI timeout '+s+' '+await run('document.body.innerText.slice(-2500)'));};
|
|
16
|
+
try {
|
|
17
|
+
await browser.context.navigate(Bun.pathToFileURL(file).href);
|
|
18
|
+
await until('!!globalThis.testBody && !testBody.currentPane.listInFlight');
|
|
19
|
+
await run('testBody.currentPane.openTerminal()');
|
|
20
|
+
await until('!!testBody.currentPane.terminalId');
|
|
21
|
+
await until('Object.values(terminalService.tabs).some(t=>t.url?.includes("reconnect-token"))');
|
|
22
|
+
const one=await run('Object.values(terminalService.tabs)[0].url');
|
|
23
|
+
await run('testBody.currentPane.openTerminal()');
|
|
24
|
+
await until('Object.values(terminalService.tabs).length===2 && Object.values(terminalService.tabs).every(t=>t.url?.includes("reconnect-token"))');
|
|
25
|
+
const urls=await run('Object.values(terminalService.tabs).map(t=>t.url)');
|
|
26
|
+
assert.equal(new URL(urls[0]).host,new URL(urls[1]).host);assert.notEqual(urls[0],urls[1]);
|
|
27
|
+
assert.equal(await run('testBody.currentPane.tabs.items.length'),3);
|
|
28
|
+
console.log('PASS two terminal tabs, shared listener, distinct sessions',new URL(one).port);
|
|
29
|
+
await run('for (const id of Object.keys(terminalService.tabs)) terminalService.release(id)');
|
|
30
|
+
await until('!terminalService.stopping && !terminalService.process.running');
|
|
31
|
+
let closed=false;try{await fetch(one)}catch{closed=true}assert(closed);
|
|
32
|
+
console.log('PASS final terminal closes shared listener');
|
|
33
|
+
await run('terminalService.acquire(testBody.currentPane.path)');
|
|
34
|
+
await until('Object.values(terminalService.tabs).some(t=>t.url)');
|
|
35
|
+
const restarted=await run('Object.values(terminalService.tabs)[0].url');
|
|
36
|
+
assert.equal((await fetch(restarted)).status,200);
|
|
37
|
+
await run('for (const id of Object.keys(terminalService.tabs)) terminalService.release(id); terminalService.acquire(testBody.currentPane.path)');
|
|
38
|
+
await until('!terminalService.stopping && Object.values(terminalService.tabs).some(t=>t.url)');
|
|
39
|
+
console.log('PASS reopen during shutdown starts a fresh listener');
|
|
40
|
+
await run('for (const id of Object.keys(terminalService.tabs)) terminalService.release(id)');
|
|
41
|
+
await until('!terminalService.stopping && !terminalService.process.running');
|
|
42
|
+
}finally{await browser.close();rmSync(file,{force:true});rmSync(dir,{recursive:true,force:true});}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// bun test/manual-terminal-ui.mjs (requires the sibling buninu-browser checkout).
|
|
2
|
+
import {mkdtempSync,mkdirSync,rmSync} from 'node:fs';
|
|
3
|
+
import {join} from 'node:path';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
const repo=Bun.fileURLToPath(new URL('../',import.meta.url));
|
|
7
|
+
const dir=mkdtempSync(join(tmpdir(),'flea-terminal-ui-'));
|
|
8
|
+
process.env.XDG_DATA_HOME=join(dir,'data');process.env.XDG_CONFIG_HOME=join(dir,'config');process.env.XDG_CACHE_HOME=join(dir,'cache');process.env.FLEA_PATH=dir;process.env.BUNINU_BUN=process.execPath;
|
|
9
|
+
for(const p of ['data','config','cache'])mkdirSync(join(dir,p));
|
|
10
|
+
const {createBrowser}=await import('../../buninu-browser/lib/headless-shell.js');
|
|
11
|
+
const file=join(repo,'ui/boot/.terminal-check-'+process.pid+'.qml');
|
|
12
|
+
await Bun.write(file,`import QtQuick\nimport Quickshell\nimport ".." as Flea\nimport "../js/Tabs.js" as Tabs\nFloatingWindow { id: win; width:900; height:600; Loader {anchors.fill:parent; source:"../WindowBody.qml";onLoaded:{item.host=win;globalThis.testTabs=Tabs;globalThis.testBody=item;globalThis.terminalService=Flea.TerminalService}} }`);
|
|
13
|
+
const browser=await createBrowser({spareRenderer:false});
|
|
14
|
+
const run=s=>browser.context.evaluate(s);
|
|
15
|
+
const until=async(s)=>{for(let i=0;i<200;i++){if(await run(s))return;await Bun.sleep(100);}throw Error('UI timeout '+s+' '+await run('document.body.innerText.slice(-2500)'));};
|
|
16
|
+
try {
|
|
17
|
+
await browser.context.navigate(Bun.pathToFileURL(file).href);
|
|
18
|
+
await until('!!globalThis.testBody && !testBody.currentPane.listInFlight');
|
|
19
|
+
await run('testBody.currentPane.openTerminal()');
|
|
20
|
+
await until('!!testBody.currentPane.terminalId');
|
|
21
|
+
await until('Object.values(terminalService.tabs).some(t=>t.url?.includes("reconnect-token"))');
|
|
22
|
+
const one=await run('Object.values(terminalService.tabs)[0].url');
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
await until('!!document.querySelector("canvas")');
|
|
27
|
+
await browser.context.click(400,100);
|
|
28
|
+
await run('testTabs.selectAt(testBody.currentPane,0)');
|
|
29
|
+
await Bun.sleep(100);
|
|
30
|
+
console.log('ordinary focus',await run('({focus:testBody.currentPane.activeFocus,active:document.activeElement.tagName,id:testBody.currentPane.terminalId})'));
|
|
31
|
+
const cursor=await run('testBody.currentPane.cursorIndex');
|
|
32
|
+
await browser.context.press('ArrowDown');
|
|
33
|
+
assert.equal(await run('testBody.currentPane.cursorIndex'),cursor+1,'ArrowDown must reach the ordinary list');
|
|
34
|
+
await browser.context.press('ArrowUp');
|
|
35
|
+
assert.equal(await run('testBody.currentPane.cursorIndex'),cursor);
|
|
36
|
+
await browser.context.press('t');
|
|
37
|
+
assert.equal(await run('testBody.currentPane.tabs.items.length'),3,'ordinary t must work after terminal switch');
|
|
38
|
+
await browser.context.press('t',{modifiers:2,code:'KeyT'});
|
|
39
|
+
assert.equal(await run('testBody.currentPane.tabs.items.length'),4,'Ctrl+T must open another terminal');
|
|
40
|
+
console.log('PASS terminal -> ordinary -> arrows -> t -> Ctrl+T');
|
|
41
|
+
await run('testTabs.closeAt(testBody.currentPane,testBody.currentPane.tabs.index)');
|
|
42
|
+
await Bun.sleep(100);
|
|
43
|
+
assert.equal(await run('testBody.currentPane.activeFocus'),true,'closing terminal returns focus');
|
|
44
|
+
await browser.context.press('t',{modifiers:2,code:'KeyT'});
|
|
45
|
+
assert.equal(await run('testBody.currentPane.tabs.items.length'),4);
|
|
46
|
+
console.log('PASS close terminal -> ordinary -> Ctrl+T');
|
|
47
|
+
|
|
48
|
+
}finally{await browser.close();rmSync(file,{force:true});rmSync(dir,{recursive:true,force:true});}
|