flea-qml 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +478 -2
- package/THIRD-PARTY-NOTICES.md +29 -0
- package/demos/App.java +5 -0
- package/demos/Dockerfile +4 -0
- package/demos/Main.kt +3 -0
- package/demos/Program.cs +1 -0
- package/demos/README.md +12 -0
- package/demos/app.dart +3 -0
- package/demos/app.deb +0 -0
- package/demos/app.js +1 -0
- package/demos/backup.tar +0 -0
- package/demos/budget.xlsx +0 -0
- package/demos/build.sh +2 -0
- package/demos/bundle.zip +0 -0
- package/demos/clip.mp4 +0 -0
- package/demos/data.sqlite +0 -0
- package/demos/disk.iso +0 -0
- package/demos/engine.cpp +5 -0
- package/demos/firmware.bin +0 -0
- package/demos/flake.nix +4 -0
- package/demos/folder/inside.txt +1 -0
- package/demos/font.ttf +0 -0
- package/demos/index.html +5 -0
- package/demos/index.php +2 -0
- package/demos/init.lua +1 -0
- package/demos/letter.docx +0 -0
- package/demos/lib.rs +3 -0
- package/demos/main.c +6 -0
- package/demos/main.h +3 -0
- package/demos/math-and-diagrams.md +200 -0
- package/demos/notes.tar.gz +0 -0
- package/demos/notes.txt +3 -0
- package/demos/photo.png +0 -0
- package/demos/program +2 -0
- package/demos/report.pdf +32 -0
- package/demos/script.py +1 -0
- package/demos/server.go +7 -0
- package/demos/settings.json +4 -0
- package/demos/slides.pptx +0 -0
- package/demos/song.mp3 +0 -0
- package/demos/style.css +4 -0
- package/demos/tool.rb +1 -0
- package/demos/types.ts +2 -0
- package/docs/pele.md +23 -0
- package/docs/protocol.md +3420 -0
- package/docs/temml.md +63 -0
- package/index.js +107 -0
- package/package.json +34 -4
- package/src/archive.js +402 -0
- package/src/archiveops.js +697 -0
- package/src/backend.js +369 -0
- package/src/bulkrename.js +95 -0
- package/src/clip-host.js +76 -0
- package/src/clip.js +462 -0
- package/src/code.js +168 -0
- package/src/compare.js +74 -0
- package/src/create.js +128 -0
- package/src/dirsize.js +147 -0
- package/src/figure-svg.js +530 -0
- package/src/figure-thread.js +23 -0
- package/src/figure.js +132 -0
- package/src/find.js +55 -0
- package/src/flea.js +39 -0
- package/src/fsinfo.js +39 -0
- package/src/highlight.js +141 -0
- package/src/html.js +52 -0
- package/src/kinds.js +89 -0
- package/src/listing.js +130 -0
- package/src/markdown-html.js +317 -0
- package/src/mathml.js +186 -0
- package/src/menu.js +180 -0
- package/src/meta.js +129 -0
- package/src/office.js +810 -0
- package/src/order.js +100 -0
- package/src/peek.js +49 -0
- package/src/pele-wrapper.js +695 -0
- package/src/permissions.js +180 -0
- package/src/places.js +313 -0
- package/src/preview-language.js +10 -0
- package/src/transfer.js +669 -0
- package/src/trash-record.js +41 -0
- package/src/trash.js +210 -0
- package/src/trashbrowse.js +326 -0
- package/src/uistate.js +393 -0
- package/src/uistore.js +168 -0
- package/src/vendor/LICENSES/Apache-2.0.txt +202 -0
- package/src/vendor/LICENSES/MIT-katex-2015.txt +27 -0
- package/src/vendor/LICENSES/MIT-katex.txt +21 -0
- package/src/vendor/LICENSES/MIT-mermaid.txt +21 -0
- package/src/vendor/LICENSES/MIT-pele.txt +40 -0
- package/src/vendor/LICENSES/MIT-perseus.txt +18 -0
- package/src/vendor/LICENSES/MIT-temml.txt +21 -0
- package/src/vendor/pele.js +145 -0
- package/src/vendor/temml.mjs +14708 -0
- package/src/watch.js +98 -0
- package/src/wayland/client.js +109 -0
- package/src/wayland/protocol.js +55 -0
- package/src/wayland/protocols/ext-data-control-v1.xml +276 -0
- package/src/wayland/protocols/wayland.xml +3248 -0
- package/src/wayland/protocols/wlr-data-control-unstable-v1.xml +278 -0
- package/src/wayland/resource-io.js +6 -0
- package/src/wayland/socket-discovery.js +26 -0
- package/src/wayland/transport-posix.js +120 -0
- package/src/wayland/transport-win32.js +11 -0
- package/src/wayland/transport.js +3 -0
- package/src/wayland/win32-resources.js +80 -0
- package/src/wayland/win32-wire.js +62 -0
- package/test/archive.test.js +80 -0
- package/test/bench-markdown-scroll.mjs +190 -0
- package/test/clip-host.test.js +113 -0
- package/test/code-preview.test.js +35 -0
- package/test/compare-ui-paths.test.js +24 -0
- package/test/compare.test.js +54 -0
- package/test/delete-trash.test.js +67 -0
- package/test/dirsize.test.js +138 -0
- package/test/file-ops.test.js +241 -0
- package/test/find-contents.test.js +161 -0
- package/test/find-input.test.js +66 -0
- package/test/fixtures/pele-parity.json +1006 -0
- package/test/highlight-launcher.test.js +35 -0
- package/test/html-preview.test.js +22 -0
- package/test/input.test.js +15 -0
- package/test/keymap.test.js +26 -0
- package/test/language-icons.test.js +36 -0
- package/test/launcher.test.js +51 -0
- package/test/manual-code-scroll.mjs +28 -0
- package/test/manual-column-scroll.mjs +24 -0
- package/test/manual-compare-ui.mjs +20 -0
- package/test/manual-find-status.mjs +37 -0
- package/test/manual-html-security.mjs +23 -0
- package/test/manual-html-ui.mjs +30 -0
- package/test/manual-syntax-preview.mjs +26 -0
- package/test/manual-trash-ui.mjs +86 -0
- package/test/markdown-diagrams.test.js +93 -0
- package/test/markdown-html.test.js +105 -0
- package/test/mathml.test.js +92 -0
- package/test/menu-transfer.test.js +57 -0
- package/test/office-preview.test.js +154 -0
- package/test/pele-parity.test.js +28 -0
- package/test/places.test.js +134 -0
- package/test/preview-keys.test.js +60 -0
- package/test/preview-language.test.js +22 -0
- package/test/status-filename.test.js +20 -0
- package/test/transfer-replace-merge.test.js +187 -0
- package/test/trash-address.test.js +23 -0
- package/test/trash-view.test.js +37 -0
- package/test/trashbrowse.test.js +135 -0
- package/ui/AboutFacts.qml +78 -0
- package/ui/ArchiveCards.qml +68 -0
- package/ui/ArchiveCompressDialog.qml +378 -0
- package/ui/ArchivePasswordDialog.qml +257 -0
- package/ui/Backend.qml +771 -0
- package/ui/BulkOldNames.qml +47 -0
- package/ui/BulkRenameDialog.qml +380 -0
- package/ui/CardFrame.qml +67 -0
- package/ui/CardScroll.qml +79 -0
- package/ui/CheckBox.qml +73 -0
- package/ui/ChromeBar.qml +360 -0
- package/ui/ChromeButton.qml +131 -0
- package/ui/ChromeModes.qml +43 -0
- package/ui/CodeLines.qml +256 -0
- package/ui/CodeRefusal.qml +52 -0
- package/ui/CollideConfirm.qml +147 -0
- package/ui/CollideHost.qml +167 -0
- package/ui/ColumnPane.qml +523 -0
- package/ui/ColumnRow.qml +380 -0
- package/ui/ColumnsArea.qml +594 -0
- package/ui/ColumnsLanding.qml +147 -0
- package/ui/CompareView.qml +321 -0
- package/ui/ContextMenu.qml +743 -0
- package/ui/ConvertDialog.qml +456 -0
- package/ui/CountedWorker.qml +7 -0
- package/ui/Crumb.qml +64 -0
- package/ui/CrumbDrop.qml +25 -0
- package/ui/CsvTable.qml +394 -0
- package/ui/CursorMark.qml +41 -0
- package/ui/DefaultClaim.qml +130 -0
- package/ui/DeviceLock.qml +47 -0
- package/ui/DeviceMounts.qml +540 -0
- package/ui/DialogButton.qml +150 -0
- package/ui/DialogField.qml +147 -0
- package/ui/DialogHeader.qml +31 -0
- package/ui/DialogScrim.qml +20 -0
- package/ui/DialogTitle.qml +37 -0
- package/ui/Divider.qml +9 -0
- package/ui/DropInto.qml +126 -0
- package/ui/DropTarget.qml +35 -0
- package/ui/DropboxMark.qml +60 -0
- package/ui/EmptyState.qml +150 -0
- package/ui/EscapeHint.qml +11 -0
- package/ui/FactsTable.qml +54 -0
- package/ui/FastScrollHandler.qml +476 -0
- package/ui/FatNamesConfirm.qml +157 -0
- package/ui/Favourites.qml +143 -0
- package/ui/FigureService.qml +120 -0
- package/ui/FileDrag.qml +267 -0
- package/ui/FilterStrip.qml +133 -0
- package/ui/FitMetrics.qml +22 -0
- package/ui/FlashDriveRows.qml +137 -0
- package/ui/FlashHost.qml +72 -0
- package/ui/FlashImageRow.qml +104 -0
- package/ui/FlashRunner.qml +119 -0
- package/ui/FlashStepRow.qml +172 -0
- package/ui/FlashStrip.qml +38 -0
- package/ui/FlashView.qml +397 -0
- package/ui/FleaMark.qml +88 -0
- package/ui/FlyoutFade.qml +23 -0
- package/ui/FontPane.qml +170 -0
- package/ui/Glass.qml +114 -0
- package/ui/GlassTagger.qml +59 -0
- package/ui/Glyph.qml +64 -0
- package/ui/GlyphSolid.qml +22 -0
- package/ui/GridArea.qml +664 -0
- package/ui/GridTile.qml +325 -0
- package/ui/GvfsBridge.qml +164 -0
- package/ui/Header.qml +433 -0
- package/ui/HoverTimers.qml +39 -0
- package/ui/HtmlClip.qml +30 -0
- package/ui/HtmlPane.qml +63 -0
- package/ui/HyprlandMark.qml +45 -0
- package/ui/Ipc.qml +1005 -0
- package/ui/IpcControls.qml +7 -0
- package/ui/IpcMenuState.qml +48 -0
- package/ui/IpcPreviewState.qml +185 -0
- package/ui/JumpPath.qml +51 -0
- package/ui/KeymapSheet.qml +561 -0
- package/ui/KeymapSheetResult.qml +89 -0
- package/ui/KindStack.qml +40 -0
- package/ui/LangMark.qml +51 -0
- package/ui/LangMarkLoader.qml +26 -0
- package/ui/List.qml +480 -0
- package/ui/LoadingState.qml +54 -0
- package/ui/LocalSend.qml +56 -0
- package/ui/LocalSendMark.qml +73 -0
- package/ui/MarkdownBlockView.qml +311 -0
- package/ui/MarkdownBlocks.qml +35 -0
- package/ui/MarkdownFigure.qml +254 -0
- package/ui/MarkdownImages.qml +72 -0
- package/ui/MarkdownList.qml +81 -0
- package/ui/MarkdownMathsText.qml +97 -0
- package/ui/MarkdownPane.qml +83 -0
- package/ui/MarkdownPictures.qml +41 -0
- package/ui/MarkdownQuote.qml +52 -0
- package/ui/MarkdownRemote.qml +82 -0
- package/ui/MarkdownSource.qml +155 -0
- package/ui/MarkdownTable.qml +222 -0
- package/ui/MarkdownTableScroll.qml +64 -0
- package/ui/MarkdownTableWheel.qml +126 -0
- package/ui/MarkdownText.qml +87 -0
- package/ui/MarkdownWorker.js +132 -0
- package/ui/MatchText.qml +99 -0
- package/ui/MediaSession.qml +38 -0
- package/ui/MediaSound.qml +26 -0
- package/ui/MediaStrip.qml +191 -0
- package/ui/MediaTrack.qml +36 -0
- package/ui/MediaVolume.qml +54 -0
- package/ui/MenuActionDialog.qml +396 -0
- package/ui/MenuEdgeFade.qml +24 -0
- package/ui/MenuHost.qml +227 -0
- package/ui/MenuRow.qml +351 -0
- package/ui/MountListing.qml +100 -0
- package/ui/NetworkDialog.qml +435 -0
- package/ui/NetworkForm.qml +426 -0
- package/ui/NetworkMounts.qml +913 -0
- package/ui/NetworkPlaces.qml +127 -0
- package/ui/OpenWithDialog.qml +613 -0
- package/ui/Opener.qml +115 -0
- package/ui/Pane.qml +1296 -0
- package/ui/PaneMenuActions.qml +527 -0
- package/ui/PanePath.qml +90 -0
- package/ui/PaneRail.qml +239 -0
- package/ui/PaneStates.qml +88 -0
- package/ui/PaneSwap.qml +339 -0
- package/ui/PaneWire.qml +605 -0
- package/ui/ParkedWait.qml +9 -0
- package/ui/PathEntry.qml +186 -0
- package/ui/PathJump.qml +400 -0
- package/ui/PdfViewer.qml +297 -0
- package/ui/PermissionsDialog.qml +658 -0
- package/ui/PermissionsHeadings.qml +31 -0
- package/ui/PhoneMounts.qml +67 -0
- package/ui/PickerAnswers.qml +75 -0
- package/ui/PickerBox.qml +26 -0
- package/ui/PickerChrome.qml +295 -0
- package/ui/PickerFilter.qml +97 -0
- package/ui/PickerGrid.qml +308 -0
- package/ui/PickerHeader.qml +28 -0
- package/ui/PickerLifecycle.qml +27 -0
- package/ui/PickerList.qml +218 -0
- package/ui/PickerListing.qml +149 -0
- package/ui/PickerPathBar.qml +127 -0
- package/ui/PickerPlaces.qml +189 -0
- package/ui/PickerRecent.qml +92 -0
- package/ui/PickerSave.qml +101 -0
- package/ui/PickerSelection.qml +155 -0
- package/ui/PickerWindow.qml +846 -0
- package/ui/PowerSectorsReader.qml +10 -0
- package/ui/PrebuildDriver.qml +146 -0
- package/ui/Preview.qml +1004 -0
- package/ui/PreviewAnimation.qml +24 -0
- package/ui/PreviewArchive.qml +66 -0
- package/ui/PreviewColumn.qml +551 -0
- package/ui/PreviewEdge.qml +63 -0
- package/ui/PreviewFacts.qml +30 -0
- package/ui/PreviewFactsLine.qml +34 -0
- package/ui/PreviewHtml.qml +233 -0
- package/ui/PreviewImage.qml +253 -0
- package/ui/PreviewImagePan.qml +13 -0
- package/ui/PreviewImageSharp.qml +32 -0
- package/ui/PreviewLines.qml +106 -0
- package/ui/PreviewMarkdown.qml +706 -0
- package/ui/PreviewMedia.qml +250 -0
- package/ui/PreviewPdf.qml +482 -0
- package/ui/PreviewScroll.qml +201 -0
- package/ui/PreviewSwap.qml +335 -0
- package/ui/PreviewText.qml +149 -0
- package/ui/ProtocolChip.qml +18 -0
- package/ui/QuickLookArchive.qml +38 -0
- package/ui/QuickLookBar.qml +103 -0
- package/ui/QuickLookPrepare.qml +355 -0
- package/ui/QuickLookSwap.qml +10 -0
- package/ui/QuickLookWarm.qml +65 -0
- package/ui/RailDrop.qml +155 -0
- package/ui/RailHeading.qml +19 -0
- package/ui/RailPlaces.qml +38 -0
- package/ui/RailTrashRow.qml +67 -0
- package/ui/RenameField.qml +353 -0
- package/ui/RendererRetry.qml +11 -0
- package/ui/ResizeHandle.qml +52 -0
- package/ui/Reveal.qml +54 -0
- package/ui/Row.qml +476 -0
- package/ui/RowDrag.qml +213 -0
- package/ui/RowDropTarget.qml +27 -0
- package/ui/RunCard.qml +286 -0
- package/ui/Scripts.qml +65 -0
- package/ui/SearchStrip.qml +232 -0
- package/ui/SelectionBand.qml +140 -0
- package/ui/SelectionPreview.qml +370 -0
- package/ui/SettingsFavourite.qml +122 -0
- package/ui/SettingsGroup.qml +96 -0
- package/ui/SettingsHint.qml +12 -0
- package/ui/SettingsMetrics.qml +29 -0
- package/ui/SettingsPane.qml +178 -0
- package/ui/SettingsPanel.qml +547 -0
- package/ui/SettingsRail.qml +128 -0
- package/ui/SettingsRow.qml +409 -0
- package/ui/SettingsRuler.qml +85 -0
- package/ui/SettingsSegment.qml +85 -0
- package/ui/ShareBrowser.qml +113 -0
- package/ui/ShareLink.qml +67 -0
- package/ui/ShelfPins.qml +101 -0
- package/ui/Sidebar.qml +732 -0
- package/ui/SidebarRow.qml +358 -0
- package/ui/Spinner.qml +60 -0
- package/ui/StableModel.qml +89 -0
- package/ui/StateMessage.qml +84 -0
- package/ui/StatusBar.qml +512 -0
- package/ui/StatusFileName.qml +27 -0
- package/ui/TabBar.qml +673 -0
- package/ui/TabDragGeometry.qml +60 -0
- package/ui/TagMenuMark.qml +52 -0
- package/ui/TagRailSection.qml +53 -0
- package/ui/TagSlot.qml +40 -0
- package/ui/TagState.qml +150 -0
- package/ui/TagSwatch.qml +28 -0
- package/ui/Taildrop.qml +86 -0
- package/ui/TailscaleMark.qml +50 -0
- package/ui/Theme.qml +460 -0
- package/ui/ThemeColors.qml +43 -0
- package/ui/TransferCard.qml +239 -0
- package/ui/TrashConfirm.qml +129 -0
- package/ui/TrashDropHandler.qml +11 -0
- package/ui/TrashHost.qml +202 -0
- package/ui/TrashMonitor.qml +30 -0
- package/ui/TrashToolbar.qml +19 -0
- package/ui/TrashView.qml +492 -0
- package/ui/UnlockDialog.qml +450 -0
- package/ui/UnlockHost.qml +206 -0
- package/ui/UpdateCheck.qml +97 -0
- package/ui/UserDirs.qml +25 -0
- package/ui/ViewState.qml +565 -0
- package/ui/ViewportScrollBar.qml +195 -0
- package/ui/ViewportScrollBars.qml +25 -0
- package/ui/WebPreview.qml +33 -0
- package/ui/WebPreviewView.qml +26 -0
- package/ui/WindowBody.qml +827 -0
- package/ui/WindowDrag.qml +35 -0
- package/ui/boot/Commons/AudioNodes.qml +50 -0
- package/ui/boot/Commons/AudioNodesModel.js +96 -0
- package/ui/boot/Commons/Border.qml +243 -0
- package/ui/boot/Commons/BorderGeometry.js +373 -0
- package/ui/boot/Commons/Color.qml +258 -0
- package/ui/boot/Commons/I18n.qml +118 -0
- package/ui/boot/Commons/InputDevices.qml +112 -0
- package/ui/boot/Commons/InputMethodState.qml +38 -0
- package/ui/boot/Commons/IpcRegistry.qml +77 -0
- package/ui/boot/Commons/Numbers.js +19 -0
- package/ui/boot/Commons/Plural.js +91 -0
- package/ui/boot/Commons/ShellIpc.qml +12 -0
- package/ui/boot/Commons/Style.qml +561 -0
- package/ui/boot/Commons/Translations.qml +100 -0
- package/ui/boot/Commons/UntypedInput.qml +152 -0
- package/ui/boot/Commons/Util.qml +188 -0
- package/ui/boot/Commons/qmldir +13 -0
- package/ui/boot/LICENSE-omarchy +20 -0
- package/ui/boot/Ui/BackgroundMedia.qml +49 -0
- package/ui/boot/Ui/BarIconButton.qml +106 -0
- package/ui/boot/Ui/BarIconContent.qml +38 -0
- package/ui/boot/Ui/BarIndicator.qml +53 -0
- package/ui/boot/Ui/BarWidget.qml +50 -0
- package/ui/boot/Ui/BorderOverlay.qml +54 -0
- package/ui/boot/Ui/BorderSurface.qml +40 -0
- package/ui/boot/Ui/Button.qml +224 -0
- package/ui/boot/Ui/ButtonGroup.qml +131 -0
- package/ui/boot/Ui/ConfirmDialog.qml +134 -0
- package/ui/boot/Ui/CursorSurface.qml +42 -0
- package/ui/boot/Ui/Dropdown.qml +245 -0
- package/ui/boot/Ui/FilterInput.qml +26 -0
- package/ui/boot/Ui/IconContentBounds.qml +141 -0
- package/ui/boot/Ui/KeyboardPanel.qml +453 -0
- package/ui/boot/Ui/MultiSelect.qml +624 -0
- package/ui/boot/Ui/NumberField.qml +86 -0
- package/ui/boot/Ui/OpticalGlyph.qml +68 -0
- package/ui/boot/Ui/OverlayWindow.qml +102 -0
- package/ui/boot/Ui/Panel.qml +60 -0
- package/ui/boot/Ui/PanelActionButton.qml +105 -0
- package/ui/boot/Ui/PanelController.qml +16 -0
- package/ui/boot/Ui/PanelHero.qml +112 -0
- package/ui/boot/Ui/PanelKeyCatcher.qml +100 -0
- package/ui/boot/Ui/PanelSectionHeader.qml +31 -0
- package/ui/boot/Ui/PanelSeparator.qml +19 -0
- package/ui/boot/Ui/PanelSlider.qml +156 -0
- package/ui/boot/Ui/PanelToolTip.qml +50 -0
- package/ui/boot/Ui/PluginBarApi.qml +89 -0
- package/ui/boot/Ui/PointerMoveGate.qml +54 -0
- package/ui/boot/Ui/PopupCard.qml +181 -0
- package/ui/boot/Ui/ScreenMoveRemap.qml +43 -0
- package/ui/boot/Ui/SearchableDropdown.qml +354 -0
- package/ui/boot/Ui/SizedImage.qml +125 -0
- package/ui/boot/Ui/SpeedTestOverlay.qml +412 -0
- package/ui/boot/Ui/TextField.qml +58 -0
- package/ui/boot/Ui/Toggle.qml +118 -0
- package/ui/boot/Ui/ToggleSwitch.qml +115 -0
- package/ui/boot/Ui/WidgetButton.qml +172 -0
- package/ui/boot/Ui/qmldir +41 -0
- package/ui/boot/fleatab.qml +50 -0
- package/ui/boot/picker.qml +39 -0
- package/ui/boot/shell.qml +122 -0
- package/ui/boot/tabtearoff.qml +51 -0
- package/ui/js/Anchor.js +379 -0
- package/ui/js/AnchorHold.js +162 -0
- package/ui/js/Archive.js +157 -0
- package/ui/js/ArchiveCard.js +44 -0
- package/ui/js/ArchiveOps.js +44 -0
- package/ui/js/BackReturn.js +118 -0
- package/ui/js/BulkRename.js +100 -0
- package/ui/js/Buttons.js +54 -0
- package/ui/js/ChromeKeys.js +116 -0
- package/ui/js/ClipMarks.js +73 -0
- package/ui/js/ClipSelection.js +67 -0
- package/ui/js/Clipboard.js +179 -0
- package/ui/js/Cloud.js +97 -0
- package/ui/js/CodeRuns.js +72 -0
- package/ui/js/Collide.js +212 -0
- package/ui/js/ColumnFit.js +28 -0
- package/ui/js/ColumnMenu.js +111 -0
- package/ui/js/Columns.js +298 -0
- package/ui/js/ColumnsCarry.js +103 -0
- package/ui/js/Compare.js +74 -0
- package/ui/js/Contrast.js +157 -0
- package/ui/js/ControlReader.js +42 -0
- package/ui/js/Convert.js +52 -0
- package/ui/js/CopyAs.js +52 -0
- package/ui/js/Crumbs.js +178 -0
- package/ui/js/Csv.js +231 -0
- package/ui/js/Density.js +44 -0
- package/ui/js/Devices.js +440 -0
- package/ui/js/DirSizes.js +180 -0
- package/ui/js/Drag.js +364 -0
- package/ui/js/DragOut.js +166 -0
- package/ui/js/DropDev.js +52 -0
- package/ui/js/Dropbox.js +41 -0
- package/ui/js/Eject.js +217 -0
- package/ui/js/Errors.js +188 -0
- package/ui/js/ExtThumbs.js +115 -0
- package/ui/js/Facts.js +402 -0
- package/ui/js/FigureDate.js +14 -0
- package/ui/js/Filter.js +285 -0
- package/ui/js/Find.js +46 -0
- package/ui/js/Flash.js +184 -0
- package/ui/js/FlashCard.js +79 -0
- package/ui/js/FlashEndings.js +125 -0
- package/ui/js/FlashPane.js +116 -0
- package/ui/js/FleaBin.js +12 -0
- package/ui/js/Focus.js +502 -0
- package/ui/js/FolderSorts.js +108 -0
- package/ui/js/FolderViews.js +44 -0
- package/ui/js/Format.js +305 -0
- package/ui/js/Fuzzy.js +141 -0
- package/ui/js/GlassTag.js +72 -0
- package/ui/js/Grid.js +33 -0
- package/ui/js/GridGeometry.js +90 -0
- package/ui/js/GridNames.js +153 -0
- package/ui/js/GvfsBridge.js +200 -0
- package/ui/js/HostClipboard.js +29 -0
- package/ui/js/Icons.js +218 -0
- package/ui/js/Input.js +50 -0
- package/ui/js/Jump.js +200 -0
- package/ui/js/KeyName.js +64 -0
- package/ui/js/Keymap.js +401 -0
- package/ui/js/Kinds.js +164 -0
- package/ui/js/LICENSES/ISC-lucide.txt +43 -0
- package/ui/js/LangMarkPaths.js +47 -0
- package/ui/js/LangMarks.js +120 -0
- package/ui/js/Lists.js +11 -0
- package/ui/js/LocalSend.js +39 -0
- package/ui/js/LockedMenu.js +63 -0
- package/ui/js/MakeDefault.js +186 -0
- package/ui/js/MarkBox.js +7 -0
- package/ui/js/Markdown.js +119 -0
- package/ui/js/MarkdownLists.js +86 -0
- package/ui/js/MarkdownMaths.js +58 -0
- package/ui/js/MarkdownPictures.js +32 -0
- package/ui/js/MarkdownPrepared.js +139 -0
- package/ui/js/MarkdownTableFit.js +34 -0
- package/ui/js/Marks.js +148 -0
- package/ui/js/Marquee.js +96 -0
- package/ui/js/Match.js +93 -0
- package/ui/js/Material.js +109 -0
- package/ui/js/MdBlocks.js +437 -0
- package/ui/js/MdBreak.js +71 -0
- package/ui/js/MdChunks.js +70 -0
- package/ui/js/MdContainer.js +137 -0
- package/ui/js/MdDocument.js +337 -0
- package/ui/js/MdEmph.js +97 -0
- package/ui/js/MdEntity.js +108 -0
- package/ui/js/MdEntityTable.js +223 -0
- package/ui/js/MdEscape.js +59 -0
- package/ui/js/MdFront.js +37 -0
- package/ui/js/MdHold.js +32 -0
- package/ui/js/MdHtml.js +307 -0
- package/ui/js/MdHtmlBlock.js +290 -0
- package/ui/js/MdHtmlHeading.js +53 -0
- package/ui/js/MdHtmlImage.js +179 -0
- package/ui/js/MdInline.js +154 -0
- package/ui/js/MdItems.js +212 -0
- package/ui/js/MdLeaf.js +309 -0
- package/ui/js/MdLink.js +196 -0
- package/ui/js/MdMath.js +67 -0
- package/ui/js/MdParagraphs.js +138 -0
- package/ui/js/MdRefs.js +232 -0
- package/ui/js/MdResolve.js +226 -0
- package/ui/js/MdRun.js +300 -0
- package/ui/js/MdUrl.js +226 -0
- package/ui/js/Menu.js +514 -0
- package/ui/js/MenuContext.js +85 -0
- package/ui/js/MenuFit.js +13 -0
- package/ui/js/MenuRefresh.js +74 -0
- package/ui/js/MenuWheel.js +35 -0
- package/ui/js/Messages.js +186 -0
- package/ui/js/Motion.js +16 -0
- package/ui/js/Mounts.js +320 -0
- package/ui/js/MouseNav.js +13 -0
- package/ui/js/Nav.js +329 -0
- package/ui/js/NetFs.js +26 -0
- package/ui/js/OpenWith.js +83 -0
- package/ui/js/Ops.js +585 -0
- package/ui/js/Palette.js +33 -0
- package/ui/js/PathBar.js +260 -0
- package/ui/js/Permissions.js +179 -0
- package/ui/js/Phones.js +112 -0
- package/ui/js/Picker.js +353 -0
- package/ui/js/PlaceMenu.js +96 -0
- package/ui/js/Places.js +265 -0
- package/ui/js/PreviewAsk.js +21 -0
- package/ui/js/PreviewKeys.js +155 -0
- package/ui/js/PreviewSettle.js +29 -0
- package/ui/js/PreviewSwap.js +101 -0
- package/ui/js/PropertyFacts.js +144 -0
- package/ui/js/Protocols.js +296 -0
- package/ui/js/QuickLookKinds.js +27 -0
- package/ui/js/QuickLookRect.js +24 -0
- package/ui/js/RailDrop.js +51 -0
- package/ui/js/RailKeys.js +87 -0
- package/ui/js/RailMenu.js +133 -0
- package/ui/js/Recent.js +135 -0
- package/ui/js/RecentMode.js +156 -0
- package/ui/js/Reload.js +60 -0
- package/ui/js/Renderer.js +9 -0
- package/ui/js/RunFile.js +143 -0
- package/ui/js/Scripts.js +28 -0
- package/ui/js/Scroll.js +304 -0
- package/ui/js/ScrollOff.js +69 -0
- package/ui/js/Search.js +478 -0
- package/ui/js/Selection.js +97 -0
- package/ui/js/Settings.js +476 -0
- package/ui/js/SettingsAbout.js +97 -0
- package/ui/js/SettingsShelf.js +52 -0
- package/ui/js/SheetKeys.js +42 -0
- package/ui/js/SheetQuery.js +281 -0
- package/ui/js/ShelfPile.js +27 -0
- package/ui/js/SlowClick.js +53 -0
- package/ui/js/SlowOp.js +6 -0
- package/ui/js/Sort.js +134 -0
- package/ui/js/Startup.js +63 -0
- package/ui/js/StateWords.js +17 -0
- package/ui/js/Status.js +238 -0
- package/ui/js/Swap.js +166 -0
- package/ui/js/TabMove.js +39 -0
- package/ui/js/Tabs.js +736 -0
- package/ui/js/Tags.js +134 -0
- package/ui/js/Taildrop.js +128 -0
- package/ui/js/Tap.js +174 -0
- package/ui/js/TextSize.js +107 -0
- package/ui/js/Thumbs.js +158 -0
- package/ui/js/Transfer.js +172 -0
- package/ui/js/Trash.js +81 -0
- package/ui/js/TrashDates.js +47 -0
- package/ui/js/TrashDrop.js +64 -0
- package/ui/js/UiState.js +438 -0
- package/ui/js/Unlock.js +152 -0
- package/ui/js/Update.js +130 -0
- package/ui/js/UserDirs.js +56 -0
- package/ui/qmldir +228 -0
package/src/clip.js
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// The clipboard requests: clipSet, clipGet, clipClear and clipWatch (docs/protocol.md), over
|
|
2
|
+
// Wayland data-control, so no window or focus is needed. Upstream: src/clip/.
|
|
3
|
+
//
|
|
4
|
+
// Unlike upstream there is no detached `--clip-own`: this backend holds its own selection and
|
|
5
|
+
// gives it up when it exits. What outlives it is the plain text, which a compositor such as
|
|
6
|
+
// buninu-land keeps after the owner is gone, so the text carries the whole selection:
|
|
7
|
+
//
|
|
8
|
+
// '/home/a/file a' '/home/a/it'\''s' a copy: each path shell-quoted, one space apart
|
|
9
|
+
// ""'/home/a/file a' '/home/a/it'\''s'"" a cut: the same, between two empty "" strings
|
|
10
|
+
//
|
|
11
|
+
// Pasted into a POSIX shell, or bunmsh on any platform, both are the same arguments, since an
|
|
12
|
+
// empty "" joins the word beside it. Single quotes keep a newline in a name as it is.
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
import { lstatSync } from "node:fs";
|
|
15
|
+
import { isAbsolute, sep } from "node:path";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
import { HostClipboard } from "./clip-host.js";
|
|
18
|
+
|
|
19
|
+
export const FLEA = "application/x-flea-clip",
|
|
20
|
+
GNOME = "x-special/gnome-copied-files",
|
|
21
|
+
URILIST = "text/uri-list",
|
|
22
|
+
KDE_CUT = "application/x-kde-cutselection",
|
|
23
|
+
TEXT = ["text/plain;charset=utf-8", "text/plain", "UTF8_STRING"],
|
|
24
|
+
PNG = "image/png";
|
|
25
|
+
|
|
26
|
+
const LIMIT = 8 * 1024 * 1024;
|
|
27
|
+
// A path's parts, split on this system's own separator and on '/', which every system accepts.
|
|
28
|
+
const PARTS = new RegExp(`[${sep === "\\" ? "\\\\" : sep}/]`);
|
|
29
|
+
|
|
30
|
+
// ---- the text form ----
|
|
31
|
+
|
|
32
|
+
export const quote = (s) => `'${s.replaceAll("'", "'\\''")}'`;
|
|
33
|
+
|
|
34
|
+
export function clipText(op, paths) {
|
|
35
|
+
const words = paths.map(quote).join(" ");
|
|
36
|
+
return op === "cut" ? `""${words}""` : words;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The selection a text names, or null when it is anything but quoted paths that all exist.
|
|
40
|
+
// Sample inputs: `'/tmp/a' '/tmp/b'` -> copy of both; `""'/tmp/a'""` -> cut of /tmp/a.
|
|
41
|
+
export function parseClipText(text) {
|
|
42
|
+
const named = readClipText(text);
|
|
43
|
+
if (!named) return null;
|
|
44
|
+
for (const path of named.paths) {
|
|
45
|
+
try {
|
|
46
|
+
lstatSync(path);
|
|
47
|
+
} catch {
|
|
48
|
+
// A spent cut's sources have moved away, so its text no longer names a selection.
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return named;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The same reading by shape alone, without asking the filesystem.
|
|
56
|
+
function readClipText(text) {
|
|
57
|
+
let s = String(text).replace(/\r?\n$/, ""),
|
|
58
|
+
op = "copy";
|
|
59
|
+
if (s.length > 4 && s.startsWith('""') && s.endsWith('""')) {
|
|
60
|
+
op = "cut";
|
|
61
|
+
s = s.slice(2, -2);
|
|
62
|
+
}
|
|
63
|
+
const paths = [];
|
|
64
|
+
let i = 0;
|
|
65
|
+
for (;;) {
|
|
66
|
+
while (s[i] === " ") i++;
|
|
67
|
+
if (i >= s.length) break;
|
|
68
|
+
let word = "";
|
|
69
|
+
while (i < s.length && s[i] !== " ") {
|
|
70
|
+
if (s[i] === "'") {
|
|
71
|
+
const end = s.indexOf("'", i + 1);
|
|
72
|
+
if (end < 0) return null;
|
|
73
|
+
word += s.slice(i + 1, end);
|
|
74
|
+
i = end + 1;
|
|
75
|
+
} else if (s.startsWith("\\'", i)) {
|
|
76
|
+
word += "'";
|
|
77
|
+
i += 2;
|
|
78
|
+
} else return null;
|
|
79
|
+
}
|
|
80
|
+
paths.push(word);
|
|
81
|
+
}
|
|
82
|
+
if (!paths.length || !paths.every(usable)) return null;
|
|
83
|
+
return { op, paths };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Absolute, NUL-free, and never through `.` or `..`.
|
|
87
|
+
function usable(path) {
|
|
88
|
+
return typeof path === "string" && isAbsolute(path) && !path.includes("\0") && !path.split(PARTS).some((p) => p === "." || p === "..");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---- the file-manager forms ----
|
|
92
|
+
|
|
93
|
+
// Bun's own conversion, the inverse of the fileURLToPath that reads a uri-list back: it encodes
|
|
94
|
+
// what a URI cannot hold and takes a Windows drive letter or \\server\share path as Windows writes it.
|
|
95
|
+
export const encodeUri = (path) => pathToFileURL(path).href;
|
|
96
|
+
|
|
97
|
+
function decodeUris(lines) {
|
|
98
|
+
const paths = [],
|
|
99
|
+
seen = new Set();
|
|
100
|
+
let skipped = 0;
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
let path = null;
|
|
103
|
+
try {
|
|
104
|
+
if (/^file:/i.test(line)) path = fileURLToPath(line);
|
|
105
|
+
} catch {}
|
|
106
|
+
if (path === null || !usable(path) || seen.has(path)) skipped++;
|
|
107
|
+
else {
|
|
108
|
+
seen.add(path);
|
|
109
|
+
paths.push(path);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { paths, skipped };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function parseGnome(text) {
|
|
116
|
+
const [op, ...rest] = text.split("\n");
|
|
117
|
+
if (op !== "copy" && op !== "cut") return null;
|
|
118
|
+
return { op, ...decodeUris(rest.filter((l) => l)) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function parseUriList(text) {
|
|
122
|
+
return decodeUris(
|
|
123
|
+
text
|
|
124
|
+
.split("\n")
|
|
125
|
+
.map((l) => l.replace(/\r$/, ""))
|
|
126
|
+
.filter((l) => l && !l.startsWith("#")),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseFlea(text) {
|
|
131
|
+
const [op, token] = text.replace(/^[\s\0]+|[\s\0]+$/g, "").split(" ");
|
|
132
|
+
return (op === "copy" || op === "cut") && /^[0-9a-f]{32}$/i.test(token ?? "") ? { op, token } : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function payloads(op, paths, token) {
|
|
136
|
+
if (op === "text") return new Map(TEXT.map((m) => [m, Buffer.from(paths[0])]));
|
|
137
|
+
const text = Buffer.from(clipText(op, paths)),
|
|
138
|
+
out = new Map([
|
|
139
|
+
[FLEA, Buffer.from(`${op} ${token} ${process.pid}`)],
|
|
140
|
+
[GNOME, Buffer.from([op, ...paths.map(encodeUri)].join("\n"))],
|
|
141
|
+
[URILIST, Buffer.from(paths.map((p) => `${encodeUri(p)}\r\n`).join(""))],
|
|
142
|
+
...TEXT.map((m) => [m, text]),
|
|
143
|
+
]);
|
|
144
|
+
if (op === "cut") out.set(KDE_CUT, Buffer.from("1"));
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---- the data-control connection ----
|
|
149
|
+
|
|
150
|
+
let control = null;
|
|
151
|
+
let connecting = null;
|
|
152
|
+
let hostSend = null;
|
|
153
|
+
|
|
154
|
+
class DataControl {
|
|
155
|
+
constructor() {
|
|
156
|
+
this.selection = null;
|
|
157
|
+
this.offers = new Map();
|
|
158
|
+
this.owned = null;
|
|
159
|
+
this.onSelection = null;
|
|
160
|
+
this.onClose = null;
|
|
161
|
+
}
|
|
162
|
+
async open() {
|
|
163
|
+
// Loaded here, so a backend that never touches the clipboard never loads libc or kernel32.
|
|
164
|
+
const { WaylandClient } = await import("./wayland/client.js");
|
|
165
|
+
const io = await import("./wayland/resource-io.js");
|
|
166
|
+
const { libc } = await import("./wayland/transport.js");
|
|
167
|
+
this.io = io;
|
|
168
|
+
this.libc = libc;
|
|
169
|
+
const c = (this.client = new WaylandClient());
|
|
170
|
+
await c.initialize();
|
|
171
|
+
this.family = c.globals.has("ext_data_control_manager_v1")
|
|
172
|
+
? "ext_data_control"
|
|
173
|
+
: c.globals.has("zwlr_data_control_manager_v1")
|
|
174
|
+
? "zwlr_data_control"
|
|
175
|
+
: null;
|
|
176
|
+
if (!this.family) {
|
|
177
|
+
c.close();
|
|
178
|
+
throw Error("the compositor offers no data-control clipboard");
|
|
179
|
+
}
|
|
180
|
+
this.manager = c.bind(`${this.family}_manager_v1`, 1);
|
|
181
|
+
const seat = c.bind("wl_seat", 1);
|
|
182
|
+
this.device = c.object(`${this.family}_device_v1`, (event, [id]) => {
|
|
183
|
+
if (event === "data_offer") {
|
|
184
|
+
const mimes = new Set();
|
|
185
|
+
this.offers.set(id, mimes);
|
|
186
|
+
c.objects.set(id, { iface: `${this.family}_offer_v1`, event: (name, [mime]) => name === "offer" && mimes.add(mime) });
|
|
187
|
+
} else if (event === "selection") {
|
|
188
|
+
this.drop(this.selection?.id, id);
|
|
189
|
+
this.selection = id ? { id, mimes: this.offers.get(id) ?? new Set() } : null;
|
|
190
|
+
this.onSelection?.();
|
|
191
|
+
} else if (event === "primary_selection") this.drop(id, this.selection?.id);
|
|
192
|
+
else if (event === "finished") c.close();
|
|
193
|
+
});
|
|
194
|
+
c.request(this.manager, "get_data_device", this.device, seat);
|
|
195
|
+
c.onClose = () => {
|
|
196
|
+
if (control === this) control = null;
|
|
197
|
+
this.onClose?.();
|
|
198
|
+
};
|
|
199
|
+
await c.sync();
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
drop(id, keep) {
|
|
203
|
+
if (!id || id === keep || !this.offers.has(id)) return;
|
|
204
|
+
this.offers.delete(id);
|
|
205
|
+
try {
|
|
206
|
+
this.client.request(id, "destroy");
|
|
207
|
+
} catch {}
|
|
208
|
+
this.client.objects.delete(id);
|
|
209
|
+
}
|
|
210
|
+
async transfer(fd, bytes) {
|
|
211
|
+
const { readSync, writeSync } = this.io,
|
|
212
|
+
chunks = [],
|
|
213
|
+
deadline = Date.now() + 2000;
|
|
214
|
+
let length = 0,
|
|
215
|
+
offset = 0;
|
|
216
|
+
try {
|
|
217
|
+
this.libc.fcntl(fd, 4, this.libc.fcntl(fd, 3, 0) | 2048);
|
|
218
|
+
while (!this.client.closed && Date.now() < deadline) {
|
|
219
|
+
try {
|
|
220
|
+
if (bytes) {
|
|
221
|
+
if (offset === bytes.length) return;
|
|
222
|
+
offset += writeSync(fd, bytes, offset, bytes.length - offset);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const chunk = Buffer.alloc(65536),
|
|
226
|
+
n = readSync(fd, chunk, 0, chunk.length, null);
|
|
227
|
+
if (!n) return Buffer.concat(chunks).toString("utf8");
|
|
228
|
+
length += n;
|
|
229
|
+
if (length > LIMIT) throw Error("the clipboard holds more than 8 MB");
|
|
230
|
+
chunks.push(chunk.subarray(0, n));
|
|
231
|
+
continue;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (!["EAGAIN", "EINTR", "EWOULDBLOCK"].includes(error.code)) throw error;
|
|
234
|
+
}
|
|
235
|
+
await Bun.sleep(4);
|
|
236
|
+
}
|
|
237
|
+
throw Error("the clipboard owner did not answer within 2 s");
|
|
238
|
+
} finally {
|
|
239
|
+
this.libc.close(fd);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// The text the current selection offers as `mime`; a selection with no such type reads "".
|
|
243
|
+
async read(mime) {
|
|
244
|
+
const selection = this.selection;
|
|
245
|
+
if (!selection?.mimes.has(mime)) return "";
|
|
246
|
+
const fds = new Int32Array(2);
|
|
247
|
+
if (this.libc.pipe2(fds, 2048 | 0x80000) < 0) throw Error("cannot make a clipboard pipe");
|
|
248
|
+
try {
|
|
249
|
+
this.client.request(selection.id, "receive", mime, fds[1]);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
this.libc.close(fds[0]);
|
|
252
|
+
throw error;
|
|
253
|
+
} finally {
|
|
254
|
+
this.libc.close(fds[1]);
|
|
255
|
+
}
|
|
256
|
+
return this.transfer(fds[0]);
|
|
257
|
+
}
|
|
258
|
+
async set(offered, owned) {
|
|
259
|
+
const c = this.client,
|
|
260
|
+
source = c.object(`${this.family}_source_v1`, (name, [mime, fd]) => {
|
|
261
|
+
if (name === "send") {
|
|
262
|
+
const bytes = offered.get(mime);
|
|
263
|
+
if (bytes) this.transfer(fd, bytes).catch(() => {});
|
|
264
|
+
else this.libc.close(fd);
|
|
265
|
+
} else if (name === "cancelled") {
|
|
266
|
+
if (this.owned?.source === source) this.owned = null;
|
|
267
|
+
try {
|
|
268
|
+
c.request(source, "destroy");
|
|
269
|
+
} catch {}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
c.request(this.manager, "create_data_source", source);
|
|
273
|
+
for (const mime of offered.keys()) c.request(source, "offer", mime);
|
|
274
|
+
c.request(this.device, "set_selection", source);
|
|
275
|
+
this.owned = { ...owned, source };
|
|
276
|
+
await c.sync();
|
|
277
|
+
}
|
|
278
|
+
async clear() {
|
|
279
|
+
const owned = this.owned;
|
|
280
|
+
this.owned = null;
|
|
281
|
+
this.client.request(this.device, "set_selection", 0);
|
|
282
|
+
if (owned)
|
|
283
|
+
try {
|
|
284
|
+
this.client.request(owned.source, "destroy");
|
|
285
|
+
} catch {}
|
|
286
|
+
await this.client.sync();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function connect() {
|
|
291
|
+
if (control && (control.hostText || !control.client.closed)) return control;
|
|
292
|
+
if (connecting) return connecting;
|
|
293
|
+
connecting = (async () => {
|
|
294
|
+
const fresh = new DataControl();
|
|
295
|
+
try { await fresh.open(); control = fresh; }
|
|
296
|
+
catch (error) {
|
|
297
|
+
fresh.client?.close();
|
|
298
|
+
if (!hostSend) throw error;
|
|
299
|
+
control = new HostClipboard(hostSend, { clipText, parseClipText, none: NONE });
|
|
300
|
+
}
|
|
301
|
+
return control;
|
|
302
|
+
})();
|
|
303
|
+
try { return await connecting; } finally { connecting = null; }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const NONE = { clip: "none", paths: [], token: "", skipped: 0 };
|
|
307
|
+
|
|
308
|
+
// The selection as files, in upstream's order: Flea's own token, GNOME's list, the uri-list with
|
|
309
|
+
// KDE's cut marker, then the quoted text, which is all that is left once a Flea owner has exited.
|
|
310
|
+
async function snapshot(dc) {
|
|
311
|
+
if (dc.hostText) return dc.snapshot();
|
|
312
|
+
const selection = dc.selection,
|
|
313
|
+
mimes = selection?.mimes;
|
|
314
|
+
if (!mimes) return NONE;
|
|
315
|
+
// This backend's own selection answers from what it offered, without a round trip through itself.
|
|
316
|
+
if (dc.owned && dc.owned.op !== "text" && mimes.has(FLEA) && parseFlea(await dc.read(FLEA))?.token === dc.owned.token)
|
|
317
|
+
return { clip: dc.owned.op, paths: dc.owned.paths, token: dc.owned.token, skipped: 0 };
|
|
318
|
+
const textMime = TEXT.find((m) => mimes.has(m));
|
|
319
|
+
const image = mimes.has(PNG) ? { ...NONE, clip: "image" } : NONE;
|
|
320
|
+
if (mimes.has(FLEA)) {
|
|
321
|
+
const flea = parseFlea(await dc.read(FLEA)),
|
|
322
|
+
listed = mimes.has(GNOME) ? parseGnome(await dc.read(GNOME)) : null;
|
|
323
|
+
if (!flea || !listed?.paths.length) return image;
|
|
324
|
+
// A host clipboard (Android, Windows) can take other text without a new Wayland selection,
|
|
325
|
+
// and buninu-land answers text from the host. Text in our shape, even an older selection's
|
|
326
|
+
// while the host catches up, leaves the live owner's lists standing; any other text ends them.
|
|
327
|
+
const text = textMime ? await dc.read(textMime) : "";
|
|
328
|
+
if (text && !readClipText(text)) return image;
|
|
329
|
+
return { clip: listed.op, paths: listed.paths, token: flea.op === listed.op ? flea.token : "", skipped: listed.skipped };
|
|
330
|
+
}
|
|
331
|
+
if (mimes.has(GNOME)) {
|
|
332
|
+
const listed = parseGnome(await dc.read(GNOME));
|
|
333
|
+
if (listed?.paths.length) return { clip: listed.op, paths: listed.paths, token: "", skipped: listed.skipped };
|
|
334
|
+
}
|
|
335
|
+
if (mimes.has(URILIST)) {
|
|
336
|
+
const listed = parseUriList(await dc.read(URILIST));
|
|
337
|
+
if (listed.paths.length) {
|
|
338
|
+
const cut = mimes.has(KDE_CUT) && (await dc.read(KDE_CUT)).replace(/^[\s\0]+|[\s\0]+$/g, "") === "1";
|
|
339
|
+
return { clip: cut ? "cut" : "copy", paths: listed.paths, token: "", skipped: listed.skipped };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (textMime) {
|
|
343
|
+
const fromText = parseClipText(await dc.read(textMime));
|
|
344
|
+
if (fromText) return { clip: fromText.op, paths: fromText.paths, token: "", skipped: 0 };
|
|
345
|
+
}
|
|
346
|
+
return image;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const sameSelection = (a, b) => a.clip === b.clip && a.token === b.token && a.paths.join("\0") === b.paths.join("\0");
|
|
350
|
+
|
|
351
|
+
// ---- the requests ----
|
|
352
|
+
|
|
353
|
+
export async function clipSet(request, send) {
|
|
354
|
+
const op = request.op,
|
|
355
|
+
paths = request.paths;
|
|
356
|
+
const refuse = (error) => send({ t: "clip", op: "set", ok: false, error });
|
|
357
|
+
if (!Array.isArray(paths) || !paths.length) return refuse("the clipboard names no path");
|
|
358
|
+
if (op === "text") {
|
|
359
|
+
if (paths.length !== 1 || typeof paths[0] !== "string" || paths[0].includes("\0")) return refuse("the text clipboard needs one NUL-free value");
|
|
360
|
+
} else if (op !== "copy" && op !== "cut") return refuse("the clipboard operation must be copy or cut");
|
|
361
|
+
else if (!paths.every(usable)) return refuse("a clipboard path is relative, holds a NUL or climbs through ..");
|
|
362
|
+
const token = op === "text" ? "" : randomBytes(16).toString("hex");
|
|
363
|
+
try {
|
|
364
|
+
const dc = await connect();
|
|
365
|
+
await dc.set(payloads(op, paths, token), { op, paths, token });
|
|
366
|
+
send({ t: "clip", op: "set", ok: true, token });
|
|
367
|
+
} catch (e) {
|
|
368
|
+
refuse(e.message);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export async function clipGet(_request, send) {
|
|
373
|
+
try {
|
|
374
|
+
send({ t: "clip", op: "get", ok: true, ...(await snapshot(await connect())) });
|
|
375
|
+
} catch (e) {
|
|
376
|
+
send({ t: "clip", op: "get", ok: false, error: e.message });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function clipClear(request, send) {
|
|
381
|
+
const answer = (cleared) => send({ t: "clip", op: "clear", ok: true, cleared });
|
|
382
|
+
try {
|
|
383
|
+
const dc = await connect();
|
|
384
|
+
if (dc.hostText) return answer(await dc.clearIf(request));
|
|
385
|
+
if (typeof request.token === "string" && request.token) {
|
|
386
|
+
if (dc.owned?.token === request.token) {
|
|
387
|
+
await dc.clear();
|
|
388
|
+
return answer(true);
|
|
389
|
+
}
|
|
390
|
+
const offered = dc.selection;
|
|
391
|
+
const flea = offered?.mimes.has(FLEA) ? parseFlea(await dc.read(FLEA)) : null;
|
|
392
|
+
if (flea?.token !== request.token || dc.selection !== offered) return answer(false);
|
|
393
|
+
await dc.client.sync();
|
|
394
|
+
if (dc.selection !== offered) return answer(false);
|
|
395
|
+
await dc.clear();
|
|
396
|
+
return answer(true);
|
|
397
|
+
}
|
|
398
|
+
if (Array.isArray(request.cut)) {
|
|
399
|
+
const offered = dc.selection,
|
|
400
|
+
now = await snapshot(dc);
|
|
401
|
+
if (now.clip !== "cut" || now.paths.join("\0") !== request.cut.join("\0") || dc.selection !== offered) return answer(false);
|
|
402
|
+
await dc.client.sync();
|
|
403
|
+
if (dc.selection !== offered) return answer(false);
|
|
404
|
+
await dc.clear();
|
|
405
|
+
return answer(true);
|
|
406
|
+
}
|
|
407
|
+
answer(false);
|
|
408
|
+
} catch (e) {
|
|
409
|
+
send({ t: "clip", op: "clear", ok: false, error: e.message });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let watching = false;
|
|
414
|
+
|
|
415
|
+
// Reports every selection as files; two identical ones in a row report once. A dropped connection
|
|
416
|
+
// reconnects at most once a second, at most 5 times.
|
|
417
|
+
export function clipWatch(_request, send) {
|
|
418
|
+
// Explicit frontend capability: older/non-QML callers still get ordinary
|
|
419
|
+
// Wayland errors instead of a request they cannot answer.
|
|
420
|
+
if (_request.hostText === true) hostSend = send;
|
|
421
|
+
if (watching) return;
|
|
422
|
+
watching = true;
|
|
423
|
+
let last = null,
|
|
424
|
+
sequence = 0,
|
|
425
|
+
retries = 0;
|
|
426
|
+
const report = (selection) => {
|
|
427
|
+
if (last && sameSelection(last, selection)) return;
|
|
428
|
+
last = selection;
|
|
429
|
+
send({ t: "clip", op: "changed", ...selection });
|
|
430
|
+
};
|
|
431
|
+
const fail = (message) => {
|
|
432
|
+
last = null;
|
|
433
|
+
send({ t: "clip", op: "changed", ...NONE, error: message });
|
|
434
|
+
};
|
|
435
|
+
// A first connection that fails (no compositor, no data-control) ends the watcher at once.
|
|
436
|
+
const start = async (first) => {
|
|
437
|
+
let dc;
|
|
438
|
+
try {
|
|
439
|
+
dc = await connect();
|
|
440
|
+
} catch (e) {
|
|
441
|
+
return first ? fail(e.message) : retry(e.message);
|
|
442
|
+
}
|
|
443
|
+
retries = 0;
|
|
444
|
+
// There is no host watcher or polling. Tell the UI to read on each Paste.
|
|
445
|
+
if (dc.hostText) return fail("Quickshell clipboard is read on demand");
|
|
446
|
+
const read = async () => {
|
|
447
|
+
const mine = ++sequence;
|
|
448
|
+
try {
|
|
449
|
+
const selection = await snapshot(dc);
|
|
450
|
+
if (mine === sequence) report(selection);
|
|
451
|
+
} catch {}
|
|
452
|
+
};
|
|
453
|
+
dc.onSelection = read;
|
|
454
|
+
dc.onClose = () => retry("the clipboard connection closed");
|
|
455
|
+
read();
|
|
456
|
+
};
|
|
457
|
+
const retry = (message) => {
|
|
458
|
+
if (++retries > 5) return fail(message);
|
|
459
|
+
setTimeout(() => start(false), 1000).unref?.();
|
|
460
|
+
};
|
|
461
|
+
start(true);
|
|
462
|
+
}
|
package/src/code.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// The `code` line: a window of a file's or a text's lines for the preview (docs/protocol.md
|
|
2
|
+
// "code"). Lines are coloured by jsmdcui when it can be found (src/highlight.js) and answered
|
|
3
|
+
// plain otherwise. Upstream: src/backend/code.rs, which runs `bat`.
|
|
4
|
+
// Not done yet: jq formatting of one-line JSON.
|
|
5
|
+
import { closeSync, fstatSync, mkdtempSync, openSync, readSync, rmSync, statSync, writeFileSync, constants } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { basename, join } from "node:path";
|
|
8
|
+
import { highlightFile } from "./highlight.js";
|
|
9
|
+
import { previewLanguage } from "./preview-language.js";
|
|
10
|
+
import { officeKind, officePreview } from "./office.js";
|
|
11
|
+
|
|
12
|
+
// Bound IO and layout work: large files show their prefix, never a refusal.
|
|
13
|
+
const BYTE_LIMIT = 1 << 20;
|
|
14
|
+
// One reply carries at most this many lines; ui/CodeLines.qml asks a longer window in parts.
|
|
15
|
+
const LINE_CAP = 400;
|
|
16
|
+
// jsmdcui renders Markdown as a document rather than colouring its source, so its lines would not
|
|
17
|
+
// be the file's; Markdown is answered plain.
|
|
18
|
+
const RENDERED = /\.(md|markdown|mkd)$/i;
|
|
19
|
+
|
|
20
|
+
// An office document's lines are its extracted text, or its first sheet as CSV (src/office.js);
|
|
21
|
+
// the container's own bytes are a zip and are never shown.
|
|
22
|
+
function readOffice(path) {
|
|
23
|
+
try {
|
|
24
|
+
const bytes = Buffer.from(officePreview(path).text);
|
|
25
|
+
return { bytes: bytes.subarray(0, BYTE_LIMIT), truncated: bytes.length > BYTE_LIMIT, plain: true };
|
|
26
|
+
} catch {
|
|
27
|
+
return { state: "failed" };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readFile(path) {
|
|
32
|
+
if (officeKind(path)) return readOffice(path);
|
|
33
|
+
let fd;
|
|
34
|
+
try {
|
|
35
|
+
if (!statSync(path).isFile()) return { state: "failed" };
|
|
36
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
37
|
+
const st = fstatSync(fd);
|
|
38
|
+
if (!st.isFile()) return { state: "failed" };
|
|
39
|
+
const bytes = Buffer.alloc(Math.min(st.size, BYTE_LIMIT));
|
|
40
|
+
const read = readSync(fd, bytes, 0, bytes.length, 0);
|
|
41
|
+
return { bytes: bytes.subarray(0, read), truncated: st.size > BYTE_LIMIT };
|
|
42
|
+
} catch {
|
|
43
|
+
return { state: "failed" };
|
|
44
|
+
} finally {
|
|
45
|
+
if (fd !== undefined) closeSync(fd);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const plain = (line) => (line === "" ? [] : [0, line]);
|
|
50
|
+
const textOf = (runs) => runs.filter((_, i) => i % 2 === 1).join("");
|
|
51
|
+
|
|
52
|
+
// The coloured lines for a request, or null to answer plain. A text request is coloured through a
|
|
53
|
+
// temporary file named like the text, so jsmdcui picks the language from `name` or `lang`.
|
|
54
|
+
async function coloured(request, bytes, signal) {
|
|
55
|
+
if (request.raw === true) return null;
|
|
56
|
+
if (typeof request.text !== "string") {
|
|
57
|
+
if (RENDERED.test(request.path ?? "")) return null;
|
|
58
|
+
return highlightFile(request.path, signal, previewLanguage(request.path, bytes));
|
|
59
|
+
}
|
|
60
|
+
const name = request.lang ? `text.${request.lang}` : basename(request.name || "text.txt");
|
|
61
|
+
if (RENDERED.test(name)) return null;
|
|
62
|
+
const dir = mkdtempSync(join(tmpdir(), "flea-qml-code-"));
|
|
63
|
+
try {
|
|
64
|
+
writeFileSync(join(dir, name), bytes);
|
|
65
|
+
return await highlightFile(join(dir, name), signal, previewLanguage(name, bytes));
|
|
66
|
+
} finally {
|
|
67
|
+
rmSync(dir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The line each RFC 4180 record starts on (0-based), for a raw `csv` table request: a quote opens
|
|
72
|
+
// only at a field's start, `""` is a quote inside quotes, and a line that ends inside quotes
|
|
73
|
+
// carries its record onto the next one. An unterminated quote runs to the end as one record.
|
|
74
|
+
function recordStarts(lines, delimiter) {
|
|
75
|
+
const starts = [];
|
|
76
|
+
let quoted = false;
|
|
77
|
+
for (let index = 0; index < lines.length; index++) {
|
|
78
|
+
if (!quoted) starts.push(index);
|
|
79
|
+
const line = lines[index];
|
|
80
|
+
let fieldStart = !quoted;
|
|
81
|
+
for (let at = 0; at < line.length; at++) {
|
|
82
|
+
const ch = line[at];
|
|
83
|
+
if (quoted) {
|
|
84
|
+
if (ch !== '"') continue;
|
|
85
|
+
if (line[at + 1] === '"') at++;
|
|
86
|
+
else quoted = false;
|
|
87
|
+
} else if (ch === '"' && fieldStart) {
|
|
88
|
+
quoted = true;
|
|
89
|
+
fieldStart = false;
|
|
90
|
+
} else fieldStart = ch === delimiter;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return starts;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The window of a raw `csv` request, as whole records (docs/protocol.md "code").
|
|
97
|
+
function recordWindow(all, request, count) {
|
|
98
|
+
const starts = recordStarts(all, request.csv);
|
|
99
|
+
const records = starts.length;
|
|
100
|
+
// The record holding a line: the last start at or before it.
|
|
101
|
+
const holding = (line) => {
|
|
102
|
+
let low = 0, high = starts.length - 1;
|
|
103
|
+
while (low < high) {
|
|
104
|
+
const middle = (low + high + 1) >> 1;
|
|
105
|
+
if (starts[middle] <= line) low = middle;
|
|
106
|
+
else high = middle - 1;
|
|
107
|
+
}
|
|
108
|
+
return low;
|
|
109
|
+
};
|
|
110
|
+
let record;
|
|
111
|
+
if (Number.isFinite(request.record)) {
|
|
112
|
+
record = Math.max(1, Math.floor(request.record)) - 1;
|
|
113
|
+
if (record >= records) return { from: all.length, to: all.length, records, firstRecord: record + 1 };
|
|
114
|
+
} else {
|
|
115
|
+
const line = Math.max(1, request.first ?? 1) - 1;
|
|
116
|
+
if (line >= all.length) return { from: all.length, to: all.length, records, firstRecord: records + 1 };
|
|
117
|
+
record = holding(line);
|
|
118
|
+
}
|
|
119
|
+
const from = starts[record];
|
|
120
|
+
const last = holding(Math.min(all.length - 1, from + Math.max(1, count) - 1));
|
|
121
|
+
const to = last + 1 < starts.length ? starts[last + 1] : all.length;
|
|
122
|
+
return { from, to, records, firstRecord: record + 1 };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The reply to one `code` request; `signal` ends it when a newer request takes its slot. */
|
|
126
|
+
export async function code(request, signal) {
|
|
127
|
+
const head = { t: "code", token: request.token ?? 0, slot: request.slot ?? "" };
|
|
128
|
+
const source = typeof request.text === "string" ? { bytes: Buffer.from(request.text.slice(0,BYTE_LIMIT)).subarray(0,BYTE_LIMIT), truncated: Buffer.byteLength(request.text) > BYTE_LIMIT } : readFile(request.path ?? "");
|
|
129
|
+
if (!source.bytes) return { ...head, state: source.state, first: request.first ?? 1, total: 0, lines: [] };
|
|
130
|
+
const bytes = source.bytes;
|
|
131
|
+
// Binary bytes remain viewable. Visible control pictures cannot execute terminal
|
|
132
|
+
// escapes, and splitting pathological lines bounds each QML text layout.
|
|
133
|
+
const decoded = bytes.toString("utf8");
|
|
134
|
+
const binary = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(decoded);
|
|
135
|
+
const safe = decoded.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ch => ch === "\x7f" ? "␡" : String.fromCharCode(0x2400 + ch.charCodeAt(0)));
|
|
136
|
+
let split = false;
|
|
137
|
+
const all = [];
|
|
138
|
+
for (let line of safe.split("\n")) {
|
|
139
|
+
if (line.endsWith("\r")) line = line.slice(0,-1);
|
|
140
|
+
while (line.length > 4096) { all.push(line.slice(0,4096));line=line.slice(4096);split=true; }
|
|
141
|
+
all.push(line);
|
|
142
|
+
}
|
|
143
|
+
// A final newline ends the last line rather than starting another.
|
|
144
|
+
if (all.length > 1 && all.at(-1) === "") all.pop();
|
|
145
|
+
if (source.truncated) all.push("[Preview truncated: first 1 MiB]");
|
|
146
|
+
if (split) all.push("[Long lines split for preview]");
|
|
147
|
+
const total = bytes.length === 0 ? 0 : all.length;
|
|
148
|
+
const first = Math.max(1, request.first ?? 1),
|
|
149
|
+
count = Math.min(LINE_CAP, Math.max(0, request.count ?? LINE_CAP));
|
|
150
|
+
if (request.raw === true && (request.csv === "," || request.csv === "\t")) {
|
|
151
|
+
if (total === 0) return { ...head, state: "plain", first: 1, total, lines: [], records: 0, firstRecord: Math.max(1, Math.floor(request.record ?? 1) || 1) };
|
|
152
|
+
const window = recordWindow(all, request, count);
|
|
153
|
+
return { ...head, state: "plain", first: window.from + 1, total, lines: all.slice(window.from, window.to).map(plain), records: window.records, firstRecord: window.firstRecord };
|
|
154
|
+
}
|
|
155
|
+
const colours = source.truncated || source.plain || binary || split ? null : await coloured(request, bytes, signal);
|
|
156
|
+
if (signal?.aborted) return null;
|
|
157
|
+
let styled = false;
|
|
158
|
+
const lines = all.slice(first - 1, first - 1 + count).map((line, i) => {
|
|
159
|
+
const runs = colours?.[first - 1 + i];
|
|
160
|
+
// A coloured line is used only when it holds exactly the file's own text.
|
|
161
|
+
if (runs && textOf(runs) === line) {
|
|
162
|
+
if (runs.some((value, at) => at % 2 === 0 && value !== 0)) styled = true;
|
|
163
|
+
return runs;
|
|
164
|
+
}
|
|
165
|
+
return plain(line);
|
|
166
|
+
});
|
|
167
|
+
return { ...head, state: styled ? "styled" : "plain", first, total, lines };
|
|
168
|
+
}
|