flea-qml 0.0.0-stage → 0.1.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 +478 -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 +402 -0
- package/src/archiveops.js +697 -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 +129 -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/transfer.js +669 -0
- package/src/trash-record.js +41 -0
- package/src/trash.js +210 -0
- package/src/trashbrowse.js +326 -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 +241 -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-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-html-security.mjs +23 -0
- package/test/manual-html-ui.mjs +30 -0
- package/test/manual-syntax-preview.mjs +26 -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 +68 -0
- package/ui/ArchiveCompressDialog.qml +378 -0
- package/ui/ArchivePasswordDialog.qml +257 -0
- package/ui/Backend.qml +771 -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 +523 -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 +115 -0
- package/ui/Pane.qml +1296 -0
- package/ui/PaneMenuActions.qml +527 -0
- package/ui/PanePath.qml +90 -0
- package/ui/PaneRail.qml +239 -0
- package/ui/PaneStates.qml +88 -0
- package/ui/PaneSwap.qml +339 -0
- package/ui/PaneWire.qml +605 -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 +1004 -0
- package/ui/PreviewAnimation.qml +24 -0
- package/ui/PreviewArchive.qml +66 -0
- package/ui/PreviewColumn.qml +551 -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 +38 -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 +732 -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/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 +827 -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 +122 -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 +364 -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 +502 -0
- package/ui/js/FolderSorts.js +108 -0
- package/ui/js/FolderViews.js +44 -0
- package/ui/js/Format.js +305 -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 +736 -0
- package/ui/js/Tags.js +134 -0
- package/ui/js/Taildrop.js +128 -0
- package/ui/js/Tap.js +174 -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 +81 -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 +228 -0
|
@@ -0,0 +1,60 @@
|
|
|
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())
|
|
4
|
+
.replace(/^\.(?:pragma|import).*$/gm, '');
|
|
5
|
+
return new Function(...Object.keys(deps), source + '; return {' + names + '};')(...Object.values(deps));
|
|
6
|
+
}
|
|
7
|
+
const Scroll = await load('Scroll', 'limitsY,stopTail,tailState');
|
|
8
|
+
const keys = await load('PreviewKeys', 'act,pdfAction,columnPdfAction,pageDirection,pageViewport', { Scroll });
|
|
9
|
+
test('page actions scroll Quick Look without changing the selected file', () => {
|
|
10
|
+
const calls = [];
|
|
11
|
+
const root = { preview: { revealStrip() {}, pageBy: n => calls.push(n) } };
|
|
12
|
+
for (const action of ['pageDown', 'pageUp', 'previewPageForward', 'previewPageBack']) keys.act(action, root);
|
|
13
|
+
expect(calls).toEqual([1, -1, 1, -1]);
|
|
14
|
+
});
|
|
15
|
+
test('both PDF key routes handle every page alias', () => {
|
|
16
|
+
for (const handler of [keys.pdfAction, keys.columnPdfAction]) {
|
|
17
|
+
const calls = [];
|
|
18
|
+
const viewer = { pageBy: n => calls.push(n) };
|
|
19
|
+
for (const action of ['pageDown', 'pageUp', 'previewPageForward', 'previewPageBack']) handler(action, viewer);
|
|
20
|
+
expect(calls).toEqual([1, -1, 1, -1]);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
test('viewport paging respects origin, margins, bounds, and stops wheel momentum', () => {
|
|
24
|
+
let cancelled = 0, layouts = 0;
|
|
25
|
+
const view = { originY: 100, topMargin: 10, bottomMargin: 20, height: 200,
|
|
26
|
+
contentHeight: 700, contentY: 90, cancelFlick() { cancelled++; }, forceLayout() { layouts++; } };
|
|
27
|
+
const tail = Scroll.tailState(view, true); tail.active = true; tail.vy = 2;
|
|
28
|
+
keys.pageViewport(view, 1); expect(view.contentY).toBe(290);
|
|
29
|
+
keys.pageViewport(view, 1); keys.pageViewport(view, 1); expect(view.contentY).toBe(620);
|
|
30
|
+
for (let i = 0; i < 5; i++) keys.pageViewport(view, -1);
|
|
31
|
+
expect(view.contentY).toBe(90);
|
|
32
|
+
expect(tail.active).toBe(false); expect(tail.vy).toBe(0);
|
|
33
|
+
expect(cancelled).toBe(8); expect(layouts).toBe(8);
|
|
34
|
+
view.contentHeight = 20; keys.pageViewport(view, 1); expect(view.contentY).toBe(90);
|
|
35
|
+
keys.pageViewport(null, 1);
|
|
36
|
+
});
|
|
37
|
+
test('Ctrl-Tab returns from Quick Look like Escape', () => {
|
|
38
|
+
let closed = 0;
|
|
39
|
+
keys.act('focusPreview', { preview: { revealStrip() {}, close() { closed++; } } });
|
|
40
|
+
expect(closed).toBe(1);
|
|
41
|
+
});
|
|
42
|
+
test('HTML Quick Look arrows follow adjacent files, including media, rather than scrolling HTML', async () => {
|
|
43
|
+
const followed = [], moved = [];
|
|
44
|
+
const rows = [{n:'1.html',i:1,s:110,k:0},{n:'1.mp3',i:2,s:200,k:1}];
|
|
45
|
+
const root = {cursorIndex:0,kindNames:['HTML','Audio'],join:(base,name)=>base+'/'+name,
|
|
46
|
+
rowFor:i=>rows[i],preview:{isHtml:true,isCompare:false,
|
|
47
|
+
markdownShown:{shownView:'rendered',scrollBy(){throw Error('Arrow must change file');}},
|
|
48
|
+
revealStrip(){},follow:(...args)=>followed.push(args)}};
|
|
49
|
+
const actions=await load('PreviewKeys','act',{
|
|
50
|
+
Filter:{moveCursor(r,n){r.cursorIndex+=n;moved.push(n);},cursorShown:()=>true},
|
|
51
|
+
Marks:{follow(){}},Search:{baseOf:()=>'/test'},Scroll,
|
|
52
|
+
});
|
|
53
|
+
actions.act('cursorDown',root);
|
|
54
|
+
expect(followed[0].slice(0,4)).toEqual(['/test/1.mp3',2,200,'Audio']);
|
|
55
|
+
actions.act('cursorUp',root);
|
|
56
|
+
expect(followed[1][0]).toBe('/test/1.html');
|
|
57
|
+
root.preview.isCompare=true;
|
|
58
|
+
actions.act('cursorDown',root);
|
|
59
|
+
expect(moved).toEqual([1,-1]);
|
|
60
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
import {previewLanguage} from '../src/preview-language.js';
|
|
3
|
+
import {code} from '../src/code.js';
|
|
4
|
+
const hint=(text,path='main.v')=>previewLanguage(path,Buffer.from(text));
|
|
5
|
+
test('V declarations distinguish .v source without stealing Verilog',()=>{
|
|
6
|
+
expect(hint('module main\nfn main() { println("hello") }')).toBe('v');
|
|
7
|
+
expect(hint('pub fn (a App) run() {}')).toBe('v');
|
|
8
|
+
expect(hint('fn map[T](x T) T { return x }')).toBe('v');
|
|
9
|
+
expect(hint('module main;\nendmodule')).toBeNull();
|
|
10
|
+
expect(hint('// fn main() {}\nmodule main;\nendmodule')).toBeNull();
|
|
11
|
+
expect(hint('/* fn main() {} */')).toBeNull();
|
|
12
|
+
expect(hint('"fn main() {}"')).toBeNull();
|
|
13
|
+
expect(hint('module main')).toBeNull();
|
|
14
|
+
expect(hint('fn main() {}','main.txt')).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
test('V and Verilog both retain source text and colour through the real preview launcher',async()=>{
|
|
17
|
+
for(const text of ['module main\nfn main() { println("hello") }\n','module counter(input clk);\nreg value;\nendmodule\n']) {
|
|
18
|
+
const reply=await code({name:'main.v',text});
|
|
19
|
+
expect(reply.state).toBe('styled');
|
|
20
|
+
expect(reply.lines.map(r=>r.filter((_,i)=>i%2).join('')).join('\n')).toBe(text.trimEnd());
|
|
21
|
+
}
|
|
22
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
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 Match=await load('Match','rowName');
|
|
7
|
+
const Status=await load('Status','fileName',{Match});
|
|
8
|
+
const rows=[{n:'folder/完整的 very long file name.txt'},{n:'Trash/files/name.2',trash:{name:'Original.txt'}},{n:'other.txt'}];
|
|
9
|
+
function pane(selected=[],cursorIndex=0){return {selection:{indices:()=>selected},cursorIndex,shown:null,rowFor:i=>rows[i]??null};}
|
|
10
|
+
test('status names a single selection, or the current row without marks',()=>{
|
|
11
|
+
expect(Status.fileName(pane())).toBe('完整的 very long file name.txt');
|
|
12
|
+
expect(Status.fileName(pane([1]))).toBe('Original.txt');
|
|
13
|
+
expect(Status.fileName(pane([0,1],2))).toBe('other.txt');
|
|
14
|
+
});
|
|
15
|
+
test('status clears absent, filtered or stale cursor names',()=>{
|
|
16
|
+
expect(Status.fileName(null)).toBe('');
|
|
17
|
+
expect(Status.fileName(pane([],50))).toBe('');
|
|
18
|
+
expect(Status.fileName({...pane(),shown:[]})).toBe('');
|
|
19
|
+
expect(Status.fileName({...pane(),listInFlight:true})).toBe('');
|
|
20
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { test, expect, afterAll, beforeAll } from "bun:test";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, existsSync, symlinkSync, renameSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
// Paste's Replace and Merge (docs/protocol.md "transfer", "collisions", "mergecollisions"): the
|
|
7
|
+
// item already there goes to flea's Trash, a folder on a folder folds in, and only the names the
|
|
8
|
+
// questions saw are ever answered.
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), "flea-replace-"));
|
|
10
|
+
const saved = process.env.XDG_DATA_HOME;
|
|
11
|
+
process.env.XDG_DATA_HOME = join(root, "data");
|
|
12
|
+
const { collisions, mergeCollisions, transfer } = await import("../src/transfer.js");
|
|
13
|
+
afterAll(() => {
|
|
14
|
+
if (saved === undefined) delete process.env.XDG_DATA_HOME;
|
|
15
|
+
else process.env.XDG_DATA_HOME = saved;
|
|
16
|
+
rmSync(root, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
const trashFiles = () => { try { return readdirSync(join(root, "data", "Trash", "files")); } catch { return []; } };
|
|
19
|
+
|
|
20
|
+
let serial = 0;
|
|
21
|
+
function place() {
|
|
22
|
+
const at = join(root, String(++serial)), src = join(at, "src"), dest = join(at, "dest");
|
|
23
|
+
mkdirSync(src, { recursive: true });
|
|
24
|
+
mkdirSync(dest, { recursive: true });
|
|
25
|
+
const messages = [];
|
|
26
|
+
return { src, dest, messages, send: (m) => messages.push(m) };
|
|
27
|
+
}
|
|
28
|
+
async function run(p, request) {
|
|
29
|
+
const from = p.messages.length;
|
|
30
|
+
transfer(request, p.send, null, 0);
|
|
31
|
+
for (let i = 0; i < 400; i++) {
|
|
32
|
+
const done = p.messages.slice(from).find((m) => m.t === "transferdone");
|
|
33
|
+
if (done) return { done, items: p.messages.slice(from).filter((m) => m.t === "transferitem") };
|
|
34
|
+
await Bun.sleep(5);
|
|
35
|
+
}
|
|
36
|
+
throw Error("no transferdone: " + JSON.stringify(p.messages));
|
|
37
|
+
}
|
|
38
|
+
const ask = (p, id, paths) => { collisions({ id, paths, dest: p.dest }, p.send, null, 0); return p.messages.at(-1); };
|
|
39
|
+
const askMerge = (p, id, paths) => { mergeCollisions({ id, paths, dest: p.dest }, p.send, null, 0); return p.messages.at(-1); };
|
|
40
|
+
|
|
41
|
+
test("Replace sends the item already there to the Trash and lands the incoming one", async () => {
|
|
42
|
+
const p = place(), before = trashFiles().length;
|
|
43
|
+
writeFileSync(join(p.src, "a.txt"), "new");
|
|
44
|
+
writeFileSync(join(p.dest, "a.txt"), "old");
|
|
45
|
+
expect(ask(p, 1, [join(p.src, "a.txt")])).toMatchObject({ t: "collisions", total: 1, mergeable: false });
|
|
46
|
+
const { done } = await run(p, { op: "copy", paths: [join(p.src, "a.txt")], dest: p.dest, collide: "replace", collideId: 1 });
|
|
47
|
+
expect(done).toMatchObject({ ok: 1, failed: 0 });
|
|
48
|
+
expect(done.into).toBeUndefined();
|
|
49
|
+
expect(readFileSync(join(p.dest, "a.txt"), "utf8")).toBe("new");
|
|
50
|
+
expect(trashFiles().length).toBe(before + 1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("Replace covers only what the question saw: a name swapped since is refused", async () => {
|
|
54
|
+
const p = place();
|
|
55
|
+
writeFileSync(join(p.src, "a.txt"), "new");
|
|
56
|
+
writeFileSync(join(p.dest, "a.txt"), "old");
|
|
57
|
+
ask(p, 2, [join(p.src, "a.txt")]);
|
|
58
|
+
renameSync(join(p.dest, "a.txt"), join(p.dest, "gone"));
|
|
59
|
+
writeFileSync(join(p.dest, "a.txt"), "swapped in");
|
|
60
|
+
const { done, items } = await run(p, { op: "copy", paths: [join(p.src, "a.txt")], dest: p.dest, collide: "replace", collideId: 2 });
|
|
61
|
+
expect(done.failed).toBe(1);
|
|
62
|
+
expect(items[0].err).toBe("an item with that name is already there");
|
|
63
|
+
expect(readFileSync(join(p.dest, "a.txt"), "utf8")).toBe("swapped in");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("Replace refuses an item already there that holds the one moving in, and a link to itself", async () => {
|
|
67
|
+
const p = place();
|
|
68
|
+
// Moving dest/box/inner over dest-side "box" would trash its own source.
|
|
69
|
+
mkdirSync(join(p.dest, "box"));
|
|
70
|
+
mkdirSync(join(p.src, "box"));
|
|
71
|
+
writeFileSync(join(p.dest, "box", "box"), "x");
|
|
72
|
+
const nested = join(p.dest, "box", "box");
|
|
73
|
+
const q = ask(p, 3, [nested]);
|
|
74
|
+
// nested lives in dest/box, not dest, so it collides with dest/box itself.
|
|
75
|
+
expect(q.total).toBe(1);
|
|
76
|
+
const first = await run(p, { op: "move", paths: [nested], dest: p.dest, collide: "replace", collideId: 3 });
|
|
77
|
+
expect(first.items[0].err).toBe("the item already there holds the one being moved in, so it was not replaced");
|
|
78
|
+
expect(existsSync(nested)).toBe(true);
|
|
79
|
+
if (process.platform === "win32") return;
|
|
80
|
+
writeFileSync(join(p.dest, "t.txt"), "target");
|
|
81
|
+
symlinkSync(join(p.dest, "t.txt"), join(p.src, "t.txt"));
|
|
82
|
+
ask(p, 4, [join(p.src, "t.txt")]);
|
|
83
|
+
const second = await run(p, { op: "copy", paths: [join(p.src, "t.txt")], dest: p.dest, collide: "replace", collideId: 4 });
|
|
84
|
+
expect(second.items[0].err).toBe("the incoming link points at the item already there, so it was not replaced");
|
|
85
|
+
expect(readFileSync(join(p.dest, "t.txt"), "utf8")).toBe("target");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a failed replace puts the item it trashed straight back", async () => {
|
|
89
|
+
if (process.platform === "win32" || process.getuid?.() === 0) return;
|
|
90
|
+
const p = place();
|
|
91
|
+
mkdirSync(join(p.src, "d"));
|
|
92
|
+
writeFileSync(join(p.src, "d", "secret"), "s");
|
|
93
|
+
const { chmodSync } = await import("node:fs");
|
|
94
|
+
chmodSync(join(p.src, "d"), 0o000);
|
|
95
|
+
writeFileSync(join(p.dest, "d"), "old file");
|
|
96
|
+
ask(p, 5, [join(p.src, "d")]);
|
|
97
|
+
const { done, items } = await run(p, { op: "copy", paths: [join(p.src, "d")], dest: p.dest, collide: "replace", collideId: 5 });
|
|
98
|
+
chmodSync(join(p.src, "d"), 0o755);
|
|
99
|
+
expect(done.failed).toBe(1);
|
|
100
|
+
expect(items[0].err).not.toContain("still in the Trash");
|
|
101
|
+
expect(readFileSync(join(p.dest, "d"), "utf8")).toBe("old file");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
function tree(p) {
|
|
105
|
+
mkdirSync(join(p.src, "trip", "day1"), { recursive: true });
|
|
106
|
+
mkdirSync(join(p.dest, "trip", "day1"), { recursive: true });
|
|
107
|
+
writeFileSync(join(p.src, "trip", "new.jpg"), "new");
|
|
108
|
+
writeFileSync(join(p.src, "trip", "day1", "same.jpg"), "incoming");
|
|
109
|
+
writeFileSync(join(p.src, "trip", "day1", "fresh.jpg"), "fresh");
|
|
110
|
+
writeFileSync(join(p.dest, "trip", "day1", "same.jpg"), "already");
|
|
111
|
+
writeFileSync(join(p.dest, "trip", "keep.jpg"), "untouched");
|
|
112
|
+
return [join(p.src, "trip")];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
test("Merge is offered for a folder on a folder, and its walk lists the names both hold", () => {
|
|
116
|
+
const p = place(), paths = tree(p);
|
|
117
|
+
expect(ask(p, 10, paths)).toMatchObject({ total: 1, mergeable: true, names: [{ n: "trip", d: true }] });
|
|
118
|
+
expect(askMerge(p, 10, paths)).toMatchObject({ t: "mergecollisions", id: 10, total: 1, names: [{ n: "same.jpg", d: false }] });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
for (const [inner, expected, keptBoth] of [["keep", "already", 1], ["replace", "incoming", 0], ["skip", "already", 0]]) {
|
|
122
|
+
test(`Merge folds the folders together, and "${inner}" answers the names both hold`, async () => {
|
|
123
|
+
const p = place(), paths = tree(p);
|
|
124
|
+
ask(p, 11, paths);
|
|
125
|
+
askMerge(p, 11, paths);
|
|
126
|
+
const { done } = await run(p, { op: "copy", paths, dest: p.dest, collide: "merge", collideId: 11, inner });
|
|
127
|
+
expect(done).toMatchObject({ ok: 1, failed: 0, into: "trip", keptBoth, merged: 2 + keptBoth + (inner === "replace" ? 1 : 0) });
|
|
128
|
+
const day1 = join(p.dest, "trip", "day1");
|
|
129
|
+
expect(readFileSync(join(day1, "same.jpg"), "utf8")).toBe(expected);
|
|
130
|
+
expect(readFileSync(join(day1, "fresh.jpg"), "utf8")).toBe("fresh");
|
|
131
|
+
expect(readFileSync(join(p.dest, "trip", "new.jpg"), "utf8")).toBe("new");
|
|
132
|
+
expect(readFileSync(join(p.dest, "trip", "keep.jpg"), "utf8")).toBe("untouched");
|
|
133
|
+
if (inner === "keep") expect(readFileSync(join(day1, "same copy.jpg"), "utf8")).toBe("incoming");
|
|
134
|
+
expect(existsSync(join(p.src, "trip", "day1", "same.jpg"))).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
test("a merging move removes the folders it emptied and leaves what it skipped", async () => {
|
|
139
|
+
const p = place(), paths = tree(p);
|
|
140
|
+
ask(p, 12, paths);
|
|
141
|
+
askMerge(p, 12, paths);
|
|
142
|
+
const { done } = await run(p, { op: "move", paths, dest: p.dest, collide: "merge", collideId: 12, inner: "skip" });
|
|
143
|
+
expect(done).toMatchObject({ ok: 1, failed: 0, merged: 2 });
|
|
144
|
+
expect(readdirSync(join(p.src, "trip"))).toEqual(["day1"]);
|
|
145
|
+
expect(readdirSync(join(p.src, "trip", "day1"))).toEqual(["same.jpg"]);
|
|
146
|
+
expect(readFileSync(join(p.dest, "trip", "day1", "fresh.jpg"), "utf8")).toBe("fresh");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("a name that appears inside after the walk is refused whatever the inner answer", async () => {
|
|
150
|
+
const p = place(), paths = tree(p);
|
|
151
|
+
ask(p, 13, paths);
|
|
152
|
+
askMerge(p, 13, paths);
|
|
153
|
+
writeFileSync(join(p.dest, "trip", "new.jpg"), "late");
|
|
154
|
+
const { done, items } = await run(p, { op: "copy", paths, dest: p.dest, collide: "merge", collideId: 13, inner: "replace" });
|
|
155
|
+
expect(done.failed).toBe(1);
|
|
156
|
+
expect(items[0].err).toBe("trip/new.jpg could not be copied: an item with that name is already there".replaceAll("/", join("a", "b")[1]));
|
|
157
|
+
expect(readFileSync(join(p.dest, "trip", "new.jpg"), "utf8")).toBe("late");
|
|
158
|
+
expect(readFileSync(join(p.dest, "trip", "day1", "same.jpg"), "utf8")).toBe("incoming");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("merge without the walk folds nothing that both hold, and a bad inner word is refused", async () => {
|
|
162
|
+
const p = place(), paths = tree(p);
|
|
163
|
+
ask(p, 14, paths);
|
|
164
|
+
// The second question was never asked: the transfer still merges, but same.jpg is refused.
|
|
165
|
+
const { done } = await run(p, { op: "copy", paths, dest: p.dest, collide: "merge", collideId: 14, inner: "replace" });
|
|
166
|
+
expect(done.failed).toBe(1);
|
|
167
|
+
expect(readFileSync(join(p.dest, "trip", "day1", "same.jpg"), "utf8")).toBe("already");
|
|
168
|
+
transfer({ op: "copy", paths, dest: p.dest, collide: "merge", collideId: 14, inner: "merge" }, p.send, null, 0);
|
|
169
|
+
expect(p.messages.at(-1)).toMatchObject({ t: "error", where: "transfer" });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("into names the merging folder only when it was the transfer's one item", async () => {
|
|
173
|
+
const p = place(), paths = tree(p);
|
|
174
|
+
writeFileSync(join(p.src, "a.txt"), "new");
|
|
175
|
+
writeFileSync(join(p.dest, "a.txt"), "old");
|
|
176
|
+
const both = [...paths, join(p.src, "a.txt")];
|
|
177
|
+
ask(p, 15, both);
|
|
178
|
+
askMerge(p, 15, both);
|
|
179
|
+
const { done } = await run(p, { op: "copy", paths: both, dest: p.dest, collide: "merge", collideId: 15, inner: "keep" });
|
|
180
|
+
expect(done.into).toBe("dest");
|
|
181
|
+
const { readFileSync: read } = await import("node:fs");
|
|
182
|
+
const source = read(new URL("../ui/js/Collide.js", import.meta.url), "utf8").replace(/^\.(?:pragma|import).*$/gm, "");
|
|
183
|
+
const { mergeDest } = new Function("Format", "Ops", "Status", "Swap", source + ";return {mergeDest};")({}, {}, {}, {});
|
|
184
|
+
const request = { dest: "/d" };
|
|
185
|
+
expect(mergeDest(request, [{ n: "trip", d: true }], 1)).toBe("/d/trip");
|
|
186
|
+
expect(mergeDest(request, [{ n: "trip", d: true }, { n: "a.txt", d: false }], 2)).toBe("/d");
|
|
187
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
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 PathBar=await load('PathBar','isTrashRoot,addressTarget,editLine,shouldNavigate',{Format:{}});
|
|
7
|
+
const Crumbs=await load('Crumbs','crumbs');
|
|
8
|
+
test('Trash root aliases canonicalize only at the address input boundary',()=>{
|
|
9
|
+
for(const value of ['trash:/','trash://','trash:///',' TRASH:/// ']) {
|
|
10
|
+
expect(PathBar.isTrashRoot(value)).toBe(true);
|
|
11
|
+
expect(PathBar.addressTarget(value,'/home/test','/home/test')).toBe('trash:///');
|
|
12
|
+
}
|
|
13
|
+
for(const value of ['trash:','trash:////','trash://host/','trash:///file','/tmp/trash:','trash'])expect(PathBar.isTrashRoot(value)).toBe(false);
|
|
14
|
+
expect(PathBar.addressTarget('/tmp','trash:///','/home/test')).toBe('/tmp');
|
|
15
|
+
expect(PathBar.addressTarget('~','trash:///','/home/test')).toBe('/home/test');
|
|
16
|
+
expect(PathBar.addressTarget('tmp','trash:///','/home/test')).toBe('/tmp');
|
|
17
|
+
});
|
|
18
|
+
test('Trash root remains a single breadcrumb and does not gain a fourth slash',()=>{
|
|
19
|
+
expect(PathBar.editLine('trash:///')).toBe('trash:///');
|
|
20
|
+
expect(PathBar.editLine('/tmp')).toBe('/tmp/');
|
|
21
|
+
expect(Crumbs.crumbs('trash:///','/home/test',null)).toEqual([{text:'trash:///',path:'trash:///',last:true}]);
|
|
22
|
+
expect(PathBar.shouldNavigate('trash:///','trash:///',false)).toBe(false);
|
|
23
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
test('Back from a trashed directory restores its collection and cursor, not filesystem root',async()=>{
|
|
7
|
+
const Back=await load('BackReturn','capture,travel');
|
|
8
|
+
const pane={path:'/',recentMode:'trash',recentFrom:'/home/test',cursorIndex:3,rowFor:()=>({n:'tmp/Trash/files/folder'})};
|
|
9
|
+
const entry=Back.capture(pane);
|
|
10
|
+
let opened=0;
|
|
11
|
+
Object.assign(pane,{path:'/tmp/Trash/files/folder',recentMode:'',history:[entry],forwardHistory:[],
|
|
12
|
+
trash:{open(){opened++}},join:(base,name)=>base+name,
|
|
13
|
+
openWithoutHistory(){throw Error('must not navigate to /')}});
|
|
14
|
+
Back.travel(pane,'history','forwardHistory');
|
|
15
|
+
expect(opened).toBe(1);expect(pane.recentFrom).toBe('/home/test');expect(pane.pendingSelect).toBe('/tmp/Trash/files/folder');
|
|
16
|
+
expect(pane.forwardHistory[0].path).toBe('/tmp/Trash/files/folder');
|
|
17
|
+
});
|
|
18
|
+
test('Trash Filter uses the visible original name, not its collision-safe storage name',async()=>{
|
|
19
|
+
const Match=await load('Match','run,base,rowName');
|
|
20
|
+
const Filter=await load('Filter','shown',{Match});
|
|
21
|
+
const rows=[{n:'tmp/Trash/files/report.2',trash:{original:'/docs/report'}},{n:'tmp/Trash/files/elsewhere',trash:{original:'/docs/notes'}}];
|
|
22
|
+
expect(Filter.shown(rows,10,'report')).toEqual([10]);
|
|
23
|
+
expect(Filter.shown(rows,10,'report.2')).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
test('Find in the virtual Trash cannot silently search the previous directory',async()=>{
|
|
26
|
+
const Search=await load('Search','start');let message='';
|
|
27
|
+
const pane={recentMode:'trash',path:'/',recentFrom:'/private',message:m=>message=m};
|
|
28
|
+
Search.start(pane);
|
|
29
|
+
expect(pane.path).toBe('/');expect(pane.recentMode).toBe('trash');expect(message).toContain('filters');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('Trash captions share original basenames in list, grid and columns',async()=>{
|
|
33
|
+
const Match=await load('Match','rowName');
|
|
34
|
+
expect(Match.rowName({n:'tmp/Trash/files/report.2',trash:{original:'/docs/report'}},false)).toBe('report');
|
|
35
|
+
expect(Match.rowName({n:'C:/Trash/files/report.2',trash:{original:'C:\\docs\\report',name:'report'}},true)).toBe('report');
|
|
36
|
+
expect(Match.rowName({n:'sub/file.txt'},false)).toBe('sub/file.txt');
|
|
37
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {test, expect, beforeEach, afterEach} from 'bun:test';
|
|
2
|
+
import {mkdtempSync,mkdirSync,writeFileSync,readFileSync,renameSync,symlinkSync,existsSync,rmSync} from 'node:fs';
|
|
3
|
+
import {join,dirname} from 'node:path';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import {trashOne,homeTrash,trashInfoPath,trashInfoOriginal} from '../src/trash.js';
|
|
6
|
+
import {trashbrowse,readTrash,watchTrashCount} from '../src/trashbrowse.js';
|
|
7
|
+
let root,saved,stop;
|
|
8
|
+
const call=(op,fields={})=>{const replies=[];trashbrowse({op,id:1,...fields},m=>replies.push(m));return replies.find(m=>m.t==='trashbrowse');};
|
|
9
|
+
beforeEach(()=>{root=mkdtempSync(join(tmpdir(),'flea-trashbrowse-'));saved=process.env.XDG_DATA_HOME;process.env.XDG_DATA_HOME=join(root,'data');mkdirSync(process.env.XDG_DATA_HOME);call('cancel',{clearSelection:true});});
|
|
10
|
+
afterEach(()=>{stop?.();stop=null;if(saved===undefined)delete process.env.XDG_DATA_HOME;else process.env.XDG_DATA_HOME=saved;rmSync(root,{recursive:true,force:true});});
|
|
11
|
+
function item(name='sample',directory=false){const path=join(root,name);if(directory){mkdirSync(path);writeFileSync(join(path,'keep'),'keep');}else writeFileSync(path,'payload');trashOne(path);return readTrash().find(i=>i.original===path);}
|
|
12
|
+
test('Trash list, restore, collisions and single-use deletion',()=>{
|
|
13
|
+
const a=item('a');expect(call('list',{count:20}).rows.some(r=>r.uri===a.uri)).toBe(true);
|
|
14
|
+
writeFileSync(a.original,'new');expect(call('restore',{uris:[a.uri],identities:[a.identity]}).failed).toBe(1);
|
|
15
|
+
expect(readFileSync(a.original,'utf8')).toBe('new');rmSync(a.original);
|
|
16
|
+
expect(call('restore',{uris:[a.uri],identities:[a.identity]}).done).toBe(1);expect(readFileSync(a.original,'utf8')).toBe('payload');
|
|
17
|
+
const b=item('b');const {token}=call('prepare',{uris:[b.uri],identities:[b.identity]});
|
|
18
|
+
expect(call('check',{token}).valid).toBe(true);expect(call('delete',{token}).done).toBe(1);expect(call('delete',{token}).ok).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
test('cancel and failed replacement reviews invalidate deletion tokens',()=>{
|
|
21
|
+
const a=item();let {token}=call('prepare',{uris:[a.uri]});call('cancel');expect(call('delete',{token}).ok).toBe(false);
|
|
22
|
+
({token}=call('prepare',{uris:[a.uri]}));expect(call('prepare',{uris:['missing']}).ok).toBe(false);
|
|
23
|
+
expect(call('delete',{token}).ok).toBe(false);expect(existsSync(a.file)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
test('new descendants invalidate the whole deletion review',()=>{
|
|
26
|
+
const a=item('folder',true),{token}=call('prepare',{uris:[a.uri]});
|
|
27
|
+
writeFileSync(join(a.file,'new'),'new');expect(call('check',{token}).valid).toBe(false);
|
|
28
|
+
expect(call('delete',{token}).stale).toBe(true);expect(existsSync(join(a.file,'keep'))).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
test('bulk selection refuses replaced files and metadata',()=>{
|
|
31
|
+
const a=item();call('list',{count:20});const {selectionToken}=call('select');
|
|
32
|
+
renameSync(a.file,a.file+'.old');writeFileSync(a.file,'replacement');
|
|
33
|
+
expect(call('restore',{selectionToken}).stale).toBe(true);
|
|
34
|
+
call('list',{count:20});const selected=call('select');writeFileSync(a.info,'[Trash Info]\nPath=/another-place\n');
|
|
35
|
+
expect(call('prepare',{selectionToken:selected.selectionToken}).stale).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
test('Trash ignores symlink directories and dot metadata names',()=>{
|
|
38
|
+
const a=item();writeFileSync(join(homeTrash(),'info','...trashinfo'),'[Trash Info]\nPath=/bad\n');
|
|
39
|
+
expect(readTrash().some(i=>i.name==='..')).toBe(false);
|
|
40
|
+
const files=join(homeTrash(),'files');renameSync(files,files+'.old');symlinkSync(files+'.old',files);
|
|
41
|
+
expect(readTrash().some(i=>i.uri===a.uri)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
test('metadata round trips punctuation and rejects escaping relative paths',()=>{
|
|
44
|
+
const path=join(root,'a:b #中文 % x');expect(trashInfoOriginal(trashInfoPath(path), '')).toBe(path);
|
|
45
|
+
expect(trashInfoOriginal(trashInfoPath(path,root),root)).toBe(path);
|
|
46
|
+
for(const bad of ['../outside','%2e%2e/outside','%2foutside','bad%00name','%ZZ',''])expect(trashInfoOriginal(bad,root)).toBe('');
|
|
47
|
+
});
|
|
48
|
+
test('count watcher reports same-count replacements and disposes',async()=>{
|
|
49
|
+
const a=item();let reports=0;stop=watchTrashCount(()=>reports++);expect(reports).toBe(1);
|
|
50
|
+
renameSync(a.file,a.file+'.old');writeFileSync(a.file,'replacement');
|
|
51
|
+
const deadline=Date.now()+3000;while(reports<2 && Date.now()<deadline)await Bun.sleep(20);
|
|
52
|
+
expect(reports).toBeGreaterThan(1);stop();stop=null;
|
|
53
|
+
});
|
|
54
|
+
test('backend routes Trash requests and exits cleanly',()=>{
|
|
55
|
+
const result=Bun.spawnSync([process.execPath,new URL('../src/flea.js',import.meta.url).pathname,'--backend'],{
|
|
56
|
+
env:{...process.env},stdin:Buffer.from('{"c":"trashbrowse","op":"list","id":27,"count":10}\n{"c":"quit"}\n'),stdout:'pipe',stderr:'pipe'});
|
|
57
|
+
expect(result.exitCode).toBe(0);
|
|
58
|
+
const messages=result.stdout.toString().trim().split('\n').map(line=>JSON.parse(line));
|
|
59
|
+
expect(messages.some(m=>m.t==='trashcount' && typeof m.revision==='number')).toBe(true);
|
|
60
|
+
expect(messages.some(m=>m.t==='trashbrowse' && m.id===27 && m.ok===true)).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('Trash collection uses ordinary rows, real paths and metadata; stale row actions are refused',()=>{
|
|
64
|
+
const a=item('preview 中文.txt');
|
|
65
|
+
const input=[{c:'listtrash',first:10000},{c:'trashbrowse',op:'restore',id:31,rows:[0],listing:-1},{c:'quit'}].map(x=>JSON.stringify(x)).join('\n')+'\n';
|
|
66
|
+
const result=Bun.spawnSync([process.execPath,new URL('../src/flea.js',import.meta.url).pathname,'--backend'],{env:{...process.env},stdin:Buffer.from(input),stdout:'pipe',stderr:'pipe'});
|
|
67
|
+
expect(result.exitCode).toBe(0);expect(result.stderr.toString()).toBe('');
|
|
68
|
+
const messages=result.stdout.toString().trim().split('\n').map(JSON.parse);
|
|
69
|
+
const rows=messages.find(x=>x.t==='rows');const row=rows.rows.find(x=>x.trash?.original===a.original);
|
|
70
|
+
expect(join('/',row.n)).toBe(a.file);expect(row.s).toBe(7);expect(row.d).toBe(false);
|
|
71
|
+
expect(messages.find(x=>x.id===31).stale).toBe(true);expect(existsSync(a.file)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('renaming a Trash entry preserves restore metadata and refuses destination collisions',async()=>{
|
|
75
|
+
const {renameWithTrashRecord}=await import('../src/trash-record.js');
|
|
76
|
+
const a=item('original.txt'),dst=join(dirname(a.file),'renamed.txt');
|
|
77
|
+
renameWithTrashRecord(a.file,dst);
|
|
78
|
+
expect(existsSync(a.file)).toBe(false);expect(existsSync(a.info)).toBe(false);
|
|
79
|
+
const moved=readTrash().find(x=>x.file===dst);expect(moved.original).toBe(join(root,'renamed.txt'));
|
|
80
|
+
writeFileSync(join(dirname(dst),'occupied'),'keep');
|
|
81
|
+
expect(()=>renameWithTrashRecord(dst,join(dirname(dst),'occupied'))).toThrow('already exists');
|
|
82
|
+
expect(readFileSync(dst,'utf8')).toBe('payload');
|
|
83
|
+
expect(call('restore',{uris:[moved.uri],identities:[moved.identity]}).done).toBe(1);
|
|
84
|
+
expect(readFileSync(join(root,'renamed.txt'),'utf8')).toBe('payload');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('moving out cleans only the captured Trash record; copy keeps its source metadata',async()=>{
|
|
88
|
+
const {transfer}=await import('../src/transfer.js');
|
|
89
|
+
const a=item('move-me.txt');const dest=join(root,'destination');mkdirSync(dest);
|
|
90
|
+
const run=moving=>new Promise(resolve=>transfer({id:40,paths:[a.file],dest,op:moving?'move':'copy'},m=>{if(m.t==='transferdone')resolve(m)},null,0));
|
|
91
|
+
const copied=await run(false);expect(copied.ok).toBe(1);expect(existsSync(a.info)).toBe(true);
|
|
92
|
+
rmSync(join(dest,'move-me.txt'));
|
|
93
|
+
const moved=await run(true);expect(moved.ok).toBe(1);expect(existsSync(a.info)).toBe(false);
|
|
94
|
+
expect(readFileSync(join(dest,'move-me.txt'),'utf8')).toBe('payload');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('Trash rename preserves literal replacement tokens in file names',async()=>{
|
|
98
|
+
const {renameWithTrashRecord}=await import('../src/trash-record.js');
|
|
99
|
+
for (const name of ['$&.txt', '$$.txt', "$'.txt", '$`.txt']) {
|
|
100
|
+
const a=item('old-'+name),dst=join(dirname(a.file),name);
|
|
101
|
+
renameWithTrashRecord(a.file,dst);
|
|
102
|
+
const moved=readTrash().find(x=>x.file===dst);
|
|
103
|
+
expect(moved.original).toBe(join(root,name));
|
|
104
|
+
expect(call('restore',{uris:[moved.uri],identities:[moved.identity]}).done).toBe(1);
|
|
105
|
+
expect(readFileSync(join(root,name),'utf8')).toBe('payload');
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('Trash menu restores its captured item after sort, not the replacement cursor',()=>{
|
|
110
|
+
const a=item('menu-a.txt'),b=item('menu-b.txt');
|
|
111
|
+
const commands=[{c:'listtrash',first:10000},
|
|
112
|
+
{c:'menuaction',op:'snapshot',id:93,paths:[a.file],cursorPath:a.file},
|
|
113
|
+
{c:'sort',by:'name',desc:true},
|
|
114
|
+
{c:'trashbrowse',op:'restore',id:94,menuId:93,rows:[0],listing:-1},
|
|
115
|
+
{c:'trashbrowse',op:'restore',id:95,menuId:999,rows:[0]},
|
|
116
|
+
{c:'quit'}];
|
|
117
|
+
const result=Bun.spawnSync([process.execPath,new URL('../src/flea.js',import.meta.url).pathname,'--backend'],{
|
|
118
|
+
env:{...process.env},stdin:Buffer.from(commands.map(JSON.stringify).join('\n')+'\n'),stdout:'pipe',stderr:'pipe'});
|
|
119
|
+
expect(result.exitCode).toBe(0);
|
|
120
|
+
const messages=result.stdout.toString().trim().split('\n').map(JSON.parse);
|
|
121
|
+
expect(messages.find(x=>x.id===94).done).toBe(1);
|
|
122
|
+
expect(messages.find(x=>x.id===95).stale).toBe(true);
|
|
123
|
+
expect(existsSync(a.original)).toBe(true);expect(existsSync(b.file)).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('Trash collision suffixes do not change visible names or media classification',async()=>{
|
|
127
|
+
const {Listing}=await import('../src/listing.js');
|
|
128
|
+
for(const name of ['image.png','music.mp3','video.mp4','source.js','Dockerfile']) {
|
|
129
|
+
item(name);item(name);const a=readTrash().find(x=>x.original===join(root,name) && x.file.endsWith('.2'));
|
|
130
|
+
expect(a.file.endsWith('.2')).toBe(true);
|
|
131
|
+
const row=new Listing('/',[{name:a.file.slice(1),dir:false,trash:a}]).window(0,1).rows[0];
|
|
132
|
+
const original=new Listing(root,[{name,dir:false}]).window(0,1).rows[0];
|
|
133
|
+
expect(row.i).toBe(original.i);expect(row.trash.name).toBe(name);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import QtQuick
|
|
2
|
+
import Quickshell
|
|
3
|
+
import Quickshell.Io
|
|
4
|
+
import "js/SettingsAbout.js" as About
|
|
5
|
+
|
|
6
|
+
// Installed facts are read only when About is shown; each failed query keeps an explicit unknown.
|
|
7
|
+
QtObject {
|
|
8
|
+
id: root
|
|
9
|
+
property bool active: false
|
|
10
|
+
property bool loaded: false
|
|
11
|
+
property var known: ({})
|
|
12
|
+
// What ui/js/SettingsAbout.js reads: the installed facts, with the updater's status and the default's claim beside them.
|
|
13
|
+
readonly property var facts: Object.assign({ update: UpdateCheck.status, handler: DefaultClaim.handler, claim: DefaultClaim.claim }, root.known)
|
|
14
|
+
readonly property string binary: Quickshell.env("FLEA_BIN") || "flea"
|
|
15
|
+
|
|
16
|
+
function setFact(key, value) {
|
|
17
|
+
var next = Object.assign({}, root.known)
|
|
18
|
+
next[key] = value
|
|
19
|
+
root.known = next
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onActiveChanged: {
|
|
23
|
+
// About opening is one of the updater's two automatic triggers; ui/js/Update.js decides whether the last answer stands.
|
|
24
|
+
if (root.active) UpdateCheck.checkIfDue()
|
|
25
|
+
if (!root.active || root.loaded) return
|
|
26
|
+
root.loaded = true
|
|
27
|
+
version.running = true
|
|
28
|
+
owner.running = true
|
|
29
|
+
DefaultClaim.read()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
property var versionQuery: Process {
|
|
33
|
+
id: version
|
|
34
|
+
command: [root.binary, "--version"]
|
|
35
|
+
property string answer: ""
|
|
36
|
+
stdout: StdioCollector { onStreamFinished: version.answer = this.text.trim() }
|
|
37
|
+
// Sample output: "0.2.0". src/main.rs prints CARGO_PKG_VERSION and nothing else, so the
|
|
38
|
+
// name is tolerated rather than required: demanding it left the Version row reading
|
|
39
|
+
// "Not reported" on every build Flea has ever shipped.
|
|
40
|
+
onExited: function (code) {
|
|
41
|
+
if (code !== 0) return
|
|
42
|
+
var text = version.answer.indexOf("flea ") === 0 ? version.answer.substring(5) : version.answer
|
|
43
|
+
if (text.length > 0) root.setFact("version", text)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
property var ownerQuery: Process {
|
|
47
|
+
id: owner
|
|
48
|
+
command: ["pacman", "-Qqo", root.binary]
|
|
49
|
+
property string answer: ""
|
|
50
|
+
stdout: StdioCollector { onStreamFinished: owner.answer = this.text.trim() }
|
|
51
|
+
// Sample output: flea
|
|
52
|
+
onExited: function (code) {
|
|
53
|
+
if (code !== 0 || owner.answer.length === 0) {
|
|
54
|
+
root.setFact("source", About.installedFrom("", false))
|
|
55
|
+
root.setFact("package", "Not owned by a package")
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
packageVersion.command = ["pacman", "-Qi", owner.answer]
|
|
59
|
+
packageVersion.running = true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// -Qi rather than -Q: the same query carries the build date, and the Built row had nothing
|
|
63
|
+
// setting it at all, so every box read "Not recorded in this build" whatever it was running.
|
|
64
|
+
// Its Validated By line is the signature src/update.rs tells OPR's flea from a local build by.
|
|
65
|
+
property var packageQuery: Process {
|
|
66
|
+
id: packageVersion
|
|
67
|
+
environment: ({ LC_ALL: "C" })
|
|
68
|
+
property string answer: ""
|
|
69
|
+
stdout: StdioCollector { onStreamFinished: packageVersion.answer = this.text }
|
|
70
|
+
onExited: function (code) {
|
|
71
|
+
// A package pacman cannot describe reads as unsigned, the way src/update.rs reads it.
|
|
72
|
+
var facts = About.packageFacts(code === 0 ? packageVersion.answer : "")
|
|
73
|
+
root.setFact("source", About.installedFrom(owner.answer, facts.signed))
|
|
74
|
+
if (facts.package.length > 0) root.setFact("package", facts.package)
|
|
75
|
+
if (facts.built.length > 0) root.setFact("built", facts.built)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import QtQuick
|
|
2
|
+
import "js/ArchiveCard.js" as ArchiveCard
|
|
3
|
+
import "js/ArchiveOps.js" as ArchiveOps
|
|
4
|
+
|
|
5
|
+
// The two password cards, built on their first open and kept like every overlay, so a launch that never opens one pays nothing.
|
|
6
|
+
Item {
|
|
7
|
+
id: root
|
|
8
|
+
|
|
9
|
+
property var compressOwner: null
|
|
10
|
+
property var extractOwner: null
|
|
11
|
+
readonly property var compressItem: compressDialog.item
|
|
12
|
+
readonly property var extractItem: extractDialog.item
|
|
13
|
+
|
|
14
|
+
function askCompressPassword(pane, paths, menuId) {
|
|
15
|
+
if (paths) ArchiveOps.compressPasswordResolved(root, pane, paths, menuId)
|
|
16
|
+
else ArchiveOps.openCompressPassword(pane, menuId, root)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// One card at a time: a second request is answered with a sentence and never dropped silently.
|
|
20
|
+
function cardBusy(holder) {
|
|
21
|
+
if (!compressDialog.opened && !extractDialog.opened)
|
|
22
|
+
return false
|
|
23
|
+
holder.message(ArchiveCard.CARD_BUSY, true)
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function openCompressDialog(source, parentLeaf, holder) {
|
|
28
|
+
if (cardBusy(holder)) return
|
|
29
|
+
compressOwner = holder
|
|
30
|
+
compressDialog.active = true
|
|
31
|
+
compressDialog.item.open(source, parentLeaf, holder)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function openExtractPassword(source, holder, err) {
|
|
35
|
+
if (cardBusy(holder)) return
|
|
36
|
+
extractOwner = holder
|
|
37
|
+
extractDialog.active = true
|
|
38
|
+
extractDialog.item.open(source, holder, err)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
anchors.fill: parent
|
|
42
|
+
|
|
43
|
+
Loader {
|
|
44
|
+
id: compressDialog
|
|
45
|
+
anchors.fill: parent
|
|
46
|
+
active: false
|
|
47
|
+
source: "ArchiveCompressDialog.qml"
|
|
48
|
+
readonly property bool opened: item !== null && item.opened
|
|
49
|
+
}
|
|
50
|
+
Connections {
|
|
51
|
+
target: compressDialog.item
|
|
52
|
+
function onAccepted(source, format, name, password, encryptNames) {
|
|
53
|
+
ArchiveOps.submitCompressPassword(root.compressOwner, source, format, name, password, encryptNames)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Loader {
|
|
58
|
+
id: extractDialog
|
|
59
|
+
anchors.fill: parent
|
|
60
|
+
active: false
|
|
61
|
+
source: "ArchivePasswordDialog.qml"
|
|
62
|
+
readonly property bool opened: item !== null && item.opened
|
|
63
|
+
}
|
|
64
|
+
Connections {
|
|
65
|
+
target: extractDialog.item
|
|
66
|
+
function onAccepted(source, password) { ArchiveOps.submitExtractPassword(root.extractOwner, source, password) }
|
|
67
|
+
}
|
|
68
|
+
}
|