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,80 @@
|
|
|
1
|
+
import { test, expect, afterAll } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { gzipSync } from 'node:zlib';
|
|
6
|
+
import { listArchive } from '../src/archive.js';
|
|
7
|
+
const dir = mkdtempSync(join(tmpdir(), 'flea-archive-'));
|
|
8
|
+
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
|
9
|
+
function header(name, size, type='0') {
|
|
10
|
+
const b=Buffer.alloc(512); b.write(name); b.write(String(size),124,12); b.write(type,156);
|
|
11
|
+
b.fill(32,148,156); const sum=b.reduce((a,v)=>a+v,0);
|
|
12
|
+
b.write(sum.toString(8).padStart(6,'0')+'\0 ',148); return b;
|
|
13
|
+
}
|
|
14
|
+
async function read(bytes, ext='tar') {
|
|
15
|
+
const path=join(dir, `${crypto.randomUUID()}.${ext}`); writeFileSync(path,bytes);
|
|
16
|
+
const rows=[]; await listArchive(path,path,r=>rows.push(r)); return rows;
|
|
17
|
+
}
|
|
18
|
+
const valid=Buffer.concat([header('hello.txt','3'),Buffer.from('hey'),Buffer.alloc(509),Buffer.alloc(1024)]);
|
|
19
|
+
for (const format of ['tar','tgz','tzst']) test(`list ${format}`,async()=>{
|
|
20
|
+
const bytes=format==='tar'?valid:format==='tgz'?gzipSync(valid):Bun.zstdCompressSync(valid);
|
|
21
|
+
expect(await read(bytes,format)).toEqual([{name:'hello.txt',size:3,dir:false}]);
|
|
22
|
+
});
|
|
23
|
+
for (const size of ['-1000','77777777777']) test(`reject invalid or truncated tar size ${size}`,async()=>{
|
|
24
|
+
await expect(read(header('bad',size))).rejects.toThrow();
|
|
25
|
+
});
|
|
26
|
+
test('reject truncated compressed member',async()=>{
|
|
27
|
+
await expect(read(gzipSync(header('bad','1000')),'tgz')).rejects.toThrow();
|
|
28
|
+
});
|
|
29
|
+
test('cap extended metadata before allocation',async()=>{
|
|
30
|
+
await expect(read(gzipSync(header('extended','10000000','x')),'tgz')).rejects.toThrow('too large');
|
|
31
|
+
});
|
|
32
|
+
test('reject partial trailing block',async()=>{await expect(read(Buffer.concat([valid,Buffer.from('bad')]))).rejects.toThrow();});
|
|
33
|
+
// One stored member: its local header and bytes, then the central directory and its end record.
|
|
34
|
+
const ZIP_CENTRAL=30+'hello'.length+3;
|
|
35
|
+
function zip(name='hello',size=3) {
|
|
36
|
+
const l=Buffer.alloc(30); l.writeUInt32LE(0x04034b50); l.writeUInt32LE(size,18); l.writeUInt32LE(size,22); l.writeUInt16LE(name.length,26);
|
|
37
|
+
const local=Buffer.concat([l,Buffer.from(name),Buffer.alloc(size,0x61)]);
|
|
38
|
+
const h=Buffer.alloc(46); h.writeUInt32LE(0x02014b50); h.writeUInt32LE(size,20); h.writeUInt32LE(size,24); h.writeUInt16LE(name.length,28);
|
|
39
|
+
const c=Buffer.concat([h,Buffer.from(name)]), e=Buffer.alloc(22);
|
|
40
|
+
e.writeUInt32LE(0x06054b50);e.writeUInt16LE(1,8);e.writeUInt16LE(1,10);e.writeUInt32LE(c.length,12);e.writeUInt32LE(local.length,16);
|
|
41
|
+
return Buffer.concat([local,c,e]);
|
|
42
|
+
}
|
|
43
|
+
test('zip central listing',async()=>{expect(await read(zip(),'zip')).toEqual([{name:'hello',size:3,dir:false}]);});
|
|
44
|
+
test('reject oversized central directory before allocation',async()=>{
|
|
45
|
+
const b=zip(); b.writeUInt32LE(0xffffffff,b.length-10);
|
|
46
|
+
await expect(read(b,'zip')).rejects.toThrow();
|
|
47
|
+
});
|
|
48
|
+
test('reject truncated central entry name',async()=>{
|
|
49
|
+
const b=zip();b.writeUInt16LE(65000,ZIP_CENTRAL+28);await expect(read(b,'zip')).rejects.toThrow();
|
|
50
|
+
});
|
|
51
|
+
test('reject a split archive and a member outside the archive',async()=>{
|
|
52
|
+
const split=zip();split.writeUInt16LE(1,split.length-18);await expect(read(split,'zip')).rejects.toThrow('split');
|
|
53
|
+
const outside=zip();outside.writeUInt32LE(1000,ZIP_CENTRAL+42);await expect(read(outside,'zip')).rejects.toThrow('range');
|
|
54
|
+
const wide=zip();wide.writeUInt32LE(0xffffffff,ZIP_CENTRAL+24);await expect(read(wide,'zip')).rejects.toThrow('zip64');
|
|
55
|
+
});
|
|
56
|
+
test('reject corrupt later tar header instead of partial success',async()=>{
|
|
57
|
+
await expect(read(Buffer.concat([valid,Buffer.alloc(512,42)]))).rejects.toThrow();
|
|
58
|
+
});
|
|
59
|
+
function pax(key,value) {
|
|
60
|
+
const body=` ${key}=${value}\n`;let n=body.length+1;
|
|
61
|
+
while(String(n).length+body.length!==n)n=String(n).length+body.length;
|
|
62
|
+
return Buffer.from(String(n)+body);
|
|
63
|
+
}
|
|
64
|
+
function member(name,body,type='0') {
|
|
65
|
+
return Buffer.concat([header(name,body.length.toString(8),type),body,Buffer.alloc((512-body.length%512)%512)]);
|
|
66
|
+
}
|
|
67
|
+
test('pax metadata controls following file, including global headers',async()=>{
|
|
68
|
+
const b=Buffer.concat([member('global',pax('path','global-name'),'g'),member('file',Buffer.from('a'))]);
|
|
69
|
+
expect((await read(b))[0].name).toBe('global-name');
|
|
70
|
+
});
|
|
71
|
+
test('negative pax size is rejected for raw and compressed tar',async()=>{
|
|
72
|
+
const b=Buffer.concat([member('pax',pax('size','-512'),'x'),header('file','0')]);
|
|
73
|
+
await expect(read(b)).rejects.toThrow('size');await expect(read(gzipSync(b),'tgz')).rejects.toThrow('size');
|
|
74
|
+
});
|
|
75
|
+
test('meta never reports a corrupt partial listing as complete',async()=>{
|
|
76
|
+
const {meta}=await import('../src/meta.js');const name='corrupt.tar';
|
|
77
|
+
writeFileSync(join(dir,name),Buffer.concat([valid,Buffer.alloc(512,42)]));
|
|
78
|
+
const result=await meta({path:dir,rows:[{name}]},{row:0,archive:true,token:42});
|
|
79
|
+
expect(result.afailed).toBe(true);expect(result.entries).toBe(0);expect(result.names).toEqual([]);expect(result.token).toBe(42);
|
|
80
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// bun test/bench-markdown-scroll.mjs [--steps=40] [--doc=readme,long,maths] [--mode=html,qml]
|
|
2
|
+
// Compares two ways to preview Markdown in buninu-browser's QML runtime, at 640x480:
|
|
3
|
+
// html: Bun.markdown.html, formulas through src/mathml.js, shown by WebEngineView.loadHtml
|
|
4
|
+
// (a sandboxed iframe the browser lays out once and scrolls itself);
|
|
5
|
+
// qml: ui/PreviewMarkdown.qml with the real backend (one delegate per block, built lazily).
|
|
6
|
+
// Each scroll step is one 120px wheel at the pane's centre followed by a 1x1 PNG capture,
|
|
7
|
+
// which waits for that frame's layout and paint; the times are wall clock, frame pacing
|
|
8
|
+
// excluded. No threshold is a test gate.
|
|
9
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { readdirSync, readFileSync, rmSync } from "node:fs";
|
|
13
|
+
import { createBrowser } from "../../buninu-browser/lib/headless-shell.js";
|
|
14
|
+
import { texToMathML } from "../src/mathml.js";
|
|
15
|
+
|
|
16
|
+
const repo = Bun.fileURLToPath(new URL("../", import.meta.url));
|
|
17
|
+
const option = (name, fallback) => process.argv.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3) ?? fallback;
|
|
18
|
+
const steps = Number(option("steps", 40));
|
|
19
|
+
const onlyDocs = option("doc", "readme,long,maths").split(",");
|
|
20
|
+
const modes = option("mode", "html,qml").split(",");
|
|
21
|
+
|
|
22
|
+
// ---- resource guard ----
|
|
23
|
+
// The browser's renderer, the QML document's flea backend and that backend's highlighter
|
|
24
|
+
// processes are all descendants of this process. A watchdog totals their memory every
|
|
25
|
+
// half second and kills the whole tree past the limits, and so does any exit, signal or
|
|
26
|
+
// stuck call: nothing this benchmark starts may outlive it or take the device down.
|
|
27
|
+
const RSS_LIMIT_MB = Number(option("rss-mb", 1200));
|
|
28
|
+
const PROCESS_LIMIT = Number(option("processes", 8));
|
|
29
|
+
const CALL_TIMEOUT_MS = 20000;
|
|
30
|
+
function descendants() {
|
|
31
|
+
const children = new Map();
|
|
32
|
+
for (const name of readdirSync("/proc")) {
|
|
33
|
+
if (!/^\d+$/.test(name)) continue;
|
|
34
|
+
try {
|
|
35
|
+
const stat = readFileSync(`/proc/${name}/stat`, "utf8");
|
|
36
|
+
// The command name is in parentheses and may hold spaces; the parent pid follows it.
|
|
37
|
+
const parent = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
38
|
+
if (!children.has(parent)) children.set(parent, []);
|
|
39
|
+
children.get(parent).push(Number(name));
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
const found = [], queue = [process.pid];
|
|
43
|
+
while (queue.length) for (const child of children.get(queue.shift()) ?? []) { found.push(child); queue.push(child); }
|
|
44
|
+
return found;
|
|
45
|
+
}
|
|
46
|
+
const rssMb = (pid) => {
|
|
47
|
+
try { return Number(/VmRSS:\s+(\d+)/.exec(readFileSync(`/proc/${pid}/status`, "utf8"))?.[1] ?? 0) / 1024; } catch { return 0; }
|
|
48
|
+
};
|
|
49
|
+
function killTree(reason) {
|
|
50
|
+
const pids = descendants();
|
|
51
|
+
for (const pid of pids.reverse()) { try { process.kill(pid, "SIGKILL"); } catch {} }
|
|
52
|
+
if (reason) console.error(`bench: stopped (${reason}); killed ${pids.length} process(es)`);
|
|
53
|
+
}
|
|
54
|
+
// Each process's memory and a short command line, so a report names what grew.
|
|
55
|
+
const commandOf = (pid) => {
|
|
56
|
+
try { return readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean).map((part) => part.split("/").pop()).join(" ").slice(0, 70); } catch { return "?"; }
|
|
57
|
+
};
|
|
58
|
+
const breakdown = (pids) => [process.pid, ...pids].map((pid) => `${pid} ${Math.round(rssMb(pid))} MB ${commandOf(pid)}`).join("\n ");
|
|
59
|
+
let samples = 0;
|
|
60
|
+
const watchdog = setInterval(() => {
|
|
61
|
+
const pids = descendants();
|
|
62
|
+
const total = pids.reduce((sum, pid) => sum + rssMb(pid), 0) + rssMb(process.pid);
|
|
63
|
+
if (++samples % 4 === 0) console.error(`bench: memory ${Math.round(total)} MB in ${pids.length + 1} processes`);
|
|
64
|
+
if (total > RSS_LIMIT_MB || pids.length > PROCESS_LIMIT) {
|
|
65
|
+
console.error(`bench: over the limits:\n ${breakdown(pids)}`);
|
|
66
|
+
killTree(`${Math.round(total)} MB in ${pids.length + 1} processes, limits ${RSS_LIMIT_MB} MB / ${PROCESS_LIMIT}`);
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
69
|
+
}, 500);
|
|
70
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, () => { killTree(signal); process.exit(130); });
|
|
71
|
+
// On any exit, also remove the wrapper documents this run wrote beside ui/boot/shell.qml.
|
|
72
|
+
process.on("exit", () => {
|
|
73
|
+
killTree(null);
|
|
74
|
+
try { for (const name of readdirSync(join(repo, "ui/boot"))) if (name.startsWith(".bench-")) rmSync(join(repo, "ui/boot", name), { force: true }); } catch {}
|
|
75
|
+
});
|
|
76
|
+
// Every browser call has a deadline; a renderer that stops answering ends the run.
|
|
77
|
+
const deadline = (promise, what) => Promise.race([promise, new Promise((_, reject) =>
|
|
78
|
+
setTimeout(() => reject(new Error(`${what} took longer than ${CALL_TIMEOUT_MS} ms`)), CALL_TIMEOUT_MS).unref?.())]);
|
|
79
|
+
process.env.BUNINU_BUN = process.execPath;
|
|
80
|
+
|
|
81
|
+
// ---- documents ----
|
|
82
|
+
const words = (n, seed) => Array.from({ length: n }, (_, i) => ["preview", "markdown", "scroll", "layout", "flea", "text", "render", "paint", "block", "line"][(i * 7 + seed) % 10]).join(" ");
|
|
83
|
+
const readme = [
|
|
84
|
+
"# Project title", "", words(40, 1), "",
|
|
85
|
+
...Array.from({ length: 12 }, (_, s) => [
|
|
86
|
+
`## Section ${s + 1}`, "", words(60, s), "", `- ${words(8, s)}`, `- ${words(9, s + 1)}`, `- **bold** and *italic* and \`code\``, "",
|
|
87
|
+
"```js", `const value${s} = ${s} * 2;`, "console.log(value" + s + ");", "```", "",
|
|
88
|
+
"| a | b | c |", "|---|---|---|", `| ${s} | ${s * 2} | ${s * 3} |`, "| x | y | z |", "",
|
|
89
|
+
].join("\n")),
|
|
90
|
+
].join("\n");
|
|
91
|
+
const long = Array.from({ length: 600 }, (_, i) => (i % 25 === 0 ? `## Part ${i / 25 + 1}\n\n` : "") + `${i + 1}. ${words(30, i)}`).join("\n\n");
|
|
92
|
+
const maths = [
|
|
93
|
+
"# Formulas", "",
|
|
94
|
+
...Array.from({ length: 60 }, (_, i) => [
|
|
95
|
+
`Paragraph ${i + 1} has inline $x_${i % 9}^2 + \\frac{a}{b}$ and $\\sqrt{${i}}$ in it, ${words(20, i)}.`, "",
|
|
96
|
+
i % 2 ? "$$\\sum_{k=1}^{n} k^2 = \\frac{n(n+1)(2n+1)}{6}$$" : "$$\\begin{pmatrix}a & b \\\\ c & d\\end{pmatrix} \\int_0^1 x\\,dx$$", "",
|
|
97
|
+
i % 5 === 0 ? "| n | n² |\n|---|---|\n| 1 | 1 |\n| 2 | 4 |\n" : "",
|
|
98
|
+
].join("\n")),
|
|
99
|
+
].join("\n");
|
|
100
|
+
const documents = { readme, long, maths };
|
|
101
|
+
// --md=path adds one document of your own, run alone.
|
|
102
|
+
const own = option("md", "");
|
|
103
|
+
if (own) { for (const key of Object.keys(documents)) delete documents[key]; documents[own.split("/").pop().replace(/\.md$/, "")] = readFileSync(own, "utf8"); onlyDocs.push(own.split("/").pop().replace(/\.md$/, "")); }
|
|
104
|
+
|
|
105
|
+
// ---- the HTML variant ----
|
|
106
|
+
// Formulas are set aside before Markdown runs, so its emphasis rules never touch TeX. The
|
|
107
|
+
// markers are private-use characters: Markdown replaces NUL with U+FFFD, so NUL would not survive.
|
|
108
|
+
function markdownHtml(source) {
|
|
109
|
+
const formulas = [];
|
|
110
|
+
const hold = (tex, display) => `\uE000${formulas.push({ tex, display }) - 1}\uE001`;
|
|
111
|
+
const marked = source
|
|
112
|
+
.replace(/\$\$([\s\S]+?)\$\$/g, (_, tex) => hold(tex, true))
|
|
113
|
+
.replace(/\$([^$\n]+?)\$/g, (_, tex) => hold(tex, false));
|
|
114
|
+
const html = Bun.markdown.html(marked, { tables: true, strikethrough: true, tasklists: true, noHtmlBlocks: true, noHtmlSpans: true });
|
|
115
|
+
return html.replace(/\uE000(\d+)\uE001/g, (_, index) => {
|
|
116
|
+
const { tex, display } = formulas[Number(index)];
|
|
117
|
+
try { return texToMathML(tex, display); } catch { return `<code>${Bun.escapeHTML(tex)}</code>`; }
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const STYLE = "body{margin:12px 16px;font:14px sans-serif;color:#d8dee9;background:#171b22;line-height:1.45}" +
|
|
121
|
+
"pre,code{background:#232833;font-family:monospace}pre{padding:8px 12px}table{border-collapse:collapse}" +
|
|
122
|
+
"td,th{border:1px solid #444;padding:2px 8px}h1{font-size:20px}h2{font-size:16px}math[display=block]{margin:6px 0}";
|
|
123
|
+
|
|
124
|
+
// ---- measuring ----
|
|
125
|
+
async function measure(browser, qmlPath, ready) {
|
|
126
|
+
const started = performance.now();
|
|
127
|
+
const context = browser.context;
|
|
128
|
+
// The QML host keeps its global object across documents: clear what the last one set.
|
|
129
|
+
await deadline(context.evaluate(`globalThis.benchReady = null; globalThis.preview = null; true`), "evaluate").catch(() => {});
|
|
130
|
+
await deadline(context.navigate(Bun.pathToFileURL(qmlPath).href), "navigate");
|
|
131
|
+
let isReady = false;
|
|
132
|
+
for (let i = 0; i < 400 && !(isReady = await deadline(context.evaluate(ready), "evaluate")); i++) await Bun.sleep(50);
|
|
133
|
+
if (!isReady) throw new Error("the preview never became ready");
|
|
134
|
+
await deadline(context.screenshot({ format: "png", clip: { x: 0, y: 0, width: 1, height: 1, scale: 1 } }), "screenshot");
|
|
135
|
+
const openMs = performance.now() - started;
|
|
136
|
+
const hash = async () => Bun.hash(await deadline(context.screenshot({ format: "png", clip: { x: 200, y: 140, width: 240, height: 200, scale: 1 } }), "screenshot")).toString();
|
|
137
|
+
const before = await hash();
|
|
138
|
+
const times = [];
|
|
139
|
+
for (let step = 0; step < steps; step++) {
|
|
140
|
+
const begin = performance.now();
|
|
141
|
+
await deadline(context.scroll(0, 120, { x: 320, y: 240 }), "scroll");
|
|
142
|
+
await deadline(context.screenshot({ format: "png", clip: { x: 0, y: 0, width: 1, height: 1, scale: 1 } }), "screenshot");
|
|
143
|
+
times.push(performance.now() - begin);
|
|
144
|
+
}
|
|
145
|
+
const moved = (await hash()) !== before;
|
|
146
|
+
const sorted = [...times].sort((a, b) => a - b);
|
|
147
|
+
const at = (q) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))];
|
|
148
|
+
return { openMs: Math.round(openMs), medianMs: +at(0.5).toFixed(1), p95Ms: +at(0.95).toFixed(1), maxMs: +sorted.at(-1).toFixed(1), moved };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const dir = await mkdtemp(join(tmpdir(), "flea-md-bench-"));
|
|
152
|
+
const browser = await createBrowser({ spareRenderer: false });
|
|
153
|
+
const results = [];
|
|
154
|
+
try {
|
|
155
|
+
await browser.context.resize(640, 480, 1);
|
|
156
|
+
const report = (result) => { results.push(result); console.log("RESULT " + JSON.stringify(result)); };
|
|
157
|
+
for (const [name, source] of Object.entries(documents)) {
|
|
158
|
+
if (!onlyDocs.includes(name)) continue;
|
|
159
|
+
const md = join(dir, `${name}.md`);
|
|
160
|
+
await Bun.write(md, source);
|
|
161
|
+
if (modes.includes("html")) {
|
|
162
|
+
// html: the page is built here, as the backend would build it.
|
|
163
|
+
const buildStart = performance.now();
|
|
164
|
+
const html = `<!doctype html><meta charset=utf-8><style>${STYLE}</style>${markdownHtml(source)}`;
|
|
165
|
+
const buildMs = Math.round(performance.now() - buildStart);
|
|
166
|
+
const htmlQml = join(repo, `ui/boot/.bench-html-${process.pid}.qml`);
|
|
167
|
+
await Bun.write(htmlQml, `import QtQuick\nimport Quickshell\nimport QtWebEngine\nFloatingWindow {width:640;height:480;color:"#171b22"\n WebEngineView {anchors.fill:parent;settings.javascriptEnabled:false\n onLoadingChanged:function(info){if(info.status===WebEngineView.LoadSucceededStatus)globalThis.benchReady=${JSON.stringify(name)}}\n Component.onCompleted:loadHtml(${JSON.stringify(html)})}}`);
|
|
168
|
+
// Each document answers its own name, so a flag left by the previous one never counts.
|
|
169
|
+
const htmlResult = await measure(browser, htmlQml, `globalThis.benchReady===${JSON.stringify(name)}`);
|
|
170
|
+
await rm(htmlQml, { force: true });
|
|
171
|
+
report({ document: name, mode: "html (iframe)", bytes: source.length, buildMs, ...htmlResult });
|
|
172
|
+
}
|
|
173
|
+
if (modes.includes("qml")) {
|
|
174
|
+
// qml: the existing preview, with its own backend reading the file.
|
|
175
|
+
const qmlQml = join(repo, `ui/boot/.bench-qml-${process.pid}.qml`);
|
|
176
|
+
await Bun.write(qmlQml, `import QtQuick\nimport Quickshell\nimport ".." as Flea\nFloatingWindow {width:640;height:480;color:"#171b22";Flea.Backend{id:backend}\n Loader {anchors.fill:parent;Component.onCompleted:setSource("${Bun.pathToFileURL(join(repo, "ui/PreviewMarkdown.qml")).href}")\n onLoaded:{item.backend=backend;item.size=${Buffer.byteLength(source)};item.path=${JSON.stringify(md)};item.active=true;globalThis.preview=item}}}`);
|
|
177
|
+
const qmlResult = await measure(browser, qmlQml, "!!globalThis.preview && preview.loadedText.length>0 && preview.viewport.count>0");
|
|
178
|
+
await rm(qmlQml, { force: true });
|
|
179
|
+
report({ document: name, mode: "qml (rich text blocks)", bytes: source.length, buildMs: 0, ...qmlResult });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
clearInterval(watchdog);
|
|
184
|
+
await deadline(browser.close(), "close").catch(() => {});
|
|
185
|
+
killTree(null);
|
|
186
|
+
await rm(dir, { recursive: true, force: true });
|
|
187
|
+
for (const name of readdirSync(join(repo, "ui/boot"))) if (name.startsWith(".bench-")) await rm(join(repo, "ui/boot", name), { force: true });
|
|
188
|
+
}
|
|
189
|
+
console.table(results);
|
|
190
|
+
console.log(JSON.stringify(results));
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { test, expect } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { HostClipboard, clipHostReply } from '../src/clip-host.js';
|
|
6
|
+
import { clipText, parseClipText } from '../src/clip.js';
|
|
7
|
+
|
|
8
|
+
const source = (await Bun.file(new URL('../ui/js/HostClipboard.js', import.meta.url)).text()).replace(/^\.pragma.*$/gm, '');
|
|
9
|
+
const makeHandler = () => new Function(source + ';return handle')();
|
|
10
|
+
const none = { clip: 'none', paths: [], token: '', skipped: 0 };
|
|
11
|
+
function fixture() {
|
|
12
|
+
let text = '', reads = 0, failRead = false, failWrite = false;
|
|
13
|
+
const writes = [], handle = makeHandler();
|
|
14
|
+
const qs = {
|
|
15
|
+
async readClipboardText(options) { expect(options.strict).toBe(true); reads++; if(failRead)throw Error('read denied'); return text; },
|
|
16
|
+
async writeClipboardText(value) { if(failWrite)throw Error('write denied'); writes.push(value);text=value; },
|
|
17
|
+
};
|
|
18
|
+
const send = message => handle({send: clipHostReply}, qs, message);
|
|
19
|
+
const dc = new HostClipboard(send, { clipText, parseClipText, none });
|
|
20
|
+
return { dc, writes, get text(){return text}, set text(v){text=v}, get reads(){return reads},
|
|
21
|
+
failRead(v){failRead=v},failWrite(v){failWrite=v},
|
|
22
|
+
second:()=>new HostClipboard(send,{clipText,parseClipText,none}) };
|
|
23
|
+
}
|
|
24
|
+
function files() {
|
|
25
|
+
const dir = mkdtempSync(join(tmpdir(), 'flea-clip-'));
|
|
26
|
+
const paths = [join(dir, "a 'quoted' 中文\nline"),join(dir,'back\\slash $` name')];
|
|
27
|
+
for(const path of paths)writeFileSync(path,'test');
|
|
28
|
+
return {paths,cleanup:()=>rmSync(dir,{recursive:true,force:true})};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('QS fallback preserves POSIX copy/cut bytes and fresh cross-window paste', async()=>{
|
|
32
|
+
const f=files(),h=fixture();
|
|
33
|
+
try {
|
|
34
|
+
await h.dc.set(null,{op:'copy',paths:f.paths,token:'a'});
|
|
35
|
+
expect(h.text).toBe(clipText('copy',f.paths));expect(h.reads).toBe(0);
|
|
36
|
+
expect((await h.dc.snapshot()).paths).toEqual(f.paths);
|
|
37
|
+
await h.dc.set(null,{op:'cut',paths:f.paths,token:'b'});
|
|
38
|
+
expect(h.text).toBe('""'+clipText('copy',f.paths)+'""');
|
|
39
|
+
expect(await h.second().snapshot()).toEqual({clip:'cut',paths:f.paths,token:'',skipped:0});
|
|
40
|
+
h.text='a newer Android clipboard';
|
|
41
|
+
expect(await h.dc.snapshot()).toEqual(none);
|
|
42
|
+
expect(await h.dc.clearIf({token:'b'})).toBe(false);
|
|
43
|
+
expect(h.text).toBe('a newer Android clipboard');
|
|
44
|
+
} finally {f.cleanup()}
|
|
45
|
+
});
|
|
46
|
+
test('spent cut clears only identical text, even after its files moved',async()=>{
|
|
47
|
+
const f=files(),h=fixture();
|
|
48
|
+
await h.dc.set(null,{op:'cut',paths:f.paths,token:'old'});
|
|
49
|
+
f.cleanup();
|
|
50
|
+
expect(await h.dc.clearIf({token:'wrong'})).toBe(false);
|
|
51
|
+
expect(await h.dc.clearIf({token:'old'})).toBe(true);expect(h.text).toBe('');
|
|
52
|
+
h.text=clipText('cut',f.paths);
|
|
53
|
+
expect(await h.second().clearIf({cut:f.paths})).toBe(true);
|
|
54
|
+
h.text=clipText('copy',f.paths);
|
|
55
|
+
expect(await h.dc.clearIf({cut:f.paths})).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
test('read/write failure is not a cached success and later operations recover',async()=>{
|
|
58
|
+
const h=fixture();h.failWrite(true);
|
|
59
|
+
await expect(h.dc.set(null,{op:'text',paths:['bad'],token:''})).rejects.toThrow('write denied');
|
|
60
|
+
expect(h.dc.owned).toBe(null);h.failWrite(false);
|
|
61
|
+
await h.dc.set(null,{op:'text',paths:['good'],token:''});expect(h.text).toBe('good');
|
|
62
|
+
h.failRead(true);await expect(h.dc.snapshot()).rejects.toThrow('read denied');
|
|
63
|
+
h.failRead(false);expect(await h.dc.snapshot()).toEqual(none);
|
|
64
|
+
});
|
|
65
|
+
test('overlapping operations are ordered; native QS property fallback uses identical text',async()=>{
|
|
66
|
+
const h=fixture();
|
|
67
|
+
await Promise.all(['one','two','three'].map(x=>h.dc.set(null,{op:'text',paths:[x],token:''})));
|
|
68
|
+
expect(h.writes).toEqual(['one','two','three']);
|
|
69
|
+
const handle=makeHandler(),qs={clipboardText:''},replies=[];
|
|
70
|
+
const text='""\'C:/has space/a\'\'\\\'\'b\'""';
|
|
71
|
+
await handle({send:m=>replies.push(m)},qs,{id:1,op:'write',text});
|
|
72
|
+
await handle({send:m=>replies.push(m)},qs,{id:2,op:'read'});
|
|
73
|
+
expect(qs.clipboardText).toBe(text);expect(replies[1].text).toBe(text);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('real backend negotiates missing-Wayland fallback and exchanges QS clipboard replies',async()=>{
|
|
77
|
+
const f=files(),dir=mkdtempSync(join(tmpdir(),'flea-no-wayland-'));
|
|
78
|
+
const child=Bun.spawn([process.execPath,new URL('../src/flea.js',import.meta.url).pathname,'--backend'],{
|
|
79
|
+
env:{...process.env,WAYLAND_DISPLAY:join(dir,'missing'),XDG_RUNTIME_DIR:dir},stdin:'pipe',stdout:'pipe',stderr:'pipe'});
|
|
80
|
+
const errors=new Response(child.stderr).text(),lines=[],waiters=[];
|
|
81
|
+
let text='';const qs={readClipboardText:async()=>text,writeClipboardText:async t=>{text=t}};
|
|
82
|
+
const send=m=>{child.stdin.write(JSON.stringify(m)+'\n');child.stdin.flush()},handle=makeHandler();
|
|
83
|
+
const reading=(async()=>{
|
|
84
|
+
let tail='';const decoder=new TextDecoder();
|
|
85
|
+
for await(const chunk of child.stdout){tail+=decoder.decode(chunk,{stream:true});let index;
|
|
86
|
+
while((index=tail.indexOf('\n'))>=0){const line=tail.slice(0,index);tail=tail.slice(index+1);if(!line)continue;
|
|
87
|
+
const m=JSON.parse(line);
|
|
88
|
+
if(m.t==='clipHost'){void handle({send},qs,m);continue;}
|
|
89
|
+
lines.push(m);for(const notify of [...waiters])notify();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
async function next(op,action){
|
|
94
|
+
const start=lines.length;
|
|
95
|
+
const promise=new Promise((resolve,reject)=>{
|
|
96
|
+
const timer=setTimeout(()=>{waiters.splice(waiters.indexOf(check),1);reject(Error('reply timeout '+op))},5000);
|
|
97
|
+
const check=()=>{const found=lines.slice(start).find(m=>m.t==='clip'&&m.op===op);if(found){clearTimeout(timer);waiters.splice(waiters.indexOf(check),1);resolve(found)}};
|
|
98
|
+
waiters.push(check);
|
|
99
|
+
});action();return promise;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const watch=await next('changed',()=>send({c:'clipWatch',hostText:true}));expect(watch.error).toContain('read on demand');
|
|
103
|
+
const set=await next('set',()=>send({c:'clipSet',op:'cut',paths:f.paths}));expect(set.ok).toBe(true);
|
|
104
|
+
expect(text).toBe(clipText('cut',f.paths));
|
|
105
|
+
const get=await next('get',()=>send({c:'clipGet'}));expect(get.paths).toEqual(f.paths);expect(get.clip).toBe('cut');
|
|
106
|
+
expect(get.token).toBe(set.token);
|
|
107
|
+
text='new Android text';
|
|
108
|
+
const clear=await next('clear',()=>send({c:'clipClear',token:set.token}));expect(clear.cleared).toBe(false);
|
|
109
|
+
const fresh=await next('get',()=>send({c:'clipGet'}));expect(fresh.clip).toBe('none');
|
|
110
|
+
send({c:'quit'});expect(await child.exited).toBe(0);await reading;
|
|
111
|
+
expect(await errors).toBe('');
|
|
112
|
+
} finally {child.kill();await child.exited;f.cleanup();rmSync(dir,{recursive:true,force:true})}
|
|
113
|
+
},15000);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,writeFileSync,openSync,ftruncateSync,closeSync,rmSync} from 'node:fs';
|
|
3
|
+
import {tmpdir} from 'node:os';
|
|
4
|
+
import {join} from 'node:path';
|
|
5
|
+
import {code} from '../src/code.js';
|
|
6
|
+
const text=reply=>reply.lines.map(r=>r.filter((_,i)=>i%2).join('')).join('\n');
|
|
7
|
+
test('unknown binary files remain viewable with visible control characters',async()=>{
|
|
8
|
+
const r=await code({text:'first\0second\x1b[31m\nlast',raw:true});
|
|
9
|
+
expect(r.state).toBe('plain');expect(text(r)).toBe('first␀second␛[31m\nlast');
|
|
10
|
+
});
|
|
11
|
+
test('large sparse files read only their prefix and split pathological lines',async()=>{
|
|
12
|
+
const dir=mkdtempSync(join(tmpdir(),'flea-preview-'));
|
|
13
|
+
try {
|
|
14
|
+
const path=join(dir,'large.unknown');writeFileSync(path,'start\n');
|
|
15
|
+
const fd=openSync(path,'r+');try{ftruncateSync(fd,1024*1024*1024);}finally{closeSync(fd);}
|
|
16
|
+
const r=await code({path,count:400});
|
|
17
|
+
expect(r.state).toBe('plain');expect(r.lines[0]).toEqual([0,'start']);
|
|
18
|
+
expect(r.total).toBeLessThan(400);
|
|
19
|
+
expect(text(r)).toContain('[Preview truncated: first 1 MiB]');
|
|
20
|
+
expect(text(r)).toContain('[Long lines split for preview]');
|
|
21
|
+
expect(r.lines.every(line=>line.length<3 && (line[1]?.length??0)<=4096)).toBe(true);
|
|
22
|
+
} finally {rmSync(dir,{recursive:true,force:true});}
|
|
23
|
+
});
|
|
24
|
+
test('prefix windows stay addressable and empty files remain empty',async()=>{
|
|
25
|
+
expect((await code({text:'',raw:true})).total).toBe(0);
|
|
26
|
+
const r=await code({text:'one\ntwo\nthree\n',first:2,count:1,raw:true});
|
|
27
|
+
expect(r.total).toBe(3);expect(text(r)).toBe('two');
|
|
28
|
+
});
|
|
29
|
+
test('unrecognized types reach text preview while media keeps its own viewer',async()=>{
|
|
30
|
+
const source=(await Bun.file(new URL('../ui/js/Kinds.js',import.meta.url)).text()).replace(/^\.(?:pragma|import).*$/gm,'');
|
|
31
|
+
const kinds=new Function('Archive','Format',source+';return {kindState,quickLookKind};')({isArchive:()=>false},{});
|
|
32
|
+
expect(kinds.kindState('unknown-format')).toBe('text');
|
|
33
|
+
expect(kinds.quickLookKind('💾','data.bin',false)).toBe('text');
|
|
34
|
+
expect(kinds.quickLookKind('🎬','movie.mp4',false)).toBe('video');
|
|
35
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
async function load(name,names,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 {'+names+'};')(...Object.values(deps));
|
|
5
|
+
}
|
|
6
|
+
const Lists=await load('Lists','isList');
|
|
7
|
+
const Format=await load('Format','S_IFMT,S_IFREG');
|
|
8
|
+
const Menu=await load('Menu','canCompare',{Lists,Format});
|
|
9
|
+
const Ops=await load('Ops','open',{Menu,Search:{baseOf:p=>p.path}});
|
|
10
|
+
test('Compare menu accepts POSIX, Windows drive and UNC paths, preserves listing order',()=>{
|
|
11
|
+
for(const base of ['/tmp/','C:/dir/','C:\\dir\\','\\\\server\\share\\']){
|
|
12
|
+
const calls=[],rows=[{n:'a.txt',p:33188},{n:'b.txt',p:33188}];
|
|
13
|
+
const pane={path:base,selectedIndices:()=>[1,0],rowFor:i=>rows[i],
|
|
14
|
+
join:(a,b)=>a+b,quickLook:()=>({openCompare:(...args)=>calls.push(args)})};
|
|
15
|
+
Ops.open(pane);
|
|
16
|
+
expect(calls[0]?.slice(0,2)).toEqual([base+'a.txt',base+'b.txt']);
|
|
17
|
+
calls.length=0;
|
|
18
|
+
Ops.open(pane,[base+'b.txt',base+'a.txt']);
|
|
19
|
+
expect(calls[0]?.slice(0,2)).toEqual([base+'a.txt',base+'b.txt']);
|
|
20
|
+
for(const bad of ['relative.txt','C:relative.txt','https://example.com/a']){
|
|
21
|
+
calls.length=0;Ops.open(pane,[bad,base+'a.txt']);expect(calls).toEqual([]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,writeFileSync,rmSync} from 'node:fs';
|
|
3
|
+
import {join} from 'node:path';import {tmpdir} from 'node:os';
|
|
4
|
+
import {compare,diffCommands} from '../src/compare.js';
|
|
5
|
+
test('diff searches sibling bunmsh, PATH bunmsh, then system using -cc builtin',()=>{
|
|
6
|
+
expect(diffCommands({which:n=>'/bin/'+n,exists:()=>true,beside:'/src/bunmsh/index.js'})).toEqual([
|
|
7
|
+
['/bin/bun','/src/bunmsh/index.js','-cc','builtin','diff'],['/bin/bunmsh','-cc','builtin','diff'],['/bin/diff']]);
|
|
8
|
+
expect(diffCommands({which:n=>n==='diff'?'/bin/diff':null,exists:()=>false})).toEqual([['/bin/diff']]);
|
|
9
|
+
});
|
|
10
|
+
test('Compare emits unified patches and all terminal states, falls back and cancels',async()=>{
|
|
11
|
+
const dir=mkdtempSync(join(tmpdir(),'flea-compare-'));const a=join(dir,"a 中文 ' $.txt"),b=join(dir,'b.txt');
|
|
12
|
+
const ask={a,b,token:42};
|
|
13
|
+
try {
|
|
14
|
+
writeFileSync(a,'first\nold\n');writeFileSync(b,'first\nnew\n');
|
|
15
|
+
const r=await compare(ask);expect(r.state).toBe('diff');expect(r.patch).toContain('-old\n+new\n');expect(r.token).toBe(42);
|
|
16
|
+
expect((await compare({...ask,b:a})).state).toBe('identical');
|
|
17
|
+
expect((await compare({...ask,b:dir})).state).toBe('failed');
|
|
18
|
+
expect((await compare({...ask,b:b+'missing'})).state).toBe('failed');
|
|
19
|
+
writeFileSync(b,'x\0y');expect((await compare(ask)).state).toBe('binary');
|
|
20
|
+
writeFileSync(b,'x'.repeat(1048577));expect((await compare(ask)).state).toBe('over');
|
|
21
|
+
writeFileSync(b,'other\n');
|
|
22
|
+
expect((await compare(ask,{commands:[[process.execPath,'-e','process.exit(2)'],...diffCommands()]})).state).toBe('diff');
|
|
23
|
+
expect((await compare(ask,{commands:[[process.execPath,'-e','setInterval(()=>{},1000)']],timeout:100})).state).toBe('failed');
|
|
24
|
+
const control=new AbortController();control.abort();expect(await compare(ask,{signal:control.signal})).toBe(null);
|
|
25
|
+
}finally{rmSync(dir,{recursive:true,force:true})}
|
|
26
|
+
},15000);
|
|
27
|
+
|
|
28
|
+
test('timeout and in-flight abort kill a provider that ignores SIGTERM',async()=>{
|
|
29
|
+
const dir=mkdtempSync(join(tmpdir(),'flea-compare-cancel-'));
|
|
30
|
+
const a=join(dir,'a'),b=join(dir,'b'),ready=join(dir,'ready');
|
|
31
|
+
writeFileSync(a,'a');writeFileSync(b,'b');
|
|
32
|
+
const provider=join(dir,'provider.js');
|
|
33
|
+
// Self-exit bounds a regression without leaving a test process alive.
|
|
34
|
+
writeFileSync(provider,`process.on('SIGTERM',()=>{});await Bun.write(${JSON.stringify(ready)},String(process.pid));setTimeout(()=>process.exit(0),6000);`);
|
|
35
|
+
try{
|
|
36
|
+
for(const abort of [false,true]){
|
|
37
|
+
rmSync(ready,{force:true});
|
|
38
|
+
const control=new AbortController();
|
|
39
|
+
const start=performance.now();
|
|
40
|
+
const pending=compare({a,b},{commands:[[process.execPath,provider]],signal:control.signal,timeout:abort?10000:1000});
|
|
41
|
+
let pid;
|
|
42
|
+
for(let i=0;i<100;i++){
|
|
43
|
+
if(await Bun.file(ready).exists()){pid=Number(await Bun.file(ready).text());break}
|
|
44
|
+
await Bun.sleep(20);
|
|
45
|
+
}
|
|
46
|
+
expect(pid).toBeGreaterThan(0);
|
|
47
|
+
if(abort)control.abort();
|
|
48
|
+
const result=await pending;
|
|
49
|
+
expect(performance.now()-start).toBeLessThan(3000);
|
|
50
|
+
if(abort)expect(result).toBe(null);else expect(result.state).toBe('failed');
|
|
51
|
+
expect(()=>process.kill(pid,0)).toThrow();
|
|
52
|
+
}
|
|
53
|
+
}finally{rmSync(dir,{recursive:true,force:true})}
|
|
54
|
+
},15000);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {test,expect,afterAll} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,mkdirSync,writeFileSync,readFileSync,existsSync,renameSync,symlinkSync,lstatSync,rmSync,readdirSync} from 'node:fs';
|
|
3
|
+
import {tmpdir} from 'node:os';
|
|
4
|
+
import {join,basename} from 'node:path';
|
|
5
|
+
import {menuaction} from '../src/menu.js';
|
|
6
|
+
import {trashOne,trash} from '../src/trash.js';
|
|
7
|
+
const root=mkdtempSync(join(tmpdir(),'flea-mutations-'));
|
|
8
|
+
const original=process.env.XDG_DATA_HOME;
|
|
9
|
+
process.env.XDG_DATA_HOME=join(root,'data');
|
|
10
|
+
afterAll(()=>{if(original===undefined)delete process.env.XDG_DATA_HOME;else process.env.XDG_DATA_HOME=original;rmSync(root,{recursive:true,force:true});});
|
|
11
|
+
let next=1000;
|
|
12
|
+
function fixture() {
|
|
13
|
+
const id=++next,path=join(root,String(id));mkdirSync(path);writeFileSync(join(path,'file'),'hello');
|
|
14
|
+
const messages=[],send=m=>messages.push(m);
|
|
15
|
+
const call=(op,rest={})=>{menuaction({op,id,...rest},send,null,0);return messages.at(-1)};
|
|
16
|
+
call('snapshot',{paths:[path]});return {path,call};
|
|
17
|
+
}
|
|
18
|
+
test('permanent delete requires a prepared token and deletes reviewed tree once',()=>{
|
|
19
|
+
const {path,call}=fixture();expect(call('checkDelete').valid).toBe(false);expect(call('delete').ok).toBe(false);
|
|
20
|
+
const prepared=call('prepareDelete');expect(prepared.name).toBe(basename(path));const {token}=prepared;expect(call('checkDelete',{token}).valid).toBe(true);
|
|
21
|
+
call('delete',{token});expect(existsSync(path)).toBe(false);expect(call('delete',{token}).ok).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
test('new descendants invalidate confirmation before any removal',()=>{
|
|
24
|
+
const {path,call}=fixture(),{token}=call('prepareDelete');writeFileSync(join(path,'new'),'new');
|
|
25
|
+
expect(call('checkDelete',{token}).valid).toBe(false);expect(call('delete',{token}).stale).toBe(true);expect(existsSync(join(path,'file'))).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
test('refresh captures replacement identity and updates the cursor',()=>{
|
|
28
|
+
const {path,call}=fixture(),old=call('prepareDelete').token;
|
|
29
|
+
renameSync(path,path+'.old');mkdirSync(path);writeFileSync(join(path,'new'),'new');
|
|
30
|
+
const {token}=call('refreshDelete');expect(token).not.toBe(old);expect(call('checkDelete',{token}).valid).toBe(true);
|
|
31
|
+
call('delete',{token});expect(existsSync(path)).toBe(false);expect(existsSync(path+'.old/file')).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
test('review and deletion do not follow symbolic links',()=>{
|
|
34
|
+
const {path,call}=fixture(),outside=join(root,'outside');mkdirSync(outside);writeFileSync(join(outside,'keep'),'keep');symlinkSync(outside,join(path,'link'));
|
|
35
|
+
const {token}=call('prepareDelete');call('delete',{token});expect(readFileSync(join(outside,'keep'),'utf8')).toBe('keep');
|
|
36
|
+
});
|
|
37
|
+
test('root deletion is refused without traversing it',()=>{
|
|
38
|
+
const id=++next, messages=[],send=m=>messages.push(m);
|
|
39
|
+
menuaction({op:'snapshot',id,paths:['/']},send,null,0);menuaction({op:'prepareDelete',id},send,null,0);
|
|
40
|
+
expect(messages.at(-1).ok).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
test('trash moves files, preserves content and encodes metadata',()=>{
|
|
43
|
+
const path=join(root,'space 中文.txt');writeFileSync(path,'payload');trashOne(path);
|
|
44
|
+
const dir=join(process.env.XDG_DATA_HOME,'Trash');expect(existsSync(path)).toBe(false);
|
|
45
|
+
expect(readFileSync(join(dir,'files','space 中文.txt'),'utf8')).toBe('payload');
|
|
46
|
+
const info=readFileSync(join(dir,'info','space 中文.txt.trashinfo'),'utf8');expect(info).toContain('%20');expect(info).toContain('%E4%B8%AD');expect(info).toContain('DeletionDate=');
|
|
47
|
+
});
|
|
48
|
+
test('trash does not overwrite an orphan or a prior trashed name',()=>{
|
|
49
|
+
const path=join(root,'same');writeFileSync(path,'first');trashOne(path);writeFileSync(path,'second');trashOne(path);
|
|
50
|
+
const files=join(process.env.XDG_DATA_HOME,'Trash/files');writeFileSync(join(files,'orphan'),'old');
|
|
51
|
+
const other=join(root,'orphan');writeFileSync(other,'new');trashOne(other);
|
|
52
|
+
expect(readFileSync(join(files,'same'),'utf8')).toBe('first');expect(readFileSync(join(files,'same.2'),'utf8')).toBe('second');
|
|
53
|
+
expect(readFileSync(join(files,'orphan'),'utf8')).toBe('old');expect(readFileSync(join(files,'orphan.2'),'utf8')).toBe('new');
|
|
54
|
+
});
|
|
55
|
+
test('trash keeps symlink as a symlink and reports per-item failures',()=>{
|
|
56
|
+
const path=join(root,'link');symlinkSync(join(root,'missing-target'),path);
|
|
57
|
+
const events=[];trash({id:1},m=>events.push(m),{paths:[path,join(root,'absent')]});
|
|
58
|
+
expect(lstatSync(join(process.env.XDG_DATA_HOME,'Trash/files/link')).isSymbolicLink()).toBe(true);
|
|
59
|
+
const result=events.find(m=>m.t==='trashed');expect(result.ok).toBe(1);expect(result.failed).toBe(1);
|
|
60
|
+
});
|
|
61
|
+
test('unsafe symlink Trash is refused without moving the source',()=>{
|
|
62
|
+
const saved=process.env.XDG_DATA_HOME,dir=join(root,'unsafe'),outside=join(root,'unrelated');mkdirSync(dir);mkdirSync(outside);
|
|
63
|
+
symlinkSync(outside,join(dir,'Trash'));process.env.XDG_DATA_HOME=dir;
|
|
64
|
+
const path=join(root,'keep');writeFileSync(path,'keep');
|
|
65
|
+
try {expect(()=>trashOne(path)).toThrow();expect(existsSync(path)).toBe(true);expect(readdirSync(outside)).toEqual([]);}
|
|
66
|
+
finally{process.env.XDG_DATA_HOME=saved;}
|
|
67
|
+
});
|