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,138 @@
|
|
|
1
|
+
import { test, expect, afterAll } from "bun:test";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, chmodSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { dirsize, dirsizeCancel, dirsizeInit, forgetSizes, keptSize } from "../src/dirsize.js";
|
|
6
|
+
import { duCommand } from "../src/find.js";
|
|
7
|
+
import { Listing } from "../src/listing.js";
|
|
8
|
+
|
|
9
|
+
// Folder sizes only when asked (src/dirsize.js): du streams each folder's size, the answers are
|
|
10
|
+
// kept by path and ride later rows as z, and a change forgets a folder and those above it only.
|
|
11
|
+
const root = mkdtempSync(join(tmpdir(), "flea-dirsize-"));
|
|
12
|
+
afterAll(() => { try { chmodSync(join(root, "w", "c", "locked"), 0o755); } catch {} rmSync(root, { recursive: true, force: true }); });
|
|
13
|
+
const work = join(root, "w");
|
|
14
|
+
mkdirSync(join(work, "a", "deep"), { recursive: true });
|
|
15
|
+
mkdirSync(join(work, "b"));
|
|
16
|
+
mkdirSync(join(work, "-dash"));
|
|
17
|
+
mkdirSync(join(work, "c", "locked"), { recursive: true });
|
|
18
|
+
writeFileSync(join(work, "a", "one"), Buffer.alloc(1000));
|
|
19
|
+
writeFileSync(join(work, "a", "deep", "two"), Buffer.alloc(2500));
|
|
20
|
+
writeFileSync(join(work, "b", "three"), Buffer.alloc(7));
|
|
21
|
+
writeFileSync(join(work, "-dash", "x"), Buffer.alloc(11));
|
|
22
|
+
writeFileSync(join(work, "c", "locked", "hid"), Buffer.alloc(5));
|
|
23
|
+
writeFileSync(join(work, "file.txt"), "not a folder");
|
|
24
|
+
|
|
25
|
+
let listing = Listing.scan(work, false);
|
|
26
|
+
listing.sort({ by: "name", desc: false, foldersFirst: true, groupByKind: false, hiddenLast: false });
|
|
27
|
+
const messages = [];
|
|
28
|
+
dirsizeInit((m) => messages.push(m), () => listing);
|
|
29
|
+
const until = async (done) => {
|
|
30
|
+
for (let i = 0; i < 400 && !done(); i++) await Bun.sleep(10);
|
|
31
|
+
if (!done()) throw Error("timed out: " + JSON.stringify(messages));
|
|
32
|
+
};
|
|
33
|
+
const rowOf = (name) => listing.indexOfName(name);
|
|
34
|
+
const sizes = () => Object.fromEntries(messages.filter((m) => m.t === "dirsized").map((m) => [listing.rows[m.row].name, [m.bytes, m.partial]]));
|
|
35
|
+
|
|
36
|
+
test("du is looked for in Find's order: bunmsh first", () => {
|
|
37
|
+
expect(duCommand().slice(-2)).toEqual(["builtin", "du"]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("nothing is measured until asked; one row asks for that folder alone", async () => {
|
|
41
|
+
expect(listing.window(0, 10).rows.some((row) => row.z !== undefined)).toBe(false);
|
|
42
|
+
dirsize({ rows: [rowOf("a"), rowOf("file.txt")] });
|
|
43
|
+
await until(() => messages.some((m) => m.t === "dirsizedone"));
|
|
44
|
+
expect(sizes()).toEqual({ a: [3500, false] });
|
|
45
|
+
expect(keptSize(join(work, "a"))).toEqual({ bytes: 3500, partial: false });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("all measures every folder, a name with a leading dash included, and an unreadable one is a floor", async () => {
|
|
49
|
+
if (process.getuid?.() !== 0 && process.platform !== "win32") chmodSync(join(work, "c", "locked"), 0);
|
|
50
|
+
messages.length = 0;
|
|
51
|
+
dirsize({ all: true });
|
|
52
|
+
await until(() => messages.some((m) => m.t === "dirsizedone"));
|
|
53
|
+
const got = sizes();
|
|
54
|
+
expect(got.a).toEqual([3500, false]);
|
|
55
|
+
expect(got.b).toEqual([7, false]);
|
|
56
|
+
expect(got["-dash"]).toEqual([11, false]);
|
|
57
|
+
if (process.getuid?.() !== 0 && process.platform !== "win32") expect(got.c[1]).toBe(true);
|
|
58
|
+
expect(got["file.txt"]).toBeUndefined();
|
|
59
|
+
chmodSync(join(work, "c", "locked"), 0o755);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("kept sizes ride later rows, follow a sort, and a change forgets only its own line upward", () => {
|
|
63
|
+
listing = Listing.scan(work, false);
|
|
64
|
+
listing.sort({ by: "name", desc: true, foldersFirst: true, groupByKind: false, hiddenLast: false });
|
|
65
|
+
const rows = Object.fromEntries(listing.window(0, 10).rows.map((row) => [row.n, row]));
|
|
66
|
+
expect(rows.a.z).toBe(3500);
|
|
67
|
+
expect(rows.b.z).toBe(7);
|
|
68
|
+
expect(rows["file.txt"].z).toBeUndefined();
|
|
69
|
+
// A write inside a/deep: a (and everything above) is no longer known, b still is.
|
|
70
|
+
forgetSizes(join(work, "a", "deep"));
|
|
71
|
+
expect(keptSize(join(work, "a"))).toBeUndefined();
|
|
72
|
+
expect(keptSize(join(work, "b"))).toEqual({ bytes: 7, partial: false });
|
|
73
|
+
forgetSizes(join(work, "b", "three-sibling-dir-does-not-exist"));
|
|
74
|
+
expect(keptSize(join(work, "b"))).toBeUndefined();
|
|
75
|
+
expect(keptSize(join(work, "-dash"))).toEqual({ bytes: 11, partial: false });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("a cancel stops the run and says so", async () => {
|
|
79
|
+
messages.length = 0;
|
|
80
|
+
dirsize({ all: true });
|
|
81
|
+
dirsizeCancel();
|
|
82
|
+
await until(() => messages.some((m) => m.t === "dirsizedone"));
|
|
83
|
+
expect(messages.find((m) => m.t === "dirsizedone").cancelled).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// The cell side (ui/js/DirSizes.js): ⚖️ until measured, … while a walk is out, then the size.
|
|
87
|
+
async function loadJs(name, names, deps = {}) {
|
|
88
|
+
const s = (await Bun.file(new URL("../ui/js/" + name + ".js", import.meta.url)).text()).replace(/^\.(?:pragma|import).*$/gm, "");
|
|
89
|
+
return new Function(...Object.keys(deps), s + ";return {" + names + "};")(...Object.values(deps));
|
|
90
|
+
}
|
|
91
|
+
const Format = { size: (n) => `${n} B` };
|
|
92
|
+
const D = await loadJs("DirSizes", "empty,cellFor,known,folderText,asking,askingAll,remember,settled,UNMEASURED,MEASURING", { Format });
|
|
93
|
+
|
|
94
|
+
test("a folder's Size cell reads ⚖️, then … while asked, then its size; a row's z stands in after a sort", () => {
|
|
95
|
+
let state = D.empty();
|
|
96
|
+
const row = { d: true };
|
|
97
|
+
expect(D.folderText(D.cellFor(state, 4), row)).toBe("⚖️");
|
|
98
|
+
state = D.asking(state, [4]);
|
|
99
|
+
expect(D.folderText(D.cellFor(state, 4), row)).toBe("…");
|
|
100
|
+
expect(D.folderText(D.cellFor(state, 5), row)).toBe("⚖️");
|
|
101
|
+
state = D.remember(state, 4, 2048, false, 100);
|
|
102
|
+
expect(D.folderText(D.cellFor(state, 4), row)).toBe("\u2696\ufe0f 2048 B");
|
|
103
|
+
expect(D.folderText(D.cellFor(D.empty(), 4), { d: true, z: 9, zp: true })).toBe("\u2696\ufe0f >9 B");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("the header's ⚖️ makes every folder wait until its answer or the run's end", () => {
|
|
107
|
+
let state = D.askingAll(D.empty());
|
|
108
|
+
expect(D.cellFor(state, 900)).toEqual({ waiting: true });
|
|
109
|
+
state = D.remember(state, 3, 10, false, 100);
|
|
110
|
+
expect(D.cellFor(state, 3)).toEqual({ bytes: 10, partial: false });
|
|
111
|
+
expect(D.cellFor(state, 7)).toEqual({ waiting: true });
|
|
112
|
+
state = D.settled(state);
|
|
113
|
+
expect(D.cellFor(state, 7)).toBe(null);
|
|
114
|
+
expect(D.cellFor(state, 3)).toEqual({ bytes: 10, partial: false });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("a size order uses measured folders' sizes and puts unmeasured folders after them, measuring nothing", () => {
|
|
118
|
+
// a and b were forgotten above; -dash (11) and c (a floor) are still measured.
|
|
119
|
+
const fresh = Listing.scan(work, false);
|
|
120
|
+
fresh.sort({ by: "name", desc: false, foldersFirst: true, groupByKind: false, hiddenLast: false });
|
|
121
|
+
fresh.sort({ by: "size", desc: true, foldersFirst: true, groupByKind: false, hiddenLast: false });
|
|
122
|
+
expect(fresh.rows.filter((r) => r.dir).map((r) => r.name)).toEqual(["-dash", "c", "a", "b"]);
|
|
123
|
+
expect(messages.filter((m) => m.t === "dirsized").length).toBe(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("= measures the selection or the cursor's folder, Alt+= every folder", async () => {
|
|
127
|
+
// Every Qt.Key_* the table names gets a distinct number; the modifiers are Qt's own.
|
|
128
|
+
let next = 1000;
|
|
129
|
+
const named = { AltModifier: 0x08000000, ControlModifier: 0x04000000, ShiftModifier: 0x02000000, MetaModifier: 0x10000000, Key_Equal: 0x3d };
|
|
130
|
+
const Qt = new Proxy(named, { get: (t, k) => (k in t ? t[k] : (t[k] = next++)) });
|
|
131
|
+
const Keymap = await loadJs("Keymap", "lookup", { Qt });
|
|
132
|
+
expect(Keymap.lookup(0x3d, "=", 0)).toBe("measureDirs");
|
|
133
|
+
expect(Keymap.lookup(0x3d, "=", named.AltModifier)).toBe("measureAllDirs");
|
|
134
|
+
// Ctrl+= stays the text size.
|
|
135
|
+
expect(Keymap.lookup(0x3d, "=", named.ControlModifier)).toBe("textSizeUp");
|
|
136
|
+
const focus = await Bun.file(new URL("../ui/js/Focus.js", import.meta.url)).text();
|
|
137
|
+
expect(focus).toContain('case "measureDirs": root.measureDirs(root.selectionCount() > 0 ? root.selectedIndices() : [root.cursorIndex])');
|
|
138
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { test, expect, afterAll } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, readdirSync, lstatSync, readlinkSync, symlinkSync, statSync, chmodSync, existsSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { mkdir, newfile, duplicate } from '../src/create.js';
|
|
6
|
+
import { bulkrename } from '../src/bulkrename.js';
|
|
7
|
+
import { permissions, permissionsBatch, parseMode } from '../src/permissions.js';
|
|
8
|
+
import { archive, archiveCancel, formats, freeName } from '../src/archiveops.js';
|
|
9
|
+
import { listArchive } from '../src/archive.js';
|
|
10
|
+
import { openSync, closeSync } from 'node:fs';
|
|
11
|
+
|
|
12
|
+
const root = mkdtempSync(join(tmpdir(), 'flea-ops-'));
|
|
13
|
+
afterAll(() => rmSync(root, { recursive: true, force: true }));
|
|
14
|
+
let n = 0;
|
|
15
|
+
const fresh = () => { const dir = join(root, `case-${++n}`); mkdirSync(dir); return dir; };
|
|
16
|
+
// PRoot resolves links itself, so an exclusive create there follows a dangling link; the kernel never does.
|
|
17
|
+
const exclusiveRefusesLinks = (() => {
|
|
18
|
+
const dir = mkdtempSync(join(tmpdir(), 'flea-excl-'));
|
|
19
|
+
try {
|
|
20
|
+
symlinkSync(join(dir, 'absent'), join(dir, 'link'));
|
|
21
|
+
try { closeSync(openSync(join(dir, 'link'), 'wx')); return false; } catch { return true; }
|
|
22
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
23
|
+
})();
|
|
24
|
+
const umask = process.umask();
|
|
25
|
+
const collector = () => { const lines = []; const send = (line) => lines.push(line); return { lines, send }; };
|
|
26
|
+
// Resolves with the first line `pick` accepts, waiting for the asynchronous operations.
|
|
27
|
+
async function until(lines, pick, ms = 10000) {
|
|
28
|
+
const end = Date.now() + ms;
|
|
29
|
+
for (;;) {
|
|
30
|
+
const found = lines.find(pick);
|
|
31
|
+
if (found) return found;
|
|
32
|
+
if (Date.now() > end) throw Error(`no line arrived: ${JSON.stringify(lines)}`);
|
|
33
|
+
await Bun.sleep(10);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('mkdir takes the first free New Folder and refuses a typed name already taken', () => {
|
|
38
|
+
const dir = fresh(), { lines, send } = collector();
|
|
39
|
+
mkdir({ path: dir }, send);
|
|
40
|
+
mkdir({ path: dir }, send);
|
|
41
|
+
expect(lines.filter((l) => l.t === 'made').map((l) => l.path)).toEqual([join(dir, 'New Folder'), join(dir, 'New Folder 2')]);
|
|
42
|
+
writeFileSync(join(dir, '報告'), '');
|
|
43
|
+
mkdir({ path: dir, name: '報告' }, send);
|
|
44
|
+
expect(lines.at(-1)).toMatchObject({ t: 'error', where: 'mkdir', msg: 'a folder or file with that name already exists' });
|
|
45
|
+
mkdir({ path: dir, name: '../x' }, send);
|
|
46
|
+
expect(lines.at(-1)).toMatchObject({ t: 'error', where: 'mkdir' });
|
|
47
|
+
mkdir({ path: 'relative' }, send);
|
|
48
|
+
expect(lines.at(-1).t).toBe('error');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('mkdir with a request id adds the creation receipt', () => {
|
|
52
|
+
const dir = fresh(), { lines, send } = collector();
|
|
53
|
+
mkdir({ path: dir, name: 'a', request: 4 }, send);
|
|
54
|
+
expect(lines[0]).toMatchObject({ t: 'made', ok: true, request: 4 });
|
|
55
|
+
expect(lines[1]).toEqual({ t: 'menuaction', op: 'creation', id: 4, kind: 'made', ok: true, path: join(dir, 'a') });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('newfile creates an empty file exclusively and never follows a dangling link', () => {
|
|
59
|
+
const dir = fresh(), { lines, send } = collector();
|
|
60
|
+
newfile({ id: 3, path: dir, name: 'New File 🙂' }, send);
|
|
61
|
+
expect(lines[0]).toEqual({ t: 'menuaction', id: 3, op: 'newFile', ok: true, path: join(dir, 'New File 🙂') });
|
|
62
|
+
expect(statSync(join(dir, 'New File 🙂')).size).toBe(0);
|
|
63
|
+
newfile({ id: 3, path: dir, name: 'New File 🙂' }, send);
|
|
64
|
+
expect(lines.at(-1)).toMatchObject({ op: 'newFile', ok: false });
|
|
65
|
+
if (exclusiveRefusesLinks) {
|
|
66
|
+
symlinkSync(join(dir, 'absent'), join(dir, 'link'));
|
|
67
|
+
newfile({ id: 3, path: dir, name: 'link' }, send);
|
|
68
|
+
expect(lines.at(-1).ok).toBe(false);
|
|
69
|
+
expect(existsSync(join(dir, 'absent'))).toBe(false);
|
|
70
|
+
}
|
|
71
|
+
newfile({ id: 3, path: dir, name: 'a/b' }, send);
|
|
72
|
+
expect(lines.at(-1).ok).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('duplicate copies a file and a tree to free copy names', async () => {
|
|
76
|
+
const dir = fresh(), { lines, send } = collector();
|
|
77
|
+
writeFileSync(join(dir, 'backup.tar.zst'), 'x');
|
|
78
|
+
mkdirSync(join(dir, 'tree/inner'), { recursive: true });
|
|
79
|
+
writeFileSync(join(dir, 'tree/inner/f'), 'deep');
|
|
80
|
+
duplicate({ path: join(dir, 'backup.tar.zst') }, send);
|
|
81
|
+
duplicate({ path: join(dir, 'backup.tar.zst') }, send);
|
|
82
|
+
duplicate({ path: join(dir, 'tree') }, send);
|
|
83
|
+
await until(lines, (l) => l.t === 'duplicated' && l.path.endsWith('tree copy'));
|
|
84
|
+
expect(lines.filter((l) => l.t === 'duplicated').map((l) => l.path)).toEqual(
|
|
85
|
+
[join(dir, 'backup.tar copy.zst'), join(dir, 'backup.tar copy 2.zst'), join(dir, 'tree copy')]);
|
|
86
|
+
expect(readFileSync(join(dir, 'tree copy/inner/f'), 'utf8')).toBe('deep');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('bulkrename swaps names through temporary names and refuses a bad batch whole', () => {
|
|
90
|
+
const dir = fresh(), { lines, send } = collector();
|
|
91
|
+
writeFileSync(join(dir, 'a'), 'A');
|
|
92
|
+
writeFileSync(join(dir, 'b'), 'B');
|
|
93
|
+
writeFileSync(join(dir, 'c'), 'C');
|
|
94
|
+
bulkrename({ paths: [join(dir, 'a'), join(dir, 'b'), join(dir, 'c')], names: ['b', 'a', 'c'] }, send);
|
|
95
|
+
expect(lines[0]).toEqual({ t: 'bulkrenamed', ok: 2, unchanged: 1 });
|
|
96
|
+
expect(readFileSync(join(dir, 'a'), 'utf8')).toBe('B');
|
|
97
|
+
expect(readFileSync(join(dir, 'b'), 'utf8')).toBe('A');
|
|
98
|
+
for (const names of [['x', 'x', 'y'], ['c', 'q'], ['ok', 'a/b', 'z'], ['', 'm', 'n']]) {
|
|
99
|
+
bulkrename({ paths: [join(dir, 'a'), join(dir, 'b'), join(dir, 'c')].slice(0, names.length), names }, send);
|
|
100
|
+
expect(lines.at(-1)).toMatchObject({ t: 'error', where: 'bulkrename' });
|
|
101
|
+
}
|
|
102
|
+
expect(readdirSync(dir).sort()).toEqual(['a', 'b', 'c']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('permissions inspects, applies and refuses a replaced item', () => {
|
|
106
|
+
if (process.platform === 'win32') return;
|
|
107
|
+
const dir = fresh(), path = join(dir, 'f'), { lines, send } = collector();
|
|
108
|
+
writeFileSync(path, '');
|
|
109
|
+
chmodSync(path, 0o644);
|
|
110
|
+
permissions({ op: 'inspect', id: 1, path }, send);
|
|
111
|
+
expect(lines[0]).toMatchObject({ t: 'permissions', id: 1, op: 'inspect', ok: true, mode: '0644', directory: false, reason: '' });
|
|
112
|
+
permissions({ op: 'apply', id: 1, mode: '600' }, send);
|
|
113
|
+
expect(lines.at(-1)).toEqual({ t: 'permissions', id: 1, op: 'apply', ok: true, mode: '0600' });
|
|
114
|
+
expect(statSync(path).mode & 0o777).toBe(0o600);
|
|
115
|
+
permissions({ op: 'apply', id: 1, mode: '9' }, send);
|
|
116
|
+
expect(lines.at(-1).error).toMatch(/octal/);
|
|
117
|
+
rmSync(path);
|
|
118
|
+
writeFileSync(path, '');
|
|
119
|
+
permissions({ op: 'apply', id: 1, mode: '644' }, send);
|
|
120
|
+
expect(lines.at(-1)).toMatchObject({ ok: false, error: 'Selected item changed; reopen Permissions.' });
|
|
121
|
+
expect(() => parseMode('1644')).toThrow();
|
|
122
|
+
expect(parseMode('0755')).toBe(0o755);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('permissionsBatch changes every item or none, and u+x adds the owner execute bit', () => {
|
|
126
|
+
if (process.platform === 'win32') return;
|
|
127
|
+
const dir = fresh(), a = join(dir, 'a'), b = join(dir, 'b'), { lines, send } = collector();
|
|
128
|
+
writeFileSync(a, '');
|
|
129
|
+
writeFileSync(b, '');
|
|
130
|
+
chmodSync(a, 0o644);
|
|
131
|
+
chmodSync(b, 0o640);
|
|
132
|
+
permissionsBatch({ id: 7, paths: [a, b], modes: ['u+x', 'u+x'] }, send);
|
|
133
|
+
expect(lines.at(-1)).toMatchObject({ op: 'applyMany', ok: true });
|
|
134
|
+
expect([statSync(a).mode & 0o777, statSync(b).mode & 0o777]).toEqual([0o744, 0o740]);
|
|
135
|
+
permissionsBatch({ id: 8, paths: [a, join(dir, 'gone')], modes: ['600', '600'] }, send);
|
|
136
|
+
expect(lines.at(-1)).toMatchObject({ op: 'applyMany', ok: false });
|
|
137
|
+
expect(statSync(a).mode & 0o777).toBe(0o744);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('formats names what this port writes and reads, and no password', () => {
|
|
141
|
+
const { lines, send } = collector();
|
|
142
|
+
formats({ id: 2 }, send);
|
|
143
|
+
expect(lines[0]).toMatchObject({ t: 'formats', archive: ['zip', 'tar', 'tar.gz', 'tar.zst'], convert: false,
|
|
144
|
+
extract: { archive: true, sevenZip: false, zip: true } });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('free archive names keep the format tail', () => {
|
|
148
|
+
expect(freeName('/a/pack.tar.gz', 'tar.gz', 1)).toBe('/a/pack copy.tar.gz');
|
|
149
|
+
expect(freeName('/a/pack.tar.gz', 'tar.gz', 2)).toBe('/a/pack copy 2.tar.gz');
|
|
150
|
+
expect(freeName('/a/folder', null, 1)).toBe('/a/folder copy');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// A tree with Chinese and emoji names, a long path, a symlink, an empty folder and a non-default mode.
|
|
154
|
+
function sampleTree(dir) {
|
|
155
|
+
const top = join(dir, '專案 📦');
|
|
156
|
+
const long = join(top, 'd'.repeat(60), 'e'.repeat(60));
|
|
157
|
+
mkdirSync(long, { recursive: true });
|
|
158
|
+
mkdirSync(join(top, 'empty'));
|
|
159
|
+
writeFileSync(join(top, '說明.txt'), '你好 👋\n');
|
|
160
|
+
writeFileSync(join(long, 'f'.repeat(90) + '.bin'), Buffer.alloc(300000, 7));
|
|
161
|
+
writeFileSync(join(top, 'run.sh'), '#!/bin/sh\n');
|
|
162
|
+
chmodSync(join(top, 'run.sh'), 0o755);
|
|
163
|
+
symlinkSync('說明.txt', join(top, 'readme'));
|
|
164
|
+
return top;
|
|
165
|
+
}
|
|
166
|
+
// Extracted modes pass through the umask, as bsdtar's do for anyone but root.
|
|
167
|
+
function expectSameTree(a, b) {
|
|
168
|
+
const walk = (base, rel = '', mask = 0) => readdirSync(join(base, rel)).sort().flatMap((name) => {
|
|
169
|
+
const path = join(base, rel, name), st = lstatSync(path), key = join(rel, name);
|
|
170
|
+
if (st.isSymbolicLink()) return [[key, 'link', readlinkSync(path)]];
|
|
171
|
+
if (st.isDirectory()) return [[key, 'dir'], ...walk(base, key, mask)];
|
|
172
|
+
return [[key, (st.mode & 0o777 & ~mask).toString(8), readFileSync(path).toString('base64')]];
|
|
173
|
+
});
|
|
174
|
+
expect(walk(b)).toEqual(walk(a, '', umask));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const format of ['zip', 'tar', 'tar.gz', 'tar.zst']) {
|
|
178
|
+
test(`compress to .${format} and extract it back to the same tree`, async () => {
|
|
179
|
+
const dir = fresh(), top = sampleTree(dir), { lines, send } = collector();
|
|
180
|
+
archive({ op: 'compress', paths: [top], dest: join(dir, `專案 📦.${format}`), format }, send);
|
|
181
|
+
const done = await until(lines, (l) => l.t === 'archivedone');
|
|
182
|
+
expect(done).toMatchObject({ ok: true, err: '', path: join(dir, `專案 📦.${format}`) });
|
|
183
|
+
// The preview's own reader lists what was written.
|
|
184
|
+
const names = [];
|
|
185
|
+
await listArchive(done.path, done.path, (entry) => names.push(entry.name));
|
|
186
|
+
expect(names).toContain('專案 📦/說明.txt');
|
|
187
|
+
expect(names.some((name) => name.endsWith('f'.repeat(90) + '.bin'))).toBe(true);
|
|
188
|
+
const out = join(dir, 'out');
|
|
189
|
+
archive({ op: 'extract', path: done.path, dest: out }, send);
|
|
190
|
+
const extracted = await until(lines, (l) => l.t === 'archivedone' && l.dest);
|
|
191
|
+
expect(extracted).toMatchObject({ ok: true, dest: out });
|
|
192
|
+
expect(lines.some((l) => l.t === 'extractstarted')).toBe(true);
|
|
193
|
+
expectSameTree(top, join(out, '專案 📦'));
|
|
194
|
+
// A second compress of the same name lands under its copy name.
|
|
195
|
+
archive({ op: 'compress', paths: [top], dest: join(dir, `專案 📦.${format}`), format }, send);
|
|
196
|
+
const again = await until(lines, (l) => l.t === 'archivedone' && l.path?.includes('copy'));
|
|
197
|
+
expect(again.path).toBe(join(dir, `專案 📦 copy.${format}`));
|
|
198
|
+
expect(readdirSync(dir).filter((name) => name.startsWith('.flea-work'))).toEqual([]);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
test('an archive that names a path outside its folder extracts nothing', async () => {
|
|
203
|
+
const dir = fresh(), { lines, send } = collector();
|
|
204
|
+
// A tar whose only member is ../escape.
|
|
205
|
+
const header = Buffer.alloc(512);
|
|
206
|
+
header.write('../escape', 0);
|
|
207
|
+
header.write('0000644\0', 100); header.write('0000000\0', 108); header.write('0000000\0', 116);
|
|
208
|
+
header.write('00000000003\0', 124); header.write('00000000000\0', 136);
|
|
209
|
+
header.fill(0x20, 148, 156); header[156] = 0x30; header.write('ustar\0', 257); header.write('00', 263);
|
|
210
|
+
let sum = 0; for (const byte of header) sum += byte;
|
|
211
|
+
header.write(sum.toString(8).padStart(6, '0') + '\0 ', 148);
|
|
212
|
+
const data = Buffer.alloc(512); data.write('bad');
|
|
213
|
+
writeFileSync(join(dir, 'evil.tar'), Buffer.concat([header, data, Buffer.alloc(1024)]));
|
|
214
|
+
archive({ op: 'extract', path: join(dir, 'evil.tar'), dest: join(dir, 'evil') }, send);
|
|
215
|
+
const done = await until(lines, (l) => l.t === 'archivedone');
|
|
216
|
+
expect(done.ok).toBe(false);
|
|
217
|
+
expect(done.err).toMatch(/outside its folder/);
|
|
218
|
+
expect(readdirSync(dir).sort()).toEqual(['evil.tar']);
|
|
219
|
+
expect(existsSync(join(root, 'escape'))).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('a password, a format it cannot read and a disk image are refused without starting', () => {
|
|
223
|
+
const { lines, send } = collector();
|
|
224
|
+
archive({ op: 'extract', path: '/x/a.zip', dest: '/x/a', password: 'p' }, send);
|
|
225
|
+
archive({ op: 'extract', path: '/x/a.7z', dest: '/x/a' }, send);
|
|
226
|
+
archive({ op: 'extract', path: '/x/a.iso', dest: '/x/a' }, send);
|
|
227
|
+
expect(lines.every((l) => l.t === 'archivedone' && l.ok === false)).toBe(true);
|
|
228
|
+
expect(lines.length).toBe(3);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('cancelling a compress removes its staging file', async () => {
|
|
232
|
+
const dir = fresh(), { lines, send } = collector();
|
|
233
|
+
mkdirSync(join(dir, 'big'));
|
|
234
|
+
for (let i = 0; i < 20; i++) writeFileSync(join(dir, 'big', `f${i}`), Buffer.alloc(2 << 20, i));
|
|
235
|
+
archive({ op: 'compress', paths: [join(dir, 'big')], dest: join(dir, 'big.zip'), format: 'zip' }, send);
|
|
236
|
+
const started = await until(lines, (l) => l.t === 'archivestarted');
|
|
237
|
+
expect(archiveCancel(started.id)).toBe(true);
|
|
238
|
+
const done = await until(lines, (l) => l.t === 'archivedone');
|
|
239
|
+
expect(done).toMatchObject({ ok: false, err: 'cancelled' });
|
|
240
|
+
expect(readdirSync(dir)).toEqual(['big']);
|
|
241
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { test, expect, afterAll } from "bun:test";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { grepCommand } from "../src/find.js";
|
|
6
|
+
import { matchOf, MATCH_TEXT_LIMIT } from "../src/backend.js";
|
|
7
|
+
import { Listing } from "../src/listing.js";
|
|
8
|
+
|
|
9
|
+
// Ctrl+G's content search: ui/js/Find.js builds grep's arguments and reads its lines, ui/js/Search.js
|
|
10
|
+
// opens and turns the mode, and the backend carries each row's matched line as `g`.
|
|
11
|
+
async function load(name, names, deps = {}) {
|
|
12
|
+
const s = (await Bun.file(new URL("../ui/js/" + name + ".js", import.meta.url)).text()).replace(/^\.(?:pragma|import).*$/gm, "");
|
|
13
|
+
return new Function(...Object.keys(deps), s + ";return {" + names + "};")(...Object.values(deps));
|
|
14
|
+
}
|
|
15
|
+
const Find = await load("Find", "grepArguments,grepLine,snippet,CONTEXT,SNIPPET_LIMIT");
|
|
16
|
+
const Qt = { Key_Return: 16777220, Key_Enter: 16777221, Key_Escape: 16777216, Key_Backspace: 16777219,
|
|
17
|
+
Key_Tab: 16777217, Key_Backtab: 16777218, Key_G: 71, ControlModifier: 0x04000000, AltModifier: 0x08000000, MetaModifier: 0x10000000 };
|
|
18
|
+
const Search = await load("Search", "start,contents,commit,rescope,abort,typeKey,placeholder,sendQuery,TYPING,RESULTS",
|
|
19
|
+
{ Qt, Input: { isPasteKey: () => false }, Format: {} });
|
|
20
|
+
|
|
21
|
+
const root = mkdtempSync(join(tmpdir(), "flea-grep-"));
|
|
22
|
+
afterAll(() => rmSync(root, { recursive: true, force: true }));
|
|
23
|
+
|
|
24
|
+
test("grep runs over the scope with the query as literal text, hidden names skipped unless shown", () => {
|
|
25
|
+
const off = Find.grepArguments("a.b*(c)", false);
|
|
26
|
+
expect(off).toEqual(["-r", "-n", "-o", "-a", "-i", "-m", "1", "-Z", "--line-buffered", "-E",
|
|
27
|
+
"--exclude=.*", "--exclude-dir=.*", "-e", ".{0,80}a\\.b\\*\\(c\\).{0,80}"]);
|
|
28
|
+
// No path operand: grep walks "." and the bare "." is never excluded.
|
|
29
|
+
expect(off.some((arg) => arg === "." || arg === "./")).toBe(false);
|
|
30
|
+
expect(Find.grepArguments("x", true)).not.toContain("--exclude=.*");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("a grep line parses into its file, line number and a clean bounded snippet", () => {
|
|
34
|
+
expect(Find.grepLine("dir/a: b.txt\u000012: let x\t= 1")).toEqual({ path: "dir/a: b.txt", line: 12, text: "let x = 1" });
|
|
35
|
+
// A binary's control bytes become dots; the text is cut at SNIPPET_LIMIT.
|
|
36
|
+
expect(Find.grepLine("bin.dat\u00003:ab\u0000\u0001cd").text).toBe("ab··cd");
|
|
37
|
+
expect(Find.grepLine("long\u00001:" + "y".repeat(5000)).text.length).toBe(Find.SNIPPET_LIMIT);
|
|
38
|
+
for (const junk of ["", "no separator", "\u00001:starts with nul", "a\u0000not a number"]) expect(Find.grepLine(junk)).toBe(null);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
for (const [label, argv] of [["bunmsh's grep", grepCommand()], ["the system grep", Bun.which("grep") ? [Bun.which("grep")] : null]]) {
|
|
42
|
+
test(`${label} answers the arguments: binary files match, hidden ones follow the setting, long lines stay short`, async () => {
|
|
43
|
+
if (!argv) return;
|
|
44
|
+
const scope = mkdtempSync(join(root, "scope-"));
|
|
45
|
+
mkdirSync(join(scope, "sub"));
|
|
46
|
+
mkdirSync(join(scope, ".hid"));
|
|
47
|
+
writeFileSync(join(scope, "sub", "notes:1.txt"), "first\nSecond Needle here\nneedle again\n");
|
|
48
|
+
writeFileSync(join(scope, "image.bin"), Buffer.from([0, 1, 2, ...Buffer.from("xxNEEDLExx"), 0, 255]));
|
|
49
|
+
writeFileSync(join(scope, "min.js"), "a".repeat(200000) + "needle" + "b".repeat(200000));
|
|
50
|
+
writeFileSync(join(scope, ".hidden.txt"), "needle\n");
|
|
51
|
+
writeFileSync(join(scope, ".hid", "in.txt"), "needle\n");
|
|
52
|
+
writeFileSync(join(scope, "none.txt"), "nothing\n");
|
|
53
|
+
const run = async (hidden) => {
|
|
54
|
+
const child = Bun.spawn([...argv, ...Find.grepArguments("needle", hidden)], { cwd: scope, stdout: "pipe", stderr: "pipe" });
|
|
55
|
+
const out = await new Response(child.stdout).text();
|
|
56
|
+
await child.exited;
|
|
57
|
+
const rows = new Map();
|
|
58
|
+
for (const line of out.split("\n")) {
|
|
59
|
+
if (line.length > 2 * Find.CONTEXT + "needle".length + 400) throw new Error(`an output line of ${line.length} characters`);
|
|
60
|
+
const hit = Find.grepLine(line);
|
|
61
|
+
if (hit && !rows.has(hit.path)) rows.set(hit.path, hit);
|
|
62
|
+
}
|
|
63
|
+
return rows;
|
|
64
|
+
};
|
|
65
|
+
const shown = await run(false);
|
|
66
|
+
expect([...shown.keys()].sort()).toEqual(["image.bin", "min.js", "sub/notes:1.txt"]);
|
|
67
|
+
expect(shown.get("sub/notes:1.txt")).toMatchObject({ line: 2, text: "Second Needle here" });
|
|
68
|
+
expect(shown.get("image.bin").text).toContain("NEEDLE");
|
|
69
|
+
expect(shown.get("min.js").text).toBe("a".repeat(Find.CONTEXT) + "needle" + "b".repeat(Find.CONTEXT));
|
|
70
|
+
expect([...(await run(true)).keys()].sort()).toEqual([".hid/in.txt", ".hidden.txt", "image.bin", "min.js", "sub/notes:1.txt"]);
|
|
71
|
+
}, 30000);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test("the backend keeps a checked, bounded match and hands it out as the row's g", () => {
|
|
75
|
+
expect(matchOf({ l: 4, t: "x" })).toEqual({ l: 4, t: "x" });
|
|
76
|
+
expect(matchOf({ l: 2, t: "z".repeat(MATCH_TEXT_LIMIT + 50) }).t.length).toBe(MATCH_TEXT_LIMIT);
|
|
77
|
+
for (const bad of [null, "x", { l: 0, t: "x" }, { l: 1.5, t: "x" }, { l: 3 }]) expect(matchOf(bad)).toBe(null);
|
|
78
|
+
const dir = mkdtempSync(join(root, "rows-"));
|
|
79
|
+
writeFileSync(join(dir, "a.txt"), "x");
|
|
80
|
+
writeFileSync(join(dir, "b.txt"), "x");
|
|
81
|
+
const listing = new Listing(dir, [{ name: "a.txt", dir: false, link: false, match: { l: 3, t: "hit" } }, { name: "b.txt", dir: false, link: false }]);
|
|
82
|
+
const { rows } = listing.window(0, 2);
|
|
83
|
+
expect(rows[0].g).toEqual({ l: 3, t: "hit" });
|
|
84
|
+
expect(rows[1].g).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// A pane as Search.js reads it, recording what it asks the backend.
|
|
88
|
+
function pane(extra = {}) {
|
|
89
|
+
const calls = [];
|
|
90
|
+
const root = {
|
|
91
|
+
calls, recentMode: "", searchMode: "", searchQuery: "", searchSent: "", searchGen: 0, searchStep: 0,
|
|
92
|
+
path: "/work", home: "/home/u", showHidden: false, windowSize: 40, sizeWish: false, cursorIndex: 0,
|
|
93
|
+
rowFor: () => null, join: (a, b) => a + "/" + b,
|
|
94
|
+
backend: {
|
|
95
|
+
searchOpen: (scope, folder, hidden) => calls.push(["open", scope]),
|
|
96
|
+
searchQuery: (query, gen, count, sizes, contents) => calls.push(["query", query, contents]),
|
|
97
|
+
searchCancel: () => calls.push(["cancel"]), searchClose: () => calls.push(["close"]),
|
|
98
|
+
},
|
|
99
|
+
openWithoutHistory: (path) => calls.push(["relist", path]),
|
|
100
|
+
...extra,
|
|
101
|
+
};
|
|
102
|
+
return root;
|
|
103
|
+
}
|
|
104
|
+
const key = (code, modifiers = 0) => ({ key: code, modifiers, text: "" });
|
|
105
|
+
|
|
106
|
+
test("Ctrl+G opens a content search that waits for Enter, as f opens a name search", () => {
|
|
107
|
+
const root = pane();
|
|
108
|
+
Search.contents(root);
|
|
109
|
+
expect(root.searchMode).toBe(Search.TYPING);
|
|
110
|
+
expect(root.searchContents).toBe(true);
|
|
111
|
+
expect(root.calls).toEqual([["open", "/work"]]);
|
|
112
|
+
expect(Search.placeholder(root.path, root.home, root.searchStep, true)).toBe("grep -ri ");
|
|
113
|
+
root.searchQuery = "needle";
|
|
114
|
+
Search.typeKey(key(Qt.Key_Return), root);
|
|
115
|
+
expect(root.calls.at(-1)).toEqual(["query", "needle", true]);
|
|
116
|
+
expect(root.searchMode).toBe(Search.RESULTS);
|
|
117
|
+
// f starts a name search afresh.
|
|
118
|
+
const named = pane();
|
|
119
|
+
Search.start(named);
|
|
120
|
+
expect(named.searchContents).toBe(false);
|
|
121
|
+
expect(Search.placeholder(named.path, named.home, 0, false)).toBe("find . -iname ");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("Ctrl+G inside Find only turns the mode; the query runs at the next Enter", () => {
|
|
125
|
+
const root = pane();
|
|
126
|
+
Search.start(root);
|
|
127
|
+
root.searchQuery = "draft";
|
|
128
|
+
Search.typeKey(key(Qt.Key_G, Qt.ControlModifier), root);
|
|
129
|
+
expect(root.searchContents).toBe(true);
|
|
130
|
+
expect(root.calls.filter((c) => c[0] === "query")).toEqual([]);
|
|
131
|
+
Search.typeKey(key(Qt.Key_Return), root);
|
|
132
|
+
expect(root.calls.at(-1)).toEqual(["query", "draft", true]);
|
|
133
|
+
expect(root.searchMode).toBe(Search.RESULTS);
|
|
134
|
+
// From the results (a query already run) it still waits, back on the query line.
|
|
135
|
+
const sent = root.calls.length;
|
|
136
|
+
Search.contents(root);
|
|
137
|
+
expect(root.searchContents).toBe(false);
|
|
138
|
+
expect(root.searchMode).toBe(Search.TYPING);
|
|
139
|
+
expect(root.calls.length).toBe(sent);
|
|
140
|
+
root.searchEnterAt = 0;
|
|
141
|
+
Search.typeKey(key(Qt.Key_Return), root);
|
|
142
|
+
expect(root.calls.at(-1)).toEqual(["query", "draft", false]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("Tab, Enter and Esc keep their meanings in a content search", () => {
|
|
146
|
+
const root = pane();
|
|
147
|
+
Search.contents(root);
|
|
148
|
+
root.searchQuery = "needle";
|
|
149
|
+
Search.typeKey(key(Qt.Key_Return), root);
|
|
150
|
+
// Tab turns the scope and reruns, still over contents.
|
|
151
|
+
Search.typeKey(key(Qt.Key_Tab), root);
|
|
152
|
+
expect(root.calls.slice(-2)).toEqual([["open", "/home/u"], ["query", "needle", true]]);
|
|
153
|
+
// Esc while the walk runs stops it and keeps the results; the next Esc leaves.
|
|
154
|
+
root.searchRunning = true;
|
|
155
|
+
Search.typeKey(key(Qt.Key_Escape), root);
|
|
156
|
+
expect(root.calls.at(-1)).toEqual(["cancel"]);
|
|
157
|
+
expect(root.searchMode).not.toBe("");
|
|
158
|
+
root.searchRunning = false;
|
|
159
|
+
Search.typeKey(key(Qt.Key_Escape), root);
|
|
160
|
+
expect(root.calls.slice(-2)).toEqual([["close"], ["relist", "/work"]]);
|
|
161
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {test,expect} from 'bun:test';
|
|
2
|
+
async function load(name,names,deps={}) {
|
|
3
|
+
const s=(await Bun.file(new URL('../ui/js/'+name+'.js',import.meta.url)).text()).replace(/^\.(?:pragma|import).*$/gm,'');
|
|
4
|
+
return new Function(...Object.keys(deps),s+';return {'+names+'};')(...Object.values(deps));
|
|
5
|
+
}
|
|
6
|
+
const Find=await load('Find','expression');
|
|
7
|
+
test('Find uses path matching for forward slashes, without rewriting Windows queries',()=>{
|
|
8
|
+
expect(Find.expression('ReadMe',true)).toEqual(['-iname','*ReadMe*']);
|
|
9
|
+
expect(Find.expression('src/ReadMe',true)).toEqual(['-ipath','*src/ReadMe*']);
|
|
10
|
+
expect(Find.expression('*/src/*.js',true)).toEqual(['-ipath','*/src/*.js']);
|
|
11
|
+
expect(Find.expression('src/File',false)).toEqual(['-mindepth','1','-name','.*','-prune','-o','-ipath','*src/File*','-print']);
|
|
12
|
+
});
|
|
13
|
+
test('focused Find owns Escape; focused Filter and listing close Filter first',async()=>{
|
|
14
|
+
const calls=[];
|
|
15
|
+
const Focus=await load('Focus','handleKey',{
|
|
16
|
+
Qt:{Key_Escape:27},Status:{NO_TRASH:''},
|
|
17
|
+
Filter:{close:r=>{calls.push('filter');r.filterTyping=false;r.filterQuery='';}},
|
|
18
|
+
Search:{abort:r=>{calls.push('find');r.searchMode='';},TYPING:'typing'},
|
|
19
|
+
shareBrowserHere:()=>false
|
|
20
|
+
});
|
|
21
|
+
const root={selectionBand:null,renameEditor:()=>null,filterTyping:true,filterQuery:'test',searchMode:'typing'};
|
|
22
|
+
Focus.handleKey({key:27},root,null,'find');
|
|
23
|
+
expect(calls).toEqual(['find']);expect(root.filterQuery).toBe('test');
|
|
24
|
+
root.searchMode='typing';
|
|
25
|
+
Focus.handleKey({key:27},root,null,'filter');
|
|
26
|
+
expect(calls).toEqual(['find','filter']);expect(root.searchMode).toBe('typing');
|
|
27
|
+
root.filterQuery='again';
|
|
28
|
+
Focus.handleKey({key:27},root,null);
|
|
29
|
+
expect(calls).toEqual(['find','filter','filter']);expect(root.searchMode).toBe('typing');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('leaving Find preserves Filter through the directory relist',async()=>{
|
|
33
|
+
const Search=await load('Search','abort');
|
|
34
|
+
let options;
|
|
35
|
+
const root={searchMode:'results',searchQuery:'item',searchGen:2,searchOpenedGen:1,
|
|
36
|
+
searchFrom:'/tmp/files',path:'/tmp/files',filterQuery:'00',backend:{searchClose(){}},
|
|
37
|
+
openWithoutHistory:(path,ask)=>{expect(path).toBe('/tmp/files');options=ask;}};
|
|
38
|
+
Search.abort(root);
|
|
39
|
+
expect(options).toEqual({holdWalk:true,relist:true,keptQuery:'00'});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('focused Find Escape stops a running search, then closes only Find',async()=>{
|
|
43
|
+
const Search=await load('Search','abort');
|
|
44
|
+
const calls=[];
|
|
45
|
+
const Focus=await load('Focus','handleKey',{
|
|
46
|
+
Qt:{Key_Escape:27},Status:{NO_TRASH:''},Search,
|
|
47
|
+
Filter:{close:()=>{throw Error('Find Escape must not close Filter')}},
|
|
48
|
+
shareBrowserHere:()=>false
|
|
49
|
+
});
|
|
50
|
+
const root={selectionBand:null,renameEditor:()=>null,
|
|
51
|
+
searchMode:'typing',searchQuery:'x',searchRunning:true,searchGen:1,searchOpenedGen:0,
|
|
52
|
+
path:'/tmp',filterQuery:'keep',backend:{searchCancel:()=>calls.push('cancel'),searchClose:()=>calls.push('close')},
|
|
53
|
+
openWithoutHistory:(path,ask)=>calls.push(ask.keptQuery)};
|
|
54
|
+
Focus.handleKey({key:27},root,null,'find');
|
|
55
|
+
expect(calls).toEqual(['cancel']);
|
|
56
|
+
expect(root.searchMode).toBe('typing');
|
|
57
|
+
expect(root.searchQuery).toBe('x');
|
|
58
|
+
expect(root.searchLeaving).not.toBe(true);
|
|
59
|
+
expect(root.filterQuery).toBe('keep');
|
|
60
|
+
// The backend's cancellation reply marks the search stopped.
|
|
61
|
+
root.searchRunning=false;
|
|
62
|
+
Focus.handleKey({key:27},root,null,'find');
|
|
63
|
+
expect(root.searchLeaving).toBe(true);
|
|
64
|
+
expect(root.filterQuery).toBe('keep');
|
|
65
|
+
expect(calls).toEqual(['cancel','close','keep']);
|
|
66
|
+
});
|