flea-qml 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +511 -2
- package/THIRD-PARTY-NOTICES.md +29 -0
- package/demos/App.java +5 -0
- package/demos/Dockerfile +4 -0
- package/demos/Main.kt +3 -0
- package/demos/Program.cs +1 -0
- package/demos/README.md +12 -0
- package/demos/app.dart +3 -0
- package/demos/app.deb +0 -0
- package/demos/app.js +1 -0
- package/demos/backup.tar +0 -0
- package/demos/budget.xlsx +0 -0
- package/demos/build.sh +2 -0
- package/demos/bundle.zip +0 -0
- package/demos/clip.mp4 +0 -0
- package/demos/data.sqlite +0 -0
- package/demos/disk.iso +0 -0
- package/demos/engine.cpp +5 -0
- package/demos/firmware.bin +0 -0
- package/demos/flake.nix +4 -0
- package/demos/folder/inside.txt +1 -0
- package/demos/font.ttf +0 -0
- package/demos/index.html +5 -0
- package/demos/index.php +2 -0
- package/demos/init.lua +1 -0
- package/demos/letter.docx +0 -0
- package/demos/lib.rs +3 -0
- package/demos/main.c +6 -0
- package/demos/main.h +3 -0
- package/demos/math-and-diagrams.md +200 -0
- package/demos/notes.tar.gz +0 -0
- package/demos/notes.txt +3 -0
- package/demos/photo.png +0 -0
- package/demos/program +2 -0
- package/demos/report.pdf +32 -0
- package/demos/script.py +1 -0
- package/demos/server.go +7 -0
- package/demos/settings.json +4 -0
- package/demos/slides.pptx +0 -0
- package/demos/song.mp3 +0 -0
- package/demos/style.css +4 -0
- package/demos/tool.rb +1 -0
- package/demos/types.ts +2 -0
- package/docs/pele.md +23 -0
- package/docs/protocol.md +3420 -0
- package/docs/temml.md +63 -0
- package/index.js +107 -0
- package/package.json +34 -4
- package/src/archive.js +407 -0
- package/src/archiveops.js +728 -0
- package/src/backend.js +369 -0
- package/src/bulkrename.js +95 -0
- package/src/clip-host.js +76 -0
- package/src/clip.js +462 -0
- package/src/code.js +168 -0
- package/src/compare.js +74 -0
- package/src/create.js +128 -0
- package/src/dirsize.js +147 -0
- package/src/figure-svg.js +530 -0
- package/src/figure-thread.js +23 -0
- package/src/figure.js +132 -0
- package/src/find.js +55 -0
- package/src/flea.js +39 -0
- package/src/fsinfo.js +39 -0
- package/src/highlight.js +141 -0
- package/src/html.js +52 -0
- package/src/kinds.js +89 -0
- package/src/listing.js +130 -0
- package/src/markdown-html.js +317 -0
- package/src/mathml.js +186 -0
- package/src/menu.js +180 -0
- package/src/meta.js +135 -0
- package/src/office.js +810 -0
- package/src/order.js +100 -0
- package/src/peek.js +49 -0
- package/src/pele-wrapper.js +695 -0
- package/src/permissions.js +180 -0
- package/src/places.js +313 -0
- package/src/preview-language.js +10 -0
- package/src/terminal-server.js +95 -0
- package/src/terminal-shell.js +26 -0
- package/src/transfer.js +669 -0
- package/src/trash-record.js +41 -0
- package/src/trash.js +210 -0
- package/src/trashbrowse.js +327 -0
- package/src/uistate.js +393 -0
- package/src/uistore.js +168 -0
- package/src/vendor/LICENSES/Apache-2.0.txt +202 -0
- package/src/vendor/LICENSES/MIT-katex-2015.txt +27 -0
- package/src/vendor/LICENSES/MIT-katex.txt +21 -0
- package/src/vendor/LICENSES/MIT-mermaid.txt +21 -0
- package/src/vendor/LICENSES/MIT-pele.txt +40 -0
- package/src/vendor/LICENSES/MIT-perseus.txt +18 -0
- package/src/vendor/LICENSES/MIT-temml.txt +21 -0
- package/src/vendor/pele.js +145 -0
- package/src/vendor/temml.mjs +14708 -0
- package/src/watch.js +98 -0
- package/src/wayland/client.js +109 -0
- package/src/wayland/protocol.js +55 -0
- package/src/wayland/protocols/ext-data-control-v1.xml +276 -0
- package/src/wayland/protocols/wayland.xml +3248 -0
- package/src/wayland/protocols/wlr-data-control-unstable-v1.xml +278 -0
- package/src/wayland/resource-io.js +6 -0
- package/src/wayland/socket-discovery.js +26 -0
- package/src/wayland/transport-posix.js +120 -0
- package/src/wayland/transport-win32.js +11 -0
- package/src/wayland/transport.js +3 -0
- package/src/wayland/win32-resources.js +80 -0
- package/src/wayland/win32-wire.js +62 -0
- package/test/archive.test.js +80 -0
- package/test/bench-markdown-scroll.mjs +190 -0
- package/test/clip-host.test.js +113 -0
- package/test/code-preview.test.js +35 -0
- package/test/compare-ui-paths.test.js +24 -0
- package/test/compare.test.js +54 -0
- package/test/delete-trash.test.js +67 -0
- package/test/dirsize.test.js +138 -0
- package/test/file-ops.test.js +307 -0
- package/test/file-url.test.js +24 -0
- package/test/find-contents.test.js +161 -0
- package/test/find-input.test.js +66 -0
- package/test/fixtures/pele-parity.json +1006 -0
- package/test/highlight-launcher.test.js +35 -0
- package/test/html-preview.test.js +22 -0
- package/test/input.test.js +15 -0
- package/test/keymap.test.js +26 -0
- package/test/language-icons.test.js +36 -0
- package/test/launcher.test.js +51 -0
- package/test/manual-archive-here-ui.mjs +47 -0
- package/test/manual-code-scroll.mjs +28 -0
- package/test/manual-column-scroll.mjs +24 -0
- package/test/manual-compare-ui.mjs +20 -0
- package/test/manual-find-status.mjs +37 -0
- package/test/manual-folder-focus.mjs +63 -0
- package/test/manual-html-security.mjs +23 -0
- package/test/manual-html-ui.mjs +30 -0
- package/test/manual-syntax-preview.mjs +26 -0
- package/test/manual-terminal-lifecycle.mjs +42 -0
- package/test/manual-terminal-navigation.mjs +48 -0
- package/test/manual-terminal-ui.mjs +47 -0
- package/test/manual-trash-ui.mjs +86 -0
- package/test/markdown-diagrams.test.js +93 -0
- package/test/markdown-html.test.js +105 -0
- package/test/mathml.test.js +92 -0
- package/test/menu-transfer.test.js +57 -0
- package/test/office-preview.test.js +154 -0
- package/test/pele-parity.test.js +28 -0
- package/test/places.test.js +134 -0
- package/test/preview-keys.test.js +60 -0
- package/test/preview-language.test.js +22 -0
- package/test/status-filename.test.js +20 -0
- package/test/transfer-replace-merge.test.js +187 -0
- package/test/trash-address.test.js +23 -0
- package/test/trash-view.test.js +37 -0
- package/test/trashbrowse.test.js +135 -0
- package/ui/AboutFacts.qml +78 -0
- package/ui/ArchiveCards.qml +95 -0
- package/ui/ArchiveCompressDialog.qml +378 -0
- package/ui/ArchiveHereConfirm.qml +127 -0
- package/ui/ArchivePasswordDialog.qml +257 -0
- package/ui/Backend.qml +775 -0
- package/ui/BulkOldNames.qml +47 -0
- package/ui/BulkRenameDialog.qml +380 -0
- package/ui/CardFrame.qml +67 -0
- package/ui/CardScroll.qml +79 -0
- package/ui/CheckBox.qml +73 -0
- package/ui/ChromeBar.qml +360 -0
- package/ui/ChromeButton.qml +131 -0
- package/ui/ChromeModes.qml +43 -0
- package/ui/CodeLines.qml +256 -0
- package/ui/CodeRefusal.qml +52 -0
- package/ui/CollideConfirm.qml +147 -0
- package/ui/CollideHost.qml +167 -0
- package/ui/ColumnPane.qml +525 -0
- package/ui/ColumnRow.qml +380 -0
- package/ui/ColumnsArea.qml +594 -0
- package/ui/ColumnsLanding.qml +147 -0
- package/ui/CompareView.qml +321 -0
- package/ui/ContextMenu.qml +743 -0
- package/ui/ConvertDialog.qml +456 -0
- package/ui/CountedWorker.qml +7 -0
- package/ui/Crumb.qml +64 -0
- package/ui/CrumbDrop.qml +25 -0
- package/ui/CsvTable.qml +394 -0
- package/ui/CursorMark.qml +41 -0
- package/ui/DefaultClaim.qml +130 -0
- package/ui/DeviceLock.qml +47 -0
- package/ui/DeviceMounts.qml +540 -0
- package/ui/DialogButton.qml +150 -0
- package/ui/DialogField.qml +147 -0
- package/ui/DialogHeader.qml +31 -0
- package/ui/DialogScrim.qml +20 -0
- package/ui/DialogTitle.qml +37 -0
- package/ui/Divider.qml +9 -0
- package/ui/DropInto.qml +126 -0
- package/ui/DropTarget.qml +35 -0
- package/ui/DropboxMark.qml +60 -0
- package/ui/EmptyState.qml +150 -0
- package/ui/EscapeHint.qml +11 -0
- package/ui/FactsTable.qml +54 -0
- package/ui/FastScrollHandler.qml +476 -0
- package/ui/FatNamesConfirm.qml +157 -0
- package/ui/Favourites.qml +143 -0
- package/ui/FigureService.qml +120 -0
- package/ui/FileDrag.qml +267 -0
- package/ui/FilterStrip.qml +133 -0
- package/ui/FitMetrics.qml +22 -0
- package/ui/FlashDriveRows.qml +137 -0
- package/ui/FlashHost.qml +72 -0
- package/ui/FlashImageRow.qml +104 -0
- package/ui/FlashRunner.qml +119 -0
- package/ui/FlashStepRow.qml +172 -0
- package/ui/FlashStrip.qml +38 -0
- package/ui/FlashView.qml +397 -0
- package/ui/FleaMark.qml +88 -0
- package/ui/FlyoutFade.qml +23 -0
- package/ui/FontPane.qml +170 -0
- package/ui/Glass.qml +114 -0
- package/ui/GlassTagger.qml +59 -0
- package/ui/Glyph.qml +64 -0
- package/ui/GlyphSolid.qml +22 -0
- package/ui/GridArea.qml +664 -0
- package/ui/GridTile.qml +325 -0
- package/ui/GvfsBridge.qml +164 -0
- package/ui/Header.qml +433 -0
- package/ui/HoverTimers.qml +39 -0
- package/ui/HtmlClip.qml +30 -0
- package/ui/HtmlPane.qml +63 -0
- package/ui/HyprlandMark.qml +45 -0
- package/ui/Ipc.qml +1005 -0
- package/ui/IpcControls.qml +7 -0
- package/ui/IpcMenuState.qml +48 -0
- package/ui/IpcPreviewState.qml +185 -0
- package/ui/JumpPath.qml +51 -0
- package/ui/KeymapSheet.qml +561 -0
- package/ui/KeymapSheetResult.qml +89 -0
- package/ui/KindStack.qml +40 -0
- package/ui/LangMark.qml +51 -0
- package/ui/LangMarkLoader.qml +26 -0
- package/ui/List.qml +480 -0
- package/ui/LoadingState.qml +54 -0
- package/ui/LocalSend.qml +56 -0
- package/ui/LocalSendMark.qml +73 -0
- package/ui/MarkdownBlockView.qml +311 -0
- package/ui/MarkdownBlocks.qml +35 -0
- package/ui/MarkdownFigure.qml +254 -0
- package/ui/MarkdownImages.qml +72 -0
- package/ui/MarkdownList.qml +81 -0
- package/ui/MarkdownMathsText.qml +97 -0
- package/ui/MarkdownPane.qml +83 -0
- package/ui/MarkdownPictures.qml +41 -0
- package/ui/MarkdownQuote.qml +52 -0
- package/ui/MarkdownRemote.qml +82 -0
- package/ui/MarkdownSource.qml +155 -0
- package/ui/MarkdownTable.qml +222 -0
- package/ui/MarkdownTableScroll.qml +64 -0
- package/ui/MarkdownTableWheel.qml +126 -0
- package/ui/MarkdownText.qml +87 -0
- package/ui/MarkdownWorker.js +132 -0
- package/ui/MatchText.qml +99 -0
- package/ui/MediaSession.qml +38 -0
- package/ui/MediaSound.qml +26 -0
- package/ui/MediaStrip.qml +191 -0
- package/ui/MediaTrack.qml +36 -0
- package/ui/MediaVolume.qml +54 -0
- package/ui/MenuActionDialog.qml +396 -0
- package/ui/MenuEdgeFade.qml +24 -0
- package/ui/MenuHost.qml +227 -0
- package/ui/MenuRow.qml +351 -0
- package/ui/MountListing.qml +100 -0
- package/ui/NetworkDialog.qml +435 -0
- package/ui/NetworkForm.qml +426 -0
- package/ui/NetworkMounts.qml +913 -0
- package/ui/NetworkPlaces.qml +127 -0
- package/ui/OpenWithDialog.qml +613 -0
- package/ui/Opener.qml +95 -0
- package/ui/Pane.qml +1320 -0
- package/ui/PaneMenuActions.qml +527 -0
- package/ui/PanePath.qml +90 -0
- package/ui/PaneRail.qml +268 -0
- package/ui/PaneStates.qml +88 -0
- package/ui/PaneSwap.qml +339 -0
- package/ui/PaneWire.qml +601 -0
- package/ui/ParkedWait.qml +9 -0
- package/ui/PathEntry.qml +186 -0
- package/ui/PathJump.qml +400 -0
- package/ui/PdfViewer.qml +297 -0
- package/ui/PermissionsDialog.qml +658 -0
- package/ui/PermissionsHeadings.qml +31 -0
- package/ui/PhoneMounts.qml +67 -0
- package/ui/PickerAnswers.qml +75 -0
- package/ui/PickerBox.qml +26 -0
- package/ui/PickerChrome.qml +295 -0
- package/ui/PickerFilter.qml +97 -0
- package/ui/PickerGrid.qml +308 -0
- package/ui/PickerHeader.qml +28 -0
- package/ui/PickerLifecycle.qml +27 -0
- package/ui/PickerList.qml +218 -0
- package/ui/PickerListing.qml +149 -0
- package/ui/PickerPathBar.qml +127 -0
- package/ui/PickerPlaces.qml +189 -0
- package/ui/PickerRecent.qml +92 -0
- package/ui/PickerSave.qml +101 -0
- package/ui/PickerSelection.qml +155 -0
- package/ui/PickerWindow.qml +846 -0
- package/ui/PowerSectorsReader.qml +10 -0
- package/ui/PrebuildDriver.qml +146 -0
- package/ui/Preview.qml +1005 -0
- package/ui/PreviewAnimation.qml +24 -0
- package/ui/PreviewArchive.qml +91 -0
- package/ui/PreviewColumn.qml +553 -0
- package/ui/PreviewEdge.qml +63 -0
- package/ui/PreviewFacts.qml +30 -0
- package/ui/PreviewFactsLine.qml +34 -0
- package/ui/PreviewHtml.qml +233 -0
- package/ui/PreviewImage.qml +253 -0
- package/ui/PreviewImagePan.qml +13 -0
- package/ui/PreviewImageSharp.qml +32 -0
- package/ui/PreviewLines.qml +106 -0
- package/ui/PreviewMarkdown.qml +706 -0
- package/ui/PreviewMedia.qml +250 -0
- package/ui/PreviewPdf.qml +482 -0
- package/ui/PreviewScroll.qml +201 -0
- package/ui/PreviewSwap.qml +335 -0
- package/ui/PreviewText.qml +149 -0
- package/ui/ProtocolChip.qml +18 -0
- package/ui/QuickLookArchive.qml +41 -0
- package/ui/QuickLookBar.qml +103 -0
- package/ui/QuickLookPrepare.qml +355 -0
- package/ui/QuickLookSwap.qml +10 -0
- package/ui/QuickLookWarm.qml +65 -0
- package/ui/RailDrop.qml +155 -0
- package/ui/RailHeading.qml +19 -0
- package/ui/RailPlaces.qml +38 -0
- package/ui/RailTrashRow.qml +67 -0
- package/ui/RenameField.qml +353 -0
- package/ui/RendererRetry.qml +11 -0
- package/ui/ResizeHandle.qml +52 -0
- package/ui/Reveal.qml +54 -0
- package/ui/Row.qml +476 -0
- package/ui/RowDrag.qml +213 -0
- package/ui/RowDropTarget.qml +27 -0
- package/ui/RunCard.qml +286 -0
- package/ui/Scripts.qml +65 -0
- package/ui/SearchStrip.qml +232 -0
- package/ui/SelectionBand.qml +140 -0
- package/ui/SelectionPreview.qml +370 -0
- package/ui/SettingsFavourite.qml +122 -0
- package/ui/SettingsGroup.qml +96 -0
- package/ui/SettingsHint.qml +12 -0
- package/ui/SettingsMetrics.qml +29 -0
- package/ui/SettingsPane.qml +178 -0
- package/ui/SettingsPanel.qml +547 -0
- package/ui/SettingsRail.qml +128 -0
- package/ui/SettingsRow.qml +409 -0
- package/ui/SettingsRuler.qml +85 -0
- package/ui/SettingsSegment.qml +85 -0
- package/ui/ShareBrowser.qml +113 -0
- package/ui/ShareLink.qml +67 -0
- package/ui/ShelfPins.qml +101 -0
- package/ui/Sidebar.qml +728 -0
- package/ui/SidebarRow.qml +358 -0
- package/ui/Spinner.qml +60 -0
- package/ui/StableModel.qml +89 -0
- package/ui/StateMessage.qml +84 -0
- package/ui/StatusBar.qml +512 -0
- package/ui/StatusFileName.qml +27 -0
- package/ui/TabBar.qml +673 -0
- package/ui/TabDragGeometry.qml +60 -0
- package/ui/TagMenuMark.qml +52 -0
- package/ui/TagRailSection.qml +53 -0
- package/ui/TagSlot.qml +40 -0
- package/ui/TagState.qml +150 -0
- package/ui/TagSwatch.qml +28 -0
- package/ui/Taildrop.qml +86 -0
- package/ui/TailscaleMark.qml +50 -0
- package/ui/TerminalService.qml +69 -0
- package/ui/TerminalTab.qml +48 -0
- package/ui/TerminalTabs.qml +27 -0
- package/ui/Theme.qml +460 -0
- package/ui/ThemeColors.qml +43 -0
- package/ui/TransferCard.qml +239 -0
- package/ui/TrashConfirm.qml +129 -0
- package/ui/TrashDropHandler.qml +11 -0
- package/ui/TrashHost.qml +202 -0
- package/ui/TrashMonitor.qml +30 -0
- package/ui/TrashToolbar.qml +19 -0
- package/ui/TrashView.qml +492 -0
- package/ui/UnlockDialog.qml +450 -0
- package/ui/UnlockHost.qml +206 -0
- package/ui/UpdateCheck.qml +97 -0
- package/ui/UserDirs.qml +25 -0
- package/ui/ViewState.qml +565 -0
- package/ui/ViewportScrollBar.qml +195 -0
- package/ui/ViewportScrollBars.qml +25 -0
- package/ui/WebPreview.qml +33 -0
- package/ui/WebPreviewView.qml +26 -0
- package/ui/WindowBody.qml +829 -0
- package/ui/WindowDrag.qml +35 -0
- package/ui/boot/Commons/AudioNodes.qml +50 -0
- package/ui/boot/Commons/AudioNodesModel.js +96 -0
- package/ui/boot/Commons/Border.qml +243 -0
- package/ui/boot/Commons/BorderGeometry.js +373 -0
- package/ui/boot/Commons/Color.qml +258 -0
- package/ui/boot/Commons/I18n.qml +118 -0
- package/ui/boot/Commons/InputDevices.qml +112 -0
- package/ui/boot/Commons/InputMethodState.qml +38 -0
- package/ui/boot/Commons/IpcRegistry.qml +77 -0
- package/ui/boot/Commons/Numbers.js +19 -0
- package/ui/boot/Commons/Plural.js +91 -0
- package/ui/boot/Commons/ShellIpc.qml +12 -0
- package/ui/boot/Commons/Style.qml +561 -0
- package/ui/boot/Commons/Translations.qml +100 -0
- package/ui/boot/Commons/UntypedInput.qml +152 -0
- package/ui/boot/Commons/Util.qml +188 -0
- package/ui/boot/Commons/qmldir +13 -0
- package/ui/boot/LICENSE-omarchy +20 -0
- package/ui/boot/Ui/BackgroundMedia.qml +49 -0
- package/ui/boot/Ui/BarIconButton.qml +106 -0
- package/ui/boot/Ui/BarIconContent.qml +38 -0
- package/ui/boot/Ui/BarIndicator.qml +53 -0
- package/ui/boot/Ui/BarWidget.qml +50 -0
- package/ui/boot/Ui/BorderOverlay.qml +54 -0
- package/ui/boot/Ui/BorderSurface.qml +40 -0
- package/ui/boot/Ui/Button.qml +224 -0
- package/ui/boot/Ui/ButtonGroup.qml +131 -0
- package/ui/boot/Ui/ConfirmDialog.qml +134 -0
- package/ui/boot/Ui/CursorSurface.qml +42 -0
- package/ui/boot/Ui/Dropdown.qml +245 -0
- package/ui/boot/Ui/FilterInput.qml +26 -0
- package/ui/boot/Ui/IconContentBounds.qml +141 -0
- package/ui/boot/Ui/KeyboardPanel.qml +453 -0
- package/ui/boot/Ui/MultiSelect.qml +624 -0
- package/ui/boot/Ui/NumberField.qml +86 -0
- package/ui/boot/Ui/OpticalGlyph.qml +68 -0
- package/ui/boot/Ui/OverlayWindow.qml +102 -0
- package/ui/boot/Ui/Panel.qml +60 -0
- package/ui/boot/Ui/PanelActionButton.qml +105 -0
- package/ui/boot/Ui/PanelController.qml +16 -0
- package/ui/boot/Ui/PanelHero.qml +112 -0
- package/ui/boot/Ui/PanelKeyCatcher.qml +100 -0
- package/ui/boot/Ui/PanelSectionHeader.qml +31 -0
- package/ui/boot/Ui/PanelSeparator.qml +19 -0
- package/ui/boot/Ui/PanelSlider.qml +156 -0
- package/ui/boot/Ui/PanelToolTip.qml +50 -0
- package/ui/boot/Ui/PluginBarApi.qml +89 -0
- package/ui/boot/Ui/PointerMoveGate.qml +54 -0
- package/ui/boot/Ui/PopupCard.qml +181 -0
- package/ui/boot/Ui/ScreenMoveRemap.qml +43 -0
- package/ui/boot/Ui/SearchableDropdown.qml +354 -0
- package/ui/boot/Ui/SizedImage.qml +125 -0
- package/ui/boot/Ui/SpeedTestOverlay.qml +412 -0
- package/ui/boot/Ui/TextField.qml +58 -0
- package/ui/boot/Ui/Toggle.qml +118 -0
- package/ui/boot/Ui/ToggleSwitch.qml +115 -0
- package/ui/boot/Ui/WidgetButton.qml +172 -0
- package/ui/boot/Ui/qmldir +41 -0
- package/ui/boot/fleatab.qml +50 -0
- package/ui/boot/picker.qml +39 -0
- package/ui/boot/shell.qml +131 -0
- package/ui/boot/tabtearoff.qml +51 -0
- package/ui/js/Anchor.js +379 -0
- package/ui/js/AnchorHold.js +162 -0
- package/ui/js/Archive.js +157 -0
- package/ui/js/ArchiveCard.js +44 -0
- package/ui/js/ArchiveOps.js +44 -0
- package/ui/js/BackReturn.js +118 -0
- package/ui/js/BulkRename.js +100 -0
- package/ui/js/Buttons.js +54 -0
- package/ui/js/ChromeKeys.js +116 -0
- package/ui/js/ClipMarks.js +73 -0
- package/ui/js/ClipSelection.js +67 -0
- package/ui/js/Clipboard.js +179 -0
- package/ui/js/Cloud.js +97 -0
- package/ui/js/CodeRuns.js +72 -0
- package/ui/js/Collide.js +212 -0
- package/ui/js/ColumnFit.js +28 -0
- package/ui/js/ColumnMenu.js +111 -0
- package/ui/js/Columns.js +298 -0
- package/ui/js/ColumnsCarry.js +103 -0
- package/ui/js/Compare.js +74 -0
- package/ui/js/Contrast.js +157 -0
- package/ui/js/ControlReader.js +42 -0
- package/ui/js/Convert.js +52 -0
- package/ui/js/CopyAs.js +52 -0
- package/ui/js/Crumbs.js +178 -0
- package/ui/js/Csv.js +231 -0
- package/ui/js/Density.js +44 -0
- package/ui/js/Devices.js +440 -0
- package/ui/js/DirSizes.js +180 -0
- package/ui/js/Drag.js +360 -0
- package/ui/js/DragOut.js +166 -0
- package/ui/js/DropDev.js +52 -0
- package/ui/js/Dropbox.js +41 -0
- package/ui/js/Eject.js +217 -0
- package/ui/js/Errors.js +188 -0
- package/ui/js/ExtThumbs.js +115 -0
- package/ui/js/Facts.js +402 -0
- package/ui/js/FigureDate.js +14 -0
- package/ui/js/Filter.js +285 -0
- package/ui/js/Find.js +46 -0
- package/ui/js/Flash.js +184 -0
- package/ui/js/FlashCard.js +79 -0
- package/ui/js/FlashEndings.js +125 -0
- package/ui/js/FlashPane.js +116 -0
- package/ui/js/FleaBin.js +12 -0
- package/ui/js/Focus.js +504 -0
- package/ui/js/FolderSorts.js +108 -0
- package/ui/js/FolderViews.js +44 -0
- package/ui/js/Format.js +314 -0
- package/ui/js/Fuzzy.js +141 -0
- package/ui/js/GlassTag.js +72 -0
- package/ui/js/Grid.js +33 -0
- package/ui/js/GridGeometry.js +90 -0
- package/ui/js/GridNames.js +153 -0
- package/ui/js/GvfsBridge.js +200 -0
- package/ui/js/HostClipboard.js +29 -0
- package/ui/js/Icons.js +218 -0
- package/ui/js/Input.js +50 -0
- package/ui/js/Jump.js +200 -0
- package/ui/js/KeyName.js +64 -0
- package/ui/js/Keymap.js +401 -0
- package/ui/js/Kinds.js +164 -0
- package/ui/js/LICENSES/ISC-lucide.txt +43 -0
- package/ui/js/LangMarkPaths.js +47 -0
- package/ui/js/LangMarks.js +120 -0
- package/ui/js/Lists.js +11 -0
- package/ui/js/LocalSend.js +39 -0
- package/ui/js/LockedMenu.js +63 -0
- package/ui/js/MakeDefault.js +186 -0
- package/ui/js/MarkBox.js +7 -0
- package/ui/js/Markdown.js +119 -0
- package/ui/js/MarkdownLists.js +86 -0
- package/ui/js/MarkdownMaths.js +58 -0
- package/ui/js/MarkdownPictures.js +32 -0
- package/ui/js/MarkdownPrepared.js +139 -0
- package/ui/js/MarkdownTableFit.js +34 -0
- package/ui/js/Marks.js +148 -0
- package/ui/js/Marquee.js +96 -0
- package/ui/js/Match.js +93 -0
- package/ui/js/Material.js +109 -0
- package/ui/js/MdBlocks.js +437 -0
- package/ui/js/MdBreak.js +71 -0
- package/ui/js/MdChunks.js +70 -0
- package/ui/js/MdContainer.js +137 -0
- package/ui/js/MdDocument.js +337 -0
- package/ui/js/MdEmph.js +97 -0
- package/ui/js/MdEntity.js +108 -0
- package/ui/js/MdEntityTable.js +223 -0
- package/ui/js/MdEscape.js +59 -0
- package/ui/js/MdFront.js +37 -0
- package/ui/js/MdHold.js +32 -0
- package/ui/js/MdHtml.js +307 -0
- package/ui/js/MdHtmlBlock.js +290 -0
- package/ui/js/MdHtmlHeading.js +53 -0
- package/ui/js/MdHtmlImage.js +179 -0
- package/ui/js/MdInline.js +154 -0
- package/ui/js/MdItems.js +212 -0
- package/ui/js/MdLeaf.js +309 -0
- package/ui/js/MdLink.js +196 -0
- package/ui/js/MdMath.js +67 -0
- package/ui/js/MdParagraphs.js +138 -0
- package/ui/js/MdRefs.js +232 -0
- package/ui/js/MdResolve.js +226 -0
- package/ui/js/MdRun.js +300 -0
- package/ui/js/MdUrl.js +226 -0
- package/ui/js/Menu.js +514 -0
- package/ui/js/MenuContext.js +85 -0
- package/ui/js/MenuFit.js +13 -0
- package/ui/js/MenuRefresh.js +74 -0
- package/ui/js/MenuWheel.js +35 -0
- package/ui/js/Messages.js +186 -0
- package/ui/js/Motion.js +16 -0
- package/ui/js/Mounts.js +320 -0
- package/ui/js/MouseNav.js +13 -0
- package/ui/js/Nav.js +329 -0
- package/ui/js/NetFs.js +26 -0
- package/ui/js/OpenWith.js +83 -0
- package/ui/js/Ops.js +585 -0
- package/ui/js/Palette.js +33 -0
- package/ui/js/PathBar.js +260 -0
- package/ui/js/Permissions.js +179 -0
- package/ui/js/Phones.js +112 -0
- package/ui/js/Picker.js +353 -0
- package/ui/js/PlaceMenu.js +96 -0
- package/ui/js/Places.js +265 -0
- package/ui/js/PreviewAsk.js +21 -0
- package/ui/js/PreviewKeys.js +155 -0
- package/ui/js/PreviewSettle.js +29 -0
- package/ui/js/PreviewSwap.js +101 -0
- package/ui/js/PropertyFacts.js +144 -0
- package/ui/js/Protocols.js +296 -0
- package/ui/js/QuickLookKinds.js +27 -0
- package/ui/js/QuickLookRect.js +24 -0
- package/ui/js/RailDrop.js +51 -0
- package/ui/js/RailKeys.js +87 -0
- package/ui/js/RailMenu.js +133 -0
- package/ui/js/Recent.js +135 -0
- package/ui/js/RecentMode.js +156 -0
- package/ui/js/Reload.js +60 -0
- package/ui/js/Renderer.js +9 -0
- package/ui/js/RunFile.js +143 -0
- package/ui/js/Scripts.js +28 -0
- package/ui/js/Scroll.js +304 -0
- package/ui/js/ScrollOff.js +69 -0
- package/ui/js/Search.js +478 -0
- package/ui/js/Selection.js +97 -0
- package/ui/js/Settings.js +476 -0
- package/ui/js/SettingsAbout.js +97 -0
- package/ui/js/SettingsShelf.js +52 -0
- package/ui/js/SheetKeys.js +42 -0
- package/ui/js/SheetQuery.js +281 -0
- package/ui/js/ShelfPile.js +27 -0
- package/ui/js/SlowClick.js +53 -0
- package/ui/js/SlowOp.js +6 -0
- package/ui/js/Sort.js +134 -0
- package/ui/js/Startup.js +63 -0
- package/ui/js/StateWords.js +17 -0
- package/ui/js/Status.js +238 -0
- package/ui/js/Swap.js +166 -0
- package/ui/js/TabMove.js +39 -0
- package/ui/js/Tabs.js +751 -0
- package/ui/js/Tags.js +134 -0
- package/ui/js/Taildrop.js +128 -0
- package/ui/js/Tap.js +180 -0
- package/ui/js/TextSize.js +107 -0
- package/ui/js/Thumbs.js +158 -0
- package/ui/js/Transfer.js +172 -0
- package/ui/js/Trash.js +84 -0
- package/ui/js/TrashDates.js +47 -0
- package/ui/js/TrashDrop.js +64 -0
- package/ui/js/UiState.js +438 -0
- package/ui/js/Unlock.js +152 -0
- package/ui/js/Update.js +130 -0
- package/ui/js/UserDirs.js +56 -0
- package/ui/qmldir +230 -0
package/docs/protocol.md
ADDED
|
@@ -0,0 +1,3420 @@
|
|
|
1
|
+
# Flea backend protocol
|
|
2
|
+
|
|
3
|
+
One JSON object per line, newline-delimited, over the `flea --backend` child's stdin
|
|
4
|
+
and stdout. No batching, no length prefix: `Quickshell.Io.Process` reads with a
|
|
5
|
+
`SplitParser` on `\n`. Implemented in `src/backend/proto.rs` over the field scanner and
|
|
6
|
+
escaper in `src/json.rs`, dispatched by `src/backend/run.rs`.
|
|
7
|
+
|
|
8
|
+
## Invariants
|
|
9
|
+
|
|
10
|
+
1. A successful `list` is always followed by a `rows` line, even when `first` is
|
|
11
|
+
0, in which case the array is empty. A `list` whose path cannot be read
|
|
12
|
+
answers a single `error` line instead, and no `rows` line follows.
|
|
13
|
+
2. Errors are messages, never exit statuses. `--backend` runs until it reads `quit`
|
|
14
|
+
or stdin closes, and returns exit code 0 either way; every failure is an `error`
|
|
15
|
+
line the client reads off stdout, not a process exit code.
|
|
16
|
+
3. Exactly one line arrives unasked: `changed`, which says the listed directory was
|
|
17
|
+
altered by another program. It can land between any request and its reply, so a
|
|
18
|
+
client that counts lines rather than reading their `t` field will misread the wire.
|
|
19
|
+
4. A row index means a file only in the numbering it was read from. Every `rows` line
|
|
20
|
+
names that numbering as `listing`, and a `trash`, `transfer`, `link`, `paths`, `collisions`, `settags`
|
|
21
|
+
or `menuaction` that names an older one is refused before anything resolves; see "listing" below.
|
|
22
|
+
|
|
23
|
+
## Requests
|
|
24
|
+
|
|
25
|
+
### listing
|
|
26
|
+
|
|
27
|
+
`"listing":<uint>` on a request that carries `rows`
|
|
28
|
+
|
|
29
|
+
Example: `{"c":"trash","rows":[0],"menuId":0,"listing":7}`
|
|
30
|
+
|
|
31
|
+
The backend numbers its rows, and the number moves whenever what an index names changes: a
|
|
32
|
+
successful `list` or `listpaths`, a `search`, an accepted `sort`, and a walk's ranking. Every `rows`
|
|
33
|
+
line carries the numbering it was written in as its last field, `"listing":<uint>`, and the first
|
|
34
|
+
listing a backend makes is 1. A client names the numbering of the rows it read an index from on any
|
|
35
|
+
request that carries `rows`. A `trash`, `transfer`, `link`, `paths`, `collisions`, `settags` or `menuaction` whose `listing` is not
|
|
36
|
+
the numbering in force is refused before a single index is resolved: `trash`, `transfer`, `link`, `paths`,
|
|
37
|
+
`collisions` and `settags` answer `{"t":"error","where":"stale","path":"<the command>","msg":"..."}` and nothing else, and a
|
|
38
|
+
`menuaction` answers its own reply with `"ok":false` and an `error` sentence. Nothing was done in
|
|
39
|
+
either case, so a client that wants the action reads the new rows and asks again.
|
|
40
|
+
|
|
41
|
+
A request that names no `listing` is resolved against the listing in force, exactly as before the
|
|
42
|
+
field existed, so an older client and a hand-written line keep working. `thumb`, `thumbcancel`,
|
|
43
|
+
`dirsize`, `meta`, `tagstates` and `window` read and never act on a file, so they are not refused; a client drops
|
|
44
|
+
their answers for a listing it has left. The race this closes is a request written after a `list`
|
|
45
|
+
and before its `rows` reached the client: the backend has the new listing by then, and without the
|
|
46
|
+
field it resolved the old index against it, so `list A`, `list B`, `paths [0]` answered B's first file.
|
|
47
|
+
|
|
48
|
+
### list
|
|
49
|
+
|
|
50
|
+
`{"c":"list","path":"<string>","first":<uint>,"hidden":<bool>}`
|
|
51
|
+
|
|
52
|
+
Example: `{"c":"list","path":"/home/gm","first":350,"hidden":false}`
|
|
53
|
+
|
|
54
|
+
Scans `path` (phase 1: names and the directory bit only), applies ordering, and
|
|
55
|
+
answers a `listed` line followed immediately by a `rows` line
|
|
56
|
+
covering rows `0..first`, or the window `select` below moves onto its row. A `path` that fails to scan answers a single `error` line
|
|
57
|
+
instead and leaves the previously listed directory in place: a failed `list` cannot
|
|
58
|
+
mix a new `path` with the old listing. A missing `path` defaults to the empty string,
|
|
59
|
+
a missing `first` defaults to `0`.
|
|
60
|
+
|
|
61
|
+
Optional `by`, `desc`, `foldersFirst`, and `groupByKind` fields use the ordering
|
|
62
|
+
rules described under `sort`. A list with no ordering fields uses name ascending,
|
|
63
|
+
directories first, without kind groups. Unlike an explicit `sort`, a `list` may
|
|
64
|
+
omit `by`. An invalid ordering key refuses the new listing and keeps the previous
|
|
65
|
+
directory and rows.
|
|
66
|
+
|
|
67
|
+
Optional `hiddenLast`, `false` unless `true`: with it on, dotfiles form an outer
|
|
68
|
+
partition, a visible block then a hidden block, for each of the four keys in both
|
|
69
|
+
directions, and `foldersFirst` or `groupByKind` orders inside each block, so visible
|
|
70
|
+
folders come first, then visible files, then hidden folders, then hidden files. Off
|
|
71
|
+
leaves dotfiles to follow the sort key like any other name. Absent is off, so an
|
|
72
|
+
older client keeps it.
|
|
73
|
+
|
|
74
|
+
`hidden` of `false`, or a missing `hidden`, drops every name starting with `.` before
|
|
75
|
+
it ever reaches the listing: the filter runs inside the scan itself, not as a later
|
|
76
|
+
pass over it, so a hidden directory costs nothing beyond the `readdir` entry it was
|
|
77
|
+
always going to read. `hidden` of `true` scans dotfiles in too, ordered the same as
|
|
78
|
+
everything else. There is no separate flag to ask for dotfiles without also
|
|
79
|
+
re-scanning: `sort` reorders whichever listing `list` last produced and cannot add or
|
|
80
|
+
remove rows, so changing `hidden` always means a fresh `list`, which is also what
|
|
81
|
+
clears the cursor and selection back to row 0.
|
|
82
|
+
|
|
83
|
+
Optional `wantChanged`, `false` unless `true`: with it on, a `list` that re-reads the
|
|
84
|
+
directory already listed names added plus removed rows in `changed`. Absent is off, so
|
|
85
|
+
every navigation and watched re-read stays silent and pays no count.
|
|
86
|
+
|
|
87
|
+
Optional `select`, a full path such as `"/home/gm/notes.txt"`: when it names a row of the new
|
|
88
|
+
listing at index `first` or past it, the `rows` line covers the `first` rows that hold that row
|
|
89
|
+
near their middle, starting at `index - first / 2` and clamped so the window ends at the last
|
|
90
|
+
row, instead of `0..first`; its `start` says where. A row inside `0..first`, a name the listing
|
|
91
|
+
does not hold, a path in another directory or an absent `select` answers `0..first` as before.
|
|
92
|
+
The lookup reads only the listing's name arena, matching names exactly the way `locate` does,
|
|
93
|
+
and stats nothing beyond the window it answers. The client asks for the directory with the row
|
|
94
|
+
it will put the cursor on, so a row sorted far down lands in the listing's first rows reply.
|
|
95
|
+
|
|
96
|
+
### locate
|
|
97
|
+
|
|
98
|
+
`{"c":"locate","path":"/directory/selected.txt"}`
|
|
99
|
+
|
|
100
|
+
Returns `{"t":"located","directory":"/directory","path":"/directory/selected.txt","index":42}`
|
|
101
|
+
for a name already in the current listing, or `index:-1` when absent or invalid. This reads only
|
|
102
|
+
the listing's name arena; it does not stat files, resolve symlinks or scan another directory.
|
|
103
|
+
The client must match both `directory` and the requested `path` before using the index, then
|
|
104
|
+
fetch only the viewport containing that row. This is location in the current listing, not proof
|
|
105
|
+
that the filesystem object still exists or has the same identity for a later operation.
|
|
106
|
+
|
|
107
|
+
`{"c":"locate","paths":["/directory/selected.txt"],"id":7,"menuId":4}` resolves a batch
|
|
108
|
+
in one scan of the name arena. The reply carries `directory`, the request `id`, `ok`, `error`,
|
|
109
|
+
and `matches:[{"path":"/directory/selected.txt","index":42}]`. Missing paths are omitted,
|
|
110
|
+
duplicates produce one match, and results follow listing order. No unrequested path is returned.
|
|
111
|
+
Without `menuId`, this has the same name-only semantics as the single-path form. With `menuId`,
|
|
112
|
+
each matched survivor must still have the device, inode and file type captured by that completed
|
|
113
|
+
permanent deletion. This explicitly requested survivor check stats only matched paths.
|
|
114
|
+
The backend retains one completed deletion's identities for this read-only purpose even after
|
|
115
|
+
`menuaction/close` has expired the mutable selection. An expired restoration identity returns
|
|
116
|
+
`ok:false` and no matches. Clients must also reject replies after navigation, selection changes,
|
|
117
|
+
or replacement of the pending request, and fetch metadata only for the visible window.
|
|
118
|
+
|
|
119
|
+
### menuaction identity and deletion
|
|
120
|
+
|
|
121
|
+
`{"c":"menuaction","op":"activate","id":4,"action":"duplicate"}` revalidates the
|
|
122
|
+
selection captured by `snapshot` before the GUI dispatches a selected-item menu action.
|
|
123
|
+
The response echoes `id`, `op`, and `action`, with `ok` and `error`. Captured selection `paths`
|
|
124
|
+
are returned for `validate`, and for `activate` when the action needs resolved paths for client-side
|
|
125
|
+
dispatch; other activations return an empty array. `taildrop:*` and `sharelink` return the captured
|
|
126
|
+
cursor path instead.
|
|
127
|
+
The client also rechecks its listing identity and current action eligibility before dispatch.
|
|
128
|
+
|
|
129
|
+
An optional `menuId` on `transfer`, `trash`, `duplicate`, `rename`, `archive`, or `convert`
|
|
130
|
+
binds that operation to the captured selection. An expired selection, replacement object,
|
|
131
|
+
or uncaptured requested source is refused. Transfer uses the captured sources directly;
|
|
132
|
+
Trash, Duplicate, Archive, and Convert require the entire requested source set to match.
|
|
133
|
+
Identity checks compare the device, inode and file type at the worker boundary; Trash checks again immediately before its existing GIO
|
|
134
|
+
batch handoff. These checks do not make subsequent external-helper filesystem operations
|
|
135
|
+
atomic. Omitting `menuId` preserves the existing keyboard and protocol entry paths.
|
|
136
|
+
|
|
137
|
+
`{"c":"menuaction","op":"snapshot","id":4,"rows":[0,2]}` captures the selected identities
|
|
138
|
+
from the active listing. A Columns neighbour menu instead sends explicit absolute `paths` and
|
|
139
|
+
`cursorPath`, without navigating or changing that listing. Both forms accept the active `listing`
|
|
140
|
+
stamp and return `count`, `modes` (complete mode bits), and `kinds`, an array of each captured item's own file
|
|
141
|
+
type bits in request order, including symlinks without following them. `prepareDelete` with that
|
|
142
|
+
`id` reviews the captured trees and returns a
|
|
143
|
+
fresh `token`, selected `count`, and total `bytes`. `checkDelete` with the `id` and `token` returns
|
|
144
|
+
`valid` without mutating files. A changed set requires `refreshDelete`: it reviews only the same
|
|
145
|
+
captured paths, drops missing paths, captures replacement identities, and issues a fresh token.
|
|
146
|
+
Unrelated paths cannot widen that confirmation.
|
|
147
|
+
`menuaction` operations `tagstates` (with a client `tagId`) and `settags` (with `change` and `tag`)
|
|
148
|
+
read and update the identities retained by `id`. They return the existing `tagstates` and `settagged`
|
|
149
|
+
replies. A changed or replaced source refuses before writing; successful writes retain tag undo.
|
|
150
|
+
The client shows this new confirmation with
|
|
151
|
+
Cancel initially focused; no prior destructive activation authorizes the replacement token.
|
|
152
|
+
|
|
153
|
+
`{"c":"menuaction","op":"delete","id":4,"token":12}` consumes that exact token and reserves
|
|
154
|
+
the ordinary mutation slot. A failed preflight returns `stale:true` and requires a fresh review;
|
|
155
|
+
it does not delete any item. A completed attempt returns `deleted`, `failed`, `cancelled`,
|
|
156
|
+
`error`, and `remaining` absolute paths whose original identities survive. Results report partial
|
|
157
|
+
completion rather than implying rollback. The caller can use the identity-checked batched
|
|
158
|
+
`locate` form to select survivors after refreshing the listing.
|
|
159
|
+
`close` expires the token and cancels pending work. An already claimed deletion root completes
|
|
160
|
+
or restores its survivors before cancellation stops the next root. The undo journal does not
|
|
161
|
+
cover permanent deletion.
|
|
162
|
+
|
|
163
|
+
`{"c":"menuaction","op":"properties","id":4}` describes the identities captured by `snapshot`.
|
|
164
|
+
For one item the existing single-item reply is unchanged. For several items it returns `kinds`,
|
|
165
|
+
combined `bytes`, and `newest` and `oldest`, the selected entries' own modification dates in local
|
|
166
|
+
time as `YYYY-MM-DD HH:MM`. Kinds are counted in this order, omitting empty groups: folders,
|
|
167
|
+
symbolic links, texts, images, videos, audio files, files and special files. Regular files use the MIME
|
|
168
|
+
name database's text, image, video and audio groups, with all other regular files grouped as files.
|
|
169
|
+
Counts use singular words for one and comma grouping for thousands, for example `2 images, 1 video`.
|
|
170
|
+
No file contents are opened to classify them, and a symbolic link keeps its own kind, size and date.
|
|
171
|
+
|
|
172
|
+
A selection containing folders first answers its known file bytes with `done:false`, then walks
|
|
173
|
+
each selected folder on demand using the same walker as `contents`. A selection on a remote mount
|
|
174
|
+
or containing a remote mountpoint has one 15-second bound covering initial metadata, every walk
|
|
175
|
+
and final identity revalidation; cancellation frees the menu worker without waiting for a blocked
|
|
176
|
+
remote syscall. A deadline ends with the retained floor, `done:true,partial:true`. A worker lost
|
|
177
|
+
before a terminal reply returns `ok:false,error:"The selected items stopped answering."`.
|
|
178
|
+
Before initial metadata completes, `kinds`, `newest` and `oldest` are empty and `bytes` includes
|
|
179
|
+
only inspected non-folder entries. A silent syscall marks its current remote mount stuck; a
|
|
180
|
+
subsequent request involving that mount returns a zero partial floor before any stat. Each total
|
|
181
|
+
includes the selected folders' own entries, stays on each starting device and never follows links.
|
|
182
|
+
Each queued child opens without following links through a held directory descriptor and must
|
|
183
|
+
still match its captured identity. A replaced or unreadable descendant leaves a partial floor.
|
|
184
|
+
Progress and the final reply have the same shape:
|
|
185
|
+
`{"t":"menuaction","id":4,"op":"properties","ok":true,"kinds":"2 images, 1 video","bytes":52300000,"newest":"2026-09-23 10:14","oldest":"2026-08-21 12:02","partial":false,"done":true}`.
|
|
186
|
+
`done:true` means the request ended, and only `done:true,partial:false` means the total is exact.
|
|
187
|
+
Unreadable or vanished descendants and already-stuck mounts leave a floor with `partial:true`.
|
|
188
|
+
The overall remote deadline also leaves a partial floor. `close` or a newer `snapshot` cancels the request, whose last reply carries
|
|
189
|
+
`cancelled:true,partial:true,done:false`; a cancellation reply may omit the facts.
|
|
190
|
+
After a close, the caller discards all replies for that snapshot. A moved, vanished or replaced
|
|
191
|
+
selected identity returns `ok:false` with an `error`, including when it changes during the walk.
|
|
192
|
+
Dates and Kinds describe selected entries, not their descendants. Selected roots are summed
|
|
193
|
+
independently, so selecting both a folder and an entry inside it counts that entry in each root.
|
|
194
|
+
|
|
195
|
+
`{"c":"menuaction","op":"contents","id":4}` counts what the one captured folder holds, for Properties on a folder.
|
|
196
|
+
It walks with the folder size walker and never crosses into a nested mount or follows a symbolic link. A local folder
|
|
197
|
+
walks on the menu worker with no deadline. A folder on a remote mount, or holding one anywhere below it (the walk
|
|
198
|
+
still reads the mount point's own entry), walks on a worker beside the menu worker under the bulk deadline (15 s): past
|
|
199
|
+
it the reply is the floor counted so far, `partial:true` and `done:true`, the walk stops, and the remote mounts it
|
|
200
|
+
could touch are marked stuck for 30 s. While one is marked, the same request answers the empty floor at once
|
|
201
|
+
(`bytes:0`, `items:0`, `partial:true`, `done:true`) without a walk. `items` counts every entry the walk visits below the folder (files, folders, links and other entries), not the
|
|
202
|
+
folder itself, and `bytes` is the folder size walk's total, which includes the folder's own entry.
|
|
203
|
+
The walk runs under the request's own cancellation, so `close` or a newer `snapshot` stops it within one directory
|
|
204
|
+
read, and a remote one is answered at once. It answers a progress line every 150 ms while it walks, and none for a folder that
|
|
205
|
+
finishes sooner: `{"t":"menuaction","id":4,"op":"contents","ok":true,"bytes":2104332800,"items":812,"partial":false,"done":false}`.
|
|
206
|
+
The last line carries `done:true`. `partial` is true when the walk could not read a directory or one vanished, and the
|
|
207
|
+
numbers are then a floor. A cancelled walk answers once with `cancelled:true` and `"done":false`. An `id` that is not the
|
|
208
|
+
captured snapshot, a selection of several items, or an item that is not a directory (a symbolic link to one is a link)
|
|
209
|
+
answers `ok:false` with an `error`.
|
|
210
|
+
|
|
211
|
+
`{"c":"menuaction","op":"facts","id":4}` reads the codecs, bit rate and photo facts for Properties on a file, for the one captured
|
|
212
|
+
item. Properties sends it once, right after the `properties` reply, for a file that is not a folder or a link, so the card opens on
|
|
213
|
+
what the listing already knows and the new pairs arrive beside it. It answers one line on the menu worker, under the request's own
|
|
214
|
+
cancellation: `{"t":"menuaction","id":4,"op":"facts","ok":true,"vcodec":"h264","acodec":"aac","bps":8100000,"make":"Canon","model":"Canon EOS R6","lens":"RF35mm F1.8 MACRO IS STM","shutter":[1,250],"fnumber":[28,10],"iso":400,"taken":"2026:09:12 18:31:07"}`.
|
|
215
|
+
Every field is raw and a fact the file does not have is `""` or `0` (`[0,0]` for a rational), so the card shows no pair for it.
|
|
216
|
+
`vcodec`, `acodec` and `bps` come from the same `ffprobe` header read `meta` uses (`format=duration,bit_rate` and the streams'
|
|
217
|
+
`codec_name`; the first video and the first audio stream name the file), and only for a video container by extension. The photo
|
|
218
|
+
fields come from the file's own EXIF, read from the head and never decoded: a JPEG's first Exif APP1, a TIFF-based RAW (CR2, NEF,
|
|
219
|
+
ARW, DNG) from its IFD0 and Exif IFD in the first MiB, and a CR3 from the CMT1, CMT2 and CMT3 boxes of its Canon `uuid` box in the
|
|
220
|
+
`moov`. Text is cut to 120 characters with control characters turned to spaces. A cancelled request answers `ok:true` with
|
|
221
|
+
`cancelled:true` and no facts. An `id` that is not the captured snapshot, a selection of several items, or an item that is not a
|
|
222
|
+
regular file answers `ok:false` with an `error`; a fifo or a device is never opened.
|
|
223
|
+
|
|
224
|
+
### gitstatus
|
|
225
|
+
|
|
226
|
+
`{"c":"gitstatus","path":"/repo","enabled":true,"refresh":false,"id":1}`
|
|
227
|
+
|
|
228
|
+
Reads one repository-wide snapshot for Git decoration without replacing the ordinary listing,
|
|
229
|
+
its row numbering or its watch. `path` is the absolute directory being displayed. `enabled`
|
|
230
|
+
defaults to false and `refresh` to false; `id` defaults to 0 and is echoed on every reply.
|
|
231
|
+
The shared Boolean setting is `showGitStatus`, off by default. Its window control is
|
|
232
|
+
`Show Git status` in Settings, View, immediately before `Highlight today's dates`.
|
|
233
|
+
|
|
234
|
+
An enabled directory change discovers the worktree and runs one read-only Git status. The same
|
|
235
|
+
directory reuses a successful cached snapshot until `refresh:true`; failed loads retry on the next
|
|
236
|
+
enabled status request, with the existing mount quarantine still throttling remote discovery.
|
|
237
|
+
Outside a repository no Git process starts. Disabling clears the cache and cancels active or queued
|
|
238
|
+
older work without discovery or process launch. Navigation also cancels the old directory's work.
|
|
239
|
+
Replies may interleave other backend replies, so clients match both `path` and their current `id`
|
|
240
|
+
before displaying them.
|
|
241
|
+
|
|
242
|
+
The existing `changed` event drives refresh through the client's 400 ms absorbing timer: the
|
|
243
|
+
first notification starts it, later notifications do not restart it, and expiry sends this request
|
|
244
|
+
with `refresh:true`. Keep one status request in flight and preserve later watch debt until its
|
|
245
|
+
reply. The existing non-recursive watch covers the displayed directory, not all repository paths
|
|
246
|
+
or `.git` metadata; another directory change or explicit refresh discovers those other changes.
|
|
247
|
+
|
|
248
|
+
The argv is `/usr/bin/git -c core.fsmonitor=false -c core.hooksPath=/dev/null -c core.untrackedCache=false
|
|
249
|
+
-c status.renames=true status --porcelain=v2 -z --branch --untracked-files=all --ignore-submodules=none --`.
|
|
250
|
+
The sandbox changes directory to the repository root; there are no path operands or shell.
|
|
251
|
+
It binds the worktree and linked-worktree metadata read-only, isolates the network, clears the
|
|
252
|
+
environment, and sets `GIT_OPTIONAL_LOCKS=0`, `GIT_CONFIG_NOSYSTEM=1`,
|
|
253
|
+
`GIT_CONFIG_GLOBAL=/dev/null` and `GIT_TERMINAL_PROMPT=0`. Capture, parsing and sorting share one
|
|
254
|
+
five-second deadline and navigation cancellation check. Stdout is bounded at 32 MiB, stderr at
|
|
255
|
+
64 KiB, with the normal sandbox resource limits. A checked 64 MiB aggregate allocation budget
|
|
256
|
+
covers both full capture buffers and cumulative snapshot capacity requests, including paths,
|
|
257
|
+
rename sources, branch text, change records and the sorted folder index. Growth is charged before
|
|
258
|
+
fallible allocation, including replacement storage and 64 bytes of overhead per allocation;
|
|
259
|
+
released allocations remain charged conservatively. Folder marks use at most two path references
|
|
260
|
+
per change, with no ancestor copies. Over-budget input refuses the whole snapshot with
|
|
261
|
+
`Git status snapshot is too large.`; an allocator refusal reports
|
|
262
|
+
`Git status could not allocate its snapshot.` One ordinary staged deletion (`1 D.` with zero
|
|
263
|
+
index and worktree modes) plus one untracked entry (`?`) at the same path is legitimate: Git
|
|
264
|
+
emits this after untracking a retained file or symlink, recreating a staged-deleted file, or
|
|
265
|
+
untracking a retained gitlink (`? path/`). These records coalesce to one `?` change, counted once
|
|
266
|
+
in the branch caption and Changes, with `deleted:false`; the untracked entry determines the
|
|
267
|
+
folder flag and path spelling. A lone deletion remains `D` with `deleted:true`. A rename's
|
|
268
|
+
source metadata can also name an untracked entry; the destination and source remain two distinct
|
|
269
|
+
changes. Other repeated destinations, repeated records, or groups larger than two refuse the
|
|
270
|
+
whole snapshot with `Git status returned a duplicate path.` Coalescing allocates no additional
|
|
271
|
+
storage and retains all existing budget charges, deadline and cancellation checks. Process
|
|
272
|
+
failures retain stderr. Remote discovery uses the existing mount bound.
|
|
273
|
+
|
|
274
|
+
### gitrows
|
|
275
|
+
|
|
276
|
+
`{"c":"gitrows","path":"/repo","paths":["/repo/Code","/repo/notes.txt"],"id":2}`
|
|
277
|
+
|
|
278
|
+
Projects the last successful snapshot onto the visible rows only. `path` must match that
|
|
279
|
+
snapshot's directory, and every `paths` entry must be an absolute immediate child. At most
|
|
280
|
+
400 paths are accepted per request; a larger viewport can use several requests. Results follow
|
|
281
|
+
request order, including clean rows, and perform no per-row stat, MIME lookup or subprocess.
|
|
282
|
+
The window draws the marks in a 14 px column before Size; the TUI uses one cell before the size.
|
|
283
|
+
Both use `M`, `A`, `D` and `?`; the TUI represents a changed folder by yellow `■`.
|
|
284
|
+
|
|
285
|
+
### gitchanges
|
|
286
|
+
|
|
287
|
+
`{"c":"gitchanges","path":"/repo","start":0,"count":1,"id":3}`
|
|
288
|
+
|
|
289
|
+
Pages every canonical changed path relative to the worktree root, deleted and untracked paths
|
|
290
|
+
included. `start` defaults to 0 and clamps to `total`; `count` defaults to 400 and caps at 400.
|
|
291
|
+
The window opens this virtual listing by clicking the branch caption. The branch becomes the
|
|
292
|
+
foreground current crumb, the original folder path becomes its muted ancestor, and Back or Esc
|
|
293
|
+
returns to that folder. Folder prefixes within changed paths are muted before the leaf name;
|
|
294
|
+
the cursor row uses foreground. Deleted rows have no Size or Modified value. There is no added
|
|
295
|
+
`esc returns` hint. Changes view is window only in 0.4.0.
|
|
296
|
+
|
|
297
|
+
This does not replace the ordinary listing or its watch. Changes-view indices never name
|
|
298
|
+
ordinary backend file-operation rows; clients resolve an existing entry against the reply's `root`.
|
|
299
|
+
|
|
300
|
+
### listpaths
|
|
301
|
+
|
|
302
|
+
`{"c":"listpaths","paths":[<string>,...],"first":<uint>}`
|
|
303
|
+
|
|
304
|
+
Example: `{"c":"listpaths","paths":["/home/gm/Pictures/a.png","/home/gm/Downloads"],"first":80}`
|
|
305
|
+
|
|
306
|
+
Builds a fresh listing out of the paths the client names, replacing the current one, and answers a
|
|
307
|
+
`listed` line followed immediately by a `rows` line covering rows `0..first`, exactly as `list`
|
|
308
|
+
does. The listing's base is `/` and each entry is its absolute path with the leading slash removed,
|
|
309
|
+
so `window`, `thumb`, `paths` and every other per-row facility keep working with no special case
|
|
310
|
+
anywhere; that is the same shape a `search` listing takes, for the same reason. The client splits
|
|
311
|
+
the last `/` itself when it wants to draw a name rather than a path.
|
|
312
|
+
|
|
313
|
+
**Nothing is sorted.** The order the client sent is the order it gets back, because the callers
|
|
314
|
+
are the picker's Recent location and the main window's Recent place, and their order is the
|
|
315
|
+
history's own, newest first; a sort by name would throw that away. `read` on the `listed` line is the time this build took and `sort` is always
|
|
316
|
+
`0.0`.
|
|
317
|
+
|
|
318
|
+
**A path that does not exist is dropped, not listed.** The build `lstat`s each one, so a history
|
|
319
|
+
entry whose file has since been deleted never reaches the client, rather than arriving as a row
|
|
320
|
+
whose `p`, `s` and `m` are all 0. A path that is not absolute, and the root itself, are dropped the
|
|
321
|
+
same way: this list is read out of a file every application on the desktop writes, so it is checked
|
|
322
|
+
here rather than trusted. The type recorded in `d` is the link's own, the same rule `list` follows,
|
|
323
|
+
so a symlink to a directory is listed as a file and a symlink to nothing is still an entry.
|
|
324
|
+
|
|
325
|
+
There is no cancel and no streaming: the build is one `lstat` per path inside the read loop, and the
|
|
326
|
+
one caller sends a few hundred at most.
|
|
327
|
+
|
|
328
|
+
A re-read over the listing already held names added plus removed rows in `changed`, so a reload
|
|
329
|
+
over Recent says how many history entries came or went; a first `listpaths` carries no `changed`.
|
|
330
|
+
|
|
331
|
+
### window
|
|
332
|
+
|
|
333
|
+
`{"c":"window","start":<uint>,"count":<uint>}`
|
|
334
|
+
|
|
335
|
+
Example: `{"c":"window","start":1200,"count":350}`
|
|
336
|
+
|
|
337
|
+
Stats rows `start..start+count` of the current listing (phase 2) and answers one
|
|
338
|
+
`rows` line. `start` and `count` are clamped to the listing's length, so a stale or
|
|
339
|
+
out-of-range window never panics, it just returns fewer rows or none. Missing fields
|
|
340
|
+
default to `0`.
|
|
341
|
+
|
|
342
|
+
A count of `0` returns an empty `rows` array and `kinds` array with `ms` of `0.000`,
|
|
343
|
+
the clamped `start`, and the current `listing` stamp. It does not read mountinfo,
|
|
344
|
+
stat rows, load row tables, start workers, or rank the remainder of a session's
|
|
345
|
+
matches. A later nonempty window uses the normal complete row format below.
|
|
346
|
+
|
|
347
|
+
### filter
|
|
348
|
+
|
|
349
|
+
TUI traversal sends `"traverse":true` on the same name-only request and receives
|
|
350
|
+
`{"t":"found","id":7,"query":"^note","rows":[0,2],"error":"","listing":1}`.
|
|
351
|
+
It uses smart-case regular expressions and retains the full listing and selection.
|
|
352
|
+
An invalid pattern returns its diagnostic in `error`. The client accepts only its
|
|
353
|
+
latest id, query and listing generation. Initial traversal includes a matching
|
|
354
|
+
current row; subsequent next/previous steps exclude it and wrap. Its state is
|
|
355
|
+
separate from the live filter.
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
`{"c":"filter","query":"<string>","id":<uint>}`
|
|
359
|
+
|
|
360
|
+
Example: `{"c":"filter","query":".PNG","id":7}`
|
|
361
|
+
|
|
362
|
+
Matches a case-insensitive literal substring against every name in the current listing,
|
|
363
|
+
including rows outside the client's held window. Missing `query` defaults to the empty
|
|
364
|
+
string and missing `id` to `0`. Filtering reads names only, fetches no metadata and
|
|
365
|
+
does not change the listing, its sort order or its numbering. Each request answers once:
|
|
366
|
+
|
|
367
|
+
`{"t":"filtered","id":7,"query":".PNG","n":1300,"rows":[0,2,4],"ms":0.012,"listing":1}`
|
|
368
|
+
|
|
369
|
+
`n` is the full listing count. `rows` contains matching indices in ascending order in
|
|
370
|
+
the current sorted listing, with `[]` for no matches. An empty query answers `rows:null`,
|
|
371
|
+
so clearing restores the integer-count model without sending an identity array.
|
|
372
|
+
`ms` measures backend matching and serialization, not UI latency.
|
|
373
|
+
|
|
374
|
+
The client accepts a reply only while its `id` and `query` name the latest request, and
|
|
375
|
+
it drops its answer and sends a fresh request whenever a list, sort, search, search
|
|
376
|
+
refresh or final search ranking renumbers the listing, so an accepted reply answers in
|
|
377
|
+
the numbering of the rows on screen, the one its `listing` stamp names. View positions
|
|
378
|
+
map through `rows`; paths and file operations use those original listing indices and
|
|
379
|
+
the listing stamp. Filtering
|
|
380
|
+
does not emit a `rows` metadata reply or refill a window, even on clear. The client
|
|
381
|
+
requests only windows needed for the displayed matches and refills its viewport after clear.
|
|
382
|
+
|
|
383
|
+
### sort
|
|
384
|
+
|
|
385
|
+
`{"c":"sort","by":"<string>","desc":<bool>,"foldersFirst":<bool>,"groupByKind":<bool>,"hiddenLast":<bool>}`
|
|
386
|
+
|
|
387
|
+
Example: `{"c":"sort","by":"size","desc":true}`
|
|
388
|
+
The TUI retains creation-time, extension and random orders for the running session
|
|
389
|
+
and navigation, without writing them into the shared four-sort settings schema.
|
|
390
|
+
Re-sorts the current listing by `by` and answers a `listed` line. The orders are:
|
|
391
|
+
|
|
392
|
+
- `"btime"` requests optional Linux `statx(STATX_BTIME)` for each entry without
|
|
393
|
+
following symlinks. The returned mask must include `STATX_BTIME`. Unsupported,
|
|
394
|
+
inaccessible or missing timestamps stay unavailable, never substituted with ctime
|
|
395
|
+
or mtime. Like yazi's optional-time comparison, unavailable sorts before known
|
|
396
|
+
times ascending and after them descending; equal times use the name tie-break.
|
|
397
|
+
- `"extension"` reads only existing filename extension bytes, without MIME lookups
|
|
398
|
+
or a metadata pass. Directories retain name order.
|
|
399
|
+
- `"random"` materializes one random rank per entry before comparing and installs
|
|
400
|
+
the resulting permutation for that listing generation. Window reads do not
|
|
401
|
+
reshuffle it and require no per-entry metadata. Both name-only orders report
|
|
402
|
+
`read:0.0`. Existing anchors and listing-generation replacement still apply.
|
|
403
|
+
|
|
404
|
+
- `"name"` works on phase-1 data alone: `read` is `0.0` and `sort` is the sort.
|
|
405
|
+
- `"size"` and `"mtime"` pay the metadata pass first, one `lstat` per row of the whole
|
|
406
|
+
listing split across the cores, then sort. A size sort also walks every folder's subtree
|
|
407
|
+
under one shared 250 ms budget for the whole sort, and that walk is part of the pass,
|
|
408
|
+
so its time lands in `read` too. `read` is that pass in milliseconds and `sort`
|
|
409
|
+
is the sort, so the two costs stay readable apart on the wire.
|
|
410
|
+
- `"kind"` compares MIME type strings obtained from filenames, with
|
|
411
|
+
`application/octet-stream` for an unknown name and `inode/directory` for a folder.
|
|
412
|
+
Files with equal kind order by extension, none first with the Finder comparison, then by name; folders keep name order.
|
|
413
|
+
It does not read file contents or run a metadata pass; `read` is `0.0`.
|
|
414
|
+
|
|
415
|
+
`foldersFirst` defaults to `true`: folders remain before files in both directions.
|
|
416
|
+
With `foldersFirst:false`, the key orders folders and files together. `groupByKind`
|
|
417
|
+
defaults to `false`; when true it takes precedence over `foldersFirst` and fixes
|
|
418
|
+
three groups in this order: folders, images (MIME type starts with `image/`), then
|
|
419
|
+
all remaining files. Groups do not reverse with `desc`. Kind grouping uses filename
|
|
420
|
+
MIME lookup, not content probing. `hiddenLast` defaults to `false`; when true it
|
|
421
|
+
is an outer partition, a visible block then a hidden block, and `foldersFirst`
|
|
422
|
+
or `groupByKind` orders inside each block. Neither block reverses with `desc`.
|
|
423
|
+
|
|
424
|
+
Inside each group, or across the whole listing when ungrouped, the key decides and
|
|
425
|
+
the name order breaks ties, except files with equal kind order by extension first, none first, then by name, so two equal sizes list the same way every run, and
|
|
426
|
+
`desc` is the exact reverse of ascending inside the group,
|
|
427
|
+
tie-break included. A size order walks each folder's subtree and orders folders by that
|
|
428
|
+
recursive size, the same number the `dirsized` line reports, so the order agrees with the
|
|
429
|
+
size column the way it already did for files; an mtime order lists them by time like
|
|
430
|
+
everything else. A folder that hit the shared budget orders by the floor it counted.
|
|
431
|
+
A size sort seeds the answered-row cache only with the folders it finished walking, so a
|
|
432
|
+
later `dirsize` for one of them is answered from the cache rather than walked twice. A folder
|
|
433
|
+
the budget cut off seeds nothing, and is walked again by the worker at its own per-row
|
|
434
|
+
deadline when the client asks, because a floor must not stand in for a size.
|
|
435
|
+
|
|
436
|
+
On a network or phone listing (the classes the client refuses folder sizes on) a size sort walks no folder: each folder
|
|
437
|
+
orders by its own entry size, the floor a walk past its deadline would give, and seeds nothing.
|
|
438
|
+
|
|
439
|
+
**A size order is best effort on a directory large enough to exhaust the 250 ms budget.** A
|
|
440
|
+
folder cut off by it is placed on the floor that was counted, and that floor is used for the
|
|
441
|
+
order alone: it is never sent, so nothing on the wire says the order was built from one. The
|
|
442
|
+
size the column shows for that folder arrives later, from a separate walk at the two second
|
|
443
|
+
per-row deadline, and the two walks carry no ordering relation to each other, so that number
|
|
444
|
+
can be larger or smaller than the floor the row was placed on. Its `dirsized` line carries
|
|
445
|
+
`partial` true only when that second walk ran out as well. On such a directory the column can
|
|
446
|
+
therefore disagree with the order it was sorted into, and a re-sort can still place two
|
|
447
|
+
folders by their floors. A client holding both the order and the later sizes can see that
|
|
448
|
+
disagreement; what it cannot tell from the wire is whether a floor or a changed tree caused
|
|
449
|
+
it. The budget is what keeps the whole pass short while it runs on the event loop, and this is the cost of that
|
|
450
|
+
choice.
|
|
451
|
+
The stat
|
|
452
|
+
is the same `lstat` that `rows` reports `s` and `m` from, so the order always agrees with the
|
|
453
|
+
column, symlinks included, and a row that vanished between the listing and the pass sorts as
|
|
454
|
+
the zeroes `rows` would send for it.
|
|
455
|
+
|
|
456
|
+
**Sort metadata is not retained between requests.** Reversing a size order stats the directory again and walks the folders again, bounded by the same budget each time,
|
|
457
|
+
because a listing in name order must not carry metadata it is not using. The stats
|
|
458
|
+
live for one request. Historical measurements from 2026-09-02, using the default
|
|
459
|
+
name ordering and directory grouping: on the 100,000 file fixture the backend's PSS
|
|
460
|
+
read 4781 kB after the listing and
|
|
461
|
+
4945 kB after a size sort (one `smaps_rollup` reading each), with a transient peak 3.2 MB
|
|
462
|
+
above that while the pass ran. Measured on that fixture, warm, on 2026-09-02, from the
|
|
463
|
+
`read` and `sort` fields of the `listed` line: the pass took 25 to 38 ms on twelve cores and
|
|
464
|
+
125 to 135 ms pinned to one with `taskset -c 0`, and the sort behind it 6 to 7 ms against
|
|
465
|
+
5 ms for name; on a three-row directory the whole request stayed under half a millisecond.
|
|
466
|
+
Cold is IO-bound, and an earlier measurement in the maintainer's notes, not in this repository,
|
|
467
|
+
put the same pass at about 1 s serial and 0.3 s on twelve threads. The pass runs inside the
|
|
468
|
+
loop, so a size or date sort of a very large directory on a slow mount stalls other backend
|
|
469
|
+
requests for its duration. An initial `list` explicitly requesting one of those orders pays
|
|
470
|
+
that same metadata pass.
|
|
471
|
+
|
|
472
|
+
**An unsupported, empty, missing, or non-string `by` answers an `error` line**,
|
|
473
|
+
never a `listed` line in name order. The error names the supplied string key, or an
|
|
474
|
+
empty key when no string was supplied. This prevents a refused request from silently
|
|
475
|
+
resetting the existing order. A refused `sort` leaves the listing in its existing
|
|
476
|
+
order. `sort` never emits a `rows` line on its own;
|
|
477
|
+
follow it with `window` to see the reordered rows. A missing `desc` defaults to `false`.
|
|
478
|
+
|
|
479
|
+
An optional `anchor` names the cursor's logical row for this re-sort as an absolute path,
|
|
480
|
+
`{"c":"sort","by":"size","anchor":"/home/gm/amber"}`, and the `listed` line answers it with
|
|
481
|
+
`"anchor"` echoing that path and `"anchorIndex"` carrying its index in the new order, or `-1`
|
|
482
|
+
when that path is not in the listing. Each reply answers only its own request's anchor, so a
|
|
483
|
+
burst of re-sorts needs no shared generation count: the client moves the cursor only when the
|
|
484
|
+
reply's anchor is still the newest one it sent, and ignores a superseded or foreign one. A
|
|
485
|
+
request without `anchor` answers exactly the `listed` line it always has, so every older client
|
|
486
|
+
keeps parsing what it knows.
|
|
487
|
+
|
|
488
|
+
`mtime` names the same stat field that `rows` carries as `m`; the GUI labels it
|
|
489
|
+
"Modified". `date` is also accepted as an alias for `mtime`, matching the saved
|
|
490
|
+
Settings value. `mode` is not a supported sort key.
|
|
491
|
+
|
|
492
|
+
### search
|
|
493
|
+
|
|
494
|
+
`{"c":"search","path":"<string>","query":"<string>","hidden":<bool>}`
|
|
495
|
+
|
|
496
|
+
Example: `{"c":"search","path":"/home/gm","query":"dwnhelp","hidden":false}`
|
|
497
|
+
|
|
498
|
+
Walks the whole subtree under `path` and streams every entry whose path relative to `path`
|
|
499
|
+
contains `query` as a case-insensitive subsequence, into a fresh listing that replaces the
|
|
500
|
+
current one. The match is fuzzy rather than a substring, and it runs over the whole relative
|
|
501
|
+
path rather than the base name, so `dwnhelp` finds `downloads/helper.txt`. `hidden` follows
|
|
502
|
+
`list`'s rule exactly: `false`, or missing, drops dot-prefixed names before they are
|
|
503
|
+
counted or descended, so a `.git` costs one `readdir` entry and nothing more.
|
|
504
|
+
|
|
505
|
+
`path` is whatever the client asks for, and the backend walks exactly that and nothing else:
|
|
506
|
+
a client searching a whole home directory sends home as the `path`, and one searching a mount
|
|
507
|
+
sends the mount. The backend has no notion of home and no scope of its own.
|
|
508
|
+
|
|
509
|
+
**Results are ranked, and the rank is what makes a subsequence match usable.** A subsequence
|
|
510
|
+
over a hundred thousand entries matches far too much to read, so every match carries a score
|
|
511
|
+
and the walk answers in score order. Characters matching consecutively score highest; a
|
|
512
|
+
character starting the candidate, a path segment, a word or a camelCase hump scores next; a
|
|
513
|
+
character in the file's own name beats one in a parent directory the query merely passed
|
|
514
|
+
through; and every candidate character skipped between two matches is charged back. Ties go
|
|
515
|
+
to the shorter path, then to the listing's own name order, so one walk over one tree always
|
|
516
|
+
answers in exactly one order.
|
|
517
|
+
|
|
518
|
+
**A search result is a listing like any other, and that is what makes it cheap.** Each match
|
|
519
|
+
is pushed as its path relative to `path`, and `path` becomes the listing's base, so
|
|
520
|
+
`window`, `thumb`, `dirsize` and every other per-row facility keep working with no special
|
|
521
|
+
case anywhere. The client splits the last `/` itself to draw the name and its location
|
|
522
|
+
column; the backend never sends a second shape.
|
|
523
|
+
|
|
524
|
+
Matches are appended in discovery order and never re-sorted mid-walk, so a `window` the
|
|
525
|
+
client already holds stays valid as the count grows. The order changes exactly once, at the
|
|
526
|
+
end: `searched` is written after the rows have been ranked, so a client re-reads its window
|
|
527
|
+
when it sees that line and every row index it held before then is stale. That is the same
|
|
528
|
+
rule a `sort` follows, for the same reason. The walk is answered in three parts:
|
|
529
|
+
|
|
530
|
+
1. One `listed` with `n` of 0, immediately, because the client's old rows are gone the
|
|
531
|
+
moment the request is read.
|
|
532
|
+
2. A `searching` line carrying the growing count and the entries scanned so far, at most one
|
|
533
|
+
every 100 ms while the walk runs.
|
|
534
|
+
3. One terminal `searched` line, written after the ranking.
|
|
535
|
+
|
|
536
|
+
There is no trailing `listed` or `searching`: `searched` carries the final count itself, so a walk that a
|
|
537
|
+
new listing replaced never announces a total it no longer has.
|
|
538
|
+
|
|
539
|
+
The walk runs in bounded slices inside the same single-threaded loop everything else uses,
|
|
540
|
+
four directories per slice, so a `searchcancel` or any other request is never queued behind
|
|
541
|
+
a subtree. A symlink reports its own type, so a link to a directory is listed but never
|
|
542
|
+
descended and no loop is possible. An unreadable directory is skipped in silence, the same
|
|
543
|
+
way `scan` skips an unreadable entry. A `path` that cannot be read at all is not an error:
|
|
544
|
+
the walk finishes at once with `n` and `scanned` both 0.
|
|
545
|
+
|
|
546
|
+
`list` and `sort` both end a running walk before they touch the listing, and answer their
|
|
547
|
+
own lines after the walk's terminal `searched`.
|
|
548
|
+
|
|
549
|
+
**A query may carry `tag:<name>` terms beside the fuzzy text**, `tag:Red holiday` matching files
|
|
550
|
+
that carry the Red tag and fuzzy-match `holiday`. The name matches case-insensitively, and any
|
|
551
|
+
Flea or foreign name is accepted. Every named tag must be present. The attribute is read only on
|
|
552
|
+
files the walk visits, one `getxattr` each, never swept ahead of it.
|
|
553
|
+
|
|
554
|
+
### searchcancel
|
|
555
|
+
|
|
556
|
+
`{"c":"searchcancel"}`
|
|
557
|
+
|
|
558
|
+
Stops the running walk and answers one `searched` line with `cancelled` true. Whatever the
|
|
559
|
+
walk already found stays in the listing, is ranked the same way a walk that ran to the end is,
|
|
560
|
+
and stays windowable. Unlike `thumbcancel` there is
|
|
561
|
+
no `rows` form: one walk runs at a time, so a cancel can only mean that one. A
|
|
562
|
+
`searchcancel` with no walk running does nothing and answers nothing.
|
|
563
|
+
|
|
564
|
+
With a live session open the walk stops and the session stands: the standing query is
|
|
565
|
+
re-answered once from the partial index, `listed`, `rows`, then `searched` with the query's
|
|
566
|
+
own gen, `walking` false and `cancelled` true, and a later query is still answered from the
|
|
567
|
+
kept index. With no standing query, or an empty one, only that `searched` line answers, for
|
|
568
|
+
the last answered gen. The session never closes on a cancel; `searchclose` is what drops it.
|
|
569
|
+
|
|
570
|
+
### searchopen
|
|
571
|
+
|
|
572
|
+
`{"c":"searchopen","path":"<string>","hidden":<bool>,"folder":"<string>"}`
|
|
573
|
+
|
|
574
|
+
Opens a live session for the scope `path`. `folder` is the pane's folder the
|
|
575
|
+
session was opened from; scope and folder differ when the scope is home.
|
|
576
|
+
The walk starts at once on a background thread and the current listing is
|
|
577
|
+
untouched: no `listed`, no `rows`, no row invalidation. Answers one line:
|
|
578
|
+
|
|
579
|
+
`{"t":"searchopened","path":"<scope>"}`
|
|
580
|
+
|
|
581
|
+
`hidden` follows `list`'s rule exactly. The scope walks once per session with
|
|
582
|
+
one mountinfo read. Every directory is classified against that snapshot by its
|
|
583
|
+
deepest enclosing mount, including local mounts below remote mounts. Local
|
|
584
|
+
directories use the bounded local walk threads. Network and FUSE directories,
|
|
585
|
+
including ones below a local scope, share one serial remote lane with the existing
|
|
586
|
+
5 s call deadline and 30 s stuck-mount TTL. A mount refused during a walk is not
|
|
587
|
+
retried by that walk's remaining directories. A remote-root scope walks serially.
|
|
588
|
+
Only one name reader may remain active per remote mount across sessions and
|
|
589
|
+
patches. A reader still blocked after cancel or timeout keeps that slot until
|
|
590
|
+
its syscall returns; another request reports partial coverage instead of starting
|
|
591
|
+
another reader, even if the stuck TTL has elapsed.
|
|
592
|
+
One session runs at a time; opening a second closes the first in silence.
|
|
593
|
+
A `list`, `listpaths`, `sort`,
|
|
594
|
+
`search` or `searchclose` closes the session too, and drops
|
|
595
|
+
its index. Closing mid-walk cancels it: no directory opens after close.
|
|
596
|
+
|
|
597
|
+
### searchquery
|
|
598
|
+
|
|
599
|
+
`{"c":"searchquery","query":"<string>","gen":<uint>,"count":<uint>[,"sizes":<uint>]}`
|
|
600
|
+
|
|
601
|
+
Asks the open session. With no session open it answers nothing. The query is
|
|
602
|
+
answered from the index with no walk. A `tag:<name>` term keeps the entries that carry that tag, the
|
|
603
|
+
same case-insensitive rule `search` uses; the names are read once per row on a worker
|
|
604
|
+
thread after the walk lands, and until they are in the answer says `walking` true and
|
|
605
|
+
the landing re-runs the query. `gen` is the client's number, newer per
|
|
606
|
+
keystroke; a query whose `gen` is not newer than the last answered one is
|
|
607
|
+
dropped in silence, and a burst answers only its newest query. `count` is the
|
|
608
|
+
first window's size, the same meaning as `window`'s count; missing is the
|
|
609
|
+
list's own first-window default. `sizes` is how many leading rows the client
|
|
610
|
+
draws a folder size for, and 0 asks for none; a query without it keeps the
|
|
611
|
+
last one the session was given, so a burst answered from its newest query
|
|
612
|
+
keeps it too. Each answered query is three lines, in this order, the order
|
|
613
|
+
the listing swap already follows for `list`:
|
|
614
|
+
|
|
615
|
+
1. One `listed` line with `n` the match count and `path` the scope, carrying the query
|
|
616
|
+
gen beside them, so a line landing after the session closed names no folder.
|
|
617
|
+
2. One `rows` line for the first window from row 0, ranked, stamped with the
|
|
618
|
+
new generation and carrying the query gen beside it, so held row indices
|
|
619
|
+
invalidate exactly as after a `sort` and a late line lands on no folder.
|
|
620
|
+
With `sizes` above 0 it may end with `"z":[[<row>,<bytes>],...]` after
|
|
621
|
+
`gen`, in row order, the same bytes a whole `dirsized` answer carries for
|
|
622
|
+
each folder row among the first `sizes` rows whose complete size is known.
|
|
623
|
+
3. One terminal line naming the generation:
|
|
624
|
+
|
|
625
|
+
`{"t":"searched","n":<matches>,"scanned":<entries>,"ms":<ms>,"cancelled":false,"gen":<gen>,"walking":<bool>,"partial":<bool>,"partial_reason":<string>}`
|
|
626
|
+
|
|
627
|
+
`scanned` is the index size the query scored over. `walking` is true while
|
|
628
|
+
the walk is still running: the answer comes from the partial index, and the
|
|
629
|
+
standing query is refreshed from completed directory blocks at most once per
|
|
630
|
+
100 ms after its initial hold, and once when the walk lands, with its own gen,
|
|
631
|
+
only if no newer query has replaced it; the landing answer has `walking`
|
|
632
|
+
false and `ms` the walk's time. A newer query's answer replaces the window
|
|
633
|
+
whole: no blank listing stands between two queries. Scoring runs on one retained
|
|
634
|
+
session execution worker; the loop alone publishes answers and discards an
|
|
635
|
+
in-flight answer superseded by a newer query or navigation. Ordinary requests
|
|
636
|
+
retain their position relative to collapsed queries. Collapse reports its highest
|
|
637
|
+
drained generation to the execution gate before a buffered readiness event can
|
|
638
|
+
publish an older answer. The published pageable
|
|
639
|
+
remainder survives a discarded answer only while its index row IDs remain valid;
|
|
640
|
+
a pending patch or a recursive watch delta that renumbers the index discards
|
|
641
|
+
that remainder as usual. The newest query rebuilds it. A watch delta that lands
|
|
642
|
+
while an answer is out waits behind it, then is reconciled and re-answered on the
|
|
643
|
+
same worker. Closing releases the execution worker.
|
|
644
|
+
|
|
645
|
+
Requested local rich rows can reuse metadata, MIME and tag results across query
|
|
646
|
+
generations, keyed by unchanged path identity rather than row index. The cache
|
|
647
|
+
clears when a folder answer claims a new generation, when an own write under the
|
|
648
|
+
scope patches the index, and when a change line for the open folder is said; a
|
|
649
|
+
typed query clears it when its requested rows change the watched directories or
|
|
650
|
+
are not covered by the inode watches, and keeps it otherwise. Reuse holds
|
|
651
|
+
at most 350 currently requested rows, with bounded payload and independent
|
|
652
|
+
directory and file-inode watches armed before reading misses. Inode changes,
|
|
653
|
+
including writes through hard links in other directories, invalidate metadata
|
|
654
|
+
and serialized rows together. Ancestor moves and path replacements also
|
|
655
|
+
invalidate reuse. Each watch set holds at most 350 paths; changing the inode set
|
|
656
|
+
briefly retains one old set until its replacement is armed and checked, so
|
|
657
|
+
intersecting identities stay covered. Overflow or failed watch coverage forces
|
|
658
|
+
fresh reads, as do remote paths, symlinks and names requiring lossy display
|
|
659
|
+
conversion. Reuse changes no row fields or response-local kind indices.
|
|
660
|
+
|
|
661
|
+
A query asked while the walk still runs waits for the walk to land, up to
|
|
662
|
+
150 ms after the first query held, the search hold (`ui/js/Swap.js`
|
|
663
|
+
`HOLD_MS`, not the listing swap's cap `Swap.CAP_MS`, 66 ms), so it is answered once from the whole index; a newer query asked
|
|
664
|
+
meanwhile replaces it and answers in its place. Past `HOLD_MS` the held query
|
|
665
|
+
is answered from the partial index with `walking` true, and every later query
|
|
666
|
+
of that walk is answered at once. A `searchcancel` answers a held query as
|
|
667
|
+
its stop.
|
|
668
|
+
|
|
669
|
+
An empty query keeps the session and its index and answers the open folder's
|
|
670
|
+
own listing, the folder the session was opened from, in its own sort, with
|
|
671
|
+
`listed.path` that folder, in the same listed, rows, searched order with that
|
|
672
|
+
gen. No walk of the scope runs; one readdir of the folder is all it costs. Its
|
|
673
|
+
rows line carries `z` for the folder's own folder rows the same way.
|
|
674
|
+
An unmount of the search scope also completes an empty standing query with one
|
|
675
|
+
`searched` status at that query's generation, `walking:false`, `partial:true`
|
|
676
|
+
and reason `watch_failed`. It preserves the folder's rows, count and listing
|
|
677
|
+
number; it emits no replacement `listed` or `rows` lines.
|
|
678
|
+
|
|
679
|
+
A size is known when the session learned it earlier, from a `dirsize` answer
|
|
680
|
+
given while it is open or from its own walk for an earlier answer, keyed by
|
|
681
|
+
path; or when the walk this answer runs finishes it, all of that answer's
|
|
682
|
+
folders sharing one 3 ms budget. A folder left out of `z` is asked with
|
|
683
|
+
`dirsize` exactly as in any listing. A remote scope never walks for `z`, and a
|
|
684
|
+
scope where a walk that started misses the budget walks for no later answer in
|
|
685
|
+
that session; both still carry the sizes the session knows. A walk the
|
|
686
|
+
scheduler had not started by the deadline marks nothing. Every patch of the
|
|
687
|
+
index, from a write or the open folder's watch, forgets the known sizes.
|
|
688
|
+
|
|
689
|
+
Local sessions own a recursive inotify instance, separate from the pane and column
|
|
690
|
+
watches. Each local directory is armed before enumeration, including directories
|
|
691
|
+
on local USB filesystems. Nested network and FUSE directories are never armed,
|
|
692
|
+
including during own-write patches and subtree replacement. Those directories
|
|
693
|
+
use bounded enumeration and own-write/manual reconciliation, as remote roots do.
|
|
694
|
+
Queued changes are replayed before the completed walk is announced.
|
|
695
|
+
External root and deep create, rename,
|
|
696
|
+
delete, replacement and subtree moves reconcile affected directory blocks without
|
|
697
|
+
another query. The standing nonempty query is then pushed again as `listed`, `rows`,
|
|
698
|
+
`searched`, with the same query `gen` and a new listing generation. Clients replace
|
|
699
|
+
the old rows even when the query generation is unchanged, and discard row indices
|
|
700
|
+
from the old listing generation. Applying an index delta always precedes this push.
|
|
701
|
+
The first idle event has no timer delay; subsequent events already queued share a
|
|
702
|
+
reconciliation. There is no per-query mtime sweep or whole-index repack per event.
|
|
703
|
+
Retained name storage is reclaimed only after retired bytes exceed both 1 MiB and
|
|
704
|
+
one percent of logical storage. Dense row swaps and reclamation invalidate internal
|
|
705
|
+
query caches and any pageable remainder before another answer.
|
|
706
|
+
|
|
707
|
+
The backend's own writes and the open folder's existing watch retain their patch
|
|
708
|
+
paths. A remote-root session uses bounded enumeration and own-write/manual
|
|
709
|
+
reconciliation; it does not claim recursive notification. Closing or cancelling a
|
|
710
|
+
running walk stops and joins the recursive pump and closes its descriptors. A new
|
|
711
|
+
scope rejects queued events carrying the old session's token.
|
|
712
|
+
An unmount event from any recursively watched directory stops the whole scope's
|
|
713
|
+
walk, releases its recursive watches, and retains any usable snapshot with
|
|
714
|
+
`walking:false`, `partial:true` and reason `watch_failed` unless an earlier failure
|
|
715
|
+
already supplies a reason. The scope remains terminally unmounted until reopen:
|
|
716
|
+
queued index patches are discarded, and later own-write or open-directory
|
|
717
|
+
notifications cannot reconcile its retained identities. Explicit user cancellation
|
|
718
|
+
keeps its existing patch behavior. Pending tag reads and retries stop too, with
|
|
719
|
+
the stop flag checked before each new path or chunk and after an in-flight read
|
|
720
|
+
returns, including remote workers and tag rereads. It does not
|
|
721
|
+
scan the exposed mountpoint or automatically resume the scope. Reopen to establish a new
|
|
722
|
+
mount snapshot. A remote read abandoned by timeout or cancellation stops before
|
|
723
|
+
its next entry when a blocked syscall returns; the syscall itself is not killed.
|
|
724
|
+
|
|
725
|
+
The index has no fixed entry-count cap. Raw basenames are interned and paths are
|
|
726
|
+
reconstructed from parent references. Checked ID or arena bounds and allocation
|
|
727
|
+
refusals stop adding entries and preserve identities already held.
|
|
728
|
+
|
|
729
|
+
Every session `searched` line carries `partial` and `partial_reason`. Once
|
|
730
|
+
`walking:false`, `partial:false` and an empty reason certify enumeration without a
|
|
731
|
+
refused directory and no reported local recursive-watch failure. This certifies
|
|
732
|
+
a snapshot, with no automatic remote freshness guarantee.
|
|
733
|
+
While enumeration or tag loading continues, `walking:true` carries `partial:true`
|
|
734
|
+
with reason `walking`, unless an earlier failure supplies a reason. Incomplete
|
|
735
|
+
coverage carries `unreadable`, `capacity` or `allocation`; cancellation carries
|
|
736
|
+
`cancelled:true`, `walking:false`, `partial:true` and reason `cancelled`. Watch
|
|
737
|
+
registration, descriptor or unmount failure carries `watch_failed`. A queue
|
|
738
|
+
overflow reconciles all registered directory blocks and carries `watch_overflow`.
|
|
739
|
+
Both watch failures remain partial until reopen, even if reconciliation recovers
|
|
740
|
+
usable results, so no failed scope claims complete live coverage. For a nonempty
|
|
741
|
+
query, `scanned` counts retained identities even when no entry matches; an empty
|
|
742
|
+
query retains its folder-row count. A patch read refusal also makes coverage partial.
|
|
743
|
+
A partial result remains usable but never certifies a complete live scope inventory.
|
|
744
|
+
|
|
745
|
+
The session has its own lexical scorer; one-shot `search` and folder `jump`
|
|
746
|
+
keep their existing scorer. The query and candidate characters fold without
|
|
747
|
+
Unicode normalization. Exact basenames rank before clean basename prefixes,
|
|
748
|
+
word prefixes, aligned paths, ordinary subsequences and typo-only matches.
|
|
749
|
+
Subsequences use the best run and boundary alignment across the whole relative
|
|
750
|
+
path, including separators, with no first-occurrence limit. ASCII letter
|
|
751
|
+
presence masks reject impossible names before scoring, and an interned basename
|
|
752
|
+
is scored once per query. Unknown and non-ASCII classes bypass this rejection.
|
|
753
|
+
|
|
754
|
+
Typo-only matching starts at a word boundary and permits letter replacement,
|
|
755
|
+
insertion, deletion and adjacent swap, including the first letter. A dropped
|
|
756
|
+
query letter counts only where the matched word ends, so `flea` never matches
|
|
757
|
+
the prefix `fla` of `flac`. Typo-only matches are listed only when no basename
|
|
758
|
+
holds the query exactly, as its prefix or at a word start; with a tag term,
|
|
759
|
+
only the basenames that carry its tags count. The budget
|
|
760
|
+
is zero below four alphanumeric query characters, one from four through seven,
|
|
761
|
+
and two from eight onward. Digits and punctuation are never edited. A query
|
|
762
|
+
beginning with punctuation, such as `.svg`, permits no edits. Explicit path
|
|
763
|
+
queries may correct letters in parent components as well as the basename.
|
|
764
|
+
Fresh lexical ties use shorter relative paths and natural raw identity order.
|
|
765
|
+
The final history and folder-distance order belongs to the read-only ranking
|
|
766
|
+
prior layer; it adds no control or result mark.
|
|
767
|
+
|
|
768
|
+
Only extensions with zero edit budgets on both sides may reuse the previous
|
|
769
|
+
matches, and only with identical tags, complete prior coverage and an unchanged
|
|
770
|
+
index. Backspace, scope or index changes, unfinished tag reads and edit-eligible
|
|
771
|
+
queries rescore candidates. The first window is selected and ordered without a full sort; the rest
|
|
772
|
+
stays scored but unranked until a row past the window is asked for, when the
|
|
773
|
+
next chunk is ordered with the same comparator and appended behind the head,
|
|
774
|
+
the request's need or one window, whichever is larger, at least doubling the
|
|
775
|
+
adopted length, so no window ever shows a row from a different order than the
|
|
776
|
+
one the `listed` line counted. A path lookup and a sort take the whole rest in
|
|
777
|
+
one go, as does any request whose need spans it, select-all among them.
|
|
778
|
+
|
|
779
|
+
### searchclose
|
|
780
|
+
|
|
781
|
+
`{"c":"searchclose"}`
|
|
782
|
+
|
|
783
|
+
Drops the index and answers one line:
|
|
784
|
+
|
|
785
|
+
`{"t":"searchclosed","path":"<scope>"}`
|
|
786
|
+
|
|
787
|
+
With no session open it answers nothing.
|
|
788
|
+
|
|
789
|
+
### thumb
|
|
790
|
+
|
|
791
|
+
`{"c":"thumb","rows":[<uint>,...]}`
|
|
792
|
+
|
|
793
|
+
Example: `{"c":"thumb","rows":[2,17,140]}`
|
|
794
|
+
|
|
795
|
+
Asks for a thumbnail for those row indices of the current listing, and answers one
|
|
796
|
+
`thumbed` line per row. **This is the only thing that ever generates a thumbnail.** The
|
|
797
|
+
backend never walks a directory looking for work: a row that no client named is never
|
|
798
|
+
looked at, at any priority.
|
|
799
|
+
|
|
800
|
+
An off storage class still asks, and the backend answers from the cache alone. A client
|
|
801
|
+
on a network, phone or USB directory whose switch is off sends
|
|
802
|
+
`{"c":"thumb","rows":[<uint>,...>],"cacheOnly":true}`, and a genuine cache miss then
|
|
803
|
+
answers an empty `file` at once rather than queueing a decode: one stat of the source
|
|
804
|
+
per visible row and no read, so a NAS folder costs a metadata round trip instead of
|
|
805
|
+
about 48 MB. A row already in the shared cache answers with that entry's path, whether
|
|
806
|
+
or not the source can still be opened, and thumbnails another application already made
|
|
807
|
+
still show. Absent `cacheOnly` is today's full path, so a local folder and an older
|
|
808
|
+
client are unchanged.
|
|
809
|
+
|
|
810
|
+
Each index is clamped to the listing: a row past the end is skipped in silence, because
|
|
811
|
+
there is no row to answer for. A missing or malformed `rows` is an empty array and the
|
|
812
|
+
request does nothing. Indices may repeat and need not be sorted; the newest request for a
|
|
813
|
+
row replaces the older one.
|
|
814
|
+
|
|
815
|
+
Four of the answers cost no work at all and come back on the same line the request
|
|
816
|
+
arrived on. A directory row, a row that is not a regular file, and a row whose MIME type no
|
|
817
|
+
validated thumbnailer declares all answer immediately with an empty `file`; that is the same
|
|
818
|
+
condition the `t` flag in a `rows` object reports, so a client that honours `t` never asks.
|
|
819
|
+
A row already in the shared cache at the file's current mtime answers immediately with that
|
|
820
|
+
entry's path, and a row recorded as failed at that mtime answers immediately with an empty
|
|
821
|
+
`file`. Only a genuine cache miss is queued, and its `thumbed` line arrives later.
|
|
822
|
+
|
|
823
|
+
The queue is bounded, and a request larger than it is still answered in full. When the queue
|
|
824
|
+
is full the oldest job in it is dropped to make room, and the row that job belonged to is
|
|
825
|
+
answered at once with an empty `file` rather than left waiting.
|
|
826
|
+
|
|
827
|
+
**What the shipped client sends.** `ui/Pane.qml` sends `thumb` only when the list settles,
|
|
828
|
+
which is a 120 ms timer restarted by every scroll and by every arriving window of rows, so a
|
|
829
|
+
fling issues nothing at all until it stops. One request names only rows currently visible,
|
|
830
|
+
only rows whose `rows` object carried `t:true`, and only rows the client's own map does not
|
|
831
|
+
already hold.
|
|
832
|
+
|
|
833
|
+
**That map has four states and only two of them are terminal.** Absent means unknown, `null`
|
|
834
|
+
means asked and waiting, a non-empty string is the answer's path, and an empty string is a
|
|
835
|
+
real answer meaning this row has no thumbnail. The two string states are terminal: every
|
|
836
|
+
answer is recorded, an empty `file` included, and an answered row is never asked about again
|
|
837
|
+
while this listing stands. The other two are not. Rows that left the viewport while still
|
|
838
|
+
waiting are named in a `thumbcancel` sent immediately before the same settle's `thumb`, and a
|
|
839
|
+
cancelled row drops back to absent rather than to a terminal state, because the backend
|
|
840
|
+
forgets the job it drops and would otherwise never answer it. **A row that scrolls out while
|
|
841
|
+
waiting and scrolls back is therefore asked for a second time, and that is correct.** A client
|
|
842
|
+
that treats "asked once" as terminal leaves a permanently empty slot on every such row.
|
|
843
|
+
`ui/js/Thumbs.js` is the shipped implementation, its `forget` is the drop back to absent, and
|
|
844
|
+
`tests/js/thumbs.js` holds it under the name "a cancelled row can be asked for again".
|
|
845
|
+
|
|
846
|
+
Neither request is ever sent with an empty `rows`, because that form cancels everything.
|
|
847
|
+
Opening a directory clears the client's whole map, since an index names a different file
|
|
848
|
+
afterwards. See AGENTS.md "Thumbnail requests in the GUI".
|
|
849
|
+
|
|
850
|
+
### thumbcancel
|
|
851
|
+
|
|
852
|
+
`{"c":"thumbcancel","rows":[<uint>,...]}`
|
|
853
|
+
|
|
854
|
+
Example: `{"c":"thumbcancel","rows":[17,140]}`
|
|
855
|
+
|
|
856
|
+
Drops queued jobs for those rows. An empty or missing `rows` cancels everything queued.
|
|
857
|
+
A job that a worker has already started cannot be cancelled and still answers with a
|
|
858
|
+
`thumbed` line; only work still in the queue is dropped. A cancelled row is not answered,
|
|
859
|
+
so a client that cancels must stop waiting for it. No response line.
|
|
860
|
+
|
|
861
|
+
A cancelled row can be asked for again straight away, in either form of the request: cancelling
|
|
862
|
+
forgets the row as well as its job, so a later `thumb` for it queues fresh work rather than
|
|
863
|
+
being deduplicated against the job that was just dropped.
|
|
864
|
+
|
|
865
|
+
### fsinfo
|
|
866
|
+
|
|
867
|
+
`{"c":"fsinfo"}`
|
|
868
|
+
|
|
869
|
+
Answers one `fsinfo` line for the directory the current listing came from:
|
|
870
|
+
`{"t":"fsinfo","fs":"<string>","free":<uint>,"path":"<string>","class":"<string>","tagable":<bool>}`.
|
|
871
|
+
`fs` is the filesystem's own name and `free` its bytes available to an unprivileged
|
|
872
|
+
process; `path` is the directory they are of, so a client that has since moved can
|
|
873
|
+
tell they describe somewhere else. `tagable` is that directory's once-per-change probe
|
|
874
|
+
for user extended attributes, so the Tags menu row is absent where the filesystem
|
|
875
|
+
cannot store them. A repeated `fsinfo` for the same directory reuses the answer and makes no
|
|
876
|
+
call. On a `network` or `phone` directory the probe runs on the statfs worker, never the loop:
|
|
877
|
+
the field reads `true` until it lands, and a refusal sends the line again with `false`. `class` is that directory's storage class,
|
|
878
|
+
`network`, `phone`, `usb` or `""` for local, computed once per directory change
|
|
879
|
+
beside this line and never per row: network is cifs, smb3, nfs, sshfs, rclone, 9p,
|
|
880
|
+
ceph and any gvfs FUSE share, phone is gvfs mtp, gphoto2 and afc, and usb is a block
|
|
881
|
+
device whose sysfs path runs through USB or whose removable flag is 1. An unreadable
|
|
882
|
+
path answers an empty `fs` with a free of 0 rather than a wrong number.
|
|
883
|
+
A local directory held by a system mountpoint adds `"system":true` after `class`, and no other line carries the key.
|
|
884
|
+
The mounts are matched by the directory's own device number in `/proc/self/mountinfo`, so a symlink,
|
|
885
|
+
a bind mount or a second mountpoint of one volume answers as the volume does, and a mountpoint counts as a
|
|
886
|
+
system one when it is `/`, `/home`, `/boot`, `/var` or anything else the rail's own list of system mounts names.
|
|
887
|
+
On a slow mount, a gvfs share or a kernel network filesystem, the statfs behind the
|
|
888
|
+
figures is a network round trip, so it runs on a worker thread, one per mount at a time, the
|
|
889
|
+
newest folder asked for going next: a share starts it once its `list` rows are sent, for a client that has asked `fsinfo`
|
|
890
|
+
before, and a kernel mount when `fsinfo` does. The statfs reads the listed folder itself,
|
|
891
|
+
because an sftp host or a phone's storages answer differently below one mount. On a gvfs share
|
|
892
|
+
it reads the folder's first plain file instead when the listing has one: gvfs answers a statfs
|
|
893
|
+
through `smbc_statvfs` on that exact path, and libsmbclient opens a folder for it by listing every
|
|
894
|
+
entry in it, where a file is one open and close. A file gone since the listing falls back to the
|
|
895
|
+
folder. `fsinfo`
|
|
896
|
+
answers at once with the class and that folder's last known figures, or unknown figures
|
|
897
|
+
(`"fs":""`, `"free":0`) on a first visit. When the worker returns, a second `fsinfo` line for
|
|
898
|
+
the same `path` carries the fresh figures, sent only when they differ from what that folder
|
|
899
|
+
last answered and only while it is still the listed directory. A client must accept that line
|
|
900
|
+
at any time: it updates the figures in place and changes nothing else.
|
|
901
|
+
Local and USB directories answer figures and class in the one line, exactly as before.
|
|
902
|
+
|
|
903
|
+
### dirsize
|
|
904
|
+
|
|
905
|
+
`{"c":"dirsize","rows":[<uint>,...]}`
|
|
906
|
+
|
|
907
|
+
Example: `{"c":"dirsize","rows":[4,9]}`
|
|
908
|
+
|
|
909
|
+
Asks for the recursive apparent size of those row indices, and answers one `dirsized` line
|
|
910
|
+
per row that names a directory. **This is the only thing that ever walks a directory looking
|
|
911
|
+
for size**, the same rule `thumb` follows for thumbnails: a row no client named is never
|
|
912
|
+
walked. A row that is not a directory, or past the end of the listing, is skipped in silence.
|
|
913
|
+
|
|
914
|
+
One persistent background worker walks one directory at a time. The event loop remains
|
|
915
|
+
available for navigation while a walk runs. A list, sort or `dirsizecancel` invalidates
|
|
916
|
+
unfinished size work using a generation counter. Cancelled or stale replies cannot populate
|
|
917
|
+
the current listing's cache. Cancellation and the 2000 ms deadline are checked between
|
|
918
|
+
filesystem calls; a blocked filesystem call may delay further sizes but never holds up the
|
|
919
|
+
navigation event loop. No replacement worker is spawned while the old one is still busy.
|
|
920
|
+
A row already answered for the current listing answers again from its cache; a row already
|
|
921
|
+
queued or running costs nothing extra.
|
|
922
|
+
|
|
923
|
+
**What the shipped client sends.** `ui/List.qml` sends `dirsize` only when the list settles, the
|
|
924
|
+
same 120 ms timer `thumb` already waits on, so a fling issues nothing at all. One request
|
|
925
|
+
names only the directory rows currently visible and not already known.
|
|
926
|
+
|
|
927
|
+
### dirsizecancel
|
|
928
|
+
|
|
929
|
+
`{"c":"dirsizecancel"}`
|
|
930
|
+
|
|
931
|
+
Drops every directory row still queued and cancels the running walk cooperatively. Unlike `thumbcancel`, there is no rows
|
|
932
|
+
form: the walker is one at a time, so a stale row left over from a scrolled-past viewport would
|
|
933
|
+
delay the row the new viewport actually wants, and the client always means "everything" when it
|
|
934
|
+
sends this. A row already answered is untouched; queued and unfinished work is discarded. No response line.
|
|
935
|
+
|
|
936
|
+
A directory a `dirsizecancel` dropped can be asked for again straight away: cancelling forgets
|
|
937
|
+
the row, so a later `dirsize` for it queues fresh work.
|
|
938
|
+
|
|
939
|
+
### transfer
|
|
940
|
+
|
|
941
|
+
`{"c":"transfer","op":"<string>","paths":["<string>",...],"dest":"<string>"}`
|
|
942
|
+
|
|
943
|
+
Example: `{"c":"transfer","op":"copy","paths":["/home/gm/a.txt","/home/gm/photos"],"dest":"/home/gm/backup"}`
|
|
944
|
+
|
|
945
|
+
Copies or moves each top-level path into `dest`. `op` of `"move"` moves; **anything else, including a
|
|
946
|
+
missing `op`, copies**, so a malformed request can never remove a source. `paths` are absolute; `dest`
|
|
947
|
+
is an absolute directory that must already exist, because Flea does not create a destination as a side
|
|
948
|
+
effect of a transfer. A `dest` that is missing, relative, not a directory, or cannot be written answers a single `error`
|
|
949
|
+
line with `where` of `transfer` and nothing is started.
|
|
950
|
+
|
|
951
|
+
Unlike `thumb` and `dirsize`, this names paths rather than row indices: a transfer outlives the listing
|
|
952
|
+
it was started from, and a row index would name a different file by the time it ran.
|
|
953
|
+
|
|
954
|
+
**A `rows` array of indices into the current listing may be sent instead of `paths`**, because a client
|
|
955
|
+
can only build a path for a row inside the window it holds, and a selection can be wider than that. The
|
|
956
|
+
indices are resolved against the listing at request time and the operation runs on the resulting paths,
|
|
957
|
+
so it still owns a snapshot that outlives whatever the listing does next. `paths` wins when both are
|
|
958
|
+
present, and an index past the end of the listing is dropped in silence.
|
|
959
|
+
|
|
960
|
+
**Copies, moves, archive compressions and extractions queue by destination drive.** Each request
|
|
961
|
+
gets its own id and start line, including a request that waits behind another write to the same drive.
|
|
962
|
+
Waiting jobs run in arrival order and name their predecessor and its target in the jobs `reason`.
|
|
963
|
+
Writes to different drives run together; another running job never causes a busy refusal for these
|
|
964
|
+
requests. An extraction writes to the filesystem holding its destination, just as compression does.
|
|
965
|
+
Ordinary `trash`, `duplicate`, `link` and menu deletion share one non-job operation slot.
|
|
966
|
+
Reviewed Trash deletion also claims that slot and refuses while its previous Trash journal is settling;
|
|
967
|
+
destination-queued transfers do not hold it. Ordinary non-job mutations, including `rename`, `mkdir`
|
|
968
|
+
and `permissionsBatch`, refuse while the slot or an unfinished job is active. `rename` and `mkdir`
|
|
969
|
+
do not claim the slot themselves, and `convert` remains independent.
|
|
970
|
+
|
|
971
|
+
The answer is a `transferstarted` line for a file transfer, then per top-level item a bounded stream of
|
|
972
|
+
`transferprogress` lines and exactly one `transferitem`, then one `transferdone`. An archive `extract`
|
|
973
|
+
uses `extractstarted` for its activity card and `archivedone` for its terminal line.
|
|
974
|
+
|
|
975
|
+
**Semantics that are decided here rather than left to the caller.** A same-filesystem move is a
|
|
976
|
+
`rename(2)`; a cross-filesystem move is a copy followed by removing the source, and the source is only
|
|
977
|
+
removed once the copy is complete, so a process killed mid-move leaves the source intact and a partial
|
|
978
|
+
file at the destination, never the reverse. A symlink is copied as a symlink and never followed. A
|
|
979
|
+
fifo or a socket is recreated at the destination with the source's own mode rather than opened,
|
|
980
|
+
because neither holds contents to stream and an open of one would wait for a writer or fail. A device
|
|
981
|
+
node takes the same path, but creating one needs `CAP_MKNOD`, so an unprivileged copy fails that item
|
|
982
|
+
with `EPERM` instead of recreating it; either way nothing streams from a device that never ends. A
|
|
983
|
+
destination that already exists is refused for that item rather than overwritten, because every write
|
|
984
|
+
here creates its target exclusively, unless the request carries a choice for it, below; even `replace`
|
|
985
|
+
never overwrites, it moves the item already there to the trash and then still creates exclusively.
|
|
986
|
+
Directory recursion is invisible on this wire: the backend walks a
|
|
987
|
+
tree to copy it and the client sees only the top-level item's lines, so the wire's shape does not depend
|
|
988
|
+
on how deep a folder is.
|
|
989
|
+
|
|
990
|
+
**An entry the destination cannot hold never ends the walk of the tree around it.** Inside a folder
|
|
991
|
+
being copied or moved, the backend skips and records an entry the drive refuses for that entry alone,
|
|
992
|
+
and goes on with every other one: a link where `symlink(2)` answers `EOPNOTSUPP` or `ENOSYS`, or `EPERM`
|
|
993
|
+
under vfat or exfat (`this drive cannot hold links`); a fifo, socket or device node where `mknod(2)`
|
|
994
|
+
answers the same (`this drive cannot hold special files`); a name the create or `mkdir` refuses with
|
|
995
|
+
`EINVAL` or `EILSEQ` (the drive's own rule, as `':' is not allowed in a name on this vfat drive`, or `this
|
|
996
|
+
drive does not allow that name`) or `ENAMETOOLONG` (`that name is too long for this drive`); `EEXIST`
|
|
997
|
+
under vfat or exfat beside a source sibling that differs only in case or in trailing dots and spaces, which
|
|
998
|
+
is the drive folding two names into one (`this drive treats that name as the same as another in the
|
|
999
|
+
folder`), while `EEXIST` on any other drive, or with no such sibling, is another writer's and ends the
|
|
1000
|
+
item; and a file of 4 GiB or more onto vfat. A link
|
|
1001
|
+
is never followed in place of the link. Any other failure ends the item. A tree that landed without some
|
|
1002
|
+
entries is a failed item, never a success: its `transferitem` answers `ok:false` with one whole sentence, `<path> could not be
|
|
1003
|
+
copied: <reason>` for one entry, `<n> items inside could not be copied; the first is <path>: <reason>` for
|
|
1004
|
+
more, the path relative to the item and the verb `moved` for a move, whose source stays whole. What
|
|
1005
|
+
landed is journaled as a partial copy with its manifest, so `undo` removes exactly it, and on a durable
|
|
1006
|
+
destination it is still confirmed before `transferdone`. At the top of the selection the same refusal is
|
|
1007
|
+
the item's own failure with the reason as its `err`. Each entry is named on `transferdone`'s `refused`.
|
|
1008
|
+
|
|
1009
|
+
**Copies onto removable, phone and network targets are durable before "done".** When the
|
|
1010
|
+
destination classes usb, phone or network, or its filesystem is vfat, exfat or ntfs, each file is
|
|
1011
|
+
fsync'd after its last write and every directory Flea created or wrote into is fsync'd before the
|
|
1012
|
+
transfer reports done. An rclone mount is the exception: the copy lands in rclone's cache first,
|
|
1013
|
+
so no fsync forces its upload and the done line names the background upload instead. Local btrfs and ext4 targets keep today's behaviour. A failed file fsync fails
|
|
1014
|
+
that item like any other copy error and journals the partial for undo. The same rule covers a
|
|
1015
|
+
same-directory duplicate, a redo of a copy, and a same-filesystem rename onto a durable target:
|
|
1016
|
+
the new files are fsync'd, and the destination directory (and the source directory for a move)
|
|
1017
|
+
with them; a rename that already landed reports success either way, because claiming otherwise
|
|
1018
|
+
would lie about what happened.
|
|
1019
|
+
|
|
1020
|
+
**A choice for names that already exist rides on two optional fields**, `collide` and `collideId`:
|
|
1021
|
+
|
|
1022
|
+
Example: `{"c":"transfer","op":"copy","paths":["/home/gm/Desktop/screenshot.png"],"dest":"/home/gm/Pictures","collide":"replace","collideId":7}`
|
|
1023
|
+
|
|
1024
|
+
`collide` is `"keep"`, `"replace"`, `"skip"`, `"refuse"` or `"merge"`, and `collideId` is the `id` of the
|
|
1025
|
+
`collisions` question it answers. A merge carries one more field, `inner`, the second card's answer
|
|
1026
|
+
for files that exist in both, with the same four words and no `merge` of its own.
|
|
1027
|
+
Example: `{"c":"transfer","op":"copy","paths":["/home/gm/trip"],"dest":"/home/gm/Pictures","collide":"merge","collideId":8,"inner":"keep"}`.
|
|
1028
|
+
A missing `collide` is the transfer exactly as it was before this
|
|
1029
|
+
field existed, so an older client is unchanged; any word but those five refuses, so a malformed choice
|
|
1030
|
+
can never replace. **The choice covers only what the question listed**: an item whose source that
|
|
1031
|
+
question named for this same `dest`, whose name there still holds the same item it held then (device,
|
|
1032
|
+
inode and file type). A name that appears after the question, or whose item was swapped since, is
|
|
1033
|
+
refused exactly as before whatever the choice, and so is every name when `collideId` names no
|
|
1034
|
+
question, another destination, or a question an earlier transfer already spent.
|
|
1035
|
+
|
|
1036
|
+
`keep` lands the incoming item under the name `duplicate` would give it, worked out in `dest`:
|
|
1037
|
+
`photo copy.png`, then `photo copy 2.png`. `skip` leaves the item where it is and counts it in
|
|
1038
|
+
`transferdone`'s `skipped`, with no `transferitem` line. `merge` folds a folder landing on a folder
|
|
1039
|
+
together instead of replacing it whole: everything missing in the destination is copied or moved
|
|
1040
|
+
in, and folders inside merge the same way all the way down; see `mergecollisions`. A folder is
|
|
1041
|
+
merged only while it still holds the item the question saw, and files that exist in both take the
|
|
1042
|
+
transfer's `inner` answer, `keep`, `replace`, `skip` or `refuse`, asked once by that walk before
|
|
1043
|
+
anything is written. No answer ever overwrites without it. `replace` moves the item already there to the
|
|
1044
|
+
freedesktop trash first, through the same `gio` call and URI capture `trash` uses, and then transfers
|
|
1045
|
+
the incoming one under the name; a folder is replaced whole and never merged into. A trash that refuses
|
|
1046
|
+
fails that item with `the item already there could not be moved to Trash, so nothing was replaced` and
|
|
1047
|
+
touches nothing. An item already there that holds any source this transfer names, the item's own or
|
|
1048
|
+
another's, fails with `the item already there holds the one being moved in, so it was not replaced`,
|
|
1049
|
+
because trashing it would take that source along; and an incoming symlink that resolves to the item
|
|
1050
|
+
already there now, or whose own text leads back to that name once the link sits there (looked up the way
|
|
1051
|
+
the kernel would, through any other link on the way), fails with `the incoming link points at the item
|
|
1052
|
+
already there, so it was not replaced`, because the copy would be a link to itself. A lookup that passes
|
|
1053
|
+
40 links without reaching that name is let through like a dangling link: the kernel refuses it either
|
|
1054
|
+
way, and the item it replaces waits in Trash for `undo`. A replace whose
|
|
1055
|
+
transfer then fails, a cancel included, puts the trashed item straight back when nothing took its name,
|
|
1056
|
+
and otherwise leaves it for `undo`. When that put-back itself fails, the item's `err` gains `; the item
|
|
1057
|
+
it replaced is still in the Trash` and the reason, and the trash step stays in the journal so `undo` can
|
|
1058
|
+
still restore it. A cancel gains it too, as `cancelled; the item it replaced is still in the Trash (<reason>)`,
|
|
1059
|
+
and that item is counted in `failed`, not `skipped`, because its name no longer holds what it held;
|
|
1060
|
+
`transferdone` still says `cancelled`. The trash and the transfer are one journal entry, see `undo`.
|
|
1061
|
+
|
|
1062
|
+
**Any `collide` word also settles an item that already lives in `dest`.** A copy lands under
|
|
1063
|
+
`duplicate`'s name, and a move is left where it is and counted in `skipped`, with no error. Without
|
|
1064
|
+
`collide` both still fail with `already in that folder`.
|
|
1065
|
+
|
|
1066
|
+
### collisions
|
|
1067
|
+
|
|
1068
|
+
`{"c":"collisions","id":<uint>,"paths":["<string>",...],"dest":"<string>"}`
|
|
1069
|
+
|
|
1070
|
+
Example: `{"c":"collisions","id":7,"paths":["/home/gm/Desktop/screenshot.png","/home/gm/Desktop/notes"],"dest":"/home/gm/Pictures"}`
|
|
1071
|
+
|
|
1072
|
+
Asks, before a `transfer` is sent, which of the items it would name already have their name taken in
|
|
1073
|
+
`dest`, and answers one `collisions` line carrying the same `id`, unless a later `collisions` is sent
|
|
1074
|
+
before it is done, below. `rows` may be sent instead of
|
|
1075
|
+
`paths` and is resolved against the listing exactly as `transfer` resolves it, and a `menuId` asks
|
|
1076
|
+
about that menu's captured selection instead, the one a `transfer` with that `menuId` runs on.
|
|
1077
|
+
|
|
1078
|
+
Example: `{"c":"collisions","id":8,"menuId":31,"dest":"/home/gm/Pictures"}`
|
|
1079
|
+
|
|
1080
|
+
It is read-only and never walks a tree: one `lstat` per source, one per destination name and a resolve
|
|
1081
|
+
of each source's folder. That still measured 0.9 s for 100,000 local sources, and a network mount pays a
|
|
1082
|
+
round trip for each, so the question runs on a thread of its own and the loop keeps answering other
|
|
1083
|
+
requests; the `collisions` line arrives whenever it is done, and a client waits for it before sending
|
|
1084
|
+
the `transfer`. A source that is not absolute, that no longer exists, or that already lives in `dest`
|
|
1085
|
+
is not counted, because the transfer settles those without a question. A `dest` that is missing,
|
|
1086
|
+
relative, not a directory, or cannot be written answers a `total` of 0, and the `transfer` that follows answers its own
|
|
1087
|
+
`error`.
|
|
1088
|
+
|
|
1089
|
+
**The backend keeps the latest question**: each colliding source with the identity of the item its
|
|
1090
|
+
name holds in `dest`, which is what lets a `transfer` naming this `id` in `collideId` apply one choice
|
|
1091
|
+
to exactly those names. It is kept before its `collisions` line is written, so a transfer sent after
|
|
1092
|
+
that line always finds it. The latest question asked is the one kept and the only one answered: an
|
|
1093
|
+
earlier question still being asked when a later one arrives is dropped without a line, the way a result
|
|
1094
|
+
for a superseded listing is, so it is never answered after the later one's; a client asks one question
|
|
1095
|
+
at a time and waits for its line. The next file transfer spends the kept question whether or not it names it. **A `menuId` question also keeps the menu's selection and destination as it saw them**,
|
|
1096
|
+
and a `transfer` carrying that `menuId` and naming this question in `collideId` runs on that capture:
|
|
1097
|
+
Copy to closes its dialog, which sends `menuaction` `close` and expires the live selection, before the
|
|
1098
|
+
answer comes back. The capture holds the same device, inode and type identities the live selection
|
|
1099
|
+
does, and they are still checked per item when the transfer runs. Collision destination identities
|
|
1100
|
+
also compare birth time when the filesystem reports it on both sides. A `menuId` whose selection has
|
|
1101
|
+
already expired answers a `total` of 0, and the `transfer` then answers `Selected items changed; reopen the menu.`.
|
|
1102
|
+
|
|
1103
|
+
**FAT-family destinations are answered inside this round trip.** When `dest` is on `vfat`, `exfat`,
|
|
1104
|
+
`fat`, `fat12`, `fat16`, `fat32`, `msdos`, `ntfs` or `ntfs3` (read off `/proc/self/mountinfo`, never
|
|
1105
|
+
from a name) and the question carries no `fat`, a source whose name that filesystem refuses is answered
|
|
1106
|
+
with one `fatnames` line (below) in place of the `collisions` line. The client asks the same question
|
|
1107
|
+
again with `"fat":"rename"` or `"fat":"skip"` and gets the `collisions` line for the names that will
|
|
1108
|
+
land: Rename and copy asks about the renamed leaves, Skip them leaves the refused names out. Rename and
|
|
1109
|
+
copy also renames, by the same rule, every UTF-8 name the transfer writes inside a folder it copies or merges into, and writes a name that is not UTF-8 as it is;
|
|
1110
|
+
Skip them skips only the listed top-level names and leaves the names inside as they are. A redo of that copy writes the same names. The
|
|
1111
|
+
`transfer` or `link` that follows names the same answer in `fat` beside `collideId`, so the names the
|
|
1112
|
+
card promised are the names the operation takes, and a skipped name never lands at all. Each forbidden
|
|
1113
|
+
character (`: ? * " < > |`) becomes an underscore, trailing dots and spaces are trimmed, and a name of
|
|
1114
|
+
only dots becomes `_`. A `dest` on any other filesystem, or one whose mount cannot be read, never
|
|
1115
|
+
answers `fatnames`, and a `fat` sent to it changes nothing, so nothing is ever renamed on a guess.
|
|
1116
|
+
Without `fat` both requests behave exactly as before, which is every older client. The merge walk
|
|
1117
|
+
takes the answer from the `collisions` question it follows, so under Rename a name inside a merging folder
|
|
1118
|
+
is asked about under the name the transfer writes, and a renamed name that meets an existing one gets the second card's question.
|
|
1119
|
+
|
|
1120
|
+
### mergecollisions
|
|
1121
|
+
|
|
1122
|
+
`{"c":"mergecollisions","id":<uint>,"paths":["<string>",...],"dest":"<string>"}`
|
|
1123
|
+
|
|
1124
|
+
Example: `{"c":"mergecollisions","id":8,"paths":["/home/gm/trip"],"dest":"/home/gm/Pictures"}`
|
|
1125
|
+
|
|
1126
|
+
Asks, after the first card answered `merge`, which files inside the merging folders already have
|
|
1127
|
+
their name taken in `dest`, and answers one `mergecollisions` line carrying the same `id`. `rows`
|
|
1128
|
+
may be sent instead of `paths` and a `menuId` asks about that menu's captured selection, exactly
|
|
1129
|
+
as `collisions` does, and the walk reuses the first card's capture rather than the live selection
|
|
1130
|
+
its dialog already expired. It is read-only and runs beside the loop like `collisions`: one walk
|
|
1131
|
+
of the sources against the destination, made before anything is written, gathering every relative
|
|
1132
|
+
name both sides hold except folders that merge further down. The transfer that follows names this
|
|
1133
|
+
question in `collideId` and carries the second card's answer in `inner`; a name that appears after
|
|
1134
|
+
the walk, or whose item was swapped since, is refused exactly as before whatever `inner` says.
|
|
1135
|
+
|
|
1136
|
+
### pasteimage
|
|
1137
|
+
|
|
1138
|
+
`{"c":"pasteimage","dest":"<string>","name":"<string>"}`
|
|
1139
|
+
|
|
1140
|
+
Example: `{"c":"pasteimage","dest":"/home/gm/Pictures","name":"Pasted image 2026-09-23 at 10.14.32.png"}`
|
|
1141
|
+
|
|
1142
|
+
Writes the clipboard's image as a named file in `dest`, the p key with no paths on the
|
|
1143
|
+
clipboard. The bytes never cross this wire: the backend runs `wl-paste --type image/png`
|
|
1144
|
+
itself, refuses an image past 256 MiB and one that never arrives within 30 s, and streams the
|
|
1145
|
+
rest into the destination through an O_EXCL temp published by a no-replace rename, which a FAT
|
|
1146
|
+
destination takes where it takes no hard link, so a name that appeared meanwhile is never replaced.
|
|
1147
|
+
A name already taken lands beside it as `stem (n).ext`; a clipboard holding no image answers an
|
|
1148
|
+
`error` line carrying `"code":"emptyclipboard"`, which the UI words itself; a name that is empty, `.` or `..`, or carries `/`, is refused before anything runs. The answer
|
|
1149
|
+
is the same `made` line a new file gets, and one journal entry named `paste`, so z undoes it; redo
|
|
1150
|
+
refuses it, because a clipboard has no recorded source to replay.
|
|
1151
|
+
|
|
1152
|
+
### link
|
|
1153
|
+
|
|
1154
|
+
TUI copy requests `paths` with `"hold":true` and a request `id`. Replies echo the id and listing generation,
|
|
1155
|
+
including failed identity captures, so a delayed failure cannot clear a newer copy request.
|
|
1156
|
+
Successful replies include `held`, one source identity per path. The identity stores absolute
|
|
1157
|
+
`path`, decimal-string `dev`, `ino`, `kind`, and `born` (null or a two-string
|
|
1158
|
+
seconds/nanoseconds array), preserving integer precision. Capture runs off the
|
|
1159
|
+
backend loop with the existing mount deadline. The client retains it through
|
|
1160
|
+
navigation and collision confirmation. The link request carries that same `held`
|
|
1161
|
+
array. The link worker checks each source against it before any destination action;
|
|
1162
|
+
a missing or changed source fails without creating its link. Existing clients may
|
|
1163
|
+
omit `held`. Every successful link still records the existing journal step.
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
`{"c":"link","op":"<string>","paths":["<string>",...],"dest":"<string>"}`
|
|
1167
|
+
|
|
1168
|
+
Example: `{"c":"link","op":"relative","paths":["/home/gm/a.png"],"dest":"/home/gm/Pictures"}`
|
|
1169
|
+
|
|
1170
|
+
Creates one link per top-level path inside `dest`. `op` of `"absolute"` stores
|
|
1171
|
+
the full path, `"hard"` creates a hard link, and anything else, including a
|
|
1172
|
+
missing `op`, links relative from the destination, so a malformed request can
|
|
1173
|
+
never write an absolute path by accident. Like `transfer`, this names paths
|
|
1174
|
+
rather than row indices, and a `rows` array of indices into the current
|
|
1175
|
+
listing may be sent instead of `paths`, resolved the same way; `paths` wins
|
|
1176
|
+
when both are present. A `link` whose `listing` is not the numbering in force
|
|
1177
|
+
is refused with `where` of `stale` before a single index is resolved, the
|
|
1178
|
+
same rule `transfer` follows.
|
|
1179
|
+
|
|
1180
|
+
This runs beside the loop and takes the non-job operation slot:
|
|
1181
|
+
the busy check and the collide answer are decided on the loop, then one
|
|
1182
|
+
thread links each source, replacing through the trash when the card chose
|
|
1183
|
+
it, and the answer is written from its message. A source or the destination
|
|
1184
|
+
on a mount with a pending slow write waits first, the same gate every other
|
|
1185
|
+
write passes. A Replace trashes through
|
|
1186
|
+
gio, which stalls on an unresponsive mount, so that work never runs on the
|
|
1187
|
+
loop's own thread. A `link` arriving while an operation runs
|
|
1188
|
+
answers an `error` line carrying `an operation is already running` and journals nothing. The answer is one `linked` line,
|
|
1189
|
+
`{"t":"linked","ok":<uint>,"failed":<uint>,"skipped":<uint>}`, and one journal
|
|
1190
|
+
entry, so one undo removes every link this request created. `note` rides on that line whenever a link this request
|
|
1191
|
+
made could be neither verified nor removed, plain or replacing, naming each leftover link and its cleanup error as `the link at <path> could not be removed: <reason>`, followed by
|
|
1192
|
+
`; the item it replaced is still in the Trash` where a replaced item was kept, and ahead of that a mixed batch's first
|
|
1193
|
+
failure, so a batch that lands some links still names what the rest failed with. An all-failed batch answers an `error` line with `where` of `link` carrying the first
|
|
1194
|
+
failure with that note appended, naming the failing source as `<source>: <message>`. A source that no longer exists is refused
|
|
1195
|
+
for that item and counts in `failed`, so one missing source never stops the rest. A name that
|
|
1196
|
+
already exists is refused for that item unless the request carries the
|
|
1197
|
+
`collide` and `collideId` choice a `transfer` carries, applied by the same
|
|
1198
|
+
rule: only what the question listed, only while the name still holds the same
|
|
1199
|
+
item. `keep` lands the link under the name `duplicate` would give it, `skip`
|
|
1200
|
+
leaves the item where it is and counts it in `skipped`, and `replace` moves
|
|
1201
|
+
the item already there to the trash first and then links under the name. A
|
|
1202
|
+
hard link across drives is refused with both filesystem names in the
|
|
1203
|
+
sentence, and a hard link to a folder is refused outright. A `dest` that is missing, relative, not a directory,
|
|
1204
|
+
or cannot be written answers a single `error` line with `where` of `link` and nothing is started.
|
|
1205
|
+
|
|
1206
|
+
### linktarget
|
|
1207
|
+
|
|
1208
|
+
The optional `target` string is the selected link's held readlink value. If the
|
|
1209
|
+
worker reads a different value, it refuses rather than following the replacement.
|
|
1210
|
+
TUI replies must also match the held request id, source path and listing generation.
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
`{"c":"linktarget","path":"<string>","id":<uint>}`
|
|
1214
|
+
|
|
1215
|
+
Example: `{"c":"linktarget","path":"/home/gm/latest","id":3}`
|
|
1216
|
+
|
|
1217
|
+
Answers one `linktarget` line,
|
|
1218
|
+
`{"t":"linktarget","path":"<string>","directory":"<string>","name":"<string>","id":<uint>}`,
|
|
1219
|
+
naming the folder the symlink's target lives in and the target's own leaf, the
|
|
1220
|
+
same path Show in folder uses. `id` echoes the request's, so a reply landing
|
|
1221
|
+
after the pane navigated elsewhere is answered only when it still names the
|
|
1222
|
+
pending one. `directory` is the target's folder with every
|
|
1223
|
+
symlinked folder and `..` resolved the way the kernel follows them, falling back
|
|
1224
|
+
to the link text's own split when it cannot be resolved, and the line is answered
|
|
1225
|
+
from a thread, as `meta` is. A path that is not a symlink answers an `error`
|
|
1226
|
+
line with `where` of `linktarget` and touches nothing.
|
|
1227
|
+
|
|
1228
|
+
### permissionsBatch
|
|
1229
|
+
|
|
1230
|
+
`{"c":"permissionsBatch","paths":["<string>",...],"modes":["<string>",...],"id":<uint>}`
|
|
1231
|
+
|
|
1232
|
+
Example: `{"c":"permissionsBatch","paths":["/home/gm/a.txt","/home/gm/b.txt"],"modes":["600","600"],"id":7}`
|
|
1233
|
+
|
|
1234
|
+
Changes the mode of every named path to its own target mode, each following
|
|
1235
|
+
the same three-or-leading-zero-four octal rule the dialog's own `apply`
|
|
1236
|
+
enforces, and answers one `permissions` line with `op` of `applyMany`,
|
|
1237
|
+
`{"t":"permissions","id":7,"op":"applyMany","ok":true,"mode":"0600","error":""}`.
|
|
1238
|
+
One journal entry holds every path the Apply changed, so one undo restores
|
|
1239
|
+
them all. A `permissionsBatch` arriving while an operation runs answers `ok` false with
|
|
1240
|
+
`an operation is already running` and changes nothing. Octal, Owner, Group and the change preview drop out for several
|
|
1241
|
+
items: the grid and Apply are the whole card. A mixed box the operator never
|
|
1242
|
+
touched keeps each file's own bit, because the client sends that file's own
|
|
1243
|
+
target mode rather than one mode for all.
|
|
1244
|
+
|
|
1245
|
+
### transfercancel
|
|
1246
|
+
|
|
1247
|
+
`{"c":"transfercancel","id":<uint>}`
|
|
1248
|
+
|
|
1249
|
+
Example: `{"c":"transfercancel","id":12}`
|
|
1250
|
+
|
|
1251
|
+
Cancels the waiting or running job named by `id`, including a transfer, archive `extract`, or
|
|
1252
|
+
compression, and does nothing for a stale id. Other jobs keep running or waiting independently.
|
|
1253
|
+
A convert is not a jobs operation and cannot be cancelled by this request. There is no response line
|
|
1254
|
+
of its own: a transfer answers with its own `transferdone` carrying `cancelled` true, and an extract
|
|
1255
|
+
answers its own `archivedone` with `ok` false and an `err` of `cancelled`, including when cancelled
|
|
1256
|
+
before it runs.
|
|
1257
|
+
|
|
1258
|
+
**The item in flight is stopped rather than allowed to finish, and what it had already written is
|
|
1259
|
+
removed: a partial file by `copy_file`, a partly-copied directory by `copy_dir`.** A cancel that waited out a multi-gigabyte copy would not be a cancel, and a half-written file
|
|
1260
|
+
at the destination is not a result anyone asked for. That item is reported `ok:false` with an `err` of
|
|
1261
|
+
`cancelled`; every item not yet started is counted in `skipped`. A cancelled extract stops and reaps
|
|
1262
|
+
its sandboxed child, removes its staging directory and publishes no destination, so the same rule holds
|
|
1263
|
+
there without a partial tree. When the archive tool has not exited ten seconds after the cancel, its work
|
|
1264
|
+
folder is left in place rather than deleted under a live writer, and the `err` names that folder.
|
|
1265
|
+
|
|
1266
|
+
A `quit`, or stdin closing, cancels a running operation the same way and waits for its terminal line
|
|
1267
|
+
before the process exits, so shutting down mid-copy also leaves nothing half-written behind. A
|
|
1268
|
+
quit sets each detached compress and convert flag too and drains them under the one 25 s shutdown
|
|
1269
|
+
budget, so each job's `Work` cleanup runs before the process exits and no `.flea-work-*` folder is
|
|
1270
|
+
left beside the destination. A job whose tool has not exited ten seconds after the cancel keeps its folder in place
|
|
1271
|
+
and its terminal `err` names it; one still running when the budget ends is abandoned with the process.
|
|
1272
|
+
|
|
1273
|
+
### flashimage
|
|
1274
|
+
|
|
1275
|
+
`{"c":"flashimage","path":"<string>"}`
|
|
1276
|
+
|
|
1277
|
+
Example: `{"c":"flashimage","path":"/home/gm/Downloads/raspios-arm64.img.xz"}`
|
|
1278
|
+
|
|
1279
|
+
Classifies one image and answers one `flashimage` line. The kind comes from the name and is
|
|
1280
|
+
confirmed by magic bytes: `.iso` (ISO 9660 `CD001` at 32769), `.img` and `.raw` (anything),
|
|
1281
|
+
`.img.xz` (`FD 37 7A 58 5A 00`), `.img.gz` (`1F 8B`), `.img.zst` (`28 B5 2F FD`),
|
|
1282
|
+
`.img.bz2` (`BZh`), `.zip` (`PK 03 04`). A name and magic that disagree answer `"kind":""`
|
|
1283
|
+
and the row is refused with "This is not a disk image Flea can flash." The unpacked size is
|
|
1284
|
+
read in the sandbox, never parsed in Flea's own process: `xz --robot --list` for xz,
|
|
1285
|
+
`zstd -lv` for a frame that records its content size, and the archive listing Flea already
|
|
1286
|
+
runs for a zip. gzip's size field wraps at 4 GiB and bzip2 has none, so both answer
|
|
1287
|
+
`known:false`. A zip qualifies only when its listing has exactly one member and that member
|
|
1288
|
+
is an image by the rules above.
|
|
1289
|
+
|
|
1290
|
+
`{"t":"flashimage","path":"<string>","kind":"iso|img|xz|gz|zst|bz2|zip","bytes":<uint>,"unpacked":<uint>,"known":<bool>,"member":"<string>"}`
|
|
1291
|
+
|
|
1292
|
+
`bytes` is the file's own size. `unpacked` is the size that will be written, or 0 when
|
|
1293
|
+
`known` is false. `member` names the zip's one image member and is empty otherwise. A
|
|
1294
|
+
refused row answers `"kind":""` with `unpacked` 0, `known` false and `member` empty.
|
|
1295
|
+
|
|
1296
|
+
### flashtargets
|
|
1297
|
+
|
|
1298
|
+
`{"c":"flashtargets","image":"<string>"}`
|
|
1299
|
+
|
|
1300
|
+
Example: `{"c":"flashtargets","image":"/home/gm/Downloads/debian-13.iso"}`
|
|
1301
|
+
|
|
1302
|
+
Answers one `flashtargets` line listing every eligible drive in `/sys/block` order, with
|
|
1303
|
+
the refusals the image causes. `image` may be empty, which is a pane opened from a drive:
|
|
1304
|
+
every drive then lists with no `small` refusal. Never lists a drive the safety model
|
|
1305
|
+
excludes; see "The image flasher" in `AGENTS.md`.
|
|
1306
|
+
|
|
1307
|
+
`{"t":"flashtargets","drives":[{"dev":"sda","seq":<uint>,"serial":"<string>","label":"128GB","model":"General USB Flash Disk","bytes":<uint>,"volumes":["128GB"],"refused":""}]}`
|
|
1308
|
+
|
|
1309
|
+
`seq` is the kernel's `diskseq`, which never repeats within a boot, so it names this
|
|
1310
|
+
plugging of this medium. `label` is the rail's name when the drive shows as one rail row,
|
|
1311
|
+
else the model. `model` joins vendor and model the way `omarchy-drive-info` does. `volumes`
|
|
1312
|
+
names the drive's partitions. `refused` is one of `small`, `readonly`, `holdsimage`,
|
|
1313
|
+
`member` (part of a RAID or volume group), or empty when the drive takes the image.
|
|
1314
|
+
|
|
1315
|
+
### flash
|
|
1316
|
+
|
|
1317
|
+
`{"c":"flash","id":<uint>,"image":"<string>","dev":"<string>","seq":<uint>,"serial":"<string>","bytes":<uint>}`
|
|
1318
|
+
|
|
1319
|
+
Example: `{"c":"flash","id":31,"image":"/home/gm/Downloads/debian-13.iso","dev":"sda","seq":2,"serial":"2003007157171692","bytes":124623257600}`
|
|
1320
|
+
|
|
1321
|
+
Writes the image to the drive the Flash step named and verifies it. The identity fields
|
|
1322
|
+
are the ones the Flash step showed when it was pressed; if any differs when the backend
|
|
1323
|
+
re-reads it, nothing is touched and the answer is `flashdone` with `err` `changed`. A
|
|
1324
|
+
flash is keyed by its own `id` and runs alongside a transfer, because its target is a
|
|
1325
|
+
device no transfer can reach; one flash runs at a time per backend, and a second answers
|
|
1326
|
+
an `error` line with `where` `flash` rather than queueing.
|
|
1327
|
+
|
|
1328
|
+
Answers: one `flashstarted`, a stream of `flashprogress` throttled to one line per
|
|
1329
|
+
150 ms, the transfer rule, and exactly one `flashdone`.
|
|
1330
|
+
|
|
1331
|
+
`{"t":"flashstarted","id":<uint>,"total":<uint>,"known":<bool>}`
|
|
1332
|
+
|
|
1333
|
+
`total` is the unpacked size when `known` is true and the file's own size otherwise.
|
|
1334
|
+
|
|
1335
|
+
`{"t":"flashprogress","id":<uint>,"phase":"prepare|password|write|flush|verify","bytes":<uint>,"total":<uint>,"permille":<uint>}`
|
|
1336
|
+
|
|
1337
|
+
`bytes` counts bytes the device accepted (write) or read back (verify). `permille` is
|
|
1338
|
+
the share done: bytes over total when the size is known, else the offset of the
|
|
1339
|
+
compressed file the decompressor has consumed over its size. Once the write ends, `total`
|
|
1340
|
+
is the byte count written, so flush and verify measure against it even when the size was
|
|
1341
|
+
unknown. `prepare` runs while the backend re-reads the drive and asks the helper to confirm
|
|
1342
|
+
it, `password` runs from the helper's spawn to its descriptor, `flush` covers the final
|
|
1343
|
+
`fsync`.
|
|
1344
|
+
|
|
1345
|
+
`{"t":"flashdone","id":<uint>,"ok":<bool>,"phase":"<string>","at":<uint>,"err":"<string>","blank":<bool>}`
|
|
1346
|
+
|
|
1347
|
+
`err` rides only on a failure: `cancelled`, `denied` (the prompt was dismissed or
|
|
1348
|
+
refused), `busy` (a volume would not unmount), `changed` (identity differs), `gone`
|
|
1349
|
+
(the device vanished), `io` (a write or read failed), `verify` (read-back differs,
|
|
1350
|
+
`at` is the first differing offset), `nospace` (a stream outgrew the drive),
|
|
1351
|
+
`decompress` (the decompressor failed or stalled), `sandbox` (bwrap or prlimit
|
|
1352
|
+
missing), `helper` (the helper or udisks failed). `blank` says whether the drive's ends
|
|
1353
|
+
were wiped, and is true only when the wipe itself succeeded: a write that ends `cancelled`,
|
|
1354
|
+
`nospace`, `decompress` or `io`, a cancel or quit during the final flush and a `verify`
|
|
1355
|
+
mismatch wipe. `blank` is false where nothing is wiped: `sandbox`, which is found before
|
|
1356
|
+
the first byte moves, every refusal before the descriptor, a drive that is `gone`, and a
|
|
1357
|
+
cancel during verify, which leaves the whole image on the drive and offers Eject alone. A pane that sent an image no probe answers
|
|
1358
|
+
receives `phase` `probe` with `err` `io`; the pane never sends one, because Flash is
|
|
1359
|
+
unavailable until the probe answers a kind.
|
|
1360
|
+
|
|
1361
|
+
### flashcancel
|
|
1362
|
+
|
|
1363
|
+
`{"c":"flashcancel","id":<uint>}`
|
|
1364
|
+
|
|
1365
|
+
Cancels the flash `id` names and nothing else, as `transfercancel` does. No line of its
|
|
1366
|
+
own; the flash answers its own `flashdone` with `err` `cancelled`. Only Cancel stops a
|
|
1367
|
+
flash: Esc leaves the pane and never cancels one, and a `quit`, or stdin closing,
|
|
1368
|
+
cancels the flash and waits for its `flashdone`, so the drive ends blank rather than
|
|
1369
|
+
half written. A cancel that lands during the verify is the one exception: the image is
|
|
1370
|
+
whole and proven so far, so it stays and `blank` is false.
|
|
1371
|
+
|
|
1372
|
+
### flasheject
|
|
1373
|
+
|
|
1374
|
+
`{"c":"flasheject","dev":"<string>","seq":<uint>}`
|
|
1375
|
+
|
|
1376
|
+
Example: `{"c":"flasheject","dev":"sda","seq":2}`
|
|
1377
|
+
|
|
1378
|
+
Answers `{"t":"flasheject","ok":<bool>,"err":"busy|gone|changed|error"}`. Unmounts every
|
|
1379
|
+
mounted filesystem on the drive, then powers it off. The message the user reads is the
|
|
1380
|
+
rail's own, so "Ejected 128GB, it is safe to unplug." is the shipped string. `seq` names
|
|
1381
|
+
the plugging the step showed; a drive that changed since answers `changed`. A `dev` that is
|
|
1382
|
+
not a bare kernel name, or a drive the flasher would not list (a system disk), answers
|
|
1383
|
+
`error` before the helper runs.
|
|
1384
|
+
|
|
1385
|
+
### trash
|
|
1386
|
+
|
|
1387
|
+
`{"c":"trash","id":<uint>,"paths":["<string>",...],"rows":[<uint>,...]}`
|
|
1388
|
+
|
|
1389
|
+
`id` is the client's request id; the `trashed` line that answers it echoes it. Omitted, it is 0.
|
|
1390
|
+
|
|
1391
|
+
Example: `{"c":"trash","id":3,"paths":["/home/gm/old.txt"]}`
|
|
1392
|
+
Example: `{"c":"trash","id":4,"rows":[4,9]}`
|
|
1393
|
+
|
|
1394
|
+
`rows` is the same alternative to `paths` that `transfer` documents above, resolved the same way.
|
|
1395
|
+
|
|
1396
|
+
Moves each path to the freedesktop trash by running `gio trash` under a 10 s deadline, and answers
|
|
1397
|
+
one `trashed` line.
|
|
1398
|
+
**Nothing about the freedesktop trash specification is implemented in this codebase**, only an argv and
|
|
1399
|
+
a result: `gio` already handles the same-filesystem-move-versus-copy question, the `.trashinfo`
|
|
1400
|
+
metadata, and the per-mount `.Trash-$uid` fallback for a volume with no home-relative trash.
|
|
1401
|
+
|
|
1402
|
+
Which paths actually went is read off the filesystem afterwards rather than from `gio`'s exit status,
|
|
1403
|
+
which covers the whole batch and cannot attribute a failure to one path.
|
|
1404
|
+
|
|
1405
|
+
**The trash URI of each item is captured at trash time, and this is a measured requirement rather than a
|
|
1406
|
+
preference.** `gio trash --restore` refuses an original path outright (`Location given doesn't start
|
|
1407
|
+
with trash:///`), and two files trashed from the same path both list that same original, so recovering
|
|
1408
|
+
the URI later is ambiguous. The backend therefore reads `gio trash --list` immediately before and after
|
|
1409
|
+
the call and keeps the entries that are new, which is what makes the operation reversible.
|
|
1410
|
+
|
|
1411
|
+
There is no confirmation step anywhere in this request, because the undo journal is the safety.
|
|
1412
|
+
|
|
1413
|
+
An accepted request first flushes `{"t":"trashstarted","id":<uint>,"n":<uint>,"cancellable":false}`,
|
|
1414
|
+
before the worker touches the Trash or the client re-lists. `n` counts the resolved top-level paths.
|
|
1415
|
+
This acknowledgement lets the client draw its pending operation immediately. It is followed by the
|
|
1416
|
+
existing `trashed` outcome; journal settlement and its ordering are unchanged. Ordinary trashing does
|
|
1417
|
+
not poll a cancel flag, so this acknowledgement must not expose a Cancel action.
|
|
1418
|
+
|
|
1419
|
+
### trashbrowse
|
|
1420
|
+
|
|
1421
|
+
`{"c":"trashbrowse","op":"prepare","id":<uint>,"all":true,"emptyTrash":true}` reviews the
|
|
1422
|
+
current Trash and answers `{"t":"trashbrowse","op":"prepare","id":<uint>,"ok":true,
|
|
1423
|
+
"token":<uint>,"all":true,"count":<uint>,"bytes":<uint>,"partial":false}`. The confirmation body
|
|
1424
|
+
uses this reviewed count and byte total, including zero bytes for empty files. A partial, pending,
|
|
1425
|
+
or failed review supplies no actionable confirmation. Preparation and validation query item details
|
|
1426
|
+
in bounded groups of 128, with the same complete snapshot and identity checks as individual queries.
|
|
1427
|
+
|
|
1428
|
+
`{"c":"trashbrowse","op":"delete","id":<uint>,"token":<uint>}` deletes that reviewed snapshot.
|
|
1429
|
+
An accepted delete claims the non-job operation slot and immediately flushes
|
|
1430
|
+
`{"t":"trashdeletestarted","id":<uint>,"requestId":<uint>}` followed by a `transferprogress` line,
|
|
1431
|
+
before any queued Trash request, validation, or filesystem work. `id` is the operation id accepted by
|
|
1432
|
+
`transfercancel`; `requestId` is the browser request's id. A busy refusal answers only the ordinary
|
|
1433
|
+
`trashbrowse` error and starts no job. A pending unsettled trash journal also refuses deletion.
|
|
1434
|
+
|
|
1435
|
+
The job uses the operations card's existing progress and terminal types:
|
|
1436
|
+
|
|
1437
|
+
`{"t":"transferprogress","id":<uint>,"index":0,"name":"<string>","itemSize":<uint>,"bytes":<uint>,"total":<uint>,"unit":"items","phase":"checking|deleting","checked":<uint>}`
|
|
1438
|
+
|
|
1439
|
+
`unit:items` changes the meaning of `bytes` and `total` for this operation only: they count deleted
|
|
1440
|
+
and reviewed filesystem entries, including descendants, directories, and zero-byte files. The totals
|
|
1441
|
+
come from the complete saved snapshot, so one trashed folder can report thousands of files. The
|
|
1442
|
+
`trashbrowse` prepare reply's `count` and the terminal item counts remain top-level Trash entries.
|
|
1443
|
+
The immediate sample has total 0 until the worker reads the confirmation; later samples carry the
|
|
1444
|
+
recursive total. During
|
|
1445
|
+
`checking`, `bytes` stays 0 and `checked` counts validated top-level items; during `deleting`, `bytes`
|
|
1446
|
+
counts successfully removed entries, including children of a folder interrupted later. Refused entries
|
|
1447
|
+
do not advance this bar. `name` is the original name of the top-level item in flight and `itemSize`
|
|
1448
|
+
is its reviewed recursive byte size; an empty name means no current item. Samples are throttled to
|
|
1449
|
+
150 ms, with immediate phase, first-item and final samples. Tree nodes poll cancel and sample progress.
|
|
1450
|
+
|
|
1451
|
+
The native card's head is `Emptying Trash`. Show validation during checking, then the determinate
|
|
1452
|
+
file-count bar and deleted-file count, with the current name and size on its file line. Byte rates
|
|
1453
|
+
cannot be derived from these counts; a remaining-time estimate must use observed file deletions per
|
|
1454
|
+
second and is unavailable during checking. Keep the Trash strip's prior count until completion,
|
|
1455
|
+
then refresh it. The Empty Trash control stays in place and is disabled while the job runs.
|
|
1456
|
+
|
|
1457
|
+
Exactly one `transferdone` releases this job's slot, followed by its original `trashbrowse` delete
|
|
1458
|
+
reply. `ok`, `failed`, and `skipped` on `transferdone` count deleted, refused, and unstarted items;
|
|
1459
|
+
`cancelled` reports interruption. A validation failure carries its sentence in `note`, and the browser
|
|
1460
|
+
reply retains the existing `ok:false`, `stale`, and `error` fields. A completed or interrupted deletion
|
|
1461
|
+
retains `done`, `failed`, and `failures`, and adds `skipped` and `cancelled`. Deletion is irreversible
|
|
1462
|
+
and creates no undo entry. Cancellation during validation preserves the confirmation for another
|
|
1463
|
+
attempt. Once deletion starts it consumes the token, so remaining items require a new review.
|
|
1464
|
+
|
|
1465
|
+
Cancel, quit, and stdin closing poll the same flag between items and between tree nodes. Completed
|
|
1466
|
+
deletions stay deleted. An interrupted folder's surviving payload and `.trashinfo` are put back
|
|
1467
|
+
together; a failed rollback retains the existing recovery record and names its quarantine in the
|
|
1468
|
+
failure. A cancel for another operation id has no effect. The terminal replies precede any client
|
|
1469
|
+
re-list, so displaying progress and completion never waits for that re-list.
|
|
1470
|
+
|
|
1471
|
+
Accepting delete supersedes earlier browser reads and confirmation checks through the browser's
|
|
1472
|
+
request generation. Its independent operation flag accepts cancel while the job is still queued;
|
|
1473
|
+
later browser reads cannot cancel the delete. A superseded check answers its ordinary cancelled error.
|
|
1474
|
+
|
|
1475
|
+
### rename
|
|
1476
|
+
|
|
1477
|
+
`{"c":"rename","path":"<string>","to":"<string>"}`
|
|
1478
|
+
|
|
1479
|
+
Example: `{"c":"rename","path":"/home/gm/old.txt","to":"new.txt"}`
|
|
1480
|
+
|
|
1481
|
+
Renames one file within its own directory and answers one `renamed` line. `to` is a bare name, not a
|
|
1482
|
+
path: an empty `to`, `.`, `..`, or anything containing `/` or an interior NUL is refused with an `error`
|
|
1483
|
+
line before any syscall runs, because a separator would move the file out of its own directory.
|
|
1484
|
+
|
|
1485
|
+
**The rename refuses to overwrite.** It runs `renameat2` with `RENAME_NOREPLACE` rather than
|
|
1486
|
+
`rename(2)`, which on Unix silently replaces the target; for a file manager that is unrecoverable data
|
|
1487
|
+
loss, and the check-then-rename alternative leaves a window in which another process can create the
|
|
1488
|
+
target. Renaming a file to the name it already has is not an error and is not work: it answers `ok` and
|
|
1489
|
+
records nothing to undo.
|
|
1490
|
+
|
|
1491
|
+
**A rename that cannot prove the source survived whole answers `rename-kept`.** Measured mounts that cannot serve
|
|
1492
|
+
`RENAME_NOREPLACE` include `fuse.rclone` directories and `fuse.megafs` paths, which answer `EINVAL`, and a path under a
|
|
1493
|
+
`/run/user/*/gvfs/dav:` WebDAV mount answers `EIO`. On those, the backend builds the new name through the same exclusive copy primitives every
|
|
1494
|
+
other write uses and removes the source only once that copy is complete. The copy is taken back only on proof the source
|
|
1495
|
+
survived whole: a source that still stats as anything but a directory after the failed removal, since
|
|
1496
|
+
`remove_file` removes every other kind with one unlink that either takes effect or does not. That answers a plain `rename`
|
|
1497
|
+
error, and so does a failure to take the copy back, in which case the copy stays under the new name
|
|
1498
|
+
and that error's `path` is the copy rather than the source. Every other state keeps the copy, a directory source because `remove_dir_all`
|
|
1499
|
+
stops at its first failure, and a source that no longer stats because an errno alone cannot tell a
|
|
1500
|
+
removal that took effect from one that did not. The request then answers an `error` line whose
|
|
1501
|
+
`where` is `rename-kept`, whose `path` is
|
|
1502
|
+
the source, and whose `msg` is the removal's own message; the target is on neither field. Neither
|
|
1503
|
+
direction journals the kept copy, so no `undo` removes it: the removal can stop partway, and one error
|
|
1504
|
+
with no account of how far it got cannot tell a whole source from a remnant or from one already gone. An `undo` reverses a
|
|
1505
|
+
rename through the same call, so a reversal that half succeeds answers this same `where`. A case twin
|
|
1506
|
+
whose move-back from its temp sibling fails answers `rename-stranded` instead: its `path` is the
|
|
1507
|
+
source, its `msg` names the temp leaf and the move-back's cause, and the file stays under that
|
|
1508
|
+
hidden name.
|
|
1509
|
+
|
|
1510
|
+
Unlike the three above, this answers on the loop's own thread on a local mount: the ordinary
|
|
1511
|
+
case is one `renameat2`, which costs less than spawning a thread. On a remote mount it runs on its
|
|
1512
|
+
own worker with the single-call deadline and answers `slow` first, then its `renamed` line when the
|
|
1513
|
+
write lands, journalled exactly as the in-time path would journal it; see `slow`. The compatibility
|
|
1514
|
+
paths above are not one syscall, and on a remote mount they run on that worker too, so a directory
|
|
1515
|
+
rename on rclone or MEGA no longer holds the loop while it copies. See `AGENTS.md`, "Write operations
|
|
1516
|
+
and the undo journal".
|
|
1517
|
+
|
|
1518
|
+
### bulkrename
|
|
1519
|
+
|
|
1520
|
+
{`"c":"bulkrename","paths":["<string>",...],"names":["<string>",...]`}
|
|
1521
|
+
|
|
1522
|
+
Example: `{"c":"bulkrename","paths":["/home/gm/a.txt","/home/gm/b.txt"],"names":["b.txt","a.txt"]}`
|
|
1523
|
+
|
|
1524
|
+
Renames every named path to its bare new name in the same directory and answers one `bulkrenamed` line,
|
|
1525
|
+
`{"t":"bulkrenamed","ok":<uint>,"unchanged":<uint>}`. The two arrays run in parallel: `names[i]` is the new bare name for `paths[i]`. The whole batch is validated before anything moves: an empty batch, a length mismatch, a non-absolute or repeated path, paths in more than one folder, a missing source or one replaced by another item before the first move, an empty name, `.`, `..`, a name containing `/`, NUL, CR or LF, a name over 255 bytes, two new names that collide, or a destination that already exists outside the batch all refuse the whole batch with an `error` line whose `where` is `bulkrename`, and nothing on disk moves. A destination that is itself one of the batch sources is allowed: swaps and cycles move through temporary names in the same folder, so a to b and b to a works. A name equal to its own source is unchanged rather than work: it counts in `unchanged` and records nothing. The whole batch is one journal entry, so one `undo` reverses every rename it made, newest first; a failure partway journals what landed the same way, so `undo` still reverts the partial batch. Like rename it answers inline on a local mount and on a remote mount runs on its own worker with the single-call deadline, answering `slow` first and its `bulkrenamed` line when the batch lands; see `slow`.
|
|
1526
|
+
|
|
1527
|
+
### duplicate
|
|
1528
|
+
|
|
1529
|
+
`{"c":"duplicate","path":"<string>"}`
|
|
1530
|
+
|
|
1531
|
+
Example: `{"c":"duplicate","path":"/home/gm/photo.jpg"}`
|
|
1532
|
+
|
|
1533
|
+
Copies one path to a free sibling name and answers one `duplicated` line. The name walks
|
|
1534
|
+
`"<stem> copy<ext>"`, `"<stem> copy 2<ext>"` and so on until one is free; `Path`'s own stem and
|
|
1535
|
+
extension split the last dot only, so `backup.tar.zst` becomes `backup.tar copy.zst` and a dotfile keeps
|
|
1536
|
+
its whole name as the stem. A directory row duplicates its whole tree.
|
|
1537
|
+
|
|
1538
|
+
`duplicate` and `mkdir` also accept an optional positive integer `request`. Without it, or with
|
|
1539
|
+
zero, their existing operation and error lines are unchanged. An opted-in request keeps that
|
|
1540
|
+
payload and adds `request` to its successful `duplicated` or `made` line, followed by one
|
|
1541
|
+
`{"t":"menuaction","op":"creation","id":<request>,"kind":"duplicated|made","ok":true,"path":"<created path>"}`.
|
|
1542
|
+
A refusal instead keeps its original `error` line and adds the same receipt with `ok:false` and
|
|
1543
|
+
`error` in place of `path`. Busy refusals and slow completions carry the exact original request,
|
|
1544
|
+
so concurrent results need not arrive in request order. Correlation changes no operation slot,
|
|
1545
|
+
pending mount claim, journal entry or creation-name rule; independent request fields retain their
|
|
1546
|
+
meaning. Both completion lines are published together before quit can observe a finished slot.
|
|
1547
|
+
|
|
1548
|
+
### mkdir
|
|
1549
|
+
|
|
1550
|
+
`{"c":"mkdir","path":"<string>","name":"<string>"}`
|
|
1551
|
+
|
|
1552
|
+
Example: `{"c":"mkdir","path":"/home/gm","name":"Invoices"}`
|
|
1553
|
+
Example: `{"c":"mkdir","path":"/home/gm"}`
|
|
1554
|
+
|
|
1555
|
+
Makes one empty directory inside `path` and answers one `made` line. `path` is the absolute parent: a
|
|
1556
|
+
relative or missing `path` is refused before any syscall, because a bare name would otherwise land in
|
|
1557
|
+
the backend's own working directory, which no listing ever names. `name` is a bare name under the rule
|
|
1558
|
+
`rename` applies to `to`: `.`, `..`, anything containing `/` or an interior NUL is refused with an
|
|
1559
|
+
`error` line whose `where` is `mkdir`. A name of only spaces is a legal name and is created as sent,
|
|
1560
|
+
as it would be by `rename`; trimming is the field's job.
|
|
1561
|
+
|
|
1562
|
+
**A missing or empty `name` asks for the first free default name**, `New Folder`, then `New Folder 2`
|
|
1563
|
+
and up, the same way `duplicate` picks ` copy N` itself rather than leaving it to the client: a client
|
|
1564
|
+
holds a window of the listing and not the directory, so it cannot know which names are taken, and a
|
|
1565
|
+
name taken by anything, a file included, is stepped past. That is the form an inline
|
|
1566
|
+
create-then-rename flow sends; the `made` line then carries the name that was chosen. A typed name is
|
|
1567
|
+
never renumbered.
|
|
1568
|
+
|
|
1569
|
+
**The directory is created with `mkdir(2)` and never merged into one already there.** A name already
|
|
1570
|
+
taken by anything, file, directory or symlink, answers `a folder or file with that name already
|
|
1571
|
+
exists` and touches nothing, the same rule `archivework.rs` states for its own staging directories.
|
|
1572
|
+
Every other failure carries `src/error.rs`'s plain sentence as `msg`: a parent that vanished since the listing
|
|
1573
|
+
answers `item not found`, a parent the user cannot write `permission denied`, a read-only mount `the drive
|
|
1574
|
+
is read-only`, a name past `NAME_MAX` `that name is too long`.
|
|
1575
|
+
|
|
1576
|
+
Like `rename`, this answers on the loop's own thread on a local mount and never takes the
|
|
1577
|
+
one-operation slot; on a remote mount it runs on its own worker with the single-call deadline and
|
|
1578
|
+
answers `slow` first, then its `made` line when the write lands; see `slow`.
|
|
1579
|
+
|
|
1580
|
+
### meta
|
|
1581
|
+
|
|
1582
|
+
`{"c":"meta","row":<uint>,"text":<bool>,"media":<bool>,"archive":<bool>}`
|
|
1583
|
+
|
|
1584
|
+
Example: `{"c":"meta","row":4,"text":false,"media":false,"archive":true}`
|
|
1585
|
+
|
|
1586
|
+
The per-row extras the preview column names and a listing row does not carry, for **one row that a
|
|
1587
|
+
client actually asked for**, never a sweep. `row` indexes the current listing, and a `row` outside it
|
|
1588
|
+
is dropped in silence, so a client that changed directory has to re-ask once the new `rows` arrive.
|
|
1589
|
+
Answers one `meta` line on a thread, because a media row costs an `ffprobe` and the loop waits on it.
|
|
1590
|
+
|
|
1591
|
+
The three booleans are the client's own hints, taken from the classification it already made: `text`
|
|
1592
|
+
asks for a line count, `media` for a duration and sample rate, `archive` for the index. Each costs
|
|
1593
|
+
something, so none is inferred here. A file whose header parses as an image is never counted for
|
|
1594
|
+
lines whatever `text` says, because the newlines in a bitmap are a number nothing should be shown.
|
|
1595
|
+
|
|
1596
|
+
### shebang
|
|
1597
|
+
|
|
1598
|
+
`{"c":"shebang","path":"<string>","id":<uint>}`
|
|
1599
|
+
|
|
1600
|
+
Example: `{"c":"shebang","path":"/home/gm/run.sh","id":3}`
|
|
1601
|
+
|
|
1602
|
+
The cursor row's two-byte `#!` probe behind the Make executable menu row, for **one path the
|
|
1603
|
+
client named**, never a sweep: only a regular file without its owner execute bit is ever asked
|
|
1604
|
+
about. Answers one `shebang` line, `{"t":"shebang","path":"<string>","hasShebang":<bool>,"id":<uint>}`,
|
|
1605
|
+
on a thread, because an open on a hung mount never returns and the loop waits on nothing. The
|
|
1606
|
+
`id` echoes the request so a later answer never arms an earlier row: a client that asked twice
|
|
1607
|
+
keeps the newest `id` and drops any line naming an older one. A path that is not a regular file,
|
|
1608
|
+
or that cannot be read, answers `hasShebang` false rather than an error, so the row stays absent.
|
|
1609
|
+
|
|
1610
|
+
### code
|
|
1611
|
+
|
|
1612
|
+
`{"c":"code","token":<uint>,"slot":"<string>","path":"<string>"|"text":"<string>","name":"<string>","lang":"<string>","first":<uint>,"count":<uint>,"raw":<bool>,"csv":"<delimiter>","record":<uint>}`
|
|
1613
|
+
|
|
1614
|
+
Example: `{"c":"code","token":4,"slot":"look","path":"/home/gm/a.rs","first":40,"count":20}` and
|
|
1615
|
+
`{"c":"code","token":5,"slot":"fence:2","text":"fn main() {}","name":"x.rs","lang":"","first":1,"count":10}`
|
|
1616
|
+
|
|
1617
|
+
The syntax-coloured lines `first` to `first + count - 1` of **one file or one text a client
|
|
1618
|
+
actually asked for**, never a sweep. A request holding `text` sends the bytes the client already
|
|
1619
|
+
holds, with `name` picking the language when `lang` is empty (a Markdown info string names it in
|
|
1620
|
+
its first word); without `text` the backend opens `path`, which a client never sends for a file on
|
|
1621
|
+
a network or phone mount. `first` counts from 1 and `count` is capped at 400 lines (the line cap,
|
|
1622
|
+
`lineCap` in `ui/CodeLines.qml`, which asks a longer window in parts), absent meaning the cap. `raw` true skips the colouring and answers the lines plain. Answers one `code` line on a
|
|
1623
|
+
thread. A newer request on the same `slot` ends the older job and the older reply is never sent,
|
|
1624
|
+
so a client matches the `token` of what it last sent and drops any other. At most 1,048,576 bytes
|
|
1625
|
+
of a file are ever read, and `bat` runs in the same read-only jail as the other helpers, on the
|
|
1626
|
+
bytes the verdict was made on.
|
|
1627
|
+
|
|
1628
|
+
A raw table request may carry `csv`: `","` for CSV or `"\t"` for TSV. The backend scans
|
|
1629
|
+
the already loaded bytes as RFC 4180 records, with quotes opening only at a field start,
|
|
1630
|
+
`""` escaping a quote inside quotes and quotes in unquoted fields staying literal. It moves
|
|
1631
|
+
`first` back to the start of the record containing the asked first line, and extends the
|
|
1632
|
+
asked end through its record's last line, or EOF for an unterminated quote. Thus the reply
|
|
1633
|
+
contains whole records and can exceed the 400-line request cap, bounded by the 1 MiB size
|
|
1634
|
+
gate. No second file read or additional stat is made. An absent or invalid `csv`, or one
|
|
1635
|
+
without `raw: true`, keeps the ordinary line window.
|
|
1636
|
+
|
|
1637
|
+
A raw `csv` ask may also carry `record`, a 1-based record ordinal with the header at 1,
|
|
1638
|
+
instead of locating its window by the physical `first` line. The backend locates that
|
|
1639
|
+
record in the same scan and asks `count` physical lines from its start, extending the
|
|
1640
|
+
end through the last record as above. This lets a table jump directly by delegate index
|
|
1641
|
+
without fetching a prefix or receiving a file-wide index. `record` is ignored without a
|
|
1642
|
+
valid raw `csv` delimiter; zero is clamped to 1. A record beyond EOF returns no lines.
|
|
1643
|
+
|
|
1644
|
+
### html
|
|
1645
|
+
|
|
1646
|
+
`{"c":"html","token":<uint>,"slot":"<string>","path":"<absolute path>","width":<uint>,"height":<uint>,"cancel":<bool>}`
|
|
1647
|
+
|
|
1648
|
+
Example: `{"c":"html","token":3,"slot":"look","path":"/home/gm/index.html","width":984,"height":625}`.
|
|
1649
|
+
|
|
1650
|
+
Renders the single page a client is previewing, never a listing. Width is the frame's device
|
|
1651
|
+
width, clamped to 1 through 4096 pixels. Optional height is the viewport's height at that width;
|
|
1652
|
+
when width is clamped, height scales with it, rounded up and bounded to 1 through 8000 pixels.
|
|
1653
|
+
An omitted height retains the column helper's 600-pixel viewport. The page must be a readable regular file at most
|
|
1654
|
+
1,048,576 bytes. A newer request in the same slot cancels the older helper and suppresses its
|
|
1655
|
+
reply. `cancel:true` ends that slot without opening a page. HTML and code slots are independent.
|
|
1656
|
+
|
|
1657
|
+
The qml6 helper runs offscreen in its own bwrap namespace with network unshared, a 30-second
|
|
1658
|
+
CPU limit, and a 15-second wall deadline. Chromium needs large virtual reservations, so this
|
|
1659
|
+
helper has no address-space limit. Its profile is off the record, JavaScript is disabled, and
|
|
1660
|
+
navigation to another page is refused. Only the page folder is bound as /page, read-only; a
|
|
1661
|
+
content security policy restricts subresources to that folder and embedded data images/fonts.
|
|
1662
|
+
The one writable bind is a private helper directory under the thumbnail cache. All helper
|
|
1663
|
+
files are created exclusively. The confined input is bound over the page's own filename and
|
|
1664
|
+
loaded from that URL, preserving relative URLs and the document's own base element.
|
|
1665
|
+
The PNG is at least the requested viewport height and at most 8000 pixels tall, and cached by canonical
|
|
1666
|
+
path, size, nanosecond mtime, inode, device, width and viewport height. A hit also requires every
|
|
1667
|
+
engine-recorded local resource to retain its size, nanosecond mtime, inode and device. Records
|
|
1668
|
+
hold at most 256 local resources and 64 KiB; an unreadable or incomplete record forces another render.
|
|
1669
|
+
The companion cache uses the preview-cache bounds
|
|
1670
|
+
of 4096 files and 512 MiB, with a 60-second reader grace and one scan per 32 publications.
|
|
1671
|
+
|
|
1672
|
+
### compare
|
|
1673
|
+
|
|
1674
|
+
`{"c":"compare","token":<uint>,"a":"<string>","b":"<string>"}`
|
|
1675
|
+
|
|
1676
|
+
Example: `{"c":"compare","token":3,"a":"/home/gm/notes-a.md","b":"/home/gm/notes-b.md"}`
|
|
1677
|
+
|
|
1678
|
+
The unified diff of two regular files a client actually named, never a sweep; `a` is the first of the
|
|
1679
|
+
pair in the listing's order and `b` the second. Absolute paths and readable regular files are checked
|
|
1680
|
+
first; a refusal answers `failed`. Either file over 1,048,576 bytes then answers `over`, judged on both
|
|
1681
|
+
files before reading their contents. The bounded contents of each file are read in pair order: a read
|
|
1682
|
+
failure answers `failed`, growth past the limit answers `over`, a NUL byte in its first 8,192 bytes
|
|
1683
|
+
answers `binary`, and invalid UTF-8 anywhere answers `failed`. Code preview displays invalid UTF-8
|
|
1684
|
+
lossily, but a copied patch must preserve bytes, so Compare refuses it.
|
|
1685
|
+
Otherwise `/usr/bin/diff -u --label <a> --label <b> -- <a> <b>` runs once in the same read-only jail as
|
|
1686
|
+
the other helpers, with the two files bound read-only and nothing writable, so the patch names the real
|
|
1687
|
+
paths. The diff has a 2 s deadline; expiry kills its process tree and answers `failed`. Invalid UTF-8
|
|
1688
|
+
in the diff output also answers `failed`, so the clipboard never receives replacement bytes.
|
|
1689
|
+
Answers one `compare` line on a thread, and `token` echoes the request's so a client matches the
|
|
1690
|
+
one it last sent and drops any other.
|
|
1691
|
+
|
|
1692
|
+
### pdfcopy
|
|
1693
|
+
|
|
1694
|
+
`{"c":"pdfcopy","id":<uint>,"slot":"<string>","path":"<string>"}`
|
|
1695
|
+
|
|
1696
|
+
Example: `{"c":"pdfcopy","id":3,"slot":"column","path":"/run/media/gm/128GB/doc.pdf"}`
|
|
1697
|
+
|
|
1698
|
+
Fetches one PDF the preview actually opened into a session-private copy under an 8 s deadline, and
|
|
1699
|
+
answers one `pdfcopied` line: `{"t":"pdfcopied","id":3,"path":"<local copy>"}` on success, or
|
|
1700
|
+
`{"t":"pdfcopied","id":3,"err":"that file is not responding","stalled":true}` when the wait
|
|
1701
|
+
runs out. `err` rides only on a failure, and `stalled` only on the wait that ran out, so a client
|
|
1702
|
+
never reads `err` to tell the two apart. The `id` is minted by the one counter in `ui/Backend.qml`,
|
|
1703
|
+
so two viewers never share one; `slot` names the viewer, so a newer fetch supersedes only its own
|
|
1704
|
+
viewer's copy.
|
|
1705
|
+
The copy runs beside the loop, so a dead mount costs the viewer a sentence and
|
|
1706
|
+
never the window; a stale reply is dropped by its `id`, the way a superseded listing's result is.
|
|
1707
|
+
|
|
1708
|
+
### officepdf
|
|
1709
|
+
|
|
1710
|
+
`{"c":"officepdf","id":<uint>,"path":"<string>"}`
|
|
1711
|
+
|
|
1712
|
+
Example: `{"c":"officepdf","id":4,"path":"/home/gm/Documents/report.docx"}`
|
|
1713
|
+
|
|
1714
|
+
Converts one DOCX, ODT, XLSX, ODS, PPTX or RTF file to a cached PDF with LibreOffice and answers
|
|
1715
|
+
`officepdf` lines carrying the request `id`: `{"t":"officepdf","id":4,"state":"ready","path":"<cached pdf>"}`
|
|
1716
|
+
when the PDF exists, `{"t":"officepdf","id":4,"state":"converting"}` when a conversion started, and
|
|
1717
|
+
`{"t":"officepdf","id":4,"state":"failed","err":"<string>"}` when it cannot be had. A `converting`
|
|
1718
|
+
line is always followed by one `ready` or `failed` line for the same `id` once the job ends, so
|
|
1719
|
+
a client waits for that second line and treats `converting` as progress only. A request that
|
|
1720
|
+
needs no conversion answers `ready` or `failed` at once and sends no follow-up.
|
|
1721
|
+
`unavailable` rides on a `failed` line only when this file or this box will never convert:
|
|
1722
|
+
`{"t":"officepdf","id":4,"state":"failed","err":"LibreOffice is not installed on this box.","unavailable":true}`.
|
|
1723
|
+
A client that reads it keeps its existing preview and does not ask again; a `failed` line without
|
|
1724
|
+
it (a converter error, a timeout, a request replaced by a newer one, a mount that is not
|
|
1725
|
+
responding) may succeed on a later request, except that a converter error (an exit status or no
|
|
1726
|
+
PDF) is remembered for that file until its modification time changes; a timeout is not.
|
|
1727
|
+
The path is resolved and its cache entry looked up beside the loop under the mount deadline, so a
|
|
1728
|
+
hung mount costs one `failed` line and never the window. One conversion runs at a time and the
|
|
1729
|
+
newest request replaces the one queued behind it, whichever request finished its lookup first; a request older than the newest one started, running or already finished, is answered replaced at once and never converts.
|
|
1730
|
+
|
|
1731
|
+
### officesheets
|
|
1732
|
+
|
|
1733
|
+
`{"c":"officesheets","id":<uint>,"path":"<string>"}`
|
|
1734
|
+
|
|
1735
|
+
Example: `{"c":"officesheets","id":5,"path":"/home/gm/Documents/budget.xlsx"}`
|
|
1736
|
+
|
|
1737
|
+
Converts one XLSX, ODS or XLS workbook to one cached CSV per sheet with LibreOffice's CSV filter
|
|
1738
|
+
(UTF-8, comma separated, values as the spreadsheet shows them) and answers `officesheets` lines
|
|
1739
|
+
carrying the request `id`:
|
|
1740
|
+
`{"t":"officesheets","id":5,"state":"ready","names":["Monthly","Savings"],"paths":["<cached csv>","<cached csv>"]}`
|
|
1741
|
+
lists the sheets in the workbook's own order, each name beside its CSV at the same index.
|
|
1742
|
+
`converting` and `failed` lines, `unavailable`, the remembered converter error, the mount deadline
|
|
1743
|
+
and the one-at-a-time queue are those of `officepdf` above, with `"t":"officesheets"`; the two
|
|
1744
|
+
requests share that queue, and a workbook's PDF and its sheets are separate cache entries.
|
|
1745
|
+
A file that is not a spreadsheet answers
|
|
1746
|
+
`{"t":"officesheets","id":5,"state":"failed","err":"that file is not a spreadsheet","unavailable":true}`.
|
|
1747
|
+
The converter runs once over the whole workbook, then once per sheet to fix the order, skipping
|
|
1748
|
+
the last sheet, which is the one left; a workbook with more than 24 sheets lists its first 24, one
|
|
1749
|
+
order run each.
|
|
1750
|
+
|
|
1751
|
+
### settags
|
|
1752
|
+
|
|
1753
|
+
`{"c":"settags","rows":[<uint>,...],"op":"<string>","tag":"<string>","listing":<uint>}`
|
|
1754
|
+
|
|
1755
|
+
Example: `{"c":"settags","rows":[2,17],"op":"toggle","tag":"Red","listing":7}`
|
|
1756
|
+
|
|
1757
|
+
Adds, removes or toggles one tag across those rows of the current listing, or clears all tags
|
|
1758
|
+
with `"op":"clear"`, and answers one `settagged` line. A `toggle` is one decision for every row it
|
|
1759
|
+
names, made from the attributes read before the first write: where every row already carries the
|
|
1760
|
+
tag it is removed from all of them, otherwise it is added to all of them, so a set where only some
|
|
1761
|
+
rows carry it never loses the tag from those rows and never ends half toggled. Flea's seven names are `Red`, `Orange`,
|
|
1762
|
+
`Yellow`, `Green`, `Blue`, `Purple` and `Gray`, matched case-insensitively; a name Flea does
|
|
1763
|
+
not know is kept untouched, never rewritten and never dropped. **Every file or none**: a
|
|
1764
|
+
filesystem without user extended attributes refuses with an `error` line naming its filesystem
|
|
1765
|
+
before the first write, each filesystem the rows sit on asked once, and a file that vanished or
|
|
1766
|
+
a write that failed refuses all with exactly the files already written put back, so nothing is
|
|
1767
|
+
half-applied. A row that is neither a file nor a folder is skipped and named in the settagged answer's skipped array while the other rows are still written, so only a real write failure rolls anything back. The change is one undo journal step per file it changed, restoring that file's
|
|
1768
|
+
previous attribute exactly, absent included, and only onto the same file (its device and inode)
|
|
1769
|
+
still holding the attribute the change wrote: a file replaced or retagged since is left in place
|
|
1770
|
+
and named in the `error`, while the other files are restored. Redo writes only onto the attribute
|
|
1771
|
+
undo left: a file retagged since is skipped and named, and a file replaced since refuses the redo. A `settags` is refused with `an operation is already running` while a
|
|
1772
|
+
`transfer`, `trash`, `duplicate` or `link` holds the slot, though a local `rename` is not. Rows on local mounts
|
|
1773
|
+
are written on the loop; when any row sits on a remote mount the write runs on a worker, and past
|
|
1774
|
+
the single-call deadline it answers `slow` first and its `settagged` or `error` line later, the
|
|
1775
|
+
same way a remote `rename` does.
|
|
1776
|
+
|
|
1777
|
+
### tagstates
|
|
1778
|
+
|
|
1779
|
+
`{"c":"tagstates","rows":[<uint>,...],"id":<uint>,"listing":<uint>}`
|
|
1780
|
+
|
|
1781
|
+
Example: `{"c":"tagstates","rows":[0,4000],"id":3,"listing":7}`
|
|
1782
|
+
|
|
1783
|
+
Reads which of Flea's seven tags those rows of the current listing carry and answers one
|
|
1784
|
+
`tagstates` line with the same `id`. It reads only, so a stale `listing` is not refused; the answer
|
|
1785
|
+
names the numbering its rows resolved in instead. The shipped client sends it when a menu opens over
|
|
1786
|
+
a selection that reaches past the held window, whose rows it holds no tags for, and draws the Tags
|
|
1787
|
+
flyout's checked and mixed marks from the answer. The read runs on its own thread, the way `shebang`
|
|
1788
|
+
does, because an attribute read on a hung mount never returns; a file that cannot be read counts as
|
|
1789
|
+
carrying no tag.
|
|
1790
|
+
|
|
1791
|
+
### undo
|
|
1792
|
+
|
|
1793
|
+
`{"c":"undo"}`
|
|
1794
|
+
|
|
1795
|
+
Reverses the most recent completed operation and answers one `undone` line naming which kind it was.
|
|
1796
|
+
An empty journal answers an `error` line with `where` of `undo`, and so does a reversal that fails
|
|
1797
|
+
removing what an operation created or restoring from the trash. A reversal that renames back goes
|
|
1798
|
+
through the same call a `rename` does, so its failure answers `rename`, `rename-kept` or `rename-stranded` instead.
|
|
1799
|
+
|
|
1800
|
+
**The journal is the session journal under $XDG_RUNTIME_DIR/flea**, newest entry from any window, surviving a backend restart but not the session, with an in-memory fallback when the dir is refused. An entry too big for the file stores a small barrier, its op name only and no paths, and no window keeps the payload: the undo that claims it answers `That operation was too large to undo.`, the next undo continues with older entries, and a barrier never enters redo. A file written by a newer version is unavailable, never rewritten: the reader falls back to memory. Each kind reverses as follows: a rename or a move renames back (still
|
|
1801
|
+
refusing to clobber, because something may occupy the old name by now), a copy or a duplicate removes
|
|
1802
|
+
what that operation created, a merge is undone only in the window that made it and elsewhere refuses
|
|
1803
|
+
rather than falling back to the whole folder, a link removes only the link it made, and a trash restores through
|
|
1804
|
+
`gio trash --restore` using the URI captured
|
|
1805
|
+
when it was trashed. A hard link whose source is gone or was replaced stays as the last name holding
|
|
1806
|
+
those bytes. A transfer that replaced an item reverses both halves in that one step, newest
|
|
1807
|
+
first: the incoming item is removed or moved back, and then the item it replaced is restored from the
|
|
1808
|
+
trash to its name. A reversal that fails stops the rest and is spent, except a `Mode` step whose file
|
|
1809
|
+
was replaced or whose mode changed since: that step is skipped with a note while the rest restore, and
|
|
1810
|
+
redo skips the same way. So when the incoming half cannot
|
|
1811
|
+
go (a partial folder copy with a file inside newer than its root) or the trash cannot be read back (a
|
|
1812
|
+
`gio` with no `trash://` to list), the replaced item stays in the Trash, restorable from the Trash
|
|
1813
|
+
browser rather than by `undo`. A partial `permissions` undo answers `N items were left in place` (`1 item was left in place` for one) and, when it restored any,
|
|
1814
|
+
names how many it restored for redo, which replays the undone half before anything older. A `mkdir` removes the folder it made only while it is still empty: a folder the
|
|
1815
|
+
user has filled since is theirs, so that reversal answers an `error` line, leaves it and its contents in
|
|
1816
|
+
place, and is spent like any failed reversal, so the next `undo` reaches the operation before it.
|
|
1817
|
+
|
|
1818
|
+
**Only what an operation actually created or moved is recorded**, never a path it merely read, so an
|
|
1819
|
+
undo can never delete a file the operation did not put there. An operation that changed nothing records
|
|
1820
|
+
nothing, and a cancelled batch records only the items that succeeded. An item that failed short of a
|
|
1821
|
+
cancel, and had already created its destination, records that partial destination too: the failure
|
|
1822
|
+
leaves it on disk, because removing it on a transient error would destroy data, and the journal is what
|
|
1823
|
+
lets `undo` remove it. A destination that already existed is never recorded, because nothing was created
|
|
1824
|
+
there. The steps of one operation reverse
|
|
1825
|
+
newest first, and a step that fails stops the rest rather than leaving the operation half-reversed with
|
|
1826
|
+
nothing recording which half, except a `Mode` step, which is skipped with a note while the rest restore.
|
|
1827
|
+
|
|
1828
|
+
### jump
|
|
1829
|
+
|
|
1830
|
+
`{"c":"jump","id":<uint>,"ranking":<uint>,"favourites":[<string>,...],"recent":[<string>,...]}`
|
|
1831
|
+
|
|
1832
|
+
Example: `{"c":"jump","id":3,"favourites":["/home/gm/Projects"],"recent":["/home/gm/Pictures/screenshots/shot.png"]}`
|
|
1833
|
+
|
|
1834
|
+
A whole ask that follows a provisional one names it: `{"c":"jump","id":4,"ranking":3,"favourites":["/home/gm/Projects"],"recent":["/home/gm/Pictures/screenshots/shot.png"]}`.
|
|
1835
|
+
|
|
1836
|
+
The path bar's folder jump, asked **once per open of the bar**, never per keystroke: the client filters
|
|
1837
|
+
the answer itself as the line changes. `favourites` are Flea's own favourites in their rail order, and
|
|
1838
|
+
`recent` is the desktop's `recently-used.xbel` newest first, which the client reads with Qt's XML reader
|
|
1839
|
+
because the backend has none, and keeps until the file changes. The backend adds the third source,
|
|
1840
|
+
zoxide's ranking, from one `zoxide query --list --all --score`, and answers one `jumped` line from a thread, because zoxide is a subprocess
|
|
1841
|
+
and a stat can block on a network mount. `id` is the client's own number for this open and comes back
|
|
1842
|
+
on the answer, so an answer to an earlier open is told apart from this one's. `ranking` is optional (absent
|
|
1843
|
+
or 0 names none): an open whose recent history is stale asks twice, a provisional ask with what the client
|
|
1844
|
+
has kept and then the whole ask under a new `id`, and the whole ask's `ranking` is the provisional ask's `id`.
|
|
1845
|
+
The backend keeps the zoxide ranking of the newest run that answered in time, with the `id` it answered, and
|
|
1846
|
+
answers a `ranking` naming that id from it with no second zoxide run, so every open runs zoxide exactly once;
|
|
1847
|
+
the cross-source dedup still happens in each answer. A `ranking` naming any other id, or one no run kept
|
|
1848
|
+
(zoxide still running or past its limit, the ranking replaced by a newer ask), runs zoxide as an ask with none does.
|
|
1849
|
+
|
|
1850
|
+
**Nothing is written.** `--all` is what keeps zoxide from pruning its own database on a query Flea made:
|
|
1851
|
+
without it zoxide deletes entries missing for 90 days and saves. zoxide is optional: absent, it is an
|
|
1852
|
+
empty source and says nothing. A zoxide that runs past 2 s is killed and draws nothing, and its answer is
|
|
1853
|
+
read up to 1 MiB and its first 1,000 rows. Only one zoxide runs at a time: an open while an earlier one is
|
|
1854
|
+
still running draws the last ranking that answered in time rather than starting a second, and an
|
|
1855
|
+
open with no ranking kept yet draws no zoxide. Each source is checked on a thread of its
|
|
1856
|
+
own against one shared 1 s budget, so a stat wedged on a dead mount drops that source's rows from that one on
|
|
1857
|
+
and never the other sources. A recent file is checked only once the folder holding it resolved, on that
|
|
1858
|
+
same budget and under that same key rule, so a wedged recent file costs its own row and never the answer. A check still running past the budget of the open that started it is wedged, and
|
|
1859
|
+
the next open skips what it names rather than wedge behind it: its whole mount when that mount is NFS, SMB,
|
|
1860
|
+
9p, Ceph, AFS or FUSE (read lexically from `/proc/self/mountinfo`, never through the mount), its own path
|
|
1861
|
+
anywhere else. A dead share therefore holds at most one thread per source for good, not more per open, and a
|
|
1862
|
+
check merely slow inside its budget is run again rather than skipped.
|
|
1863
|
+
|
|
1864
|
+
### devs
|
|
1865
|
+
|
|
1866
|
+
`{"c":"devs","paths":[<string>,...],"id":<uint optional>}`
|
|
1867
|
+
|
|
1868
|
+
Example: `{"c":"devs","paths":["/","/home/gm","/home/gm/Documents"]}`
|
|
1869
|
+
|
|
1870
|
+
The device number of each path named, symlinks followed, so a path reached through a link names the volume the
|
|
1871
|
+
link points at. The device is the number `listed`'s `v` already gives for the listing itself, which is what lets
|
|
1872
|
+
a client compare the two without a table of mounts: a plain drop onto a path moves when its device equals the
|
|
1873
|
+
drag's source device and copies otherwise. The client asks **once per listing** for the ancestors of the listed
|
|
1874
|
+
path, the breadcrumb's drop targets, and **once per drag** for the visible folder rows of the neighbour columns,
|
|
1875
|
+
whose peeked rows carry no device, and keeps the answers by path. Answers one `devs` line from a thread of its
|
|
1876
|
+
own, because a stat on a mount that stopped answering never returns and the loop waits on nothing. A path on a
|
|
1877
|
+
network or FUSE mount runs under the same 5 s bound a listing has, then the mount is marked stuck for 30 s and
|
|
1878
|
+
answers 0 at once, so a dead share costs one thread and one wait, never one per ask. An absent `paths` asks nothing.
|
|
1879
|
+
Rail hover requests include a positive `id`, echoed in the answer, so leaving or changing a row discards its late answer.
|
|
1880
|
+
|
|
1881
|
+
### clipSet
|
|
1882
|
+
|
|
1883
|
+
`"op":"text"` uses the same queued detached owner with `paths` containing exactly
|
|
1884
|
+
one NUL-free text value. It offers only `text/plain;charset=utf-8`, `text/plain` and
|
|
1885
|
+
`UTF8_STRING`, with no file URI list, copy/cut marker or file token. It never changes
|
|
1886
|
+
the TUI's copied-file selection. TUI copy-as-text joins held selected paths, URIs,
|
|
1887
|
+
parent paths/URIs, filenames or stems with newlines. URI encoding preserves UTF-8
|
|
1888
|
+
bytes and escapes every byte outside the unreserved set and slash.
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
`{"c":"clipSet","op":"copy"|"cut","paths":[<string>,...]}`
|
|
1892
|
+
|
|
1893
|
+
Owns the system clipboard for those files, so another Flea window (or Nautilus, or
|
|
1894
|
+
Thunar) pastes what this one copied. `paths` are absolute; anything relative, holding a
|
|
1895
|
+
NUL or climbing through `..` is refused with an `ok:false` clip line and nothing is
|
|
1896
|
+
owned. The backend makes a token, spawns a detached `flea --clip-own` holding the same
|
|
1897
|
+
binary, writes it the payload and waits up to 2 s for its `ready`. Runs off the request
|
|
1898
|
+
thread, the way `clipGet` does, so an owner that never answers holds up no other request.
|
|
1899
|
+
Answers one `clip` line with `op` of `set`; see `clip` below.
|
|
1900
|
+
|
|
1901
|
+
### clipGet
|
|
1902
|
+
|
|
1903
|
+
`{"c":"clipGet"}`
|
|
1904
|
+
|
|
1905
|
+
Reads the current system selection as files. Runs off the request thread, the way other
|
|
1906
|
+
slow reads do, because a foreign source can take up to 2 s to close its pipe. The
|
|
1907
|
+
owner's own token type is read first, then GNOME's copied-files shape, then the
|
|
1908
|
+
uri-list with KDE's cut marker. With no file paths, an `image/png` offer answers
|
|
1909
|
+
`image` without reading its bytes, including when HTML or browser types are offered
|
|
1910
|
+
beside it. An empty or text-only selection answers `none`. Answers
|
|
1911
|
+
one `clip` line with `op` of `get`; see `clip` below.
|
|
1912
|
+
|
|
1913
|
+
### clipClear
|
|
1914
|
+
|
|
1915
|
+
`{"c":"clipClear","token":<string>}` or `{"c":"clipClear","cut":[<string>,...]}`
|
|
1916
|
+
|
|
1917
|
+
Clears the selection only while it still carries that token, the one `clipSet`
|
|
1918
|
+
answered with or `clipGet` read from another Flea process. A live owner started by
|
|
1919
|
+
this backend is withdrawn first. Otherwise, including when that owner has exited,
|
|
1920
|
+
the token form uses the same token check, queued-selection drain and null selection
|
|
1921
|
+
as `flea --clip clear`, so it can clear a selection another Flea process owns.
|
|
1922
|
+
`cleared:true` means a live owner was signalled or the verified clear sent the null
|
|
1923
|
+
selection. Both clear forms run on the clipboard mutation queue, off the request
|
|
1924
|
+
thread. The `cut` form is a spent cut from another application, which
|
|
1925
|
+
Nautilus and Thunar clear after pasting: it clears only when the current selection is
|
|
1926
|
+
still a cut whose path list equals the given one exactly, same order, and never
|
|
1927
|
+
clears a copy. The verified token clear, `cut` form and `flea --clip clear` check the
|
|
1928
|
+
selection, then drain what the compositor queued behind it with one more round trip,
|
|
1929
|
+
and send the null selection only while the checked offer is still the current one;
|
|
1930
|
+
a newer copy answers `cleared:false`. The protocol has no compare-and-clear request,
|
|
1931
|
+
so a copy made after that last round trip and before the null selection lands can
|
|
1932
|
+
still be wiped. Answers one `clip` line with `op` of `clear`, saying whether it
|
|
1933
|
+
cleared; see `clip` below.
|
|
1934
|
+
|
|
1935
|
+
### clipWatch
|
|
1936
|
+
|
|
1937
|
+
In flea-qml, the QML frontend sends `{"c":"clipWatch","hostText":true}` to
|
|
1938
|
+
advertise its Quickshell clipboard fallback. If Wayland connection/data-control
|
|
1939
|
+
initialization fails, the backend uses this bridge for the lifetime of that
|
|
1940
|
+
backend. Older callers without the capability keep the ordinary Wayland error.
|
|
1941
|
+
The changed/error notification switches Paste to on-demand reads; no host polling
|
|
1942
|
+
is introduced. A working Wayland connection keeps its existing MIME offers.
|
|
1943
|
+
|
|
1944
|
+
Bridge requests are `{"t":"clipHost","id":N,"op":"read"|"write"|"clear","text":S}`
|
|
1945
|
+
(`text` is absent for read). The frontend answers
|
|
1946
|
+
`{"c":"clipHostReply","id":N,"ok":true,"text":S}` for reads, an empty success
|
|
1947
|
+
for writes, or `cleared:true|false` for compare-and-clear. Failures use `ok:false`
|
|
1948
|
+
and `error`. Replies time out after 15 seconds and late/unknown IDs are ignored.
|
|
1949
|
+
The frontend serializes operations across panes, with fresh reads on Paste.
|
|
1950
|
+
Clear compares the expected text before writing an empty value; external OS
|
|
1951
|
+
writers can still race this check because the OS has no atomic compare-and-clear.
|
|
1952
|
+
|
|
1953
|
+
The text format remains POSIX-quoted paths separated by spaces; cut adds two
|
|
1954
|
+
literal double quotes at **each** end: `""'/path/a' '/path/b'""`. This format is
|
|
1955
|
+
unchanged on Windows and is never PowerShell-escaped. Parsing and path validation
|
|
1956
|
+
remain in the backend. Non-file text returns `none`; the fallback does not add
|
|
1957
|
+
image clipboard support. Browser QS uses awaitable read/write results; native QS
|
|
1958
|
+
uses its standard synchronous `clipboardText` property. Browser QS always tries
|
|
1959
|
+
the host first, but falls back to its last local text on denied or empty reads,
|
|
1960
|
+
matching Land. An external clear is indistinguishable from an Android denied
|
|
1961
|
+
empty read; explicitly writing empty text clears the local cache. Host write
|
|
1962
|
+
failures still report an error even though the local text remains available.
|
|
1963
|
+
|
|
1964
|
+
`{"c":"clipWatch"}`
|
|
1965
|
+
|
|
1966
|
+
Starts the clipboard watcher: one thread holding one data-control connection, reporting
|
|
1967
|
+
every clipboard selection (never the primary one, so a text highlight changes nothing)
|
|
1968
|
+
as files the same way `clipGet` reads them. Idempotent; the UI sends it
|
|
1969
|
+
once at start and later ones answer nothing. No reply of its own: file selections arrive
|
|
1970
|
+
as `clip` lines with `op` of `changed`, a PNG offer without files as `image`, an
|
|
1971
|
+
empty or text-only selection as `none`, and
|
|
1972
|
+
two identical selections in a row emit once. No compositor or manager ends the thread
|
|
1973
|
+
after one `changed` `none` line carrying the error; a dropped connection reconnects at
|
|
1974
|
+
most once a second, at most 5 times.
|
|
1975
|
+
|
|
1976
|
+
Flea's private `application/x-flea-clip` payload is `copy|cut TOKEN PID`, where TOKEN
|
|
1977
|
+
is 32 hex characters and PID is the detached owner's process id. The older
|
|
1978
|
+
`copy|cut TOKEN` payload remains readable but supplies no owner-exit trigger.
|
|
1979
|
+
The two-field form was written only by 0.3.8 development builds, never by a release.
|
|
1980
|
+
Such a build rejects the three-field payload and reads the selection through the GNOME
|
|
1981
|
+
fallback, so the paths and the cut operation still arrive and only the token is lost.
|
|
1982
|
+
Flea 0.3.7 and older read no file selection.
|
|
1983
|
+
Every watching backend, including windows that did not start the owner, holds a
|
|
1984
|
+
pidfd for a reported Flea selection's live owner. A pid already gone at report time
|
|
1985
|
+
(pidfd open returns ESRCH or the pidfd is already readable) triggers the same single
|
|
1986
|
+
guarded fresh read as a later exit. A missing pid, a live process whose executable
|
|
1987
|
+
or arguments do not identify this Flea binary's `--clip-own`, or another pidfd open
|
|
1988
|
+
failure supplies no trigger and no report.
|
|
1989
|
+
The watcher uses one reusable waiter thread and replaces its pidfd wait slot when
|
|
1990
|
+
the reported selection changes; dropping the watching connection closes the slot
|
|
1991
|
+
and stops the waiter. No per-copy thread or descriptor accumulates.
|
|
1992
|
+
|
|
1993
|
+
An owner's exit triggers a fresh connection and the same selection-reading path
|
|
1994
|
+
as `clipGet`. It is never evidence that the clipboard is empty: a clipboard manager
|
|
1995
|
+
may still be re-serving that owner's token. The reading shares the watcher's
|
|
1996
|
+
deduping emitter and is applied only if that owner's token is still the last
|
|
1997
|
+
reported token and no newer selection event has arrived when the reading returns.
|
|
1998
|
+
A watcher offer read captures the report generation when its selection event arrives
|
|
1999
|
+
and is discarded if an exit read begun after that event reports while its bytes are
|
|
2000
|
+
pending, so a delayed offer cannot restore an ended token.
|
|
2001
|
+
Reading the same token emits nothing; a failed reading emits nothing
|
|
2002
|
+
and is not retried. The spawning backend's reaper only reaps, with no separate
|
|
2003
|
+
clipboard notification hook.
|
|
2004
|
+
|
|
2005
|
+
Hyprland gives data-control clients no event when a selection is emptied. The
|
|
2006
|
+
pidfd trigger covers Flea owners; a selection owned by another program and emptied
|
|
2007
|
+
on Hyprland reports nothing until the next selection. Paste always reads fresh,
|
|
2008
|
+
so no file operation can act on that stale watcher state. See `clip` below.
|
|
2009
|
+
|
|
2010
|
+
### plugins
|
|
2011
|
+
|
|
2012
|
+
`{"c":"plugins","op":"list","id":1}`, and `enable`, `disable`, `update` and `remove` with `"plugin":"<id>"`,
|
|
2013
|
+
`set` with `"plugin"`, `"key"` and `"value"`, and `add` with `"source"` (a Git URL, or an absolute folder or `~/`
|
|
2014
|
+
folder). For `validate`, supply `"source"` as an absolute folder path; it neither clones Git nor expands a literal
|
|
2015
|
+
`~/` prefix. `runtime` takes `"plugin"` and a nested `"request"` object, as described under **plugins runtime**.
|
|
2016
|
+
|
|
2017
|
+
Settings > Plugins and `flea plugin <verb>` use the management operations. Requests are answered on their own
|
|
2018
|
+
threads, so a clone, a pull or a start check does not hold the loop. `list` answers one `plugins` line. The management
|
|
2019
|
+
operations `enable`, `disable`, `set`, `add`, `update`, `remove` and `validate` answer one `pluginop` line and then
|
|
2020
|
+
the `plugins` listing they left, both carrying the request's `id`. Management changes run one at a time. A `runtime`
|
|
2021
|
+
request instead answers one terminal `pluginruntime` line, optionally preceded by `pluginprogress` lines; it does
|
|
2022
|
+
not append a `pluginop` line or a `plugins` listing.
|
|
2023
|
+
|
|
2024
|
+
The listing names the four built-ins first (Shelf, LocalSend, Tailscale, Dropbox), then every folder under
|
|
2025
|
+
`$XDG_CONFIG_HOME/flea/plugins/` whose name does not start with a dot, in name order. Each plugin carries its
|
|
2026
|
+
manifest's facts, `enabled`, `source` (`builtin`, `git` with the origin in the plugin's own `.git` folder, `folder`
|
|
2027
|
+
with the folder it was added from, or empty), its `schema` in the manifest's own shape and its `settings` clamped to that schema. The Shelf also carries
|
|
2028
|
+
`shelf`, the ui.json shelf group and its pins. `failure` names why an enabled plugin cannot run, or why a folder is
|
|
2029
|
+
not a plugin, as a code with one `detail`; it is empty otherwise.
|
|
2030
|
+
|
|
2031
|
+
The Shelf's switch is `shelf.enabled`, and a change of it installs or removes the bar plugin as `--ui-state` does. Every
|
|
2032
|
+
other switch and setting lives in ui.json's `plugins` map, and `set` stores the value clamped. `add` copies or clones
|
|
2033
|
+
into a dot folder first and renames it into place only when its manifest is valid and its id is free, so a failed add
|
|
2034
|
+
leaves nothing; an added plugin arrives disabled. A folder add copies regular files and folders only, skips every dot
|
|
2035
|
+
entry (`.git` included), and refuses a tree past 4096 entries or 64 MB with `write` before anything is written; each
|
|
2036
|
+
file opens without following a link and without blocking, and anything but a regular file refuses the add. A Git add
|
|
2037
|
+
checks its clone against the same 4096 entries and 64 MB, counting every tracked entry, dot entries included, and
|
|
2038
|
+
setting aside only the clone's own top-level `.git`; past either it refuses with `write` before the clone moves into
|
|
2039
|
+
place. The listing reads a Git plugin's `.git/config` only as a regular file of at most 64 KiB, and `update` lists it
|
|
2040
|
+
through Git under a ten second deadline. A folder add records its folder in `.sources/<id>` under the plugins folder, never inside the
|
|
2041
|
+
plugin; the listing reads that record only as a regular file of at most 4 KiB, and `remove` deletes it.
|
|
2042
|
+
`enable` saves the switch and then sends the plugin's executable `hello`; the five second deadline starts before
|
|
2043
|
+
`hello` is written, and no `ready` within it records `start` until the plugin is switched off, updated or starts.
|
|
2044
|
+
`update` is `git pull --ff-only` and reaches only a plugin added from Git. Every Git call runs without system or user
|
|
2045
|
+
config, hooks or fsmonitor, and `update` refuses with `git` a plugin whose `.git` is not a plain folder or whose config
|
|
2046
|
+
holds anything a clone does not write (`core.worktree`, `core.hooksPath` and `core.fsmonitor` among them).
|
|
2047
|
+
`remove` deletes the plugin's folder and its settings; a built-in refuses `remove` and `update` with `builtin`.
|
|
2048
|
+
|
|
2049
|
+
### plugins runtime
|
|
2050
|
+
|
|
2051
|
+
```json
|
|
2052
|
+
{"c":"plugins","op":"runtime","id":41,"plugin":"example.send-target","request":{"op":"targets"}}
|
|
2053
|
+
```
|
|
2054
|
+
|
|
2055
|
+
`plugin` identifies the installed plugin. `id` correlates the outer request with its progress and terminal replies.
|
|
2056
|
+
`request` is the plugin operation object. The outer `op` is always `runtime`; the nested `op` selects the operation.
|
|
2057
|
+
This envelope is accepted by `flea --backend` and the application-wide `flea --plugin-host` process. The latter
|
|
2058
|
+
accepts only `c:"plugins"` requests.
|
|
2059
|
+
|
|
2060
|
+
Except for `cancel`, the plugin must be enabled and have no recorded failure. Ordinary operations must match a kind
|
|
2061
|
+
declared by the manifest. `run` must also name a declared action. A `folder` field requires declared `folder` access
|
|
2062
|
+
and an absolute path without NUL. If supplied, `paths` is an array of at most 512 absolute paths without NUL;
|
|
2063
|
+
`targets` and `section` refuse a `paths` field. An action that declares text input requires the matching one-key
|
|
2064
|
+
`input` object with a nonblank string within its declared character limit and no control characters.
|
|
2065
|
+
|
|
2066
|
+
| Nested `op` | Declared kind | Terminal plugin `op` |
|
|
2067
|
+
| --- | --- | --- |
|
|
2068
|
+
| `targets` | `send-target` | `targets`, with a `targets` array. |
|
|
2069
|
+
| `send` | `send-target` | `done`. |
|
|
2070
|
+
| `run` | `menu-action` | `done`. |
|
|
2071
|
+
| `section` | `rail-section` | `section`, with a `rows` array. |
|
|
2072
|
+
| `list` | `place` | `list`, with an `entries` array. |
|
|
2073
|
+
| `copy`, `move`, `rename`, `delete` | `place` | `done`. |
|
|
2074
|
+
| `preview` | `previewer` | `preview`. |
|
|
2075
|
+
| `shelf` | `shelf` | `done`. |
|
|
2076
|
+
| `settings` | A plugin eligible for a lasting session | `done`. |
|
|
2077
|
+
|
|
2078
|
+
Every ordinary operation may finish with `{"op":"error","message":"..."}` instead. An operation whose normal
|
|
2079
|
+
terminal response is `done` may emit progress before that response. See [PLUGINS.md](../PLUGINS.md) for the
|
|
2080
|
+
operation-specific request and response fields and the shelf routing rules.
|
|
2081
|
+
|
|
2082
|
+
For a preview, the backend envelope names the actual selected local file:
|
|
2083
|
+
|
|
2084
|
+
```json
|
|
2085
|
+
{"c":"plugins","op":"runtime","id":47,"plugin":"example.torrent-preview","request":{"op":"preview","paths":["/home/me/sample.torrent"],"width":900,"height":600}}
|
|
2086
|
+
```
|
|
2087
|
+
|
|
2088
|
+
The file must be a regular file matching the previewer's declared MIME patterns. The backend binds that file
|
|
2089
|
+
read-only inside the sandbox and sends the executable `{"op":"preview","path":"/in/file","outDir":"/out","width":900,"height":600}`.
|
|
2090
|
+
The `/in/file` path in the guide's executable exchange is a sandbox path, not the host file to send in the outer
|
|
2091
|
+
request. `width` and `height` accept numbers from 1 through 8,192 and are rounded; each defaults to 900 when absent
|
|
2092
|
+
or invalid.
|
|
2093
|
+
|
|
2094
|
+
Two nested operations are handled by the host rather than forwarded as ordinary plugin requests:
|
|
2095
|
+
|
|
2096
|
+
```json
|
|
2097
|
+
{"c":"plugins","op":"runtime","id":42,"plugin":"example.send-target","request":{"op":"ready"}}
|
|
2098
|
+
{"c":"plugins","op":"runtime","id":43,"plugin":"example.send-target","request":{"op":"cancel"}}
|
|
2099
|
+
```
|
|
2100
|
+
|
|
2101
|
+
For an installed executable plugin, `ready` checks startup and returns `message:{"op":"ready"}` on success.
|
|
2102
|
+
Built-ins use in-process adapters rather than the executable handshake. `cancel` stops that installed plugin's
|
|
2103
|
+
lasting session and returns `message:{"op":"done"}`. Cancellation identifies the plugin, not a particular request
|
|
2104
|
+
id. When a lasting subprocess is stopped, Flea attempts to write `{"op":"cancel"}` and immediately kills its
|
|
2105
|
+
process group; the subprocess has no guaranteed cleanup interval.
|
|
2106
|
+
|
|
2107
|
+
The subprocess protocol uses UTF-8 JSON objects separated by newlines. Protocol version 1 is negotiated on that
|
|
2108
|
+
separate stream, rather than on the outer backend envelope. The initial exchange is:
|
|
2109
|
+
|
|
2110
|
+
```json
|
|
2111
|
+
{"op":"hello","protocolVersion":1,"flea":"0.4.0","language":"en","decimal":".","settings":{},"kinds":["send-target"]}
|
|
2112
|
+
{"op":"ready","protocolVersion":1}
|
|
2113
|
+
```
|
|
2114
|
+
|
|
2115
|
+
`flea` is the package version; `language` and `decimal` describe Flea's locale. `settings` contains values clamped to
|
|
2116
|
+
the manifest's schema, and `kinds` contains its declared kinds. The startup deadline is five seconds. A `ready`
|
|
2117
|
+
response may omit `protocolVersion`; if present, it must be the number `1`.
|
|
2118
|
+
|
|
2119
|
+
A lasting session handles one call at a time. A concurrent ordinary request is refused with `busy` rather than
|
|
2120
|
+
queued. If the host's `ready` check collides with an active call, its refusal instead uses `start`.
|
|
2121
|
+
Previewer-only plugins do not accept runtime `settings` calls; their fresh startup hello supplies settings.
|
|
2122
|
+
Ordinary lasting-session requests have a 30-second deadline. The subprocess is stopped after 30 seconds idle; a
|
|
2123
|
+
later request starts a new subprocess. Previewers instead run once per request in the thumbnail sandbox, with a
|
|
2124
|
+
10-second response deadline after readiness.
|
|
2125
|
+
|
|
2126
|
+
Subprocess messages are limited to 65,536 bytes, result arrays to 256 rows, and progress to 128 messages and
|
|
2127
|
+
1,048,576 aggregate bytes per request. These are subprocess message limits, not the size of a terminal backend
|
|
2128
|
+
envelope that also contains retained progress. Required row strings and `op`, error `message`, progress `message`
|
|
2129
|
+
and preview `png` strings are at most 4,096 bytes and contain no NUL. Row identities (`id`, or `name` for entries)
|
|
2130
|
+
must be nonempty and unique within an answer. Entry names additionally cannot be `.`, `..` or contain `/`.
|
|
2131
|
+
Preview `text` is bounded by the message size rather than the 4,096-byte string limit.
|
|
2132
|
+
|
|
2133
|
+
### quit
|
|
2134
|
+
|
|
2135
|
+
`{"c":"quit"}`
|
|
2136
|
+
|
|
2137
|
+
Stops the read loop, then answers the thumbnail work already asked for: every queued and
|
|
2138
|
+
running job is drained and reported before the process exits, because a worker inside a
|
|
2139
|
+
child owns a temp file in the shared cache that only its own return publishes or removes.
|
|
2140
|
+
Draining is bounded; see `thumbed` below. Apart from those `thumbed` lines, no response.
|
|
2141
|
+
|
|
2142
|
+
**A queued `dirsize` row is not drained.** It answers nothing that outlives this process, unlike
|
|
2143
|
+
a thumbnail's shared on-disk cache, so a row still waiting when `quit` arrives is simply dropped;
|
|
2144
|
+
the client that asked for it is going away too. A `collisions` question still being asked is dropped the
|
|
2145
|
+
same way, which is why a scripted client waits for its line before sending `quit`.
|
|
2146
|
+
|
|
2147
|
+
## Responses
|
|
2148
|
+
|
|
2149
|
+
### gitstatus
|
|
2150
|
+
|
|
2151
|
+
`{"t":"gitstatus","id":1,"path":"/repo","ok":true,"enabled":true,"root":"/repo","branch":"main","label":"main · 8 changed","changed":8}`
|
|
2152
|
+
|
|
2153
|
+
`t` names the request, `id` and `path` echo its correlation, and `ok` reports success. `root`
|
|
2154
|
+
is the canonical worktree root, `branch` its branch name (`detached` for detached HEAD),
|
|
2155
|
+
`changed` the unique changed-path count after coalescing, and `label` the exact branch-caption text derived
|
|
2156
|
+
from that count. An unborn branch keeps its real name; a clean repository says `main · 0 changed`.
|
|
2157
|
+
The normal window's branch caption is muted; in Changes it is foreground. The illustrated
|
|
2158
|
+
`main · 8 changed` count is data, independent of the ordinary listing's item count.
|
|
2159
|
+
|
|
2160
|
+
Disabled replies have `enabled:false`, empty `root`, `branch` and `label`, and `changed:0`.
|
|
2161
|
+
An enabled directory outside a repository has the same empty values with `enabled:true`.
|
|
2162
|
+
Clients hide Git decoration in both cases. A failed status clears stale decoration while
|
|
2163
|
+
ordinary file browsing remains usable. A failed load retries on the next enabled status request;
|
|
2164
|
+
the existing mount quarantine refuses filesystem work until its stuck TTL expires.
|
|
2165
|
+
|
|
2166
|
+
### gitrows
|
|
2167
|
+
|
|
2168
|
+
`{"t":"gitrows","id":2,"path":"/repo","ok":true,"marks":[{"path":"/repo/Code","status":"","folder":true},{"path":"/repo/notes.txt","status":"M","folder":false}]}`
|
|
2169
|
+
|
|
2170
|
+
`marks` contains each asked absolute `path`, its `status`, and whether it is a `folder` with
|
|
2171
|
+
changes inside. A clean row has `status:"",folder:false`. Non-empty statuses are exactly
|
|
2172
|
+
`M` modified, `A` added, `D` deleted and `?` untracked. A deleted flag in an ordinary or
|
|
2173
|
+
rename/copy record takes precedence, so an `RD` destination uses `D`; other rename/copy
|
|
2174
|
+
destinations use `A`, and other tracked changes use `M`.
|
|
2175
|
+
An unmerged record uses its worktree mode: a present conflicted path uses `M`, including `UD` and
|
|
2176
|
+
`DU` modify/delete conflicts, while a zero worktree mode uses `D`. No per-file stat is needed.
|
|
2177
|
+
A coalesced index-deletion/untracked pair instead emits one `?`, as described under `gitstatus`.
|
|
2178
|
+
A folder square takes precedence over a simultaneous
|
|
2179
|
+
letter for a submodule or embedded repository. The window square is 6 px, yellow and centred
|
|
2180
|
+
in the 14 px mark column. Resting letters use yellow, green, error and muted roles respectively,
|
|
2181
|
+
each at least 4.5:1; cursor-row text uses foreground. `folder` supplies the same yellow `■`
|
|
2182
|
+
to the TUI's one-cell column. No marks outside the viewport are requested or painted.
|
|
2183
|
+
|
|
2184
|
+
### gitchanges
|
|
2185
|
+
|
|
2186
|
+
`{"t":"gitchanges","id":3,"path":"/repo","ok":true,"root":"/repo","label":"main · 8 changed","total":8,"start":0,"entries":[{"path":"Code/lib.rs","status":"D","original":"","deleted":true}]}`
|
|
2187
|
+
|
|
2188
|
+
`root` and `label` identify the same cached snapshot as `gitstatus`; `total` is its complete
|
|
2189
|
+
unique changed-path count after coalescing, `start` the clamped page offset, and `entries` the requested page in
|
|
2190
|
+
repository-relative path order. Each entry supplies `path`, `status`, `original` and `deleted`.
|
|
2191
|
+
`status` has the same four-letter alphabet as `gitrows`. `original` is the source path for
|
|
2192
|
+
a rename or copy and otherwise empty; it is metadata, not another counted entry. `deleted`
|
|
2193
|
+
is true for a `D` row even when that path no longer exists. These replies contain no size or
|
|
2194
|
+
date; clients resolve metadata off-thread for existing visible paths and skip it for deleted rows.
|
|
2195
|
+
The Changes footer counts `total`, for example `8 items`, separately from ordinary listing rows.
|
|
2196
|
+
|
|
2197
|
+
All three Git requests fail with their own `t`, echoed `id` and `path`, `ok:false` and an
|
|
2198
|
+
`error` sentence, for example `{"t":"gitstatus","id":1,"path":"/repo","ok":false,"error":"Git status timed out."}`.
|
|
2199
|
+
Errors include unavailable snapshots or sandbox, invalid paths or visible-row requests,
|
|
2200
|
+
cancellation, timeout, over-limit output, malformed porcelain and underlying I/O or Git diagnostics.
|
|
2201
|
+
These are requested replies; `changed` remains the only unsolicited notification.
|
|
2202
|
+
|
|
2203
|
+
### listed
|
|
2204
|
+
|
|
2205
|
+
`{"t":"listed","n":<uint>,"read":<float>,"sort":<float>,"v":<uint>,"w":<bool>,"path":<string>}`
|
|
2206
|
+
|
|
2207
|
+
Example: `{"t":"listed","n":100000,"read":26.400,"sort":2.500,"v":56,"w":true,"path":"/home/gm"}`
|
|
2208
|
+
|
|
2209
|
+
`n` is the row count. `read` and `sort` are milliseconds, formatted to three decimal
|
|
2210
|
+
places (`{:.3}`). Sent after a successful `list` and after a successful `sort`. `read` is
|
|
2211
|
+
the scan plus any requested metadata pass after `list`, the metadata pass after a
|
|
2212
|
+
`sort` by `size` or `mtime` (including its `date` alias), and `0.0` after a `sort` by
|
|
2213
|
+
`name` or `kind`, which needs no metadata pass.
|
|
2214
|
+
|
|
2215
|
+
`v` is the listing directory's own filesystem id, sent once here rather than on every row because
|
|
2216
|
+
every file in the directory shares it. A client compares it against the `v` of the directory row
|
|
2217
|
+
being dropped on: equal is one volume and the drag moves, different is two and it copies. `v` is 0
|
|
2218
|
+
when the directory could not be stat'd, and a client reads 0 as unknown and copies, because copying
|
|
2219
|
+
where a move was meant is an annoyance and moving where a copy was meant loses the original.
|
|
2220
|
+
`w` is whether this user can create or delete entries in that directory. A drag from a directory
|
|
2221
|
+
with `w` false copies, because a move would have to delete the originals and cannot. A reply that
|
|
2222
|
+
omits `w` is an older backend, and a client reads that the same as true.
|
|
2223
|
+
|
|
2224
|
+
`path` is the directory exactly as the `list` that made this listing spelled it, byte for byte, with a
|
|
2225
|
+
trailing slash, a symlink or a `..` left unresolved, because a client drops any `listed` line whose
|
|
2226
|
+
`path` differs from the one it asked for (`ui/js/Swap.js` `onListed`).
|
|
2227
|
+
|
|
2228
|
+
`changed` rides only on a `list` with `wantChanged` that re-read the directory
|
|
2229
|
+
already listed, and on a `listpaths` that re-read the listing it already held, and names
|
|
2230
|
+
added plus removed rows between the two scans, so a rename counts 2 against a net delta of 0.
|
|
2231
|
+
A first listing and a navigation carry no `changed`, and a client reads a missing one as unknown rather than 0.
|
|
2232
|
+
|
|
2233
|
+
### rows
|
|
2234
|
+
|
|
2235
|
+
Each viewport row also carries `b`, an optional creation timestamp as Unix seconds
|
|
2236
|
+
or null. The source is Linux `statx(STATX_BTIME)`, with no-follow and no-automount
|
|
2237
|
+
flags and the returned mask checked. Missing support stays null. It is read only
|
|
2238
|
+
in the existing bounded viewport metadata worker, except an explicit creation-time
|
|
2239
|
+
sort. Serialization performs no extra filesystem read. Cached GIO rows without
|
|
2240
|
+
a creation-time fact report null. TUI Creation
|
|
2241
|
+
detail displays a localized date or the localized unavailable value. Properties'
|
|
2242
|
+
single-item `menuaction` payload carries the same optional value as `birth`.
|
|
2243
|
+
|
|
2244
|
+
|
|
2245
|
+
`{"t":"rows","start":<uint>,"rows":[{"n":<string>,"d":<bool>,"s":<uint>,"m":<int>,"p":<uint>,"i":<string>,"t":<bool>,"k":<uint>[,"l":<string>][,"v":<uint>][,"tg":[<string>,...]][,"a":true][,"x":false]},...],"kinds":[<string>,...],"ms":<float>,"listing":<uint>}`
|
|
2246
|
+
|
|
2247
|
+
Example:
|
|
2248
|
+
`{"t":"rows","start":0,"rows":[{"n":"say \"hi\".txt","d":false,"s":12,"m":1787790423,"p":33188,"i":"text-x-generic","t":false,"k":0},{"n":"photos","d":true,"s":4096,"m":1787790424,"p":16877,"i":"folder","t":false,"k":1,"v":56}],"kinds":["Plain text document","Folder"],"ms":1.250,"listing":1}`
|
|
2249
|
+
|
|
2250
|
+
`start` echoes the requested start, clamped to the listing's length: a `window`
|
|
2251
|
+
whose `start` lands past the end of the listing answers with `start` equal to the
|
|
2252
|
+
listing's length and an empty `rows` array, not the value that was requested.
|
|
2253
|
+
|
|
2254
|
+
Each row object holds a name (`n`, JSON-escaped; a valid UTF-8 name may carry any
|
|
2255
|
+
character including a quote or a newline, but a non-UTF8 name arrives lossy and then
|
|
2256
|
+
cannot be stat'd, so it reports zeroes, see AGENTS.md "Deliberate corners"), a
|
|
2257
|
+
directory flag (`d`, the link's own type rather than its target's: a symlink to a
|
|
2258
|
+
directory reports `d:false` and carries `S_IFLNK` in `p`), a size in bytes (`s`), an
|
|
2259
|
+
mtime as a unix timestamp (`m`), a raw `st_mode` (`p`), a freedesktop icon name
|
|
2260
|
+
(`i`), a thumbnailable flag (`t`), and a Kind index (`k`, into the envelope's own
|
|
2261
|
+
`kinds` array).
|
|
2262
|
+
|
|
2263
|
+
**`x:false` means the reported execute bits do not establish per-file executable status.** Only those
|
|
2264
|
+
rows carry it; ordinary rows gain no wire bytes. The icon and executable name colour use
|
|
2265
|
+
the same backend decision, while `p` stays the reported mode for permission display and
|
|
2266
|
+
operations. A symlink's name still uses symlink colour, and a lifted name uses foreground.
|
|
2267
|
+
|
|
2268
|
+
**`l` is where a symlink points, and only a symlink row carries it.** It is the link's own bytes,
|
|
2269
|
+
verbatim and unresolved, so a relative target stays relative and a broken link still names where it
|
|
2270
|
+
points; a `readlink` that fails leaves the field off entirely, the same as any other row. The row it
|
|
2271
|
+
sits on is the one that already pays a second stat for its icon, so the extra call lands on the rows
|
|
2272
|
+
that were already the exception and on no others, and the scale fixture holds no links at all. A row
|
|
2273
|
+
never carries both `l` and `v`: `d` is the link's own type, so a symlink's `d` is `false` and it is
|
|
2274
|
+
never the directory row `v` is for. The client draws the target beside the name and the word `link`
|
|
2275
|
+
in the size column, because a link's own `st_size` is the length of that target path and not a size
|
|
2276
|
+
anyone means.
|
|
2277
|
+
|
|
2278
|
+
**`tg` names the file tags Dolphin reads, and only a tagged row carries it.** It is the
|
|
2279
|
+
`user.xdg.tags` value filtered to names Flea knows, in the stored order and canonical case, so a
|
|
2280
|
+
row draws its swatches with no call per row; an untagged row carries no `tg` at all, and a
|
|
2281
|
+
foreign word stays untouched and shows in Properties instead. One `getxattr` per emitted row
|
|
2282
|
+
reads it, which is exactly the rows a window stats and never a sweep; on a network or phone
|
|
2283
|
+
mount no row carries it, because each read there would be a round trip.
|
|
2284
|
+
|
|
2285
|
+
**`a` marks a thumbnailable AppImage, and only that row carries it.** Its own icon is its identity, so
|
|
2286
|
+
the Images thumbnail mode draws it while a row with any other executable icon, such as a Windows
|
|
2287
|
+
installer a third-party thumbnailer handles, stays a mark; every other row carries no `a` at all.
|
|
2288
|
+
|
|
2289
|
+
**`v` is the row's filesystem id, and only a directory row carries it.** A drop destination is
|
|
2290
|
+
always a directory, so a file row's device would never be read, and the scale fixture is 100,000
|
|
2291
|
+
files with no directories at all: sending it per row would be paid 100,000 times on the one path
|
|
2292
|
+
that produces the headline listing number, for a field nothing reads. A client compares the `v` of
|
|
2293
|
+
the rows being dragged against the `v` of the directory being dropped on: equal means one volume and
|
|
2294
|
+
the drag moves, different means two and it copies, which is what Finder does. A directory whose stat
|
|
2295
|
+
failed reports `p` 0 and `v` 0 with it. `ms` is the phase-2 stat time for this window, formatted to three
|
|
2296
|
+
decimal places. Sent after `list` and after `window`. The last field is `listing`, the numbering these
|
|
2297
|
+
rows are in; see "listing".
|
|
2298
|
+
|
|
2299
|
+
A row whose stat failed sends `s`, `m` and `p` all 0, and `p` is what says so: 0 is outside
|
|
2300
|
+
`st_mode`'s domain, because a real one always carries its file-type bits. So `p` 0 needs no flag
|
|
2301
|
+
beside it the way `lines` 0 needs `lfailed`, and it condemns that row's `s` and `m` with it: those
|
|
2302
|
+
two are only readable when `p` is non-zero.
|
|
2303
|
+
|
|
2304
|
+
**`kinds` is a dictionary, not a string per row, because an earlier plan measured the
|
|
2305
|
+
per-row-string shape and it cost milliseconds.** A directory has few distinct Kind
|
|
2306
|
+
strings even at 100,000 files (a handful of file types, plus `"Folder"`), so every row
|
|
2307
|
+
paying one integer instead of a repeated string keeps the response small without a
|
|
2308
|
+
second round trip. A directory's Kind is always the literal `"Folder"`; a file's Kind
|
|
2309
|
+
is `src/backend/kind.rs`'s freedesktop `<comment>` for its resolved MIME type,
|
|
2310
|
+
canonicalised through `/usr/share/mime/aliases` the same way the thumbnailer spec
|
|
2311
|
+
lookup already is, falling back to the literal `"Data"` both for a type with no comment
|
|
2312
|
+
at all (`application/x-ms-dos-executable` is the one shipped example) and for a name no
|
|
2313
|
+
glob matched. **An icon name is an internal string and this column is a human
|
|
2314
|
+
description, so one never leaks into the other**, and a name nothing identified must not
|
|
2315
|
+
claim to be text however far the icon ladder itself falls. Like the icon and
|
|
2316
|
+
MIME tables, the comment file is read once per distinct type for the life of the
|
|
2317
|
+
backend process, never per row. The selected description is cached per type and
|
|
2318
|
+
refreshed when Omarchy's language changes: the client is given a human description and never the
|
|
2319
|
+
MIME type, because a display should not classify.
|
|
2320
|
+
|
|
2321
|
+
**English uses the first `<comment>` carrying no `xml:lang` attribute, preserving its
|
|
2322
|
+
bytes. Other languages select an exact language and region, then the base language,
|
|
2323
|
+
then that untranslated comment.** Underscores and hyphens name the same locale;
|
|
2324
|
+
the roster's Chinese regions and Cyrillic Serbian also match their explicit XML
|
|
2325
|
+
script tags. Translated XML entities are decoded before display. 8 of the first 200
|
|
2326
|
+
`/usr/share/mime/*.xml` files on this box open with a translated comment before their
|
|
2327
|
+
untranslated one; `application/msword.xml` is one of them, opening `xml:lang="zh-Hant-TW"`
|
|
2328
|
+
with its untranslated comment further down the file, after every alias and glob line. A type
|
|
2329
|
+
whose file carries no matching translated or untranslated comment,
|
|
2330
|
+
answers `None`, which is why `Kinds::comment` returns `Option<String>` rather than an empty
|
|
2331
|
+
string.
|
|
2332
|
+
|
|
2333
|
+
An explicit `mime.markdown` catalog value overrides the localized Markdown comment
|
|
2334
|
+
outside English. Only Japanese supplies one: `Markdown 文書`, as HIG
|
|
2335
|
+
`CHANGES.md:2800` requires, correcting the installed incomplete `Markdown ` comment.
|
|
2336
|
+
Every other locale continues through the MIME selection and fallback above.
|
|
2337
|
+
|
|
2338
|
+
**No wording in that file is a property of the format, and nothing may assert one.** Every
|
|
2339
|
+
`/usr/share/mime/<type>.xml` is generated by `update-mime-database` from the files in
|
|
2340
|
+
`/usr/share/mime/packages/`, and no package owns the generated file, so the untranslated
|
|
2341
|
+
string is whatever the applications installed on the box supply. `application/msword` reads
|
|
2342
|
+
`Word document` from `shared-mime-info`'s own `freedesktop.org.xml` and `Microsoft Word
|
|
2343
|
+
Document` from `libreoffice-fresh`'s `libreoffice.xml`, which outranks it; a box with
|
|
2344
|
+
LibreOffice and a box without therefore disagree on the same distribution and the same
|
|
2345
|
+
`shared-mime-info` version. Issue 1 was a test asserting the second string. The parser is
|
|
2346
|
+
tested against a fixture and the live database is asserted only for shape: reachable, ASCII,
|
|
2347
|
+
and free of markup.
|
|
2348
|
+
|
|
2349
|
+
Note that `t` inside a row object is a boolean, while the `t` at the top of every
|
|
2350
|
+
response line is the message type, a string. They never collide: one is a member of a
|
|
2351
|
+
row, the other a member of the envelope.
|
|
2352
|
+
|
|
2353
|
+
`i` is never empty. The backend resolves the row's name to a MIME type against
|
|
2354
|
+
`/usr/share/mime/globs2`, then that type to an icon name against
|
|
2355
|
+
`/usr/share/mime/generic-icons`, falling back to the media class (`image-x-generic`
|
|
2356
|
+
and friends) and finally to `text-x-generic`; a directory is always `folder`. Both
|
|
2357
|
+
tables are read once per backend process, never per row. The client is given the icon
|
|
2358
|
+
name and never the MIME type, because a display should not classify.
|
|
2359
|
+
|
|
2360
|
+
Two rules sit above that resolution and neither follows from the name alone. **The icon of a
|
|
2361
|
+
symlink follows the link's target while `d` still describes the link**: a symlink to a
|
|
2362
|
+
directory reports `d:false`, because `d` is what the client navigates on, and `i` is `folder`,
|
|
2363
|
+
because that is what the row looks like. Only symlink rows pay for it, through one extra stat
|
|
2364
|
+
inside the window the client asked for. **An `application/*` type with no `generic-icons` entry
|
|
2365
|
+
resolves by the row's own execute bit**, not by its class: 190 of the 613 such types in
|
|
2366
|
+
`globs2` have no entry on this box, so the execute bits in `p` decide between
|
|
2367
|
+
`application-x-executable` and `application-x-generic`. Both counts move with the installed
|
|
2368
|
+
applications, the same merge described above: a root built from `freedesktop.org.xml` alone
|
|
2369
|
+
gives 138 of 552. Without that rule every one of the 190 drew as a program, which is
|
|
2370
|
+
how a `.pem` came to look executable.
|
|
2371
|
+
|
|
2372
|
+
`t` is true only when a validated thumbnailer declares the row's MIME type or an alias
|
|
2373
|
+
of it. The backend resolves the row's name to a MIME type the same way it resolves the
|
|
2374
|
+
icon, then looks that type up in the thumbnailer specs read from the freedesktop search
|
|
2375
|
+
path, whose declarations and queries are both canonicalised through
|
|
2376
|
+
`/usr/share/mime/aliases`, so either side of an alias pair matches. A spec whose
|
|
2377
|
+
program is not a runnable executable is dropped when the table is built, so `t` is
|
|
2378
|
+
false for a declared type with no working thumbnailer. `t` also reads the file type out of the
|
|
2379
|
+
row's own `p`: a directory, a fifo, a socket and a device node are always false however they are
|
|
2380
|
+
named, and a row that vanished between the listing and the stat reports `p` of 0 and is false
|
|
2381
|
+
too. A symlink is true when its name resolves to a declared type, because the thumbnail request
|
|
2382
|
+
stats the target; a symlink to a special file is then refused when the row is actually asked
|
|
2383
|
+
for. Like
|
|
2384
|
+
the icon tables, the alias and spec tables are read once per backend process, never per
|
|
2385
|
+
row; the whole per-row cost of `t` is one hash lookup, tens of nanoseconds per row, so
|
|
2386
|
+
it is invisible at a viewport of a few hundred rows. The client is still never given the
|
|
2387
|
+
MIME type itself: `t` exists so a display does not have to reason about content types to
|
|
2388
|
+
know whether asking for a thumbnail is worthwhile.
|
|
2389
|
+
|
|
2390
|
+
### searching
|
|
2391
|
+
|
|
2392
|
+
`{"t":"searching","n":<uint>,"scanned":<uint>,"ms":<float>}`
|
|
2393
|
+
|
|
2394
|
+
Example: `{"t":"searching","n":812,"scanned":41200,"ms":300.114}`
|
|
2395
|
+
|
|
2396
|
+
The streaming progress of a `search`, at most one every 100 ms while the walk runs. `n` is
|
|
2397
|
+
the match count so far and is what the client sets its row total to; `scanned` is directory
|
|
2398
|
+
entries looked at, which is the number the status bar counts up. It is deliberately not a
|
|
2399
|
+
`listed` line: a mid-walk update is not a fresh listing and carries no `read` or `sort`
|
|
2400
|
+
timing to report.
|
|
2401
|
+
|
|
2402
|
+
Matches are only ever appended, so a `window` the client already holds stays valid across
|
|
2403
|
+
every one of these.
|
|
2404
|
+
|
|
2405
|
+
### searched
|
|
2406
|
+
|
|
2407
|
+
`{"t":"searched","n":<uint>,"scanned":<uint>,"ms":<float>,"cancelled":<bool>}`
|
|
2408
|
+
|
|
2409
|
+
Example: `{"t":"searched","n":6252,"scanned":45807,"ms":48.711,"cancelled":true}`
|
|
2410
|
+
|
|
2411
|
+
The terminal line of a `search`. `n` is the final match count and is authoritative: no
|
|
2412
|
+
`listed` follows it. `scanned` is how many directory entries the walk looked at, which is
|
|
2413
|
+
what the status bar counts up while it runs. `ms` is the whole walk, to three decimal
|
|
2414
|
+
places. `cancelled` is true when a `searchcancel`, a `list` or a `sort` ended the walk early,
|
|
2415
|
+
and false when it ran the subtree out. The rows are in rank order by the time this line is
|
|
2416
|
+
written, so a client holding a window from before it has stale indices and re-reads.
|
|
2417
|
+
|
|
2418
|
+
### thumbed
|
|
2419
|
+
|
|
2420
|
+
`{"t":"thumbed","row":<uint>,"file":"<string>","ms":<float>}`
|
|
2421
|
+
|
|
2422
|
+
Example: `{"t":"thumbed","row":2,"file":"/home/gm/.cache/thumbnails/large/b98fa4082faa4a9cbb9728c94831a4b5.png","ms":75.823}`
|
|
2423
|
+
|
|
2424
|
+
Answers one row of one `thumb` request. `row` is the index that was asked for. `file` is
|
|
2425
|
+
the absolute path of the PNG in the shared thumbnail cache, JSON-escaped like every other
|
|
2426
|
+
string on this wire, and `ms` is milliseconds to three decimal places: the whole job for a
|
|
2427
|
+
generated row, or just the lookup for a row answered from the cache, which is tens of
|
|
2428
|
+
microseconds here against tens of milliseconds and up for a decode.
|
|
2429
|
+
|
|
2430
|
+
**`file` is empty rather than absent on failure**, so a client never waits forever for a
|
|
2431
|
+
row that will not arrive. Every reason a row cannot be thumbnailed reads the same on the
|
|
2432
|
+
wire: a directory, a file that is not a regular file, no thumbnailer for the type, a recorded
|
|
2433
|
+
failure, a thumbnailer that failed, hung, or exited successfully having written nothing, or a
|
|
2434
|
+
shutdown that cut the job short. A client that asked for a row
|
|
2435
|
+
therefore gets exactly one `thumbed` line for it, unless it cancelled the row itself or
|
|
2436
|
+
replaced the listing under it.
|
|
2437
|
+
|
|
2438
|
+
**A result for a superseded listing is dropped, never reported against the current one.**
|
|
2439
|
+
The backend maps only the rows a client actually asked for back to their paths, and a
|
|
2440
|
+
`list` or a `sort` clears that map and cancels the queue, because both change which row an
|
|
2441
|
+
index names. A job already inside a worker still runs to its own end, and its result is
|
|
2442
|
+
discarded on arrival rather than printed against a row it no longer describes.
|
|
2443
|
+
|
|
2444
|
+
Shutdown is bounded at 25 seconds in total, which is longer than the pool's own 20-second
|
|
2445
|
+
job deadline, so no single running job can be cut short by it. If the budget does run out,
|
|
2446
|
+
every row still unanswered is answered with an empty `file` before the process exits.
|
|
2447
|
+
|
|
2448
|
+
### dirsized
|
|
2449
|
+
|
|
2450
|
+
`{"t":"dirsized","row":<uint>,"bytes":<uint>,"partial":<bool>,"ms":<float>}`
|
|
2451
|
+
|
|
2452
|
+
Example: `{"t":"dirsized","row":4,"bytes":1048576,"partial":false,"ms":12.500}`
|
|
2453
|
+
|
|
2454
|
+
Answers one row of one `dirsize` request. `row` is the index that was asked for, `bytes` is the
|
|
2455
|
+
recursive apparent size (every entry's own `st_size`, symlinks not followed and counted at their
|
|
2456
|
+
own small size rather than their target's), and `ms` is the walk's own wall-clock time. The
|
|
2457
|
+
target's own directory entry counts too, matching what `du -s` reports for the directory itself.
|
|
2458
|
+
|
|
2459
|
+
**`partial` is true when the 2000 ms deadline cut the walk short, or a subtree inside it
|
|
2460
|
+
answered permission denied.** Either way `bytes` is a floor, honestly labelled, never a wrong
|
|
2461
|
+
exact number: everything the walk actually saw before it had to stop is still counted. The
|
|
2462
|
+
shipped client renders a partial answer with a leading `>`.
|
|
2463
|
+
|
|
2464
|
+
**A result for a superseded listing is dropped, never reported against the current one.** A
|
|
2465
|
+
`list` or a `sort` clears the answered-row cache and cancels the queue, the same rule and the
|
|
2466
|
+
same reason `thumbed` follows: both change which row an index names.
|
|
2467
|
+
|
|
2468
|
+
### settagged
|
|
2469
|
+
|
|
2470
|
+
`{"t":"settagged","ok":<uint>,"op":"<string>"[, "skipped":["<name>",...]]}`
|
|
2471
|
+
|
|
2472
|
+
Example: `{"t":"settagged","ok":2,"op":"remove"}`
|
|
2473
|
+
|
|
2474
|
+
Answers one `settags` request. `ok` is the number of files whose attribute changed; a request
|
|
2475
|
+
that changed nothing answers 0 and records nothing to undo. `op` is what was applied, `add`,
|
|
2476
|
+
`remove` or `clear`, so a `toggle` answers the direction it resolved to, and the client words its
|
|
2477
|
+
status line from it. `skipped` is present only when a row was skipped, naming the rows that were neither a file nor a folder, so the client words that line instead. A refusal answers an `error`
|
|
2478
|
+
line with `where` of `settags` instead, and nothing was written.
|
|
2479
|
+
|
|
2480
|
+
### tagstates
|
|
2481
|
+
|
|
2482
|
+
`{"t":"tagstates","id":<uint>,"applied":["<name>",...],"mixed":["<name>",...],"listing":<uint>}`
|
|
2483
|
+
|
|
2484
|
+
Example: `{"t":"tagstates","id":3,"applied":["Red"],"mixed":["Blue"],"listing":7}`
|
|
2485
|
+
|
|
2486
|
+
The answer to one `tagstates` request, carrying its `id` back. `applied` names, in menu order, the
|
|
2487
|
+
tags every row carries and `mixed` the tags only some rows carry; a tag no row carries is in neither.
|
|
2488
|
+
A name Flea does not know is never listed. `listing` is the numbering the rows resolved in, so a
|
|
2489
|
+
client drops an answer whose numbering is not the one it asked in.
|
|
2490
|
+
|
|
2491
|
+
### transferstarted
|
|
2492
|
+
|
|
2493
|
+
`{"t":"transferstarted","id":<uint>,"n":<uint>,"moving":<bool>}`
|
|
2494
|
+
|
|
2495
|
+
Example: `{"t":"transferstarted","id":12,"n":2,"moving":true}`
|
|
2496
|
+
|
|
2497
|
+
`id` names this operation for the life of the process and is what `transfercancel` takes. `n` is the
|
|
2498
|
+
count of top-level paths, **never a recursive file count**: computing that before answering would be a
|
|
2499
|
+
metadata sweep over an unbounded subtree, which is the shape this codebase refuses everywhere else, and
|
|
2500
|
+
a transfer's first line is on the critical path of every operation.
|
|
2501
|
+
|
|
2502
|
+
`moving` is the verb the request actually resolved to, after the rule that anything which is not
|
|
2503
|
+
exactly `"move"` copies. It is on the wire because the client cannot derive it: a paste spends a cut
|
|
2504
|
+
clipboard before this line arrives, and a move to Dropbox never touches the clipboard at all, so a
|
|
2505
|
+
client reading its own clipboard reports a move as a copy.
|
|
2506
|
+
|
|
2507
|
+
### extractstarted
|
|
2508
|
+
|
|
2509
|
+
`{"t":"extractstarted","id":<uint>}`
|
|
2510
|
+
|
|
2511
|
+
An archive `extract` sends this line before its one zero-byte `transferprogress` line. A current client
|
|
2512
|
+
routes it into the existing transfer card as `n` 1, `moving` false and `extract` true, so the card says
|
|
2513
|
+
Extracting without inventing bytes, a rate or an ETA. Older clients ignore this new type, while the
|
|
2514
|
+
existing `archivedone` terminal line remains unchanged for archive clients.
|
|
2515
|
+
|
|
2516
|
+
### meta
|
|
2517
|
+
|
|
2518
|
+
`{"t":"meta","row":<uint>,"w":<uint>,"h":<uint>,"orient":<uint>,"ms":<uint>,"rate":<uint>,"entries":<uint>,"unpacked":<uint>,"afailed":<bool>,"names":[{"n":"<string>","d":<bool>},...],"lines":<uint>,"partial":<bool>,"lfailed":<bool>,"frames":<uint>,"photo":<object|null>,"target":"<string>","targetdir":<bool>,"raw":{"file":"<string>","w":<uint>,"h":<uint>,"orient":<uint>}|null,"owner":"<string>"}`
|
|
2519
|
+
|
|
2520
|
+
Example: `{"t":"meta","row":4,"w":0,"h":0,"orient":1,"ms":0,"rate":0,"entries":214,"unpacked":3400,"afailed":false,"names":[{"n":"ui","d":true}],"lines":0,"partial":false,"lfailed":false,"frames":0,"photo":null,"target":"","targetdir":false,"raw":null,"owner":"gm"}`
|
|
2521
|
+
|
|
2522
|
+
Every field a row did not ask for is zero, false or empty. `w` and `h` are pixels, read from the
|
|
2523
|
+
file's own header, as stored and before any turn. `ms` and `rate` come from the probe. `lines` is a
|
|
2524
|
+
newline count with `partial` true when it stopped at its 1 MiB budget, so the column states it as a floor.
|
|
2525
|
+
|
|
2526
|
+
`photo` is the camera file's EXIF facts as stored, from the same RAW walk and in the same reply: `{"make":"Canon","model":"Canon EOS R6","lens":"RF35mm F1.8 MACRO IS STM","exp":[1,250],"fnum":[28,10],"iso":400,"focal":[35,1],"taken":"2026:09:12 18:31:00","w":6000,"h":4000}`, where `exp`, `fnum` and `focal` are numerator and denominator pairs, `w` and `h` are the sensor size from the EXIF pixel dimensions (sent together or not at all) and an absent fact is left out. It is `null` for every row that is not a RAW and for a RAW whose walk names none of them; a JPEG's facts are never in this reply. The `properties` reply of `menuaction` carries the same `photo` object, and for a `.jpg` or `.jpeg` it is read from the first `Exif` APP1 within the file's first 64 KiB.
|
|
2527
|
+
|
|
2528
|
+
`orient` is the EXIF orientation, 1 to 8, from the same read of the same open. The walk that finds a
|
|
2529
|
+
JPEG's frame header reads the head of its first `Exif` APP1 on the way past. It is 1 for every file that
|
|
2530
|
+
names none, every non-JPEG included. Orientations 5 to 8 swap the sides, and the Columns frame and Quick
|
|
2531
|
+
Look ask Qt for their decode size turned the same way, because Qt fits its decode before it turns.
|
|
2532
|
+
|
|
2533
|
+
`entries` and `unpacked` are **exact totals whenever they are non-zero**, because the index is
|
|
2534
|
+
streamed and counting all of it costs no memory. `names` is capped at the first `ARCHIVE_NAME_CAP`
|
|
2535
|
+
entries, which is the only part that is bounded: the tile lists those and states the difference as
|
|
2536
|
+
its own "+ N more" line.
|
|
2537
|
+
|
|
2538
|
+
`lfailed` is true when the row produced no count at all, which on this box
|
|
2539
|
+
means permission denied, a row that is not a regular file, or a row that vanished between the
|
|
2540
|
+
listing and the request. **Nothing but a regular file is ever read here or for `w` and `h`**: a
|
|
2541
|
+
`stat` refuses every other kind before any open, so a FIFO that `stat` sees is never opened at all
|
|
2542
|
+
and its waiting writer is left where it was, and the open behind that stat is `O_NONBLOCK` with a
|
|
2543
|
+
second `fstat` on the descriptor, so a row swapped for a FIFO inside that window is closed again
|
|
2544
|
+
instead of leaving the row waiting forever. That one open does wake a writer parked in `open(2)`, and
|
|
2545
|
+
closing it unread hands that writer an `EPIPE` on its next write if nothing else is reading: it is
|
|
2546
|
+
the cost of refusing a swap the `stat` cannot see, not a case the `stat` avoids. A FIFO, a socket, a
|
|
2547
|
+
device or a directory answers its `stat` facts with no dimensions and no count. It is what tells
|
|
2548
|
+
`lines` 0 apart from an empty file, whose `lines` is also 0: zero is a real count, so
|
|
2549
|
+
unlike `mode` on an `error` line it cannot carry the failure itself. A row that never asked for a
|
|
2550
|
+
count sends `lines` 0 and `lfailed` false, the same as a row whose count really is zero, because
|
|
2551
|
+
nothing was attempted; the client knows which kind it asked about.
|
|
2552
|
+
|
|
2553
|
+
`afailed` is true when the listing was not completed, which covers a tool that could not read the
|
|
2554
|
+
archive and a read that outran its wall-clock budget. **A failed read sends `entries` 0, `unpacked` 0
|
|
2555
|
+
and an empty `names`**, so a partial count never reaches a client that has been told the number is a
|
|
2556
|
+
total. That is a different answer from an archive holding nothing, which sends the same zeroes with
|
|
2557
|
+
`afailed` false, and the two are drawn as the error state and the empty state respectively.
|
|
2558
|
+
|
|
2559
|
+
`target` is a symlink's own target and `targetdir` says whether that target is a directory, so the
|
|
2560
|
+
column's mark can follow the target the way a listing row's does.
|
|
2561
|
+
|
|
2562
|
+
`raw` is an object for a camera RAW whose largest embedded JPEG preview was copied out, and `null` for every
|
|
2563
|
+
other file and for a RAW holding none. `file` is the copy under `$XDG_CACHE_HOME/flea/raw/`, named by the md5
|
|
2564
|
+
of the RAW's file URI and carrying the RAW's mtime, so a changed RAW is copied again and an unchanged one is
|
|
2565
|
+
neither walked nor written twice.
|
|
2566
|
+
The directory is held to 4096 copies and 512 MiB, least recently used first (by ctime, since a copy's mtime is its RAW's:
|
|
2567
|
+
a cache hit sets the copy's atime to now, which moves the ctime), and sweeps temps a minute old. A copy made or served less
|
|
2568
|
+
than a minute ago is never evicted, because the thread that has it is still reading it. Each process scans on its own first
|
|
2569
|
+
new copy and then on one in 32 of its own, and the directory is shared, so it can stand over its caps: by the copies of the
|
|
2570
|
+
last minute, which stay until the first scan after they age, and by fewer than 32 more per process. `w`, `h` and `orient` read like
|
|
2571
|
+
the top-level fields, taken from the copy: stored
|
|
2572
|
+
pixels and the EXIF turn, which the copy carries even when the camera put it only in the RAW's IFD0. The top-level
|
|
2573
|
+
`w` and `h` stay 0 for a RAW, because they name the file's own pixels and the preview is smaller.
|
|
2574
|
+
|
|
2575
|
+
`owner` is the login name of the user who owns the path itself (a symlink reports the link's owner, as
|
|
2576
|
+
its row does), resolved from `/etc/passwd` alone and never through `getpwuid`: that call goes through
|
|
2577
|
+
NSS and can wait on a network directory, and the meta thread must never hang the column on one. A uid
|
|
2578
|
+
no local account carries answers the empty string, never the number dressed as a name.
|
|
2579
|
+
|
|
2580
|
+
### owner
|
|
2581
|
+
|
|
2582
|
+
`{"t":"owner","row":<uint>,"token":<uint>,"owner":"<string>","retry":<bool>}`
|
|
2583
|
+
|
|
2584
|
+
Example: `{"t":"owner","row":7,"token":91,"owner":"gm","retry":false}`
|
|
2585
|
+
|
|
2586
|
+
The answer to one `{"c":"owner","row":<uint>,"token":<uint>}` request, which the TUI's Owner detail
|
|
2587
|
+
column sends for one drawn row at a time, echoing its `row` and `token`. `owner` reads as `meta.owner`
|
|
2588
|
+
does, the login name from `/etc/passwd`, and is empty for a uid no local account carries, a path that
|
|
2589
|
+
cannot be stat'ed or a row past the listing. `retry` is true only when the stat could not run, because
|
|
2590
|
+
its remote mount already holds an owner stat, eight remote owner stats are held across all mounts, the
|
|
2591
|
+
mount is marked stuck, or the stat missed its five-second deadline. `owner` is empty then too, so a
|
|
2592
|
+
reader that ignores `retry` reads the line it always did, and an absent `retry` reads as false. The
|
|
2593
|
+
TUI keeps every `retry:false` answer, empty ones included, until its rows change, and asks a
|
|
2594
|
+
`retry:true` row again only while the Owner column still draws it, at most once a second.
|
|
2595
|
+
|
|
2596
|
+
### shebang
|
|
2597
|
+
|
|
2598
|
+
`{"t":"shebang","path":"<string>","hasShebang":<bool>,"id":<uint>}`
|
|
2599
|
+
|
|
2600
|
+
Example: `{"t":"shebang","path":"/home/gm/run.sh","hasShebang":true,"id":3}`
|
|
2601
|
+
|
|
2602
|
+
The answer to one `shebang` request, carrying its `id` back. `hasShebang` is true only when the
|
|
2603
|
+
path opened as a regular file and its first two bytes were `#!`; anything else, a directory, a
|
|
2604
|
+
FIFO, a missing path or an unreadable file, answers false. The open is the backend's own
|
|
2605
|
+
regular-file open, so a FIFO is refused before any open and a row swapped for one inside that
|
|
2606
|
+
window is closed again, the same rule `meta` follows.
|
|
2607
|
+
|
|
2608
|
+
### code
|
|
2609
|
+
|
|
2610
|
+
`{"t":"code","token":<uint>,"slot":"<string>","state":"<string>","first":<uint>,"total":<uint>,"lines":[[<role>,"<string>",...],...][,"records":<uint>,"firstRecord":<uint>]}`
|
|
2611
|
+
|
|
2612
|
+
Example: `{"t":"code","token":4,"slot":"look","state":"styled","first":1,"total":2,"lines":[[5,"fn",0," main"],[]]}`
|
|
2613
|
+
|
|
2614
|
+
The answer to one `code` request, echoing its `token` and `slot`. `first` is the actual first
|
|
2615
|
+
line returned: normally the asked line, or the record start for a raw `csv` table request.
|
|
2616
|
+
`state` is `styled`
|
|
2617
|
+
(`bat` coloured the lines), `plain` (the lines are the file's own text, every role 0: a line over
|
|
2618
|
+
5,000 bytes, or no `bat` on the host), `binary` (a NUL in the first 1,024 bytes), `large` (over
|
|
2619
|
+
1,048,576 bytes) or `failed` (unreadable, or `bat` refused it); the last three carry no lines.
|
|
2620
|
+
`total` is the line count of the whole text. Each entry of `lines` is one line as a flat run list,
|
|
2621
|
+
a role from 0 to 6 then the run's text, in order, an empty line being an empty list; the lines
|
|
2622
|
+
returned are those asked for that exist, extended to whole records for a raw `csv` table
|
|
2623
|
+
request. `total` remains the physical-line count in either case. A plain raw `csv` reply
|
|
2624
|
+
also carries `records`, the whole file's record count including its header, and `firstRecord`,
|
|
2625
|
+
the ordinal of the first returned record. An unterminated quote at EOF still counts as one
|
|
2626
|
+
record. An empty file has zero records; an empty reply beyond EOF has the requested record
|
|
2627
|
+
ordinal, or the next ordinal after the file for a physical-line ask. Every other reader's
|
|
2628
|
+
reply omits these two fields. The roles are the foreground (0) and bat's ANSI colours
|
|
2629
|
+
31 to 36 in order: red, green, yellow, blue, magenta, cyan. A one-line `.json` file is formatted
|
|
2630
|
+
with `jq` first, so its `total` and lines are those of the formatted text.
|
|
2631
|
+
|
|
2632
|
+
### html
|
|
2633
|
+
|
|
2634
|
+
`{"t":"html","token":<uint>,"slot":"<string>","state":"<string>","file":"<PNG path>","total":<uint>,"width":<uint>,"title":"<string>"}`
|
|
2635
|
+
|
|
2636
|
+
Example: `{"t":"html","token":3,"slot":"look","state":"ready","file":"/cache/thumbnails/flea-html/a.png","total":61,"width":984,"title":"Local page"}`.
|
|
2637
|
+
|
|
2638
|
+
The answer to the latest request in one HTML slot. State is `ready`, `failed`, or `cancelled`;
|
|
2639
|
+
only `ready` names a PNG and a whole-file line count. Failure uses the existing unreadable-file
|
|
2640
|
+
frame. Width is the render width, not a CSS zoom. Title is the helper's observed static page
|
|
2641
|
+
title on a new render and empty on a cache hit. The client matches token and slot, and drops
|
|
2642
|
+
replies after closing or switching to Source.
|
|
2643
|
+
|
|
2644
|
+
### compare
|
|
2645
|
+
|
|
2646
|
+
`{"t":"compare","token":<uint>,"state":"<string>","patch":"<string>"}`
|
|
2647
|
+
|
|
2648
|
+
Example: `{"t":"compare","token":3,"state":"diff","patch":"--- /a\n+++ /b\n@@ -1 +1 @@\n-x\n+y\n"}`
|
|
2649
|
+
|
|
2650
|
+
The answer to one `compare` request, echoing its `token`. `state` is `diff` (`diff` exited 1 and `patch`
|
|
2651
|
+
holds what it printed, headers included), `identical` (it exited 0), `binary` (screening found a NUL in
|
|
2652
|
+
a file's first 8,192 bytes), `over` (either file is over 1,048,576 bytes) or `failed` (a path
|
|
2653
|
+
that cannot be read as a regular file, invalid UTF-8 in a file or the patch, any other exit, `diff` or
|
|
2654
|
+
the jail not starting, or a patch past 8 MiB). `patch` is the empty string in every state but `diff`.
|
|
2655
|
+
|
|
2656
|
+
Screening first requires both absolute paths to open as regular files with readable lengths;
|
|
2657
|
+
either initial length over 1,048,576 bytes returns `over`. It then reads `a`, followed by `b`, up to
|
|
2658
|
+
1,048,577 bytes each, checking read failure (`failed`), growth past the limit (`over`), a NUL in the
|
|
2659
|
+
first 8,192 bytes (`binary`), then UTF-8 validity (`failed`), stopping at the first refusal. An earlier
|
|
2660
|
+
file's `failed` or `over` therefore takes precedence over a later file's NUL.
|
|
2661
|
+
|
|
2662
|
+
### collisions
|
|
2663
|
+
|
|
2664
|
+
`{"t":"collisions","id":<uint>,"total":<uint>,"names":[{"n":"<string>","d":<bool>,"i":"<string>"},...],"mergeable":<bool>}`
|
|
2665
|
+
|
|
2666
|
+
Example: `{"t":"collisions","id":7,"total":2,"names":[{"n":"screenshot.png","d":false,"i":"image-x-generic"},{"n":"notes","d":true,"i":"folder"}],"mergeable":true}`
|
|
2667
|
+
|
|
2668
|
+
`total` counts every colliding source. `names` is the first three of them in request order, which is
|
|
2669
|
+
all a client's card lists before its "and N more" line, so the answer stays small however wide the
|
|
2670
|
+
selection. `n` is the incoming item's name, `d` whether it is a directory and `i` the same icon name a
|
|
2671
|
+
`rows` row carries, for the card's kind mark. `mergeable` is true when a folder lands on a folder,
|
|
2672
|
+
which is the only shape the card offers `merge` for. A `total` of 0 means nothing collides; the shipped client
|
|
2673
|
+
then sends its transfer with `collide` `refuse`, so a name that appears in the meantime is still refused.
|
|
2674
|
+
|
|
2675
|
+
### fatnames
|
|
2676
|
+
|
|
2677
|
+
`{"t":"fatnames","id":<uint>,"fstype":"<string>","volume":"<string>","renames":[{"old":"<string>","new":"<string>","d":<bool>,"i":"<string>"},...]}`
|
|
2678
|
+
|
|
2679
|
+
Example: `{"t":"fatnames","id":7,"fstype":"exfat","volume":"128GB","renames":[{"old":"clip: final.mp4","new":"clip_ final.mp4","d":false,"i":"video-x-generic"}]}`
|
|
2680
|
+
|
|
2681
|
+
Answers a `collisions` question that carried no `fat` for a FAT-family destination, with the same
|
|
2682
|
+
`id`, in place of the `collisions` line. `fstype` is the mount table's own word and `volume` the
|
|
2683
|
+
owning mount's folder name, so the card names the drive from fields and never from a sentence.
|
|
2684
|
+
`renames` lists every source whose name the filesystem refuses, in request order, `old` as it is and
|
|
2685
|
+
`new` as Rename and copy would land it; `d` and `i` are the `rows` row's directory bit and icon name.
|
|
2686
|
+
|
|
2687
|
+
### mergecollisions
|
|
2688
|
+
|
|
2689
|
+
`{"t":"mergecollisions","id":<uint>,"total":<uint>,"names":[{"n":"<string>","d":<bool>,"i":"<string>"},...]}`
|
|
2690
|
+
|
|
2691
|
+
Example: `{"t":"mergecollisions","id":8,"total":3,"names":[{"n":"IMG_4120.HEIC","d":false,"i":"image-x-generic"}]}`
|
|
2692
|
+
|
|
2693
|
+
`total` counts every file-level name both sides hold; folders that merge further down are not
|
|
2694
|
+
listed because they need no answer. `names` is the first three in walk order with the same `n`,
|
|
2695
|
+
`d` and `i` the card draws, `n` relative to the merging folder when one folder merges and to the
|
|
2696
|
+
destination when several do. A `total` of 0 sends no second card: the transfer goes out at once
|
|
2697
|
+
with `inner` `refuse`.
|
|
2698
|
+
|
|
2699
|
+
### clip
|
|
2700
|
+
|
|
2701
|
+
`{"t":"clip","op":"set","ok":<bool>,"token":<string>}`
|
|
2702
|
+
`{"t":"clip","op":"get","ok":<bool>,"clip":"copy"|"cut"|"image"|"none","paths":[<string>,...],"token":<string>,"skipped":<uint>}`
|
|
2703
|
+
`{"t":"clip","op":"clear","ok":<bool>,"cleared":<bool>}`
|
|
2704
|
+
`{"t":"clip","op":"changed","clip":"copy"|"cut"|"image"|"none","paths":[<string>,...],"token":<string>,"skipped":<uint>}`
|
|
2705
|
+
|
|
2706
|
+
One shape for all three clipboard requests, read by `clipResult`. A refusal carries
|
|
2707
|
+
`ok:false` and an `error` sentence instead of the success fields. A `get` with no
|
|
2708
|
+
files on the clipboard answers `clip:image` if PNG is offered, otherwise `clip:none`,
|
|
2709
|
+
with empty `paths` and `token` in either case. Only file selections enable Paste As;
|
|
2710
|
+
`image` enables ordinary Paste, which requests the PNG bytes through `pasteimage`.
|
|
2711
|
+
`skipped`
|
|
2712
|
+
counts the offered URIs that were refused (bad escapes, NUL, relative paths, `.` or
|
|
2713
|
+
`..` parts, non-UTF-8, duplicates, other schemes) rather than failing the read. The
|
|
2714
|
+
`token` on a `get` names Flea's own copy when the selection carries it, which is what
|
|
2715
|
+
a later `clipClear` hands back; a foreign selection answers an empty one. `changed`
|
|
2716
|
+
is the watcher's line and carries no `ok`: it reports the selection as files, with the
|
|
2717
|
+
same fields as `get`, and a failed watch reports `none` with an `error` sentence.
|
|
2718
|
+
A selection event supersedes any exit read begun before it, while an exit read begun after that event invalidates its pending watcher bytes only if it reports, with both guards ordered by sequences under the emitter lock.
|
|
2719
|
+
|
|
2720
|
+
|
|
2721
|
+
### transferprogress
|
|
2722
|
+
|
|
2723
|
+
`{"t":"transferprogress","id":<uint>,"index":<uint>,"name":"<string>","bytes":<uint>,"total":<uint>[,"phase":"writing"]}`
|
|
2724
|
+
|
|
2725
|
+
Example: `{"t":"transferprogress","id":12,"index":0,"name":"a.txt","bytes":40000000,"total":120000000}`
|
|
2726
|
+
|
|
2727
|
+
Throttled to one window per 150 ms for the whole operation, not per item: the first sample goes out
|
|
2728
|
+
at once and a run of small files shares one window, and a window sends the item's own line with at most
|
|
2729
|
+
one owed line beside it (below), so a fast copy of a thousand small files sends a few
|
|
2730
|
+
lines (plus one `transferitem` per item, below), and a single large file sends one per window as it
|
|
2731
|
+
always did. A thousand small files once sent a progress line and an item line each, so the card model
|
|
2732
|
+
was written 2002 times; the throttle cuts the progress lines from 1000 to 2 and the writes to 1004. An item
|
|
2733
|
+
that sends no line of its own is correct and not a missing message.
|
|
2734
|
+
|
|
2735
|
+
**Bytes ride on the next line, never get lost.** A client counts an item's bytes from the last sample it
|
|
2736
|
+
saw for that item (`ui/js/Transfer.js` `itemDone`), so the bytes of items that sent no line go out once, ahead
|
|
2737
|
+
of the next item's first line: a sample with `total` 0 under the `index` and `name` of the last item to end,
|
|
2738
|
+
which the client has already closed, so it joins the running sum and moves nothing on the card. The next item
|
|
2739
|
+
then sends its own `bytes` and `total`, so a large file keeps its size on every line. A closing sample of the
|
|
2740
|
+
same shape follows the last item when anything is still unreported, ahead of the drive flush below. What the
|
|
2741
|
+
card draws is then the figure an unthrottled run draws, to the byte, whatever the
|
|
2742
|
+
throttle skipped; an item that ends short (a failure or a cancel) counts as the total its last line
|
|
2743
|
+
named, as it always did. Only a copy or a same-device move sent by the transfer loop paces this way; a
|
|
2744
|
+
cross-device move batch and a folder merge keep their own per-item windows.
|
|
2745
|
+
|
|
2746
|
+
**Only a regular file reports bytes.** A directory has no total without the sweep this codebase does not
|
|
2747
|
+
do, and a same-filesystem move is a single `rename(2)` with nothing to report partway through, so
|
|
2748
|
+
neither emits these lines at all. A client renders an item with no progress as indeterminate.
|
|
2749
|
+
On a durable target (usb, phone, network, or vfat/exfat/ntfs) those bytes are reported only after
|
|
2750
|
+
the drive confirms them, so the rate is the drive's real rate.
|
|
2751
|
+
|
|
2752
|
+
**The final phase rides one more of these lines.** After the last file, the transfer emits
|
|
2753
|
+
`{"t":"transferprogress","id":<uint>,"index":0,"name":"","bytes":0,"total":0,"scanned":0,"phase":"writing","drive":"<string>"}`
|
|
2754
|
+
while it fsyncs every directory it created or wrote into. Cancel is not honoured there because
|
|
2755
|
+
every file is already complete. `drive` is the destination's own name, so the card's headline
|
|
2756
|
+
names where the flush is going without guessing from timing. Older clients ignore the fields
|
|
2757
|
+
they never asked for.
|
|
2758
|
+
|
|
2759
|
+
**An extract's one progress line is not a transfer's.** It carries the archive's own file name as
|
|
2760
|
+
`name` with `bytes` and `total` both 0: the unpacking tool streams no per-item bytes, so any figure for
|
|
2761
|
+
it would be invented and no percentage or time left is offered for it.
|
|
2762
|
+
|
|
2763
|
+
### transferitem
|
|
2764
|
+
|
|
2765
|
+
`{"t":"transferitem","id":<uint>,"index":<uint>,"name":"<string>","ok":<bool>,"err":"<string>"}`
|
|
2766
|
+
|
|
2767
|
+
Example: `{"t":"transferitem","id":12,"index":0,"name":"a.txt","ok":true}`
|
|
2768
|
+
Example: `{"t":"transferitem","id":12,"index":1,"name":"photos","ok":false,"err":"permission denied"}`
|
|
2769
|
+
|
|
2770
|
+
Exactly one per top-level item the transfer starts, in the order the items were named; an item it
|
|
2771
|
+
never starts, one a cancel reached first, one a `skip` left or a move already in place, answers none. **`err` rides only on a failure**, so
|
|
2772
|
+
a successful item's line carries no empty field to reason about, and a permission error on one file is
|
|
2773
|
+
that item's data rather than the operation's: the batch carries on to the next item. What a failed item
|
|
2774
|
+
had already written stays where it is and is journaled as that operation's own creation, so an `undo`
|
|
2775
|
+
removes it; only a cancel removes its partial itself, see `transfercancel`.
|
|
2776
|
+
|
|
2777
|
+
### transferdone
|
|
2778
|
+
|
|
2779
|
+
`{"t":"transferdone","id":<uint>,"ok":<uint>,"failed":<uint>,"skipped":<uint>,"cancelled":<bool>,"retryPaths":[<string>,...],"durable":<bool>,"note":"<string>"[,"merged":<uint>,"keptBoth":<uint>,"into":"<string>"][,"refused":[{"name":"<string>","err":"<string>"}]]}`
|
|
2780
|
+
|
|
2781
|
+
Example: `{"t":"transferdone","id":12,"ok":1,"failed":1,"skipped":0,"cancelled":false,"retryPaths":[],"durable":false,"note":""}`
|
|
2782
|
+
Example: `{"t":"transferdone","id":13,"ok":0,"failed":1,"skipped":0,"cancelled":false,"retryPaths":["/home/a/thesis"],"durable":true,"note":"","refused":[{"name":"thesis/refs/paper.pdf","err":"this drive cannot hold links"}]}`
|
|
2783
|
+
|
|
2784
|
+
The whole operation's terminal line. `skipped` counts the items the transfer did not start by design:
|
|
2785
|
+
those a cancel reached before they started, the colliding items a `collide` of `skip` left in place,
|
|
2786
|
+
and an item a `collide` move would have put back where it already is. A cancelled `replace` whose
|
|
2787
|
+
put-back failed is counted in `failed` instead, see `transfer`. `cancelled` is true when a
|
|
2788
|
+
`transfercancel`, a `quit` or stdin closing ended it early. `durable` is true only when the
|
|
2789
|
+
destination needed durability and every flush succeeded, so the UI can say "written to the drive";
|
|
2790
|
+
local targets report false and never claim it, and so do rclone ones, which skip the flush by design.
|
|
2791
|
+
`note` carries the sentence the UI prints beside the done count: `copied, but the drive did not
|
|
2792
|
+
confirm the folder` when the files landed but a folder flush did not, and `rclone uploads them in
|
|
2793
|
+
the background` for a copy onto an rclone mount, and is empty otherwise. `refused` rides only when an item
|
|
2794
|
+
failed for entries a drive refused inside its tree, see "An entry the destination cannot hold" above: one
|
|
2795
|
+
object per entry, its path under `dest` and its reason, in the order the walk met them and at most 100 of
|
|
2796
|
+
them across the transfer; that item's own `err` carries its full count. An item that ended on any other
|
|
2797
|
+
error adds nothing to `refused`. A merge carries two more
|
|
2798
|
+
counts, `merged` for every entry it landed (the inner entries of folded folders and every other
|
|
2799
|
+
landed item, including ones that waited on a move batch) and `keptBoth` for the landed entries its
|
|
2800
|
+
answer kept both, top-level and inner alike, and `into` naming the one merging folder, or the
|
|
2801
|
+
destination several folders folded into, so the status line needs no client state; older clients
|
|
2802
|
+
ignore the fields they never asked for. A merge's `refused`, when present, follows `into`.
|
|
2803
|
+
|
|
2804
|
+
### flashstarted
|
|
2805
|
+
|
|
2806
|
+
`{"t":"flashstarted","id":<uint>,"total":<uint>,"known":<bool>}`
|
|
2807
|
+
|
|
2808
|
+
The flash worker's first line, answering what will be written: `total` is the unpacked size
|
|
2809
|
+
when `known` is true and the file's own size otherwise. Every `flashprogress` and the one
|
|
2810
|
+
`flashdone` behind it carry the same `id`.
|
|
2811
|
+
|
|
2812
|
+
### flashprogress
|
|
2813
|
+
|
|
2814
|
+
`{"t":"flashprogress","id":<uint>,"phase":"prepare|password|write|flush|verify","bytes":<uint>,"total":<uint>,"permille":<uint>}`
|
|
2815
|
+
|
|
2816
|
+
At most one line per 150 ms, the transfer rule, whatever the drive answers. `password` runs
|
|
2817
|
+
from the helper's spawn to its descriptor, so the card says "waiting for your password";
|
|
2818
|
+
`flush` covers the final `fsync`, during which Cancel is dimmed, and `prepare` precedes
|
|
2819
|
+
the password prompt while the drive is re-read. See `flash` for what
|
|
2820
|
+
`bytes` and `permille` count in each phase.
|
|
2821
|
+
|
|
2822
|
+
### flashdone
|
|
2823
|
+
|
|
2824
|
+
`{"t":"flashdone","id":<uint>,"ok":<bool>,"phase":"<string>","at":<uint>,"err":"<string>","blank":<bool>}`
|
|
2825
|
+
|
|
2826
|
+
The flash worker's one terminal line, which releases the backend's flash slot. `phase`
|
|
2827
|
+
names where it ended: `verify` on success, the failing phase otherwise. `at` is the first
|
|
2828
|
+
differing offset on a `verify` failure and 0 elsewhere. See `flash` for every `err` and
|
|
2829
|
+
what `blank` promises about the drive.
|
|
2830
|
+
|
|
2831
|
+
### flasheject
|
|
2832
|
+
|
|
2833
|
+
`{"t":"flasheject","ok":<bool>,"err":"busy|gone|changed|error"}`
|
|
2834
|
+
|
|
2835
|
+
The helper's eject verdict: `busy` keeps the rail's shipped "still mounted" sentence,
|
|
2836
|
+
`gone` names a drive that left, `changed` one that changed since it was chosen, and
|
|
2837
|
+
`error` everything else.
|
|
2838
|
+
|
|
2839
|
+
### trashed
|
|
2840
|
+
|
|
2841
|
+
`{"t":"trashed","id":<uint>,"ok":<uint>,"failed":<uint>,"moved":[<string>,...]}`
|
|
2842
|
+
|
|
2843
|
+
Example: `{"t":"trashed","id":3,"ok":1,"failed":0,"moved":["/home/gm/old.txt"]}`
|
|
2844
|
+
|
|
2845
|
+
`id` echoes the request. `moved` names the original paths that went, so a client records an undo step for only those. Counts, plus the batch's own reason when anything failed: `{"t":"trashed","id":3,"ok":0,"failed":1,"err":"<string>","moved":[]}`.
|
|
2846
|
+
Unlike `transferitem` there is no per-path error text, because trash is one `gio` call for
|
|
2847
|
+
the batch and its exit status cannot attribute a failure to a single path; a path that is still on disk
|
|
2848
|
+
afterwards is counted in `failed`. `err` rides only on a failure, so a successful line has no `err` key.
|
|
2849
|
+
`gio` runs under a 10 s deadline, so a hung mount answers "The Trash took too long to answer"
|
|
2850
|
+
instead of holding the single operation slot.
|
|
2851
|
+
A batch that ran `gio` sends the line once `gio` has returned and each path has been checked on disk (`lstat`), whether
|
|
2852
|
+
or not every path went, so a failure goes out the same way with its `failed` count and `err`. It is sent before the second
|
|
2853
|
+
`gio trash --list`, which only names the undo journal's URIs. The operation slot is freed with that line: the next write
|
|
2854
|
+
(a second `trash` included) is accepted at once, while `undo` and `redo` answer busy naming the batch's first path until
|
|
2855
|
+
the list lands and the batch's undo entry is journaled, so an undo never reverses an earlier entry in its place. Entries
|
|
2856
|
+
journal in the order operations were accepted, so a write accepted while the list is out lands after the trash. A batch
|
|
2857
|
+
that ran no `gio` (every path renamed into the home Trash, or none present) sends its line once, with its journal entry.
|
|
2858
|
+
The line is sent once per batch.
|
|
2859
|
+
|
|
2860
|
+
### slow
|
|
2861
|
+
|
|
2862
|
+
`{"t":"slow","op":"<string>","path":"<string>","msg":"<string>"}`
|
|
2863
|
+
|
|
2864
|
+
Example: `{"t":"slow","op":"rename","path":"/hung/a.txt","msg":"/hung is slow. The rename will finish on its own."}`
|
|
2865
|
+
|
|
2866
|
+
A remote `rename`, `mkdir`, `bulkrename` or `settags` past the single-call deadline answers this line and moves
|
|
2867
|
+
on: the write stays running on its own worker, so the loop keeps answering every other request,
|
|
2868
|
+
and the mount is never marked stuck for a write still running. `op` names the request (`rename`,
|
|
2869
|
+
`mkdir`, `bulkrename` or `settags`), `path` the path it is writing (the rename source, the mkdir parent, the
|
|
2870
|
+
bulk batch's folder or the folder of the first remote row a settags names), and `msg` the sentence the client shows as information, never as an error. The
|
|
2871
|
+
`slow` line releases the one-at-a-time slot at once, so writes on other mounts and on local paths
|
|
2872
|
+
still run; the write stays pending under its mount root instead. A write touching a mount with a
|
|
2873
|
+
pending slow write answers `an operation is already running` and journals nothing, and `undo` and
|
|
2874
|
+
`redo` answer the same while any slow write is pending, naming its path. The late reply clears
|
|
2875
|
+
only its own pending entry. There is still no cancel id and no progress for the running write.
|
|
2876
|
+
The write's own reply follows whenever it lands: the same `renamed`, `made`, `bulkrenamed` or `settagged` line the
|
|
2877
|
+
in-time path writes, or the same `error` line if the write failed, journalled exactly once either
|
|
2878
|
+
way, so one `undo` reverses it.
|
|
2879
|
+
|
|
2880
|
+
### renamed
|
|
2881
|
+
|
|
2882
|
+
`{"t":"renamed","ok":<bool>,"path":"<string>"}`
|
|
2883
|
+
|
|
2884
|
+
Example: `{"t":"renamed","ok":true,"path":"/home/gm/new.txt"}`
|
|
2885
|
+
|
|
2886
|
+
`path` is the full path the file now has. A refusal is an `error` line instead, never this line with
|
|
2887
|
+
`ok` false.
|
|
2888
|
+
|
|
2889
|
+
### duplicated
|
|
2890
|
+
|
|
2891
|
+
`{"t":"duplicated","ok":<bool>,"path":"<string>"}`
|
|
2892
|
+
|
|
2893
|
+
Example: `{"t":"duplicated","ok":true,"path":"/home/gm/photo copy.jpg"}`
|
|
2894
|
+
|
|
2895
|
+
`path` is the sibling that was written. A failure is an `error` line with `where` of `duplicate`, and a
|
|
2896
|
+
copy that failed partway leaves its partial sibling in place and journals it, so an `undo` removes it.
|
|
2897
|
+
|
|
2898
|
+
### made
|
|
2899
|
+
|
|
2900
|
+
`{"t":"made","ok":<bool>,"path":"<string>"}`
|
|
2901
|
+
|
|
2902
|
+
Example: `{"t":"made","ok":true,"path":"/home/gm/New Folder"}`
|
|
2903
|
+
|
|
2904
|
+
`path` is the full path of the directory that now exists, the chosen default included when the request
|
|
2905
|
+
sent no `name`, which is what lets a client re-list with that row under the cursor and open its rename
|
|
2906
|
+
field on it. A refusal is an `error` line with `where` of `mkdir` instead, never this line with `ok`
|
|
2907
|
+
false.
|
|
2908
|
+
|
|
2909
|
+
### undone
|
|
2910
|
+
|
|
2911
|
+
`{"t":"undone","op":"<string>","ok":<bool>}`
|
|
2912
|
+
|
|
2913
|
+
Example: `{"t":"undone","op":"move","ok":true}`
|
|
2914
|
+
|
|
2915
|
+
`op` is the kind of operation that was reversed, one of `rename`, `duplicate`, `mkdir`, `trash`, `copy`,
|
|
2916
|
+
`move` or `merge`, which is what lets the status bar say what it just put back. A copy merge journals
|
|
2917
|
+
`merge`, and the bar words its undo whole: "Undid the merge." Its redo refuses instead, since the
|
|
2918
|
+
pre-existing folder is still there. A move merge journals `move`, so its redo reads as a move.
|
|
2919
|
+
|
|
2920
|
+
### jumped
|
|
2921
|
+
|
|
2922
|
+
`{"t":"jumped","id":<uint>,"favourites":[<string>,...],"zoxide":[<string>,...],"recent":[<string>,...],"frecency":{<string>:<float>,...},"ms":<float>}`
|
|
2923
|
+
|
|
2924
|
+
Example: `{"t":"jumped","id":3,"favourites":["/home/gm/Projects"],"zoxide":["/home/gm/Documents"],"recent":["/home/gm/Pictures/screenshots"],"frecency":{"/home/gm/Documents":80},"ms":4.210}`
|
|
2925
|
+
|
|
2926
|
+
The answer to one `jump`: the folders of each source that exist now, each source in its own order.
|
|
2927
|
+
A favourite or a zoxide row must itself be a directory; a recent entry stands for the folder it sits in,
|
|
2928
|
+
unless it is a folder itself. A folder appears once, in the first source that names it, so a favourite
|
|
2929
|
+
zoxide also ranks is drawn as the favourite. `frecency` is zoxide's score for every folder answered that
|
|
2930
|
+
zoxide ranks, whichever source draws it, and the client ranks the one list by it after the match itself.
|
|
2931
|
+
Anything that is not an absolute path is dropped, and so is a score that is not a finite number. `ms` is
|
|
2932
|
+
the whole answer's time, zoxide and the existence checks together.
|
|
2933
|
+
|
|
2934
|
+
### devs
|
|
2935
|
+
|
|
2936
|
+
`{"t":"devs","devs":{<string>:<uint>,...},"id":<uint optional>}`
|
|
2937
|
+
|
|
2938
|
+
Example: `{"t":"devs","devs":{"/":66305,"/home":66306,"/home/gm/gone":0}}`
|
|
2939
|
+
|
|
2940
|
+
The answer to one `devs`: each asked path once, in the order asked, with its device number. A path that cannot
|
|
2941
|
+
be read, a missing one, an empty one, a relative one (it would name the backend's own directory, which the client
|
|
2942
|
+
never means) and one on a mount that is not answering all answer 0, and the client reads 0 as unknown and copies:
|
|
2943
|
+
copying where a move was possible is an annoyance, and moving across volumes loses the original. The line carries
|
|
2944
|
+
the request's positive `id` when supplied; the rail accepts only the current hover's id and path. Untagged answers
|
|
2945
|
+
continue to fill the breadcrumb and column device map, while tagged hover answers never enter that cache.
|
|
2946
|
+
|
|
2947
|
+
### changed
|
|
2948
|
+
|
|
2949
|
+
`{"t":"changed","path":"<string>"}`
|
|
2950
|
+
|
|
2951
|
+
Example: `{"t":"changed","path":"/home/gm/Downloads"}`
|
|
2952
|
+
|
|
2953
|
+
**The one line no request asks for.** Every other response answers a request; this one says the
|
|
2954
|
+
directory the current listing came from is no longer what `list` answered with, because another
|
|
2955
|
+
program created, deleted, renamed, wrote or chmod'd something in it. Nothing the backend holds
|
|
2956
|
+
changes with it: the rows, the count and the sort order are exactly what they were, and a client
|
|
2957
|
+
that wants the new directory sends `list` again. `path` is the watched directory, so a client that
|
|
2958
|
+
has navigated since the notification was written can tell it is not about the folder it is on now,
|
|
2959
|
+
and drop it.
|
|
2960
|
+
|
|
2961
|
+
A `list` starts watching its path **before it reads the directory**, not after, because a change
|
|
2962
|
+
landing while the read runs is missing from the rows that `list` is about to answer with and is
|
|
2963
|
+
therefore exactly the change the client has to be told about. It is armed **beside** the watch the
|
|
2964
|
+
client is already on rather than in place of it, so a `list` that then fails to scan costs the
|
|
2965
|
+
directory still listed nothing at all: its watch was never removed, and the descriptor its own
|
|
2966
|
+
events carry is still the current one. `search` and `listpaths` both stop watching, because a set of
|
|
2967
|
+
matches and a set of named paths are not directories.
|
|
2968
|
+
|
|
2969
|
+
**A column's directory is watched too, and answers this same line.** A `peek` that carries
|
|
2970
|
+
`"watch":true`, which only the columns view sends, arms a non-recursive watch on its directory before
|
|
2971
|
+
it scans, on a second inotify descriptor of its own so that removing one can never remove the listed
|
|
2972
|
+
folder's watch. The watch fires the `changed` line with `path` naming the peeked directory, and the
|
|
2973
|
+
client re-asks that column; a client on another path drops the line as it always did. Only the
|
|
2974
|
+
directories the latest watching `peek` names in `"keep"` stay watched, and a peek without the flag watches nothing.
|
|
2975
|
+
`"keep":[...]` is every column directory the client draws, the peeked one among them: the backend unwatches every
|
|
2976
|
+
other peeked directory before it arms this one, and unwatches at once a directory armed after the client moved on.
|
|
2977
|
+
Eight directories is a guard for a client that sends no `keep`, the oldest dropped first, and a directory the
|
|
2978
|
+
kernel stops watching (deleted, unmounted) leaves the set after its last `changed` line.
|
|
2979
|
+
|
|
2980
|
+
The mechanism is one inotify watch on that one directory, non-recursive, with the mask
|
|
2981
|
+
`IN_ATTRIB | IN_CLOSE_WRITE | IN_MOVED_FROM | IN_MOVED_TO | IN_CREATE | IN_DELETE | IN_MOVE_SELF`,
|
|
2982
|
+
which is exactly the set of events that changes what a listing says: which names are in it, and the
|
|
2983
|
+
size, date and mode its columns draw. Deleting the watched directory needs no bit of its own: the
|
|
2984
|
+
kernel removes the watch along with it and reports that removal whatever the mask holds.
|
|
2985
|
+
**A file growing under an open handle is not one of them.** `IN_MODIFY` fires on every `write(2)`
|
|
2986
|
+
and a listing does not draw a partial size, so a row's size follows the writer closing the file
|
|
2987
|
+
rather than the writer writing to it.
|
|
2988
|
+
|
|
2989
|
+
**One burst is one line.** The event payload is read only far enough to name its watch descriptor,
|
|
2990
|
+
never for which file moved, and the reader then pauses 100 ms before reading again, so a directory
|
|
2991
|
+
being rewritten costs one `changed` line per 100 ms rather than one per file. Anything the kernel
|
|
2992
|
+
drops in that window costs nothing, because every event in a burst says the same thing to a client
|
|
2993
|
+
that re-reads the whole directory anyway.
|
|
2994
|
+
|
|
2995
|
+
`--backend` on a box whose inotify instance limit is exhausted prints one sentence to stderr at
|
|
2996
|
+
startup and then never sends this line, and a `list` whose directory the kernel refuses a watch on
|
|
2997
|
+
(`max_user_watches`, most often) prints one naming that directory; every other request answers
|
|
2998
|
+
exactly as before. A client must therefore treat a live listing as an improvement it may not get,
|
|
2999
|
+
not as a guarantee. A
|
|
3000
|
+
network mount is the other case: inotify sees the local kernel's own view of a directory, so a
|
|
3001
|
+
change another machine makes to an NFS or SMB share is not delivered, and Flea is stale there in
|
|
3002
|
+
exactly the way it was before this line existed.
|
|
3003
|
+
|
|
3004
|
+
### plugins
|
|
3005
|
+
|
|
3006
|
+
`{"t":"plugins","id":1,"plugins":[{"id":"flea.shelf","name":"Shelf","version":"0.4.0","author":"Flea","license":"MIT","description":"...","kinds":["shelf"],"glyph":"flea","mark":"","access":["shelf:pile"],"source":"builtin","origin":"","enabled":false,"failure":"","detail":"","schema":[],"settings":{},"shelf":{"enabled":false,"bar":true,"rail":"off","screenshots":true,"recordings":true,"recent":3,"pins":[]}}]}`
|
|
3007
|
+
|
|
3008
|
+
The whole installed list after the request named by `id`; see `plugins` above. `mark` is an absolute path to the
|
|
3009
|
+
plugin's own SVG when it has one. `shelf` is `null` on every plugin but the Shelf.
|
|
3010
|
+
|
|
3011
|
+
### pluginop
|
|
3012
|
+
|
|
3013
|
+
`{"t":"pluginop","id":2,"op":"add","plugin":"mtorvik.checksums","ok":true,"code":"","detail":"","version":"0.2.0"}`
|
|
3014
|
+
|
|
3015
|
+
One change's outcome. A refusal has `ok` false and a `code` with one `detail` argument; the codes are the ones
|
|
3016
|
+
`src/ui_logic_st2.rs` and `src/ui_logic_st2/wire.rs` list, each selecting a `plugins.error.*` or
|
|
3017
|
+
`plugins.invalid.*` catalog sentence. `version` is the plugin's version after an `add` or `update`.
|
|
3018
|
+
|
|
3019
|
+
### pluginprogress
|
|
3020
|
+
|
|
3021
|
+
```json
|
|
3022
|
+
{"t":"pluginprogress","id":44,"plugin":"example.send-target","message":{"op":"progress","done":1,"total":1,"message":"Receiving selection."}}
|
|
3023
|
+
```
|
|
3024
|
+
|
|
3025
|
+
A progress event for the outer runtime request identified by `id` and `plugin`. The outer `message` holds the
|
|
3026
|
+
plugin's progress object. Its `done` and `total` are whole numbers from 0 through 9,000,000,000,000,000, with
|
|
3027
|
+
`done` no greater than `total`. The inner `message` is an optional string. Progress is streamed while the request
|
|
3028
|
+
runs, before its terminal `pluginruntime` reply. Clients should correlate events by `id` rather than count lines.
|
|
3029
|
+
|
|
3030
|
+
### pluginruntime
|
|
3031
|
+
|
|
3032
|
+
```json
|
|
3033
|
+
{"t":"pluginruntime","id":41,"plugin":"example.send-target","ok":true,"message":{"op":"targets","targets":[{"id":"receiver","label":"Local Receiver","state":"ready"},{"id":"offline","label":"Offline Receiver","state":"offline"}]},"progress":[],"code":"","error":""}
|
|
3034
|
+
```
|
|
3035
|
+
|
|
3036
|
+
The terminal outcome of one runtime request. The envelope always includes `t`, `id`, `plugin`, `ok`, `message`,
|
|
3037
|
+
`progress`, `code` and `error`. `id` and `plugin` identify the outer request. On success, `ok` is true, `message`
|
|
3038
|
+
contains the result object, and `code` and `error` are empty strings. `progress` holds the accepted progress objects
|
|
3039
|
+
in order, including objects already sent through `pluginprogress`. Do not apply them twice.
|
|
3040
|
+
|
|
3041
|
+
For example, this send emits the progress line above and then the terminal line below:
|
|
3042
|
+
|
|
3043
|
+
```json
|
|
3044
|
+
{"c":"plugins","op":"runtime","id":44,"plugin":"example.send-target","request":{"op":"send","target":"receiver","paths":["/home/me/notes.txt"]}}
|
|
3045
|
+
{"t":"pluginruntime","id":44,"plugin":"example.send-target","ok":true,"message":{"op":"done","message":"Selection received."},"progress":[{"op":"progress","done":1,"total":1,"message":"Receiving selection."}],"code":"","error":""}
|
|
3046
|
+
```
|
|
3047
|
+
|
|
3048
|
+
A plugin's `error` result is an operation failure: `ok` is false, `code` is `operation`, `error` is the plugin's
|
|
3049
|
+
message string, and the original error object remains in `message`. Accepted progress is retained. This result
|
|
3050
|
+
does not itself mark the plugin failed.
|
|
3051
|
+
|
|
3052
|
+
```json
|
|
3053
|
+
{"t":"pluginruntime","id":45,"plugin":"example.send-target","ok":false,"message":{"op":"error","message":"Choose the ready receiver and at least one file."},"progress":[],"code":"operation","error":"Choose the ready receiver and at least one file."}
|
|
3054
|
+
```
|
|
3055
|
+
|
|
3056
|
+
A host refusal or runtime failure instead has `ok:false`, `message:null`, `progress:[]`, and a `code` and `error`
|
|
3057
|
+
describing the failure. Previously streamed progress can therefore exist even when the terminal array is empty.
|
|
3058
|
+
Codes include `unknown` for an unrecognized installed id, `request` for an invalid operation or its validated
|
|
3059
|
+
fields, `busy` for a concurrent ordinary lasting-session request or a full plugin-host worker pool, `start` for startup or
|
|
3060
|
+
runtime failures and for a disabled or failed plugin, and `write` when the host cannot initialize plugin storage.
|
|
3061
|
+
These fields are `code` and `error`, rather than the management reply's `code` and `detail`.
|
|
3062
|
+
|
|
3063
|
+
```json
|
|
3064
|
+
{"t":"pluginruntime","id":46,"plugin":"example.send-target","ok":false,"message":null,"progress":[],"code":"busy","error":"plugin busy"}
|
|
3065
|
+
```
|
|
3066
|
+
|
|
3067
|
+
Preview results are normalized before they reach this envelope. Text is reduced to at most 1,000 plain lines with
|
|
3068
|
+
control characters other than tab removed from each line. A PNG result's `png` names the retained host file, with
|
|
3069
|
+
`width` and `height`; it is not the subprocess's `/out/preview.png` path.
|
|
3070
|
+
|
|
3071
|
+
### error
|
|
3072
|
+
|
|
3073
|
+
`{"t":"error","where":"<string>","path":"<string>","msg":"<string>"[,"mode":<uint>][,"code":"<string>"]}`
|
|
3074
|
+
|
|
3075
|
+
Example: `{"t":"error","where":"scan","path":"/root","msg":"permission denied","mode":16872}`
|
|
3076
|
+
|
|
3077
|
+
`where` names the failing operation, `path` names the input that failed, `msg` is the
|
|
3078
|
+
underlying message. `code` rides only on a refusal the UI words itself (`emptyclipboard`, from `pasteimage`). Over `--backend`'s stdout, `where` is `scan` (a `list` whose path
|
|
3079
|
+
failed to read), `sort` (a `sort` whose key names no order this wire defines; `path`
|
|
3080
|
+
carries the key as sent), `window` (a remote metadata window whose mount did not
|
|
3081
|
+
respond or whose worker stopped; `path` matches the current listing header's `path`
|
|
3082
|
+
byte for byte, including its original spelling), `stale` (a row-indexed request naming
|
|
3083
|
+
a numbering the listing has left; `path` carries the command, see "listing"), `flash` (a second
|
|
3084
|
+
flash while one runs; `path` carries the device), or `read` (the
|
|
3085
|
+
stdin stream itself could not be decoded; the loop stops right after emitting this
|
|
3086
|
+
line, because the framing cannot be trusted past that point; thumbnail work already
|
|
3087
|
+
running is still drained after it, so a `thumbed` line can follow). The write
|
|
3088
|
+
operations answer over the same stdout and name themselves the same way, and
|
|
3089
|
+
`rename-kept` and `rename-stranded` (see `rename`) are the only values on this wire that are not one lowercase
|
|
3090
|
+
word, so a client matching this field must allow the hyphen. `error_line`
|
|
3091
|
+
is also how `flea --prewarm` reports a failure, to stderr rather than over this wire
|
|
3092
|
+
protocol. `where` names whichever operation actually failed: a missing or unreadable
|
|
3093
|
+
`path` propagates `scan`'s error unchanged, so `where` reads `scan` there too; only a
|
|
3094
|
+
failure inside prewarm's own file handling (creating, writing or renaming the temp
|
|
3095
|
+
file) reports `where":"prewarm"`. An `error` line is sent instead of `listed` or
|
|
3096
|
+
`rows`; it never changes the backend process's exit code.
|
|
3097
|
+
|
|
3098
|
+
`mode` is the `st_mode` of the path a `scan` could not read, in decimal, and it is written only when
|
|
3099
|
+
the backend actually read it. It is a directory's mode in the case the field exists for; a `list` of
|
|
3100
|
+
a regular file fails with `ENOTDIR` and carries that file's mode instead, which no client draws,
|
|
3101
|
+
because only a denial reaches the Locked state. The stat outlives the denial: `/root` answers 16872, which is
|
|
3102
|
+
`0o40750`, while `opendir` on it is refused. No other `where` ever carries the field, and a `scan`
|
|
3103
|
+
whose path could not be stat'd either leaves it out, which a client reads as "I could not look" and
|
|
3104
|
+
draws as a sentence with no permission string under it. It exists because a typed path, or
|
|
3105
|
+
`FLEA_PATH`, reaches the denial with no parent listing to remember the mode from.
|
|
3106
|
+
|
|
3107
|
+
## Prewarm reuses this wire format
|
|
3108
|
+
|
|
3109
|
+
`flea --prewarm <path> <first> <dest>` writes exactly a `listed` line then a `rows`
|
|
3110
|
+
line, the same two lines `--backend` would print for an equivalent `list`, to `dest`
|
|
3111
|
+
instead of stdout. See `AGENTS.md`, "Prewarm".
|
|
3112
|
+
|
|
3113
|
+
`--prewarm` exits 0 if and only if `dest` holds a complete listing of `path`; on
|
|
3114
|
+
failure it writes an error line to stderr, exits non-zero, leaves no temp file, and
|
|
3115
|
+
does not touch `dest`, so a caller must check the exit status before reading the
|
|
3116
|
+
file.
|
|
3117
|
+
|
|
3118
|
+
## Undocumented requests
|
|
3119
|
+
|
|
3120
|
+
`peek`, `paths`, `archive`, `convert` and `formats` are on the wire and are not documented
|
|
3121
|
+
here yet. `tools/flea-acceptance` derives its checklist from this file, so each one is a gap in that
|
|
3122
|
+
battery until its section is written.
|
|
3123
|
+
|
|
3124
|
+
The `formats` and `archive` parts of that set are named here exactly, because they changed. A `formats` reply's `extract`
|
|
3125
|
+
object carries one bit per extension class: `{"archive":<bool>,"sevenZip":<bool>,"zip":<bool>}`, where
|
|
3126
|
+
`archive` is the tar class on `bsdtar`, `sevenZip` is the `.7z` and package (`.deb`, `.rpm`, `.apk`, `.jar`,
|
|
3127
|
+
`.cab`, `.cpio`, `.AppImage`) class on `7z`, and `zip` is the
|
|
3128
|
+
`.zip`/`.rar` class served by whichever of those two is installed. The client offers Extract on a package only
|
|
3129
|
+
when `sevenZip` is set; the backend's own listing of a package still falls back to `bsdtar` where `7z` is missing,
|
|
3130
|
+
as v0.3.8 did, except an `.AppImage`, whose squashfs image only `7z` reads. An `archive` request with an `op` of
|
|
3131
|
+
`extract` joins the destination-drive queue `transfer` describes. It answers `archivestarted` with its
|
|
3132
|
+
own id even while waiting, drives that same transfer card through `extractstarted` and
|
|
3133
|
+
`transferprogress`, and answers `archivedone`, never `transferdone`. A `transfercancel` naming its
|
|
3134
|
+
id cancels only that extraction, whether waiting or running. It is not journaled, so no `undo`
|
|
3135
|
+
reverses it.
|
|
3136
|
+
|
|
3137
|
+
An `archive` request is `{"c":"archive","op":"compress"|"extract","paths":[<string>,...],"path":"<string>","dest":"<string>","format":"<string>","password":"<string>","encryptNames":<bool>}`.
|
|
3138
|
+
A `compress` names absolute `paths` sharing one parent and the `format` to write; an `extract` names the
|
|
3139
|
+
one archive in `path`. `password` is empty for a plain operation: only a 7z compress or an extract that
|
|
3140
|
+
7z reads (a `.zip` too, where 7z is installed) takes a non-empty one, and any other reader answers `err` of `"only an archive that 7-Zip reads
|
|
3141
|
+
can take a password"` without piping the secret. The secret travels this stdin wire and leaves it only on
|
|
3142
|
+
7z's own stdin pipe, never in argv, an environment variable, a log or `/proc/<pid>/cmdline`. A compress
|
|
3143
|
+
argv carries a bare `-p` (7z then reads one stdin line), an extract carries none (7z reads a bare `-p`
|
|
3144
|
+
as an empty password). `encryptNames` hides the entry names of a password `.7z` (`-mhe=on`) and is
|
|
3145
|
+
ignored for `.zip`, which a password writes as AES-256 (`-mem=AES256`). Absent `password` is today's
|
|
3146
|
+
plain path, so every older client is unchanged. A plain `.zip` is written by `7z -tzip` when 7z is
|
|
3147
|
+
installed and by `bsdtar` otherwise, and every 7z compress stores a symbolic link as a link (`-snl`), as
|
|
3148
|
+
bsdtar does, so a dangling one no longer fails the archive.
|
|
3149
|
+
|
|
3150
|
+
Before a plain extract starts, an archive that 7z reads is probed with `7z l -slt` and stdin closed: exit
|
|
3151
|
+
255 (the headers are encrypted) or exit 0 with an `Encrypted = +` line (entries are encrypted) answers
|
|
3152
|
+
`err` of `"encrypted"`, and so does a plain extract that still exits 255. A password run that exits 2
|
|
3153
|
+
answers `"wrong-password"` only when the same technical listing proves a password guards the archive: 7z
|
|
3154
|
+
exits 2 for a wrong password, for damaged encrypted data and for a file that is no archive at all, so
|
|
3155
|
+
without that proof the run answers as a plain failure and the card never asks again. Both are codes, never sentences, read from exit
|
|
3156
|
+
codes and technical listing keys and never from the tool's words; the card words its own sentence from a
|
|
3157
|
+
template, the client asks again on both rather than messaging, and Cancel runs nothing. A line carrying
|
|
3158
|
+
either code also names the retry: `{"t":"archivedone","id":<uint>,"ok":false,"verified":true,"err":"encrypted"|"wrong-password","path":"<archive>","dest":"<destination>"}`,
|
|
3159
|
+
so the card opens over the archive that asked and no client-side stash can name another extract's.
|
|
3160
|
+
|
|
3161
|
+
A `.iso` lists and is never extracted: an `extract` naming one answers one `archivedone` with `ok` false
|
|
3162
|
+
and `err` of `"a disk image opens as a listing and is never extracted"`, and starts nothing, so no `archivestarted`
|
|
3163
|
+
line is sent and no slot is taken. Before extracting, the archive's unpacked
|
|
3164
|
+
size from its own index is weighed against the destination's free space, and a fit that cannot land is
|
|
3165
|
+
refused the same way: `{"t":"archivedone","id":<uint>,"ok":false,"verified":true,"err":"Extracting
|
|
3166
|
+
<leaf> needs <needed>, and <target> has <free> free."}`, where `<target>` is the name of the folder the
|
|
3167
|
+
extract lands in, or `/` for the root. An index that cannot be read refuses nothing, because unknown is not evidence
|
|
3168
|
+
the extract cannot fit.
|
|
3169
|
+
|
|
3170
|
+
A `peek` request with `"details":true` is used by TUI folder previews. Only returned
|
|
3171
|
+
rows, already limited by `first` and `PEEK_CAP`, pay one no-follow metadata read
|
|
3172
|
+
inside the existing bounded mount worker. Their `s` (entry bytes) and `p` (mode)
|
|
3173
|
+
fields let the TUI draw child sizes. A vanished or unreadable entry carries zero
|
|
3174
|
+
mode and size, which the TUI leaves unavailable. The default is false: ancestor,
|
|
3175
|
+
network and other existing peeks keep their name-only path and perform no
|
|
3176
|
+
per-entry metadata reads.
|
|
3177
|
+
|
|
3178
|
+
One `peek` field is worth naming ahead of that section, because it is new. A `peeked` line whose scan
|
|
3179
|
+
failed carries `"failed":true`, with `"mode":<uint>` beside it under the same rule the `error` line
|
|
3180
|
+
uses, so a column can tell an unreadable directory from an empty one instead of drawing both as the
|
|
3181
|
+
empty state. A `peeked` line that succeeded carries neither field, whatever `n` is.
|
|
3182
|
+
|
|
3183
|
+
More `peek` fields are worth naming for the same reason. Every `peeked` line, failed or not,
|
|
3184
|
+
echoes the `hidden`, `hiddenLast` and `first` its request carried, because three clients read this wire at once: `ui/ColumnsArea.qml`
|
|
3185
|
+
peeks the pane's ancestors with the listing's own flags and the pane's window size, `ui/NetworkMounts.qml` peeks with a first of 1 or of 512,
|
|
3186
|
+
and the path bar's Tab peeks with whatever the typed leaf asks for. The repair legs ask 512 and the folder check asks 1. `path` alone cannot tell one client's reply from another's, and `path`
|
|
3187
|
+
plus `hidden` plus `hiddenLast` plus `first` can, which is all the correlation any needs: the same quad answers the same rows,
|
|
3188
|
+
so no request id has to be threaded through. A client that ignores the fields reads the line exactly
|
|
3189
|
+
as it did before. A `peeked` line echoes `by` and `desc` only when its request carried `by`, so a column re-asked after its folder's remembered order changed tells a reply sorted under the old order from the one it waits for, and a request without `by` gets the line it always got. A `peek` request carries optional `hiddenLast`, `false` unless `true`: with
|
|
3190
|
+
it on the peek sorts dotfiles last the way the listing does, so the columns beside a hidden-last
|
|
3191
|
+
listing draw the same order instead of today's dotfiles-first one. A `peek` request carries optional `by` (name, size or mtime) and `desc`, sent only by the columns view for a folder with its own remembered order while remembering is on; absent is name order, a key the wire never defined answers the same `"where":"sort"` error a `sort` does and no `peeked`, and only size and mtime pay a stat per entry. A `peek` request carries optional `watch`,
|
|
3192
|
+
`false` unless `true`, and `keep`, a list of paths: the columns view sets both so the backend arms a watch on that
|
|
3193
|
+
directory, see "A column's directory is watched too" above; the `peeked` reply is the same either way. A `peeked` line also carries `v`, the directory's device, and `w`, whether it can be written, the
|
|
3194
|
+
same two fields a `listed` line carries, read once per peek so a drag from a column takes the verb a drag from the
|
|
3195
|
+
listing takes; a failed peek carries neither.
|
|
3196
|
+
|
|
3197
|
+
## Known gaps
|
|
3198
|
+
|
|
3199
|
+
- The two-line prewarm file names its directory, as every `listed` line does, but nothing
|
|
3200
|
+
in it says whether that directory changed after it was written, so a stale file cannot be
|
|
3201
|
+
told from a fresh one and production ignores `FLEA_PREWARM`. Add a freshness proof and
|
|
3202
|
+
prove a first-paint win before re-enabling a reader.
|
|
3203
|
+
- The pool is built when the backend starts, not when the first `thumb` arrives. That
|
|
3204
|
+
costs every backend process about 1.2 ms of startup and about 0.5 MB of PSS for a
|
|
3205
|
+
subsystem a client may never use; see AGENTS.md "Thumbnail requests".
|
|
3206
|
+
- `thumbcancel` names rows, and only rows this process actually queued can be cancelled.
|
|
3207
|
+
Cancelling a row that was answered from the cache, or that no thumbnailer declares, is
|
|
3208
|
+
silently a no-op, because there was never a job to drop.
|
|
3209
|
+
- `t` is true for a symlink whose name resolves to a declared type, whatever the link points at.
|
|
3210
|
+
The flag reads the file type out of the row's own `p`, which for a symlink is `S_IFLNK` and says
|
|
3211
|
+
nothing about the target, so a symlink to a directory, to a FIFO, to a loop, or to nothing at
|
|
3212
|
+
all still reports `t:true`. Every one of them is answered with an empty `file` in microseconds
|
|
3213
|
+
when it is actually asked for, because the request path stats the target, so nothing blocks and
|
|
3214
|
+
no marker is written. It is an over-promise on the wire rather than a hazard, and a client that
|
|
3215
|
+
renders per-row state from `t` alone will show a thumbnail slot that never fills. The shipped
|
|
3216
|
+
GUI renders defensively instead of the wire narrowing: an empty `file` is a terminal answer in
|
|
3217
|
+
its row map and the delegate falls back to the themed icon, so such a row simply keeps the icon
|
|
3218
|
+
it already drew. Narrowing the flag stays undone on purpose, because it is a wire change with
|
|
3219
|
+
its own documentation and test surface and half-narrowing it is worse than leaving it whole.
|
|
3220
|
+
- A non-UTF8 filename arrives lossy, and the thumbnail path inherits that. Two names in one
|
|
3221
|
+
directory differing only in their invalid bytes collapse to the same string, so the backend
|
|
3222
|
+
maps one path where the client named two rows and the second row's `thumbed` line arrives
|
|
3223
|
+
only at the next `list`, `sort` or shutdown. See AGENTS.md "Deliberate corners".
|
|
3224
|
+
- The `file` a `thumbed` line names is read from the shared cache, which every application on
|
|
3225
|
+
the box writes, and nothing sandboxes the client that decodes it. Generation runs under
|
|
3226
|
+
`bwrap`; display does not. The shipped GUI does hand this path straight to a QML `Image`, so
|
|
3227
|
+
Qt's PNG decoder runs unconfined in the shell process on bytes it did not write. That is a
|
|
3228
|
+
known residual risk, parked for a later plan rather than closed here. See AGENTS.md
|
|
3229
|
+
"Deliberate corners".
|
|
3230
|
+
|
|
3231
|
+
## Jobs and Actions (0.4.0)
|
|
3232
|
+
|
|
3233
|
+
Jobs are additive to the existing transfer, archive, and LocalSend requests and replies. The backend still emits the existing single-operation events with their existing fields. Clients that do not consume jobs can ignore the new event types. Each copy, move, compression, extraction, background action, and LocalSend send has a backend job id, independent of a caller's request id. Empty Trash adopts its existing operation id and cancellation flag through the same model, with the head `Emptying Trash`.
|
|
3234
|
+
|
|
3235
|
+
Requests:
|
|
3236
|
+
|
|
3237
|
+
```json
|
|
3238
|
+
{"c":"jobs","op":"list"}
|
|
3239
|
+
{"c":"jobs","op":"cancel","id":12}
|
|
3240
|
+
{"c":"jobs","op":"retry","id":12}
|
|
3241
|
+
{"c":"jobs","op":"dismiss","id":12}
|
|
3242
|
+
{"c":"jobs","op":"undo","id":12}
|
|
3243
|
+
```
|
|
3244
|
+
|
|
3245
|
+
`list` returns a `jobs` snapshot. A snapshot also accompanies membership and scheduling changes. `jobs` contains the ordered list, newest first, `unfinished` counts only running and waiting jobs, and `newest` identifies the newest unfinished job or is null. `head` is that job's head, or an empty string. This supplies the TUI status row's newest operation and its `· 3 jobs` suffix. A `job` update replaces the matching id without moving its row. Progress updates are limited to one per job per 150 ms; scheduling, completion, cancellation, and Undo availability changes are immediate.
|
|
3246
|
+
|
|
3247
|
+
```json
|
|
3248
|
+
{"t":"jobs","jobs":[],"unfinished":0,"newest":null,"head":""}
|
|
3249
|
+
{"t":"job","job":{"id":12,"kind":"copy","head":"Copying 10 of 18 to NAS","file":"DSC_4944.jpg","done":9,"total":18,"bytes":5300000,"totalBytes":9700000,"itemBytes":458300,"rate":1100000,"left":4,"state":"running","reason":"","output":[],"undo":false,"cancelled":false},"unfinished":3}
|
|
3250
|
+
{"t":"joberror","id":12,"error":"Only a failed job can be retried."}
|
|
3251
|
+
```
|
|
3252
|
+
|
|
3253
|
+
Kinds are `copy`, `move`, `compress`, `extract`, `send`, `action`, and the adoption kind `emptyTrash`. States are `waiting`, `running`, `failed`, and `done`. `done` counts completed top-level items; `total` counts this attempt's items. Extraction starts with zero unknown entry counts until its producer supplies archive entry counts and reports unpacked bytes through `OpMsg::JobEntries`, without changing the wire. `file` is the current item's name; `itemBytes` is its known size. `bytes` is cumulative progress and `totalBytes` is the settled byte total, with zero meaning unknown. `rate` is bytes per second and `left` is estimated seconds or null when no estimate is available. A background action has null byte fields and a file-count bar. Empty Trash has null byte fields and counts successfully deleted filesystem nodes, including descendants, against the reviewed recursive total. Its initial total is zero until the worker reads the confirmation. It retains the current top-level item's reviewed size in `itemBytes`; `left` uses deleted nodes and elapsed time. Its terminal legacy replies still count top-level Trash entries, cancellation keeps completed deletions, and retry requires a fresh Trash confirmation.
|
|
3254
|
+
|
|
3255
|
+
Copies, moves, compressions, and extractions are queued in arrival order per destination filesystem. The device is resolved off the request loop, following a symlinked target. Btrfs subvolumes share the mount's source key because their anonymous `st_dev` values can differ. Separate destination filesystems run concurrently. A waiting copy uses `Copy 8 items to NAS` and names its predecessor in `reason`, for example `waiting for the copy to NAS`. Cancellation addresses only the named job, including a queued job. `transfercancel` also reaches jobs with that id, including compression. A clean shutdown cancels queued and running jobs through the existing drain.
|
|
3256
|
+
|
|
3257
|
+
A failure retains its position, with a failure head and its reason. `retry` preserves the row and id, requeues behind jobs already accepted, and narrows the attempt to failed items. The queued work retains each failed source's identity, including its birth time when available. Transfers check that identity before each item, actions before each invocation, and indivisible archive or send operations before their invocation. A replacement is refused and never becomes the identity of a subsequent retry. A compression or extraction is one indivisible failed operation. `dismiss` accepts only failed or done rows.
|
|
3258
|
+
|
|
3259
|
+
A successful journalled transfer has `undo:true` only after its journal entry is published. The offer belongs to that backend's job and expires when another operation, Undo, or Redo changes the history, including in another window. The shared journal claim compares the saved history revision and exact entry under its lock; an expired offer is refused with `That job can no longer be undone.` and revoked without consuming any entry. Reversal replies use the existing `undone` or `error` events and update search indexes and tag state through the same path as ordinary Undo, including paths changed before a partial failure. `undo` refuses while another job is unfinished. Compressions, extractions, sends, and arbitrary actions do not invent undo records. A done row is a completion notification and is omitted from the visible card; the backend retains it until the next job starts so its Undo remains addressable. Failed and done jobs do not contribute to `unfinished`.
|
|
3260
|
+
|
|
3261
|
+
Archive workers capture source identities before their first invocation and carry them with the completion, including menu selections. Completion handling never inspects those paths to construct a failed set. Later retries retain the captured identities even when a changed source is refused. Job-offer expiry uses its own shared history revision; the generation that guards Redo publication changes only on a new operation push, so overlapping successful Undos both remain redoable when no new operation was pushed.
|
|
3262
|
+
|
|
3263
|
+
Actions requests use absolute selection paths, resolved by the caller from one listing before sending. A preview request carries only the cursor file, never the marked selection.
|
|
3264
|
+
|
|
3265
|
+
```json
|
|
3266
|
+
{"c":"actions","op":"list","id":4,"paths":["/home/user/Photos/photo.jpg"]}
|
|
3267
|
+
{"t":"actions","id":4,"actions":[{"id":"action:resize.toml","name":"Resize to 1600 px","mode":"background","selection":"many","script":false}],"errors":[],"error":""}
|
|
3268
|
+
{"c":"actions","op":"run","id":5,"action":"action:resize.toml","paths":["/home/user/Photos/photo.jpg"]}
|
|
3269
|
+
{"t":"actionstarted","requestId":5,"id":13,"ok":true,"error":""}
|
|
3270
|
+
{"c":"actions","op":"preview","id":6,"action":"action:describe.toml","paths":["/home/user/Photos/photo.jpg"]}
|
|
3271
|
+
{"t":"actionpreview","id":6,"ok":true,"text":"Description from the previewer\n","error":""}
|
|
3272
|
+
```
|
|
3273
|
+
|
|
3274
|
+
A terminal handoff or refused run answers `actionstarted` with `requestId`, `ok`, and `error`, without a job id. A terminal action opens each expanded argv through `xdg-terminal-exec --dir=... -- ...`, the Open in Terminal launcher. A preview-only action refuses `run`. A preview answers text suitable for the existing `code` request's `text` input and the code renderer. Its process runs in the existing read-only preview jail with only the cursor file and executable added, a five-second wall deadline, the existing CPU and address-space limits, and a 1,048,576-byte output cap. A newer preview request cancels and suppresses the older reply.
|
|
3275
|
+
|
|
3276
|
+
The manifest directory is `$XDG_CONFIG_HOME/flea/actions` or `$HOME/.config/flea/actions`. The parser accepts the specimen's TOML subset: five unique required keys, double-quoted basic strings with JSON-compatible escapes, and possibly multiline string arrays with comments and trailing commas. Raw C0 control bytes other than tab, and raw DEL, are prohibited in strings; supported escaped values remain valid. Other keys, tables, duplicate keys, missing fields, wrong types, empty arrays, invalid modes or selection rules, invalid escapes or placeholders, and files over 16,384 bytes are refused whole. Directory discovery examines at most 257 entries to detect overflow, counting unrelated entries too, and reports when that entry limit truncates discovery; it reads at most 256 manifests; arrays hold at most 128 elements. Nonregular files are refused without blocking. Each refusal appears in the `actions.errors` array as `{"file":"bad.toml","error":"..."}`; valid siblings remain available.
|
|
3277
|
+
|
|
3278
|
+
```toml
|
|
3279
|
+
name = "Resize to 1600 px"
|
|
3280
|
+
mime = ["image/*"]
|
|
3281
|
+
selection = "many"
|
|
3282
|
+
run = ["magick", "{path}", "-resize",
|
|
3283
|
+
"1600x1600>", "{dir}/{stem}-1600.{ext}"]
|
|
3284
|
+
mode = "background"
|
|
3285
|
+
```
|
|
3286
|
+
|
|
3287
|
+
MIME patterns match the entire MIME string and accept literal characters, `*`, and `?`. Every selected item must match a pattern; `one` requires exactly one item, while `many` accepts one or more. MIME comes from the existing name database, with directories reported as `inode/directory` and unknown files as `application/octet-stream`. Listing and running recheck the actual selected paths. No directory listing is MIME-scanned to populate Actions.
|
|
3288
|
+
|
|
3289
|
+
`{path}`, `{dir}`, `{name}`, `{stem}`, and `{ext}` expand separately within each argv element, once per selected file. Replacements are not expanded again. The program is literal and cannot contain placeholders. Paths remain absolute; a placeholder-led argument whose result begins with `-` is prefixed with `./`, so a filename cannot become an option. Invocation uses argv without a shell unless the manifest explicitly names one. A background job retains only its last two merged stdout and stderr lines, each bounded to 4,096 decoded UTF-8 bytes with lossy decoding. `output` replaces the file line once nonempty. The invocation owns its entire process group: cancellation and leader exit, successful or unsuccessful, kill remaining descendants with SIGKILL before reaping the leader, including children that ignore SIGTERM.
|
|
3290
|
+
|
|
3291
|
+
Executable regular files directly under `$XDG_CONFIG_HOME/flea/scripts` remain Actions entries, with ids `script:<filename>` and labels omitting the final extension. They retain the existing Run script contract: one invocation receives all selected absolute paths. The Actions switch retains the `runScript` id in `menu.hidden` and remains hidden by default. No `src/uischema.rs` defaults change.
|
|
3292
|
+
|
|
3293
|
+
## Checked rclone sends (0.4.0)
|
|
3294
|
+
|
|
3295
|
+
The menu's executable capabilities include `providers.rclone` with the existing `installed`, `command`, and `reason` fields. Request configured remotes only when opening the send menu:
|
|
3296
|
+
|
|
3297
|
+
```json
|
|
3298
|
+
{"c":"rclone","op":"remotes","id":71}
|
|
3299
|
+
{"t":"rcloneremotes","id":71,"visible":true,"remotes":["gdrive","b2-photos","nas-crypt"]}
|
|
3300
|
+
{"c":"rclone","op":"send","id":72,"remote":"gdrive","folder":"Uploads","paths":["/absolute/photo.jpg"]}
|
|
3301
|
+
{"t":"rclonestarted","requestId":72,"id":12,"ok":true,"error":""}
|
|
3302
|
+
```
|
|
3303
|
+
|
|
3304
|
+
`listremotes` runs off the loop, inherits `RCLONE_CONFIG`, contacts no remote and has a two-second deadline. A missing or unusable rclone, no remotes, or failed discovery answers `visible:false` and `remotes:[]`: hide the menu row. Names omit the trailing colon. Send rechecks that the remote is configured. `folder` is a path within it; empty means its configured root. A `folder` with any `..` component is refused before a job starts. Source paths must be absolute and are resolved once by the caller. A refused request answers `rclonestarted` with `requestId`, `ok:false`, `error`, and no job id.
|
|
3305
|
+
|
|
3306
|
+
A send uses the ordinary jobs list, `kind:"send"`, with independent per-item `copyto` or directory `copy` invocations followed by a separate verification pass using `check --one-way`. Source and target argv follow `--`; no shell is involved. Directory copies preserve the selected directory and empty descendants. A remote that cannot store empty directories refuses any selected tree containing one instead of issuing a checked receipt. Selected symlinks, unsupported item kinds and duplicate destination basenames are refused. Directory descendants are inspected without following links; symlinks, pipes, sockets and other unsupported descendants fail the item with their path. A skip reported during copy or check also fails the item, even when rclone exits successfully. A nonempty remote folder ending in a colon still separates its selected basename with a slash. Source identities are retained through retries and revalidated before every invocation. Originals are never removed.
|
|
3307
|
+
|
|
3308
|
+
Job updates add `phase`, `check`, `receipt`, and `note` strings, empty for existing producers. Rclone phases are `sending`, `checking`, `checked`, and `finished`; checks are `none`, `checksum`, and `size`. Sending uses the existing counts and byte fields, with monotonic absolute JSON-stat bytes and rclone's speed and ETA. Checking remains `state:"running"`, with a heading such as `Checking 3 items on gdrive`, `file:"comparing checksums"` or `file:"comparing sizes"`, an indeterminate bar and no rendered byte line. Numeric byte fields retain their prior values. State and phase changes are immediate; ordinary progress remains limited to one update per 150 ms.
|
|
3309
|
+
|
|
3310
|
+
Inherited global copy, filter and reporting options, including quiet/verbose and local traversal aliases, are removed from rclone children, while configuration, authentication and unrelated backend options remain inherited. Copy and check explicitly disable link following, link translation, filesystem-boundary filtering and silent skip settings, overriding backend and per-remote traversal settings. A checked receipt covers the explicitly selected content. Remote features are queried before copying and determine available hashes and empty-directory support; a remote with no locally supported hash uses size-only verification. If an individual object cannot supply a hash, the receipt is downgraded to size. Only a successful copy and completed check produce a verified receipt. The terminal notification follows the terminal job update:
|
|
3311
|
+
|
|
3312
|
+
```json
|
|
3313
|
+
{"t":"rclonesent","id":12,"requestId":72,"ok":true,"cancelled":false,"check":"checksum","receipt":"Sent 3 items to Uploads on gdrive","note":"","error":""}
|
|
3314
|
+
```
|
|
3315
|
+
|
|
3316
|
+
The receipt has no full stop; clients append muted ` · checked by checksum` or ` · checked by size`. A mismatch fails the job with `Send failed` and a reason naming the failed file and remote. The existing jobs Cancel kills the current process group, including descendants that ignore SIGTERM. Files already copied remain remote; a cancelled check has `check:"none"`, `note:"not checked"`, and `ok:false`. Retry keeps the id and row and resends only failed top-level items with their captured identities. It forces retransmission so a checksum failure can be repaired even when the destination retains the source size and timestamp. Retry counts describe that attempt. Dismiss uses the ordinary job contract. Sends never offer Undo.
|
|
3317
|
+
|
|
3318
|
+
## Contents search
|
|
3319
|
+
|
|
3320
|
+
`searchcontents` takes `path` (an absolute folder), `query` (one nonempty literal,
|
|
3321
|
+
case-sensitive line), `hidden`, positive `gen` and optional `count`. The default
|
|
3322
|
+
count is the existing search first-window size; counts above 4096 are refused.
|
|
3323
|
+
The producer runs ripgrep once in the read-only sandbox, honors ignore files,
|
|
3324
|
+
does not follow discovered symlinks, skips binary matches, and keeps the first
|
|
3325
|
+
matching line per file. No shell interprets the query or path.
|
|
3326
|
+
|
|
3327
|
+
The previous complete listing remains current until ripgrep finishes successfully.
|
|
3328
|
+
The new snapshot emits `contentsmatches` for the first window, then ordinary
|
|
3329
|
+
`listed`, `rows`, and terminal `searched`. All carry the query generation;
|
|
3330
|
+
`contentsmatches`, `listed` and `rows` also carry the operation `listing` identity.
|
|
3331
|
+
`window` emits its matching metadata before its rows, retaining both identities.
|
|
3332
|
+
The complete result set remains pageable. Match metadata may use several bounded
|
|
3333
|
+
envelopes for a window; each envelope's `start` and contiguous `row` values name
|
|
3334
|
+
its portion. Consumers assemble metadata and rows before presenting the snapshot.
|
|
3335
|
+
|
|
3336
|
+
```json
|
|
3337
|
+
{"c":"searchcontents","path":"/work","query":"needle","hidden":false,"gen":7,"count":40}
|
|
3338
|
+
{"t":"contentsmatches","gen":7,"listing":2,"start":0,"matches":[{"row":0,"path":"notes.txt","pathBytes":"6e6f7465732e747874","lineNo":3,"line":"needle first","operable":true}]}
|
|
3339
|
+
```
|
|
3340
|
+
|
|
3341
|
+
`pathBytes` is hexadecimal encoding of the exact relative filename bytes.
|
|
3342
|
+
`path` is display text only. Line numbers are one-based. Non-UTF-8 paths retain
|
|
3343
|
+
their lossy display, matching the previous listing behavior, but carry
|
|
3344
|
+
`operable:false`. A real UTF-8 filename sharing that display also carries false
|
|
3345
|
+
while this snapshot is held. Backend row and explicit-path requests for either
|
|
3346
|
+
ambiguous identity are refused with `where:"contentsidentity"` before resolution.
|
|
3347
|
+
An adapter must refuse external open, reveal, clipboard, drag and operation
|
|
3348
|
+
dispatch for these rows; it must never construct an operation path from display
|
|
3349
|
+
text. The shared `ui_logic_sr1` parser validates metadata for both front ends.
|
|
3350
|
+
This producer does not enable either adapter's contents capability.
|
|
3351
|
+
|
|
3352
|
+
`searchcancel` and `searchclose` stop and reap the running jail, including startup.
|
|
3353
|
+
Their cancellation or close receipt follows cleanup and names the newest requested
|
|
3354
|
+
generation, including a request waiting for an older worker to stop. Older or
|
|
3355
|
+
duplicate generations and stale completion tickets publish nothing. Replacement
|
|
3356
|
+
queries serialize after cleanup. Navigation discards the old worker's result.
|
|
3357
|
+
A file or folder that ripgrep cannot open or read, the root included, is skipped
|
|
3358
|
+
as the names walk skips it: when ripgrep exits 2 after writing its summary and
|
|
3359
|
+
every line it wrote to stderr names a path with its OS error (`(os error 13)`),
|
|
3360
|
+
the snapshot publishes what it read and the terminal `searched` carries
|
|
3361
|
+
`partial:true` with `partial_reason:"unreadable"`. Any other stderr line makes the
|
|
3362
|
+
exit a failure.
|
|
3363
|
+
On missing ripgrep, a sandbox that refuses to start, any other exit failure,
|
|
3364
|
+
malformed output, output limits or the 30-second wall deadline, the backend emits
|
|
3365
|
+
a generation-keyed `error` and preserves the previous listing, with no success
|
|
3366
|
+
receipt. The GUI ends that generation's walk with nothing found and says the
|
|
3367
|
+
error's sentence in the footer.
|
|
3368
|
+
Retained match data and a single pending rg JSON record each have an independent
|
|
3369
|
+
64 MiB bound. Both stores may coexist, in addition to transient parser and wire
|
|
3370
|
+
allocations. Exceeding either bound fails the request rather than returning an
|
|
3371
|
+
apparently complete cap. Full matching lines within these bounds are retained.
|
|
3372
|
+
The worker reads at most 8 KiB per pipe iteration. A finished search ignores
|
|
3373
|
+
`searchcancel`; `searchclose` acknowledges its scope once. Closing retains the
|
|
3374
|
+
displayed snapshot and its identity guard until navigation or replacement, so
|
|
3375
|
+
closing cannot make an ambiguous filename operable. Failed or cancelled queries
|
|
3376
|
+
also preserve the previous folder's watch and any previous name-search session.
|
|
3377
|
+
An explicit `searchclose` closes that previous names session and walk too, before
|
|
3378
|
+
the contents close receipt; late names completion events then publish nothing.
|
|
3379
|
+
Sorting a snapshot reorders match metadata with its listing and advances the
|
|
3380
|
+
listing identity. Name and kind sorts work for every snapshot; size and date
|
|
3381
|
+
sorts refuse snapshots containing non-operable paths to avoid statting aliases.
|
|
3382
|
+
|
|
3383
|
+
### flea-qml shared Trash listing
|
|
3384
|
+
|
|
3385
|
+
`{"c":"listtrash","first":100}` responds with ordinary `listed` and `rows`
|
|
3386
|
+
messages. Rows name actual stored files relative to `/` (absolute drive paths
|
|
3387
|
+
on Windows), with `trash: {original, deleted, identity}` display metadata.
|
|
3388
|
+
`trashbrowse` restore/prepare may use `rows` plus `listing`; a stale listing
|
|
3389
|
+
is rejected before row indices are translated to captured Trash identities.
|
|
3390
|
+
The existing review token still protects permanent deletion.
|
|
3391
|
+
|
|
3392
|
+
### flea-qml probed places
|
|
3393
|
+
|
|
3394
|
+
The sidebar's folders and drives are found by looking at the filesystem; no
|
|
3395
|
+
outside program runs. The backend sends one line at start, one for every
|
|
3396
|
+
`{"c":"places"}` request, and one whenever a watched answer changes:
|
|
3397
|
+
|
|
3398
|
+
`{"t":"places","places":[{"path":"/sdcard","label":"Internal storage","glyph":"smartphone"}],"volumes":[{"path":"/","label":"/","glyph":"drive","size":1002836697088}]}`
|
|
3399
|
+
|
|
3400
|
+
`places` are drawn under PLACES after Home, `volumes` under DEVICES; a volume's
|
|
3401
|
+
`size` is its filesystem's byte size or `null`. `{"c":"places","watch":true}`
|
|
3402
|
+
also looks again every five seconds until `{"c":"places","watch":false}`,
|
|
3403
|
+
which is not answered. A rail sends the first while it is shown and the second
|
|
3404
|
+
when it is parked or unloaded, so nothing looks while no rail asks. One look
|
|
3405
|
+
at a path is awaited for two seconds; a path that has not answered by then is
|
|
3406
|
+
left out of that line and joins a later one.
|
|
3407
|
+
|
|
3408
|
+
Rows, in order: the folders under home (`user-dirs.dirs`' path for a role
|
|
3409
|
+
first, then Desktop, Downloads or Download, Documents, Pictures, DCIM/Camera
|
|
3410
|
+
or DCIM or Pictures/Camera Roll, Pictures/Screenshots or DCIM/Screenshots,
|
|
3411
|
+
Music, Videos or Movies), each only when it directly holds a regular file;
|
|
3412
|
+
on Android, told by `/system/bin/linker64` being there, `/sdcard` and
|
|
3413
|
+
`/sdcard/Download` always, the same folders under `/sdcard` by the same rule,
|
|
3414
|
+
and, when the temporary directory lies in `/data/data/PACKAGE` or
|
|
3415
|
+
`/data/user/N/PACKAGE`, `/sdcard/Android/media/PACKAGE` (made once per run when
|
|
3416
|
+
missing) and that private folder; then each of `os.tmpdir()`, `$TMPDIR` and
|
|
3417
|
+
`/tmp` that is a folder. Volumes: every root that can be listed (`/`, or the
|
|
3418
|
+
drive letters where paths have them), then what is mounted under `/Volumes`,
|
|
3419
|
+
`/media`, `/run/media` and, on Android, `/storage` beside `emulated` and
|
|
3420
|
+
`self`. One folder is one row however many names reach it.
|