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.
Files changed (637) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +511 -2
  3. package/THIRD-PARTY-NOTICES.md +29 -0
  4. package/demos/App.java +5 -0
  5. package/demos/Dockerfile +4 -0
  6. package/demos/Main.kt +3 -0
  7. package/demos/Program.cs +1 -0
  8. package/demos/README.md +12 -0
  9. package/demos/app.dart +3 -0
  10. package/demos/app.deb +0 -0
  11. package/demos/app.js +1 -0
  12. package/demos/backup.tar +0 -0
  13. package/demos/budget.xlsx +0 -0
  14. package/demos/build.sh +2 -0
  15. package/demos/bundle.zip +0 -0
  16. package/demos/clip.mp4 +0 -0
  17. package/demos/data.sqlite +0 -0
  18. package/demos/disk.iso +0 -0
  19. package/demos/engine.cpp +5 -0
  20. package/demos/firmware.bin +0 -0
  21. package/demos/flake.nix +4 -0
  22. package/demos/folder/inside.txt +1 -0
  23. package/demos/font.ttf +0 -0
  24. package/demos/index.html +5 -0
  25. package/demos/index.php +2 -0
  26. package/demos/init.lua +1 -0
  27. package/demos/letter.docx +0 -0
  28. package/demos/lib.rs +3 -0
  29. package/demos/main.c +6 -0
  30. package/demos/main.h +3 -0
  31. package/demos/math-and-diagrams.md +200 -0
  32. package/demos/notes.tar.gz +0 -0
  33. package/demos/notes.txt +3 -0
  34. package/demos/photo.png +0 -0
  35. package/demos/program +2 -0
  36. package/demos/report.pdf +32 -0
  37. package/demos/script.py +1 -0
  38. package/demos/server.go +7 -0
  39. package/demos/settings.json +4 -0
  40. package/demos/slides.pptx +0 -0
  41. package/demos/song.mp3 +0 -0
  42. package/demos/style.css +4 -0
  43. package/demos/tool.rb +1 -0
  44. package/demos/types.ts +2 -0
  45. package/docs/pele.md +23 -0
  46. package/docs/protocol.md +3420 -0
  47. package/docs/temml.md +63 -0
  48. package/index.js +107 -0
  49. package/package.json +34 -4
  50. package/src/archive.js +407 -0
  51. package/src/archiveops.js +728 -0
  52. package/src/backend.js +369 -0
  53. package/src/bulkrename.js +95 -0
  54. package/src/clip-host.js +76 -0
  55. package/src/clip.js +462 -0
  56. package/src/code.js +168 -0
  57. package/src/compare.js +74 -0
  58. package/src/create.js +128 -0
  59. package/src/dirsize.js +147 -0
  60. package/src/figure-svg.js +530 -0
  61. package/src/figure-thread.js +23 -0
  62. package/src/figure.js +132 -0
  63. package/src/find.js +55 -0
  64. package/src/flea.js +39 -0
  65. package/src/fsinfo.js +39 -0
  66. package/src/highlight.js +141 -0
  67. package/src/html.js +52 -0
  68. package/src/kinds.js +89 -0
  69. package/src/listing.js +130 -0
  70. package/src/markdown-html.js +317 -0
  71. package/src/mathml.js +186 -0
  72. package/src/menu.js +180 -0
  73. package/src/meta.js +135 -0
  74. package/src/office.js +810 -0
  75. package/src/order.js +100 -0
  76. package/src/peek.js +49 -0
  77. package/src/pele-wrapper.js +695 -0
  78. package/src/permissions.js +180 -0
  79. package/src/places.js +313 -0
  80. package/src/preview-language.js +10 -0
  81. package/src/terminal-server.js +95 -0
  82. package/src/terminal-shell.js +26 -0
  83. package/src/transfer.js +669 -0
  84. package/src/trash-record.js +41 -0
  85. package/src/trash.js +210 -0
  86. package/src/trashbrowse.js +327 -0
  87. package/src/uistate.js +393 -0
  88. package/src/uistore.js +168 -0
  89. package/src/vendor/LICENSES/Apache-2.0.txt +202 -0
  90. package/src/vendor/LICENSES/MIT-katex-2015.txt +27 -0
  91. package/src/vendor/LICENSES/MIT-katex.txt +21 -0
  92. package/src/vendor/LICENSES/MIT-mermaid.txt +21 -0
  93. package/src/vendor/LICENSES/MIT-pele.txt +40 -0
  94. package/src/vendor/LICENSES/MIT-perseus.txt +18 -0
  95. package/src/vendor/LICENSES/MIT-temml.txt +21 -0
  96. package/src/vendor/pele.js +145 -0
  97. package/src/vendor/temml.mjs +14708 -0
  98. package/src/watch.js +98 -0
  99. package/src/wayland/client.js +109 -0
  100. package/src/wayland/protocol.js +55 -0
  101. package/src/wayland/protocols/ext-data-control-v1.xml +276 -0
  102. package/src/wayland/protocols/wayland.xml +3248 -0
  103. package/src/wayland/protocols/wlr-data-control-unstable-v1.xml +278 -0
  104. package/src/wayland/resource-io.js +6 -0
  105. package/src/wayland/socket-discovery.js +26 -0
  106. package/src/wayland/transport-posix.js +120 -0
  107. package/src/wayland/transport-win32.js +11 -0
  108. package/src/wayland/transport.js +3 -0
  109. package/src/wayland/win32-resources.js +80 -0
  110. package/src/wayland/win32-wire.js +62 -0
  111. package/test/archive.test.js +80 -0
  112. package/test/bench-markdown-scroll.mjs +190 -0
  113. package/test/clip-host.test.js +113 -0
  114. package/test/code-preview.test.js +35 -0
  115. package/test/compare-ui-paths.test.js +24 -0
  116. package/test/compare.test.js +54 -0
  117. package/test/delete-trash.test.js +67 -0
  118. package/test/dirsize.test.js +138 -0
  119. package/test/file-ops.test.js +307 -0
  120. package/test/file-url.test.js +24 -0
  121. package/test/find-contents.test.js +161 -0
  122. package/test/find-input.test.js +66 -0
  123. package/test/fixtures/pele-parity.json +1006 -0
  124. package/test/highlight-launcher.test.js +35 -0
  125. package/test/html-preview.test.js +22 -0
  126. package/test/input.test.js +15 -0
  127. package/test/keymap.test.js +26 -0
  128. package/test/language-icons.test.js +36 -0
  129. package/test/launcher.test.js +51 -0
  130. package/test/manual-archive-here-ui.mjs +47 -0
  131. package/test/manual-code-scroll.mjs +28 -0
  132. package/test/manual-column-scroll.mjs +24 -0
  133. package/test/manual-compare-ui.mjs +20 -0
  134. package/test/manual-find-status.mjs +37 -0
  135. package/test/manual-folder-focus.mjs +63 -0
  136. package/test/manual-html-security.mjs +23 -0
  137. package/test/manual-html-ui.mjs +30 -0
  138. package/test/manual-syntax-preview.mjs +26 -0
  139. package/test/manual-terminal-lifecycle.mjs +42 -0
  140. package/test/manual-terminal-navigation.mjs +48 -0
  141. package/test/manual-terminal-ui.mjs +47 -0
  142. package/test/manual-trash-ui.mjs +86 -0
  143. package/test/markdown-diagrams.test.js +93 -0
  144. package/test/markdown-html.test.js +105 -0
  145. package/test/mathml.test.js +92 -0
  146. package/test/menu-transfer.test.js +57 -0
  147. package/test/office-preview.test.js +154 -0
  148. package/test/pele-parity.test.js +28 -0
  149. package/test/places.test.js +134 -0
  150. package/test/preview-keys.test.js +60 -0
  151. package/test/preview-language.test.js +22 -0
  152. package/test/status-filename.test.js +20 -0
  153. package/test/transfer-replace-merge.test.js +187 -0
  154. package/test/trash-address.test.js +23 -0
  155. package/test/trash-view.test.js +37 -0
  156. package/test/trashbrowse.test.js +135 -0
  157. package/ui/AboutFacts.qml +78 -0
  158. package/ui/ArchiveCards.qml +95 -0
  159. package/ui/ArchiveCompressDialog.qml +378 -0
  160. package/ui/ArchiveHereConfirm.qml +127 -0
  161. package/ui/ArchivePasswordDialog.qml +257 -0
  162. package/ui/Backend.qml +775 -0
  163. package/ui/BulkOldNames.qml +47 -0
  164. package/ui/BulkRenameDialog.qml +380 -0
  165. package/ui/CardFrame.qml +67 -0
  166. package/ui/CardScroll.qml +79 -0
  167. package/ui/CheckBox.qml +73 -0
  168. package/ui/ChromeBar.qml +360 -0
  169. package/ui/ChromeButton.qml +131 -0
  170. package/ui/ChromeModes.qml +43 -0
  171. package/ui/CodeLines.qml +256 -0
  172. package/ui/CodeRefusal.qml +52 -0
  173. package/ui/CollideConfirm.qml +147 -0
  174. package/ui/CollideHost.qml +167 -0
  175. package/ui/ColumnPane.qml +525 -0
  176. package/ui/ColumnRow.qml +380 -0
  177. package/ui/ColumnsArea.qml +594 -0
  178. package/ui/ColumnsLanding.qml +147 -0
  179. package/ui/CompareView.qml +321 -0
  180. package/ui/ContextMenu.qml +743 -0
  181. package/ui/ConvertDialog.qml +456 -0
  182. package/ui/CountedWorker.qml +7 -0
  183. package/ui/Crumb.qml +64 -0
  184. package/ui/CrumbDrop.qml +25 -0
  185. package/ui/CsvTable.qml +394 -0
  186. package/ui/CursorMark.qml +41 -0
  187. package/ui/DefaultClaim.qml +130 -0
  188. package/ui/DeviceLock.qml +47 -0
  189. package/ui/DeviceMounts.qml +540 -0
  190. package/ui/DialogButton.qml +150 -0
  191. package/ui/DialogField.qml +147 -0
  192. package/ui/DialogHeader.qml +31 -0
  193. package/ui/DialogScrim.qml +20 -0
  194. package/ui/DialogTitle.qml +37 -0
  195. package/ui/Divider.qml +9 -0
  196. package/ui/DropInto.qml +126 -0
  197. package/ui/DropTarget.qml +35 -0
  198. package/ui/DropboxMark.qml +60 -0
  199. package/ui/EmptyState.qml +150 -0
  200. package/ui/EscapeHint.qml +11 -0
  201. package/ui/FactsTable.qml +54 -0
  202. package/ui/FastScrollHandler.qml +476 -0
  203. package/ui/FatNamesConfirm.qml +157 -0
  204. package/ui/Favourites.qml +143 -0
  205. package/ui/FigureService.qml +120 -0
  206. package/ui/FileDrag.qml +267 -0
  207. package/ui/FilterStrip.qml +133 -0
  208. package/ui/FitMetrics.qml +22 -0
  209. package/ui/FlashDriveRows.qml +137 -0
  210. package/ui/FlashHost.qml +72 -0
  211. package/ui/FlashImageRow.qml +104 -0
  212. package/ui/FlashRunner.qml +119 -0
  213. package/ui/FlashStepRow.qml +172 -0
  214. package/ui/FlashStrip.qml +38 -0
  215. package/ui/FlashView.qml +397 -0
  216. package/ui/FleaMark.qml +88 -0
  217. package/ui/FlyoutFade.qml +23 -0
  218. package/ui/FontPane.qml +170 -0
  219. package/ui/Glass.qml +114 -0
  220. package/ui/GlassTagger.qml +59 -0
  221. package/ui/Glyph.qml +64 -0
  222. package/ui/GlyphSolid.qml +22 -0
  223. package/ui/GridArea.qml +664 -0
  224. package/ui/GridTile.qml +325 -0
  225. package/ui/GvfsBridge.qml +164 -0
  226. package/ui/Header.qml +433 -0
  227. package/ui/HoverTimers.qml +39 -0
  228. package/ui/HtmlClip.qml +30 -0
  229. package/ui/HtmlPane.qml +63 -0
  230. package/ui/HyprlandMark.qml +45 -0
  231. package/ui/Ipc.qml +1005 -0
  232. package/ui/IpcControls.qml +7 -0
  233. package/ui/IpcMenuState.qml +48 -0
  234. package/ui/IpcPreviewState.qml +185 -0
  235. package/ui/JumpPath.qml +51 -0
  236. package/ui/KeymapSheet.qml +561 -0
  237. package/ui/KeymapSheetResult.qml +89 -0
  238. package/ui/KindStack.qml +40 -0
  239. package/ui/LangMark.qml +51 -0
  240. package/ui/LangMarkLoader.qml +26 -0
  241. package/ui/List.qml +480 -0
  242. package/ui/LoadingState.qml +54 -0
  243. package/ui/LocalSend.qml +56 -0
  244. package/ui/LocalSendMark.qml +73 -0
  245. package/ui/MarkdownBlockView.qml +311 -0
  246. package/ui/MarkdownBlocks.qml +35 -0
  247. package/ui/MarkdownFigure.qml +254 -0
  248. package/ui/MarkdownImages.qml +72 -0
  249. package/ui/MarkdownList.qml +81 -0
  250. package/ui/MarkdownMathsText.qml +97 -0
  251. package/ui/MarkdownPane.qml +83 -0
  252. package/ui/MarkdownPictures.qml +41 -0
  253. package/ui/MarkdownQuote.qml +52 -0
  254. package/ui/MarkdownRemote.qml +82 -0
  255. package/ui/MarkdownSource.qml +155 -0
  256. package/ui/MarkdownTable.qml +222 -0
  257. package/ui/MarkdownTableScroll.qml +64 -0
  258. package/ui/MarkdownTableWheel.qml +126 -0
  259. package/ui/MarkdownText.qml +87 -0
  260. package/ui/MarkdownWorker.js +132 -0
  261. package/ui/MatchText.qml +99 -0
  262. package/ui/MediaSession.qml +38 -0
  263. package/ui/MediaSound.qml +26 -0
  264. package/ui/MediaStrip.qml +191 -0
  265. package/ui/MediaTrack.qml +36 -0
  266. package/ui/MediaVolume.qml +54 -0
  267. package/ui/MenuActionDialog.qml +396 -0
  268. package/ui/MenuEdgeFade.qml +24 -0
  269. package/ui/MenuHost.qml +227 -0
  270. package/ui/MenuRow.qml +351 -0
  271. package/ui/MountListing.qml +100 -0
  272. package/ui/NetworkDialog.qml +435 -0
  273. package/ui/NetworkForm.qml +426 -0
  274. package/ui/NetworkMounts.qml +913 -0
  275. package/ui/NetworkPlaces.qml +127 -0
  276. package/ui/OpenWithDialog.qml +613 -0
  277. package/ui/Opener.qml +95 -0
  278. package/ui/Pane.qml +1320 -0
  279. package/ui/PaneMenuActions.qml +527 -0
  280. package/ui/PanePath.qml +90 -0
  281. package/ui/PaneRail.qml +268 -0
  282. package/ui/PaneStates.qml +88 -0
  283. package/ui/PaneSwap.qml +339 -0
  284. package/ui/PaneWire.qml +601 -0
  285. package/ui/ParkedWait.qml +9 -0
  286. package/ui/PathEntry.qml +186 -0
  287. package/ui/PathJump.qml +400 -0
  288. package/ui/PdfViewer.qml +297 -0
  289. package/ui/PermissionsDialog.qml +658 -0
  290. package/ui/PermissionsHeadings.qml +31 -0
  291. package/ui/PhoneMounts.qml +67 -0
  292. package/ui/PickerAnswers.qml +75 -0
  293. package/ui/PickerBox.qml +26 -0
  294. package/ui/PickerChrome.qml +295 -0
  295. package/ui/PickerFilter.qml +97 -0
  296. package/ui/PickerGrid.qml +308 -0
  297. package/ui/PickerHeader.qml +28 -0
  298. package/ui/PickerLifecycle.qml +27 -0
  299. package/ui/PickerList.qml +218 -0
  300. package/ui/PickerListing.qml +149 -0
  301. package/ui/PickerPathBar.qml +127 -0
  302. package/ui/PickerPlaces.qml +189 -0
  303. package/ui/PickerRecent.qml +92 -0
  304. package/ui/PickerSave.qml +101 -0
  305. package/ui/PickerSelection.qml +155 -0
  306. package/ui/PickerWindow.qml +846 -0
  307. package/ui/PowerSectorsReader.qml +10 -0
  308. package/ui/PrebuildDriver.qml +146 -0
  309. package/ui/Preview.qml +1005 -0
  310. package/ui/PreviewAnimation.qml +24 -0
  311. package/ui/PreviewArchive.qml +91 -0
  312. package/ui/PreviewColumn.qml +553 -0
  313. package/ui/PreviewEdge.qml +63 -0
  314. package/ui/PreviewFacts.qml +30 -0
  315. package/ui/PreviewFactsLine.qml +34 -0
  316. package/ui/PreviewHtml.qml +233 -0
  317. package/ui/PreviewImage.qml +253 -0
  318. package/ui/PreviewImagePan.qml +13 -0
  319. package/ui/PreviewImageSharp.qml +32 -0
  320. package/ui/PreviewLines.qml +106 -0
  321. package/ui/PreviewMarkdown.qml +706 -0
  322. package/ui/PreviewMedia.qml +250 -0
  323. package/ui/PreviewPdf.qml +482 -0
  324. package/ui/PreviewScroll.qml +201 -0
  325. package/ui/PreviewSwap.qml +335 -0
  326. package/ui/PreviewText.qml +149 -0
  327. package/ui/ProtocolChip.qml +18 -0
  328. package/ui/QuickLookArchive.qml +41 -0
  329. package/ui/QuickLookBar.qml +103 -0
  330. package/ui/QuickLookPrepare.qml +355 -0
  331. package/ui/QuickLookSwap.qml +10 -0
  332. package/ui/QuickLookWarm.qml +65 -0
  333. package/ui/RailDrop.qml +155 -0
  334. package/ui/RailHeading.qml +19 -0
  335. package/ui/RailPlaces.qml +38 -0
  336. package/ui/RailTrashRow.qml +67 -0
  337. package/ui/RenameField.qml +353 -0
  338. package/ui/RendererRetry.qml +11 -0
  339. package/ui/ResizeHandle.qml +52 -0
  340. package/ui/Reveal.qml +54 -0
  341. package/ui/Row.qml +476 -0
  342. package/ui/RowDrag.qml +213 -0
  343. package/ui/RowDropTarget.qml +27 -0
  344. package/ui/RunCard.qml +286 -0
  345. package/ui/Scripts.qml +65 -0
  346. package/ui/SearchStrip.qml +232 -0
  347. package/ui/SelectionBand.qml +140 -0
  348. package/ui/SelectionPreview.qml +370 -0
  349. package/ui/SettingsFavourite.qml +122 -0
  350. package/ui/SettingsGroup.qml +96 -0
  351. package/ui/SettingsHint.qml +12 -0
  352. package/ui/SettingsMetrics.qml +29 -0
  353. package/ui/SettingsPane.qml +178 -0
  354. package/ui/SettingsPanel.qml +547 -0
  355. package/ui/SettingsRail.qml +128 -0
  356. package/ui/SettingsRow.qml +409 -0
  357. package/ui/SettingsRuler.qml +85 -0
  358. package/ui/SettingsSegment.qml +85 -0
  359. package/ui/ShareBrowser.qml +113 -0
  360. package/ui/ShareLink.qml +67 -0
  361. package/ui/ShelfPins.qml +101 -0
  362. package/ui/Sidebar.qml +728 -0
  363. package/ui/SidebarRow.qml +358 -0
  364. package/ui/Spinner.qml +60 -0
  365. package/ui/StableModel.qml +89 -0
  366. package/ui/StateMessage.qml +84 -0
  367. package/ui/StatusBar.qml +512 -0
  368. package/ui/StatusFileName.qml +27 -0
  369. package/ui/TabBar.qml +673 -0
  370. package/ui/TabDragGeometry.qml +60 -0
  371. package/ui/TagMenuMark.qml +52 -0
  372. package/ui/TagRailSection.qml +53 -0
  373. package/ui/TagSlot.qml +40 -0
  374. package/ui/TagState.qml +150 -0
  375. package/ui/TagSwatch.qml +28 -0
  376. package/ui/Taildrop.qml +86 -0
  377. package/ui/TailscaleMark.qml +50 -0
  378. package/ui/TerminalService.qml +69 -0
  379. package/ui/TerminalTab.qml +48 -0
  380. package/ui/TerminalTabs.qml +27 -0
  381. package/ui/Theme.qml +460 -0
  382. package/ui/ThemeColors.qml +43 -0
  383. package/ui/TransferCard.qml +239 -0
  384. package/ui/TrashConfirm.qml +129 -0
  385. package/ui/TrashDropHandler.qml +11 -0
  386. package/ui/TrashHost.qml +202 -0
  387. package/ui/TrashMonitor.qml +30 -0
  388. package/ui/TrashToolbar.qml +19 -0
  389. package/ui/TrashView.qml +492 -0
  390. package/ui/UnlockDialog.qml +450 -0
  391. package/ui/UnlockHost.qml +206 -0
  392. package/ui/UpdateCheck.qml +97 -0
  393. package/ui/UserDirs.qml +25 -0
  394. package/ui/ViewState.qml +565 -0
  395. package/ui/ViewportScrollBar.qml +195 -0
  396. package/ui/ViewportScrollBars.qml +25 -0
  397. package/ui/WebPreview.qml +33 -0
  398. package/ui/WebPreviewView.qml +26 -0
  399. package/ui/WindowBody.qml +829 -0
  400. package/ui/WindowDrag.qml +35 -0
  401. package/ui/boot/Commons/AudioNodes.qml +50 -0
  402. package/ui/boot/Commons/AudioNodesModel.js +96 -0
  403. package/ui/boot/Commons/Border.qml +243 -0
  404. package/ui/boot/Commons/BorderGeometry.js +373 -0
  405. package/ui/boot/Commons/Color.qml +258 -0
  406. package/ui/boot/Commons/I18n.qml +118 -0
  407. package/ui/boot/Commons/InputDevices.qml +112 -0
  408. package/ui/boot/Commons/InputMethodState.qml +38 -0
  409. package/ui/boot/Commons/IpcRegistry.qml +77 -0
  410. package/ui/boot/Commons/Numbers.js +19 -0
  411. package/ui/boot/Commons/Plural.js +91 -0
  412. package/ui/boot/Commons/ShellIpc.qml +12 -0
  413. package/ui/boot/Commons/Style.qml +561 -0
  414. package/ui/boot/Commons/Translations.qml +100 -0
  415. package/ui/boot/Commons/UntypedInput.qml +152 -0
  416. package/ui/boot/Commons/Util.qml +188 -0
  417. package/ui/boot/Commons/qmldir +13 -0
  418. package/ui/boot/LICENSE-omarchy +20 -0
  419. package/ui/boot/Ui/BackgroundMedia.qml +49 -0
  420. package/ui/boot/Ui/BarIconButton.qml +106 -0
  421. package/ui/boot/Ui/BarIconContent.qml +38 -0
  422. package/ui/boot/Ui/BarIndicator.qml +53 -0
  423. package/ui/boot/Ui/BarWidget.qml +50 -0
  424. package/ui/boot/Ui/BorderOverlay.qml +54 -0
  425. package/ui/boot/Ui/BorderSurface.qml +40 -0
  426. package/ui/boot/Ui/Button.qml +224 -0
  427. package/ui/boot/Ui/ButtonGroup.qml +131 -0
  428. package/ui/boot/Ui/ConfirmDialog.qml +134 -0
  429. package/ui/boot/Ui/CursorSurface.qml +42 -0
  430. package/ui/boot/Ui/Dropdown.qml +245 -0
  431. package/ui/boot/Ui/FilterInput.qml +26 -0
  432. package/ui/boot/Ui/IconContentBounds.qml +141 -0
  433. package/ui/boot/Ui/KeyboardPanel.qml +453 -0
  434. package/ui/boot/Ui/MultiSelect.qml +624 -0
  435. package/ui/boot/Ui/NumberField.qml +86 -0
  436. package/ui/boot/Ui/OpticalGlyph.qml +68 -0
  437. package/ui/boot/Ui/OverlayWindow.qml +102 -0
  438. package/ui/boot/Ui/Panel.qml +60 -0
  439. package/ui/boot/Ui/PanelActionButton.qml +105 -0
  440. package/ui/boot/Ui/PanelController.qml +16 -0
  441. package/ui/boot/Ui/PanelHero.qml +112 -0
  442. package/ui/boot/Ui/PanelKeyCatcher.qml +100 -0
  443. package/ui/boot/Ui/PanelSectionHeader.qml +31 -0
  444. package/ui/boot/Ui/PanelSeparator.qml +19 -0
  445. package/ui/boot/Ui/PanelSlider.qml +156 -0
  446. package/ui/boot/Ui/PanelToolTip.qml +50 -0
  447. package/ui/boot/Ui/PluginBarApi.qml +89 -0
  448. package/ui/boot/Ui/PointerMoveGate.qml +54 -0
  449. package/ui/boot/Ui/PopupCard.qml +181 -0
  450. package/ui/boot/Ui/ScreenMoveRemap.qml +43 -0
  451. package/ui/boot/Ui/SearchableDropdown.qml +354 -0
  452. package/ui/boot/Ui/SizedImage.qml +125 -0
  453. package/ui/boot/Ui/SpeedTestOverlay.qml +412 -0
  454. package/ui/boot/Ui/TextField.qml +58 -0
  455. package/ui/boot/Ui/Toggle.qml +118 -0
  456. package/ui/boot/Ui/ToggleSwitch.qml +115 -0
  457. package/ui/boot/Ui/WidgetButton.qml +172 -0
  458. package/ui/boot/Ui/qmldir +41 -0
  459. package/ui/boot/fleatab.qml +50 -0
  460. package/ui/boot/picker.qml +39 -0
  461. package/ui/boot/shell.qml +131 -0
  462. package/ui/boot/tabtearoff.qml +51 -0
  463. package/ui/js/Anchor.js +379 -0
  464. package/ui/js/AnchorHold.js +162 -0
  465. package/ui/js/Archive.js +157 -0
  466. package/ui/js/ArchiveCard.js +44 -0
  467. package/ui/js/ArchiveOps.js +44 -0
  468. package/ui/js/BackReturn.js +118 -0
  469. package/ui/js/BulkRename.js +100 -0
  470. package/ui/js/Buttons.js +54 -0
  471. package/ui/js/ChromeKeys.js +116 -0
  472. package/ui/js/ClipMarks.js +73 -0
  473. package/ui/js/ClipSelection.js +67 -0
  474. package/ui/js/Clipboard.js +179 -0
  475. package/ui/js/Cloud.js +97 -0
  476. package/ui/js/CodeRuns.js +72 -0
  477. package/ui/js/Collide.js +212 -0
  478. package/ui/js/ColumnFit.js +28 -0
  479. package/ui/js/ColumnMenu.js +111 -0
  480. package/ui/js/Columns.js +298 -0
  481. package/ui/js/ColumnsCarry.js +103 -0
  482. package/ui/js/Compare.js +74 -0
  483. package/ui/js/Contrast.js +157 -0
  484. package/ui/js/ControlReader.js +42 -0
  485. package/ui/js/Convert.js +52 -0
  486. package/ui/js/CopyAs.js +52 -0
  487. package/ui/js/Crumbs.js +178 -0
  488. package/ui/js/Csv.js +231 -0
  489. package/ui/js/Density.js +44 -0
  490. package/ui/js/Devices.js +440 -0
  491. package/ui/js/DirSizes.js +180 -0
  492. package/ui/js/Drag.js +360 -0
  493. package/ui/js/DragOut.js +166 -0
  494. package/ui/js/DropDev.js +52 -0
  495. package/ui/js/Dropbox.js +41 -0
  496. package/ui/js/Eject.js +217 -0
  497. package/ui/js/Errors.js +188 -0
  498. package/ui/js/ExtThumbs.js +115 -0
  499. package/ui/js/Facts.js +402 -0
  500. package/ui/js/FigureDate.js +14 -0
  501. package/ui/js/Filter.js +285 -0
  502. package/ui/js/Find.js +46 -0
  503. package/ui/js/Flash.js +184 -0
  504. package/ui/js/FlashCard.js +79 -0
  505. package/ui/js/FlashEndings.js +125 -0
  506. package/ui/js/FlashPane.js +116 -0
  507. package/ui/js/FleaBin.js +12 -0
  508. package/ui/js/Focus.js +504 -0
  509. package/ui/js/FolderSorts.js +108 -0
  510. package/ui/js/FolderViews.js +44 -0
  511. package/ui/js/Format.js +314 -0
  512. package/ui/js/Fuzzy.js +141 -0
  513. package/ui/js/GlassTag.js +72 -0
  514. package/ui/js/Grid.js +33 -0
  515. package/ui/js/GridGeometry.js +90 -0
  516. package/ui/js/GridNames.js +153 -0
  517. package/ui/js/GvfsBridge.js +200 -0
  518. package/ui/js/HostClipboard.js +29 -0
  519. package/ui/js/Icons.js +218 -0
  520. package/ui/js/Input.js +50 -0
  521. package/ui/js/Jump.js +200 -0
  522. package/ui/js/KeyName.js +64 -0
  523. package/ui/js/Keymap.js +401 -0
  524. package/ui/js/Kinds.js +164 -0
  525. package/ui/js/LICENSES/ISC-lucide.txt +43 -0
  526. package/ui/js/LangMarkPaths.js +47 -0
  527. package/ui/js/LangMarks.js +120 -0
  528. package/ui/js/Lists.js +11 -0
  529. package/ui/js/LocalSend.js +39 -0
  530. package/ui/js/LockedMenu.js +63 -0
  531. package/ui/js/MakeDefault.js +186 -0
  532. package/ui/js/MarkBox.js +7 -0
  533. package/ui/js/Markdown.js +119 -0
  534. package/ui/js/MarkdownLists.js +86 -0
  535. package/ui/js/MarkdownMaths.js +58 -0
  536. package/ui/js/MarkdownPictures.js +32 -0
  537. package/ui/js/MarkdownPrepared.js +139 -0
  538. package/ui/js/MarkdownTableFit.js +34 -0
  539. package/ui/js/Marks.js +148 -0
  540. package/ui/js/Marquee.js +96 -0
  541. package/ui/js/Match.js +93 -0
  542. package/ui/js/Material.js +109 -0
  543. package/ui/js/MdBlocks.js +437 -0
  544. package/ui/js/MdBreak.js +71 -0
  545. package/ui/js/MdChunks.js +70 -0
  546. package/ui/js/MdContainer.js +137 -0
  547. package/ui/js/MdDocument.js +337 -0
  548. package/ui/js/MdEmph.js +97 -0
  549. package/ui/js/MdEntity.js +108 -0
  550. package/ui/js/MdEntityTable.js +223 -0
  551. package/ui/js/MdEscape.js +59 -0
  552. package/ui/js/MdFront.js +37 -0
  553. package/ui/js/MdHold.js +32 -0
  554. package/ui/js/MdHtml.js +307 -0
  555. package/ui/js/MdHtmlBlock.js +290 -0
  556. package/ui/js/MdHtmlHeading.js +53 -0
  557. package/ui/js/MdHtmlImage.js +179 -0
  558. package/ui/js/MdInline.js +154 -0
  559. package/ui/js/MdItems.js +212 -0
  560. package/ui/js/MdLeaf.js +309 -0
  561. package/ui/js/MdLink.js +196 -0
  562. package/ui/js/MdMath.js +67 -0
  563. package/ui/js/MdParagraphs.js +138 -0
  564. package/ui/js/MdRefs.js +232 -0
  565. package/ui/js/MdResolve.js +226 -0
  566. package/ui/js/MdRun.js +300 -0
  567. package/ui/js/MdUrl.js +226 -0
  568. package/ui/js/Menu.js +514 -0
  569. package/ui/js/MenuContext.js +85 -0
  570. package/ui/js/MenuFit.js +13 -0
  571. package/ui/js/MenuRefresh.js +74 -0
  572. package/ui/js/MenuWheel.js +35 -0
  573. package/ui/js/Messages.js +186 -0
  574. package/ui/js/Motion.js +16 -0
  575. package/ui/js/Mounts.js +320 -0
  576. package/ui/js/MouseNav.js +13 -0
  577. package/ui/js/Nav.js +329 -0
  578. package/ui/js/NetFs.js +26 -0
  579. package/ui/js/OpenWith.js +83 -0
  580. package/ui/js/Ops.js +585 -0
  581. package/ui/js/Palette.js +33 -0
  582. package/ui/js/PathBar.js +260 -0
  583. package/ui/js/Permissions.js +179 -0
  584. package/ui/js/Phones.js +112 -0
  585. package/ui/js/Picker.js +353 -0
  586. package/ui/js/PlaceMenu.js +96 -0
  587. package/ui/js/Places.js +265 -0
  588. package/ui/js/PreviewAsk.js +21 -0
  589. package/ui/js/PreviewKeys.js +155 -0
  590. package/ui/js/PreviewSettle.js +29 -0
  591. package/ui/js/PreviewSwap.js +101 -0
  592. package/ui/js/PropertyFacts.js +144 -0
  593. package/ui/js/Protocols.js +296 -0
  594. package/ui/js/QuickLookKinds.js +27 -0
  595. package/ui/js/QuickLookRect.js +24 -0
  596. package/ui/js/RailDrop.js +51 -0
  597. package/ui/js/RailKeys.js +87 -0
  598. package/ui/js/RailMenu.js +133 -0
  599. package/ui/js/Recent.js +135 -0
  600. package/ui/js/RecentMode.js +156 -0
  601. package/ui/js/Reload.js +60 -0
  602. package/ui/js/Renderer.js +9 -0
  603. package/ui/js/RunFile.js +143 -0
  604. package/ui/js/Scripts.js +28 -0
  605. package/ui/js/Scroll.js +304 -0
  606. package/ui/js/ScrollOff.js +69 -0
  607. package/ui/js/Search.js +478 -0
  608. package/ui/js/Selection.js +97 -0
  609. package/ui/js/Settings.js +476 -0
  610. package/ui/js/SettingsAbout.js +97 -0
  611. package/ui/js/SettingsShelf.js +52 -0
  612. package/ui/js/SheetKeys.js +42 -0
  613. package/ui/js/SheetQuery.js +281 -0
  614. package/ui/js/ShelfPile.js +27 -0
  615. package/ui/js/SlowClick.js +53 -0
  616. package/ui/js/SlowOp.js +6 -0
  617. package/ui/js/Sort.js +134 -0
  618. package/ui/js/Startup.js +63 -0
  619. package/ui/js/StateWords.js +17 -0
  620. package/ui/js/Status.js +238 -0
  621. package/ui/js/Swap.js +166 -0
  622. package/ui/js/TabMove.js +39 -0
  623. package/ui/js/Tabs.js +751 -0
  624. package/ui/js/Tags.js +134 -0
  625. package/ui/js/Taildrop.js +128 -0
  626. package/ui/js/Tap.js +180 -0
  627. package/ui/js/TextSize.js +107 -0
  628. package/ui/js/Thumbs.js +158 -0
  629. package/ui/js/Transfer.js +172 -0
  630. package/ui/js/Trash.js +84 -0
  631. package/ui/js/TrashDates.js +47 -0
  632. package/ui/js/TrashDrop.js +64 -0
  633. package/ui/js/UiState.js +438 -0
  634. package/ui/js/Unlock.js +152 -0
  635. package/ui/js/Update.js +130 -0
  636. package/ui/js/UserDirs.js +56 -0
  637. package/ui/qmldir +230 -0
@@ -0,0 +1,728 @@
1
+ // Compress and Extract: `formats` and `archive` (docs/protocol.md "Undocumented requests").
2
+ // Upstream: src/backend/archive*.rs, which runs bsdtar and 7z. This port runs no program: zip,
3
+ // tar, tar.gz and tar.zst are written and read here, with node:zlib for deflate, gzip and zstd.
4
+ //
5
+ // Both directions work in a hidden staging name beside the result and publish it under the first
6
+ // free name ("pack.zip", then "pack copy.zip", "pack copy 2.zip"), so a failed or cancelled run
7
+ // leaves nothing half-written under a real name. Extraction never writes through a link the
8
+ // archive made, never leaves the staging folder (absolute and ".." names are refused), and
9
+ // copies a tar hard-link member's bytes instead of linking.
10
+ //
11
+ // Not yet: passwords (7z), .7z/.rar/.xz/.bz2 and packages, the free-space check before an
12
+ // extract, a progress bar with bytes, per-drive queueing, and undo.
13
+ import { constants, createWriteStream, lstatSync, promises as fsp, statSync } from "node:fs";
14
+ import { basename, dirname, join, isAbsolute, resolve, sep } from "node:path";
15
+ import { Readable } from "node:stream";
16
+ import { pipeline } from "node:stream/promises";
17
+ import { crc32, createDeflateRaw, createGunzip, createGzip, createInflateRaw, createZstdCompress, createZstdDecompress } from "node:zlib";
18
+ import { BLOCK, MAX_RECORD, listArchive, topLevelArchiveName, checkedSize, checksumMatches, fileReader, octal, paxRecords, scanZip, text } from "./archive.js";
19
+ import { capturedPaths } from "./menu.js";
20
+ import { copyName, nextJobId, reason } from "./transfer.js";
21
+
22
+ /** What Compress offers, in menu order. */
23
+ export const FORMATS = ["zip", "tar", "tar.gz", "tar.zst"];
24
+ const NAME_TRIES = 1000,
25
+ CHUNK = 1 << 20;
26
+
27
+ const slash = (path) => (process.platform === "win32" ? path.replaceAll("\\", "/") : path);
28
+ const lstatOrNull = (path) => {
29
+ try {
30
+ return lstatSync(path);
31
+ } catch {
32
+ return null;
33
+ }
34
+ };
35
+ class Cancelled extends Error {}
36
+
37
+ // A transform's output as an async iterable, with a failure on either side thrown to the reader;
38
+ // `.pipe` alone would leave the reader waiting forever when the source fails.
39
+ function through(source, transform) {
40
+ const pump = pipeline(source, transform);
41
+ pump.catch(() => {});
42
+ return (async function* () {
43
+ for await (const chunk of transform) yield chunk;
44
+ await pump;
45
+ })();
46
+ }
47
+
48
+ // Archive extensions this port reads, longest first; null for one it does not.
49
+ const readKind = (lower) =>
50
+ lower.endsWith(".zip") ? "zip"
51
+ : lower.endsWith(".tar") ? "tar"
52
+ : /\.(tar\.gz|tgz)$/.test(lower) ? "tgz"
53
+ : /\.(tar\.zst|tzst)$/.test(lower) ? "tzst"
54
+ : null;
55
+
56
+ export function formats(request, send) {
57
+ // `archive` is tar's class and `zip` the zip class; `sevenZip` stays false, so .7z and packages
58
+ // never offer Extract, and no format offers a password.
59
+ send({ t: "formats", id: request.id ?? 0, archive: FORMATS, convert: false,
60
+ extract: { archive: true, sevenZip: false, zip: true }, providers: {} });
61
+ }
62
+
63
+ // ---- jobs ----
64
+
65
+ const jobs = new Map();
66
+ /** Cancels a running archive job; false when the id names none. */
67
+ export function archiveCancel(id) {
68
+ const job = jobs.get(id);
69
+ if (!job) return false;
70
+ job.cancelled = true;
71
+ return true;
72
+ }
73
+ const check = (job) => {
74
+ if (job.cancelled) throw new Cancelled("cancelled");
75
+ };
76
+
77
+ // The first free name for `dest`: an archive keeps its format's tail after " copy N", a folder its whole name.
78
+ export function freeName(dest, format, n) {
79
+ if (n === 0) return dest;
80
+ const name = basename(dest),
81
+ suffix = n === 1 ? " copy" : ` copy ${n}`;
82
+ if (format === null) return join(dirname(dest), name + suffix);
83
+ const tail = `.${format}`;
84
+ if (name.length > tail.length && name.endsWith(tail)) return join(dirname(dest), name.slice(0, -tail.length) + suffix + tail);
85
+ return join(dirname(dest), copyName(name, n));
86
+ }
87
+
88
+ // rename(2) replaces a file and, for an empty folder, a folder, so each name is checked first; the
89
+ // window between check and rename is the one place another program could slip in.
90
+ async function publish(staged, dest, format) {
91
+ for (let n = 0; n <= NAME_TRIES; n++) {
92
+ const target = freeName(dest, format, n);
93
+ if (lstatOrNull(target)) continue;
94
+ await fsp.rename(staged, target);
95
+ return target;
96
+ }
97
+ throw Error(`every copy name for this ${format === null ? "folder" : "archive"} is already taken`);
98
+ }
99
+
100
+ const stagingName = (dest, kind, id) => join(dirname(dest), `.flea-work-${kind}-${process.pid}-${id}`);
101
+
102
+ export function archive(request, send) {
103
+ const id = nextJobId();
104
+ const refuse = (err, extra = {}) => send({ t: "archivedone", id, ok: false, verified: true, err, ...extra });
105
+ if (typeof request.password === "string" && request.password !== "")
106
+ return refuse("only an archive that 7-Zip reads can take a password");
107
+ if (request.op === "compress") return compress(request, send, id, refuse);
108
+ if (request.op === "extract") return extract(request, send, id, refuse);
109
+ refuse(`no archive operation is named ${request.op}`);
110
+ }
111
+
112
+ // ---- compress ----
113
+
114
+ function compress(request, send, id, refuse) {
115
+ const format = request.format,
116
+ dest = request.dest;
117
+ if (!FORMATS.includes(format)) return refuse(`flea-qml cannot write .${format} archives`);
118
+ if (typeof dest !== "string" || !isAbsolute(dest)) return refuse("the archive needs an absolute destination");
119
+ const paths = request.menuId ? capturedPaths(request.menuId) : request.paths;
120
+ if (!paths) return refuse("Selected items changed; reopen the menu.");
121
+ if (!Array.isArray(paths) || paths.length === 0) return refuse("nothing was selected to compress");
122
+ const parent = dirname(resolve(String(paths[0])));
123
+ for (const path of paths) {
124
+ if (typeof path !== "string" || !isAbsolute(path)) return refuse("every item needs an absolute path");
125
+ if (dirname(resolve(path)) !== parent) return refuse("every item must be in the same folder");
126
+ if (!lstatOrNull(path)) return refuse(`${basename(path)} no longer exists`);
127
+ }
128
+ const job = { cancelled: false };
129
+ jobs.set(id, job);
130
+ send({ t: "archivestarted", id });
131
+ const staged = stagingName(dest, "zip", id);
132
+ (async () => {
133
+ const entries = walkAll(paths.map((path) => resolve(path)), staged, job);
134
+ const body = format === "zip" ? zipStream(entries, job) : tarStream(entries, job);
135
+ const squeeze = format === "tar.gz" ? [createGzip()] : format === "tar.zst" ? [createZstdCompress()] : [];
136
+ await pipeline(Readable.from(body), ...squeeze, createWriteStream(staged, { flags: "wx", mode: 0o644 }));
137
+ check(job);
138
+ const written = await publish(staged, dest, format);
139
+ send({ t: "archivedone", id, ok: true, verified: true, err: "", path: slash(written) });
140
+ })()
141
+ .catch(async (e) => {
142
+ await fsp.rm(staged, { force: true }).catch(() => {});
143
+ send({ t: "archivedone", id, ok: false, verified: true, err: e instanceof Cancelled ? "cancelled" : e?.sentence ?? reason(e) });
144
+ })
145
+ .finally(() => {
146
+ jobs.delete(id);
147
+ send({ t: "changed", path: slash(dirname(dest)) });
148
+ });
149
+ }
150
+
151
+ // Every item and what is under it, parents first, names in code-point order; a link is stored as a
152
+ // link, and fifos, sockets and devices are left out. `rel` uses "/" on every system.
153
+ async function* walkAll(paths, skip, job) {
154
+ for (const path of paths) yield* walk(path, basename(path), skip, job);
155
+ }
156
+ async function* walk(abs, rel, skip, job) {
157
+ check(job);
158
+ if (abs === skip) return;
159
+ const st = await fsp.lstat(abs);
160
+ if (st.isSymbolicLink()) return yield { rel, st, kind: "link", link: await fsp.readlink(abs) };
161
+ if (st.isFile()) return yield { rel, st, kind: "file", abs };
162
+ if (!st.isDirectory()) return;
163
+ yield { rel, st, kind: "dir" };
164
+ const names = (await fsp.readdir(abs)).sort();
165
+ for (const name of names) yield* walk(join(abs, name), `${rel}/${name}`, skip, job);
166
+ }
167
+
168
+ // A file's bytes in chunks, read through one handle, checked for a cancel between chunks.
169
+ async function* fileBytes(abs, job, size) {
170
+ const handle = await fsp.open(abs, "r");
171
+ try {
172
+ let left = size;
173
+ while (left > 0) {
174
+ check(job);
175
+ const buffer = Buffer.allocUnsafe(Math.min(CHUNK, left));
176
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
177
+ if (!bytesRead) break;
178
+ left -= bytesRead;
179
+ yield buffer.subarray(0, bytesRead);
180
+ }
181
+ // A file that shrank since its stat is padded, so the header's size stays true.
182
+ if (left > 0) throw Object.assign(Error(`${basename(abs)} changed while it was being compressed`), { sentence: `${basename(abs)} changed while it was being compressed` });
183
+ } finally {
184
+ await handle.close();
185
+ }
186
+ }
187
+
188
+ // ---- tar writer (POSIX ustar, with pax records for what ustar cannot hold) ----
189
+
190
+ const MAX_OCTAL_11 = 0o77777777777;
191
+ function field(block, offset, length, value) {
192
+ Buffer.from(value).copy(block, offset, 0, length);
193
+ }
194
+ function octalField(block, offset, length, value) {
195
+ const digits = Math.max(0, Math.min(value, 8 ** (length - 1) - 1)).toString(8).padStart(length - 1, "0");
196
+ field(block, offset, length, `${digits}\0`);
197
+ }
198
+ function paxRecord(key, value) {
199
+ const rest = Buffer.byteLength(` ${key}=${value}\n`);
200
+ let n = rest + 1;
201
+ while (String(n).length + rest !== n) n = String(n).length + rest;
202
+ return `${n} ${key}=${value}\n`;
203
+ }
204
+ function header({ name, prefix = "", mode, uid, gid, size, mtime, type, link = "" }) {
205
+ const block = Buffer.alloc(BLOCK);
206
+ field(block, 0, 100, name);
207
+ octalField(block, 100, 8, mode);
208
+ octalField(block, 108, 8, uid);
209
+ octalField(block, 116, 8, gid);
210
+ octalField(block, 124, 12, size);
211
+ octalField(block, 136, 12, mtime);
212
+ block.fill(0x20, 148, 156);
213
+ block[156] = type.charCodeAt(0);
214
+ field(block, 157, 100, link);
215
+ field(block, 257, 6, "ustar\0");
216
+ field(block, 263, 2, "00");
217
+ field(block, 345, 155, prefix);
218
+ let sum = 0;
219
+ for (const byte of block) sum += byte;
220
+ field(block, 148, 8, `${sum.toString(8).padStart(6, "0")}\0 `);
221
+ return block;
222
+ }
223
+ const padding = (size) => Buffer.alloc((BLOCK - (size % BLOCK)) % BLOCK);
224
+
225
+ // ustar's name splits once at a "/" into a 155-byte prefix and a 100-byte name; a name that fits
226
+ // neither way, a long link and a size past 8 GiB go in a pax record ahead of the header.
227
+ function tarHeaders(entry) {
228
+ const name = entry.kind === "dir" ? `${entry.rel}/` : entry.rel,
229
+ bytes = Buffer.from(name),
230
+ size = entry.kind === "file" ? entry.st.size : 0,
231
+ pax = [];
232
+ let short = name,
233
+ prefix = "";
234
+ if (bytes.length > 100) {
235
+ let split = -1;
236
+ for (let i = bytes.length - 1; i > 0; i--)
237
+ if (bytes[i] === 0x2f && i <= 155 && bytes.length - i - 1 <= 100 && bytes.length - i - 1 > 0) {
238
+ split = i;
239
+ break;
240
+ }
241
+ if (split > 0) {
242
+ prefix = bytes.subarray(0, split).toString();
243
+ short = bytes.subarray(split + 1).toString();
244
+ } else {
245
+ pax.push(paxRecord("path", name));
246
+ short = bytes.subarray(0, 100).toString("latin1");
247
+ }
248
+ }
249
+ const link = entry.link ?? "";
250
+ if (Buffer.byteLength(link) > 100) pax.push(paxRecord("linkpath", link));
251
+ if (size > MAX_OCTAL_11) pax.push(paxRecord("size", String(size)));
252
+ const mtime = Math.max(0, Math.floor(entry.st.mtimeMs / 1000));
253
+ const out = [];
254
+ if (pax.length) {
255
+ const data = Buffer.from(pax.join(""));
256
+ if (data.length > MAX_RECORD) throw Error(`${entry.rel} has a name too long to store`);
257
+ out.push(header({ name: `PaxHeader/${basename(entry.rel)}`.slice(0, 99), mode: 0o644, uid: 0, gid: 0, size: data.length, mtime, type: "x" }), data, padding(data.length));
258
+ }
259
+ out.push(header({ name: short, prefix, mode: entry.st.mode & 0o7777, uid: entry.st.uid, gid: entry.st.gid, size, mtime,
260
+ type: entry.kind === "dir" ? "5" : entry.kind === "link" ? "2" : "0", link: Buffer.byteLength(link) > 100 ? "" : link }));
261
+ return out;
262
+ }
263
+
264
+ async function* tarStream(entries, job) {
265
+ for await (const entry of entries) {
266
+ yield* tarHeaders(entry);
267
+ if (entry.kind !== "file") continue;
268
+ yield* fileBytes(entry.abs, job, entry.st.size);
269
+ yield padding(entry.st.size);
270
+ }
271
+ yield Buffer.alloc(BLOCK * 2);
272
+ }
273
+
274
+ // ---- zip writer (deflate, UTF-8 names, data descriptors, zip64 past 4 GiB or 65535 entries) ----
275
+
276
+ const U32 = 0xffffffff;
277
+ function dosStamp(ms) {
278
+ const date = new Date(ms);
279
+ if (date.getFullYear() < 1980) return { time: 0, date: (1 << 5) | 1 };
280
+ return {
281
+ time: (date.getHours() << 11) | (date.getMinutes() << 5) | (date.getSeconds() >> 1),
282
+ date: ((Math.min(date.getFullYear(), 2107) - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
283
+ };
284
+ }
285
+
286
+ // The "UT" extra field: the modification time in whole Unix seconds, which a DOS stamp rounds to two.
287
+ function timeExtra(ms) {
288
+ const extra = Buffer.alloc(9);
289
+ extra.writeUInt16LE(0x5455, 0);
290
+ extra.writeUInt16LE(5, 2);
291
+ extra[4] = 1;
292
+ extra.writeInt32LE(Math.max(-0x80000000, Math.min(0x7fffffff, Math.floor(ms / 1000))), 5);
293
+ return extra;
294
+ }
295
+
296
+ async function* zipStream(entries, job) {
297
+ const central = [];
298
+ let offset = 0;
299
+ const emit = (buffer) => {
300
+ offset += buffer.length;
301
+ return buffer;
302
+ };
303
+ for await (const entry of entries) {
304
+ const name = Buffer.from(entry.kind === "dir" ? `${entry.rel}/` : entry.rel),
305
+ stamp = dosStamp(entry.st.mtimeMs),
306
+ deflated = entry.kind === "file",
307
+ big = deflated && entry.st.size >= 0xffff0000,
308
+ start = offset;
309
+ const local = Buffer.alloc(30);
310
+ local.writeUInt32LE(0x04034b50, 0);
311
+ local.writeUInt16LE(big ? 45 : 20, 4);
312
+ local.writeUInt16LE(0x0808, 6); // data descriptor, UTF-8 name
313
+ local.writeUInt16LE(deflated ? 8 : 0, 8);
314
+ local.writeUInt16LE(stamp.time, 10);
315
+ local.writeUInt16LE(stamp.date, 12);
316
+ if (big) {
317
+ local.writeUInt32LE(U32, 18);
318
+ local.writeUInt32LE(U32, 22);
319
+ }
320
+ local.writeUInt16LE(name.length, 26);
321
+ const localExtra = Buffer.concat([timeExtra(entry.st.mtimeMs), ...(big ? [Buffer.from([1, 0, 16, 0]), Buffer.alloc(16)] : [])]);
322
+ local.writeUInt16LE(localExtra.length, 28);
323
+ yield emit(Buffer.concat([local, name, localExtra]));
324
+ let crc = 0,
325
+ size = 0,
326
+ packed = 0;
327
+ if (entry.kind === "link") {
328
+ const data = Buffer.from(entry.link);
329
+ crc = crc32(data);
330
+ size = packed = data.length;
331
+ yield emit(data);
332
+ } else if (deflated) {
333
+ const source = Readable.from((async function* () {
334
+ for await (const chunk of fileBytes(entry.abs, job, entry.st.size)) {
335
+ crc = crc32(chunk, crc);
336
+ size += chunk.length;
337
+ yield chunk;
338
+ }
339
+ })());
340
+ for await (const chunk of through(source, createDeflateRaw())) {
341
+ check(job);
342
+ packed += chunk.length;
343
+ yield emit(chunk);
344
+ }
345
+ }
346
+ const descriptor = Buffer.alloc(big ? 24 : 16);
347
+ descriptor.writeUInt32LE(0x08074b50, 0);
348
+ descriptor.writeUInt32LE(crc >>> 0, 4);
349
+ if (big) {
350
+ descriptor.writeBigUInt64LE(BigInt(packed), 8);
351
+ descriptor.writeBigUInt64LE(BigInt(size), 16);
352
+ } else {
353
+ descriptor.writeUInt32LE(packed, 8);
354
+ descriptor.writeUInt32LE(size, 12);
355
+ }
356
+ yield emit(descriptor);
357
+ const unixType = entry.kind === "dir" ? 0o040000 : entry.kind === "link" ? 0o120000 : 0o100000;
358
+ central.push({ name, stamp, mtimeMs: entry.st.mtimeMs, method: deflated ? 8 : 0, crc: crc >>> 0, size, packed, start, big,
359
+ external: (((unixType | (entry.st.mode & 0o7777)) << 16) | (entry.kind === "dir" ? 0x10 : 0)) >>> 0 });
360
+ }
361
+ const directoryStart = offset;
362
+ for (const item of central) {
363
+ const wide = [];
364
+ if (item.size >= U32) wide.push(item.size);
365
+ if (item.packed >= U32) wide.push(item.packed);
366
+ if (item.start >= U32) wide.push(item.start);
367
+ const field = Buffer.alloc(wide.length ? 4 + 8 * wide.length : 0);
368
+ if (wide.length) {
369
+ field.writeUInt16LE(1, 0);
370
+ field.writeUInt16LE(8 * wide.length, 2);
371
+ wide.forEach((value, i) => field.writeBigUInt64LE(BigInt(value), 4 + 8 * i));
372
+ }
373
+ const extra = Buffer.concat([timeExtra(item.mtimeMs), field]);
374
+ const record = Buffer.alloc(46);
375
+ record.writeUInt32LE(0x02014b50, 0);
376
+ record.writeUInt16LE((3 << 8) | 63, 4); // made by Unix, so the mode in `external` is read
377
+ record.writeUInt16LE(item.big || wide.length ? 45 : 20, 6);
378
+ record.writeUInt16LE(0x0808, 8);
379
+ record.writeUInt16LE(item.method, 10);
380
+ record.writeUInt16LE(item.stamp.time, 12);
381
+ record.writeUInt16LE(item.stamp.date, 14);
382
+ record.writeUInt32LE(item.crc, 16);
383
+ record.writeUInt32LE(Math.min(item.packed, U32), 20);
384
+ record.writeUInt32LE(Math.min(item.size, U32), 24);
385
+ record.writeUInt16LE(item.name.length, 28);
386
+ record.writeUInt16LE(extra.length, 30);
387
+ record.writeUInt32LE(item.external, 38);
388
+ record.writeUInt32LE(Math.min(item.start, U32), 42);
389
+ yield emit(Buffer.concat([record, item.name, extra]));
390
+ }
391
+ const directorySize = offset - directoryStart,
392
+ count = central.length;
393
+ if (count > 0xffff || directoryStart >= U32 || directorySize >= U32) {
394
+ const zip64 = Buffer.alloc(56),
395
+ at = offset;
396
+ zip64.writeUInt32LE(0x06064b50, 0);
397
+ zip64.writeBigUInt64LE(44n, 4);
398
+ zip64.writeUInt16LE((3 << 8) | 63, 12);
399
+ zip64.writeUInt16LE(45, 14);
400
+ zip64.writeBigUInt64LE(BigInt(count), 24);
401
+ zip64.writeBigUInt64LE(BigInt(count), 32);
402
+ zip64.writeBigUInt64LE(BigInt(directorySize), 40);
403
+ zip64.writeBigUInt64LE(BigInt(directoryStart), 48);
404
+ const locator = Buffer.alloc(20);
405
+ locator.writeUInt32LE(0x07064b50, 0);
406
+ locator.writeBigUInt64LE(BigInt(at), 8);
407
+ locator.writeUInt32LE(1, 16);
408
+ yield emit(Buffer.concat([zip64, locator]));
409
+ }
410
+ const end = Buffer.alloc(22);
411
+ end.writeUInt32LE(0x06054b50, 0);
412
+ end.writeUInt16LE(Math.min(count, 0xffff), 8);
413
+ end.writeUInt16LE(Math.min(count, 0xffff), 10);
414
+ end.writeUInt32LE(Math.min(directorySize, U32), 12);
415
+ end.writeUInt32LE(Math.min(directoryStart, U32), 16);
416
+ yield emit(end);
417
+ }
418
+
419
+ // ---- extract ----
420
+
421
+ function extract(request, send, id, refuse) {
422
+ const path = request.path,
423
+ dest = request.dest;
424
+ if (typeof path !== "string" || !isAbsolute(path)) return refuse("the archive needs an absolute path");
425
+ if (typeof dest !== "string" || !isAbsolute(dest)) return refuse("the archive needs an absolute destination");
426
+ const lower = path.toLowerCase();
427
+ if (lower.endsWith(".iso")) return refuse("a disk image opens as a listing and is never extracted");
428
+ const kind = readKind(lower);
429
+ if (!kind) return refuse(`flea-qml cannot extract ${basename(path)} yet; it reads .zip, .tar, .tar.gz and .tar.zst`);
430
+ let st;
431
+ try {
432
+ st = statSync(path);
433
+ } catch (e) {
434
+ return refuse(reason(e));
435
+ }
436
+ if (!st.isFile()) return refuse(`${basename(path)} is not a file`);
437
+ const job = { cancelled: false };
438
+ jobs.set(id, job);
439
+ send({ t: "archivestarted", id });
440
+ send({ t: "extractstarted", id });
441
+ send({ t: "transferprogress", id, index: 0, name: basename(path), bytes: 0, total: 0, scanned: 0 });
442
+ const here = request.here === true;
443
+ const staged = stagingName(here ? join(dest, "contents") : dest, "ext", id);
444
+ let ownsStaged = false;
445
+ (async () => {
446
+ if (here) {
447
+ if (!lstatOrNull(dest)?.isDirectory()) throw Error("Extract here needs an existing real folder");
448
+ if (request.confirmed !== true) {
449
+ const entries = new Set();
450
+ await listArchive(path, path, member => {
451
+ check(job);
452
+ const root = topLevelArchiveName(member.name);
453
+ if (root) entries.add(root);
454
+ if (entries.size > 5) throw Error("Extract here needs confirmation for more than 5 top-level items");
455
+ });
456
+ }
457
+ }
458
+ await fsp.mkdir(staged, { mode: 0o700 });
459
+ ownsStaged = true;
460
+ const out = new Extractor(staged, job);
461
+ if (kind === "zip") await readZip(path, out, job);
462
+ else await readTar(path, kind, out, job);
463
+ await out.finish();
464
+ check(job);
465
+ let written = dest;
466
+ if (here) {
467
+ // Publish complete top-level items without merging existing folders. Roll back our
468
+ // moves if publishing fails; cancellation is checked before this short commit phase.
469
+ const moved = [];
470
+ try {
471
+ for (const name of await fsp.readdir(staged)) {
472
+ const source = join(staged, name);
473
+ const target = await publish(source, join(dest, name), lstatOrNull(source)?.isDirectory() ? null : "");
474
+ moved.push([source, target]);
475
+ }
476
+ } catch (error) {
477
+ for (const [source, target] of moved.reverse()) await fsp.rename(target, source);
478
+ throw error;
479
+ }
480
+ await fsp.rmdir(staged);
481
+ } else written = await publish(staged, dest, null);
482
+ send({ t: "archivedone", id, ok: true, verified: true, err: "", path: slash(path), dest: slash(written) });
483
+ })()
484
+ .catch(async (e) => {
485
+ if (ownsStaged) await fsp.rm(staged, { recursive: true, force: true }).catch(() => {});
486
+ send({ t: "archivedone", id, ok: false, verified: true, path: slash(path), dest: slash(dest),
487
+ err: e instanceof Cancelled ? "cancelled" : e?.sentence ?? reason(e) });
488
+ })
489
+ .finally(() => {
490
+ jobs.delete(id);
491
+ send({ t: "changed", path: slash(here ? dest : dirname(dest)) });
492
+ });
493
+ }
494
+
495
+ const refusal = (sentence) => Object.assign(Error(sentence), { sentence });
496
+
497
+ // Writes members under one root. A member's parents are made as real folders; a member whose path
498
+ // runs through a link, or names an absolute path or "..", is refused, so nothing lands outside.
499
+ class Extractor {
500
+ constructor(root, job) {
501
+ this.root = root;
502
+ this.job = job;
503
+ this.folders = [];
504
+ }
505
+ parts(name) {
506
+ const text = String(name).replaceAll("\\", "/");
507
+ if (text.startsWith("/") || /^[A-Za-z]:/.test(text)) throw refusal(`the archive names an absolute path, ${name}, so nothing was extracted`);
508
+ const parts = text.split("/").filter((part) => part !== "" && part !== ".");
509
+ if (parts.includes("..")) throw refusal(`the archive names a path outside its folder, ${name}, so nothing was extracted`);
510
+ return parts;
511
+ }
512
+ async parents(parts) {
513
+ let at = this.root;
514
+ for (const part of parts.slice(0, -1)) {
515
+ at = join(at, part);
516
+ const st = lstatOrNull(at);
517
+ if (!st) await fsp.mkdir(at, { mode: 0o755 });
518
+ else if (!st.isDirectory()) throw refusal(`the archive writes through ${parts.join("/")}, which is not a folder, so nothing was extracted`);
519
+ }
520
+ return join(this.root, ...parts);
521
+ }
522
+ // A later member of the same name replaces an earlier one, as tar does; a folder is never replaced.
523
+ async clear(path) {
524
+ const st = lstatOrNull(path);
525
+ if (!st) return;
526
+ if (st.isDirectory()) throw refusal(`the archive holds a file and a folder both named ${basename(path)}`);
527
+ await fsp.unlink(path);
528
+ }
529
+ async folder(name, mode, mtime) {
530
+ const parts = this.parts(name);
531
+ if (!parts.length) return;
532
+ const path = await this.parents(parts),
533
+ st = lstatOrNull(path);
534
+ if (st && !st.isDirectory()) await fsp.unlink(path);
535
+ if (!st || !st.isDirectory()) await fsp.mkdir(path, { mode: 0o755 });
536
+ this.folders.push({ path, mode, mtime });
537
+ }
538
+ async file(name, mode, mtime, chunks) {
539
+ const parts = this.parts(name);
540
+ if (!parts.length) {
541
+ for await (const _ of chunks);
542
+ return;
543
+ }
544
+ const path = await this.parents(parts);
545
+ await this.clear(path);
546
+ const handle = await fsp.open(path, "wx", (mode & 0o777) || 0o644);
547
+ try {
548
+ for await (const chunk of chunks) {
549
+ check(this.job);
550
+ let written = 0;
551
+ while (written < chunk.length) written += (await handle.write(chunk, written, chunk.length - written)).bytesWritten;
552
+ }
553
+ } finally {
554
+ await handle.close();
555
+ }
556
+ if (mtime) await fsp.utimes(path, mtime, mtime).catch(() => {});
557
+ }
558
+ async link(name, target) {
559
+ const parts = this.parts(name);
560
+ if (!parts.length) return;
561
+ const path = await this.parents(parts);
562
+ await this.clear(path);
563
+ await fsp.symlink(target, path);
564
+ }
565
+ // A hard-link member names an earlier member; its bytes are copied, never linked.
566
+ async copyOf(name, earlier, mode, mtime) {
567
+ const parts = this.parts(name),
568
+ from = join(this.root, ...this.parts(earlier));
569
+ const st = lstatOrNull(from);
570
+ if (!st?.isFile()) throw refusal(`${name} links to ${earlier}, which the archive does not hold before it`);
571
+ const path = await this.parents(parts);
572
+ await this.clear(path);
573
+ await fsp.copyFile(from, path, constants.COPYFILE_EXCL);
574
+ await fsp.chmod(path, ((mode & 0o777) || st.mode & 0o777) & ~process.umask()).catch(() => {});
575
+ if (mtime) await fsp.utimes(path, mtime, mtime).catch(() => {});
576
+ }
577
+ // Folder modes and times last and deepest first: a read-only folder would refuse its own members.
578
+ async finish() {
579
+ for (const { path, mode, mtime } of this.folders.reverse()) {
580
+ // chmod skips the umask an open applies, so it is applied here, as bsdtar does for anyone but root.
581
+ if (mode) await fsp.chmod(path, mode & 0o777 & ~process.umask()).catch(() => {});
582
+ if (mtime) await fsp.utimes(path, mtime, mtime).catch(() => {});
583
+ }
584
+ }
585
+ }
586
+
587
+ // The bytes of an async iterable read as exactly-sized pieces.
588
+ class Bytes {
589
+ constructor(iterable) {
590
+ this.it = iterable[Symbol.asyncIterator]();
591
+ this.buffer = Buffer.alloc(0);
592
+ }
593
+ async more() {
594
+ const { value, done } = await this.it.next();
595
+ if (done) return false;
596
+ this.buffer = this.buffer.length ? Buffer.concat([this.buffer, value]) : value;
597
+ return true;
598
+ }
599
+ async read(n) {
600
+ while (this.buffer.length < n) if (!(await this.more())) break;
601
+ const out = this.buffer.subarray(0, n);
602
+ this.buffer = this.buffer.subarray(out.length);
603
+ return out;
604
+ }
605
+ async *take(n) {
606
+ while (n > 0) {
607
+ if (!this.buffer.length && !(await this.more())) throw refusal("the archive is truncated");
608
+ const out = this.buffer.subarray(0, Math.min(n, this.buffer.length));
609
+ this.buffer = this.buffer.subarray(out.length);
610
+ n -= out.length;
611
+ yield out;
612
+ }
613
+ }
614
+ async skip(n) {
615
+ for await (const _ of this.take(n));
616
+ }
617
+ }
618
+
619
+ async function* handleChunks(handle, start, length) {
620
+ for (let done = 0; done < length; ) {
621
+ const buffer = Buffer.allocUnsafe(Math.min(CHUNK, length - done));
622
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, start + done);
623
+ if (!bytesRead) throw refusal("the archive is truncated");
624
+ done += bytesRead;
625
+ yield buffer.subarray(0, bytesRead);
626
+ }
627
+ }
628
+
629
+ async function readTar(path, kind, out, job) {
630
+ const handle = await fsp.open(path, "r");
631
+ try {
632
+ const { size } = await handle.stat();
633
+ const raw = Readable.from(handleChunks(handle, 0, size));
634
+ const bytes = new Bytes(kind === "tgz" ? through(raw, createGunzip()) : kind === "tzst" ? through(raw, createZstdDecompress()) : raw);
635
+ let longName = null,
636
+ longLink = null,
637
+ extended = {},
638
+ global = {};
639
+ for (;;) {
640
+ check(job);
641
+ const block = await bytes.read(BLOCK);
642
+ if (block.length < BLOCK) break;
643
+ if (block.every((byte) => byte === 0)) continue;
644
+ if (!checksumMatches(block)) throw refusal("the archive is damaged: a tar header is invalid");
645
+ const flag = String.fromCharCode(block[156] || 0x30),
646
+ attrs = { ...global, ...extended },
647
+ metadata = ["L", "K", "x", "X", "g"].includes(flag),
648
+ size = checkedSize(!metadata && attrs.size !== undefined ? Number(attrs.size) : octal(block, 124, 12)),
649
+ pad = (BLOCK - (size % BLOCK)) % BLOCK;
650
+ if (metadata) {
651
+ if (size > MAX_RECORD) throw refusal("the archive is damaged: a tar record is too large");
652
+ const data = Buffer.concat(await Array.fromAsync(bytes.take(size)));
653
+ await bytes.skip(pad);
654
+ if (flag === "L") longName = data.toString().replace(/\0+$/, "");
655
+ else if (flag === "K") longLink = data.toString().replace(/\0+$/, "");
656
+ else if (flag === "g") global = { ...global, ...paxRecords(data) };
657
+ else extended = { ...extended, ...paxRecords(data) };
658
+ continue;
659
+ }
660
+ const prefix = text(block, 257, 5) === "ustar" ? text(block, 345, 155) : "",
661
+ stored = text(block, 0, 100),
662
+ name = attrs.path ?? longName ?? (prefix ? `${prefix}/${stored}` : stored),
663
+ link = attrs.linkpath ?? longLink ?? text(block, 157, 100),
664
+ mode = octal(block, 100, 8),
665
+ mtime = attrs.mtime !== undefined ? Number(attrs.mtime) : octal(block, 136, 12);
666
+ longName = longLink = null;
667
+ extended = {};
668
+ if (flag === "0" || flag === "\0" || flag === "7") {
669
+ if (name.endsWith("/")) await out.folder(name, mode, mtime);
670
+ else await out.file(name, mode, mtime, bytes.take(size));
671
+ } else if (flag === "5") await out.folder(name, mode, mtime);
672
+ else if (flag === "2") await out.link(name, link);
673
+ else if (flag === "1") await out.copyOf(name, link, mode, mtime);
674
+ // Devices, fifos and anything newer carry nothing a folder of files can hold.
675
+ await bytes.skip(flag === "0" || flag === "\0" || flag === "7" ? (name.endsWith("/") ? size : 0) : size);
676
+ await bytes.skip(pad);
677
+ }
678
+ } finally {
679
+ await handle.close();
680
+ }
681
+ }
682
+
683
+ // Info-ZIP's DOS stamp in local time, as unzip reads it.
684
+ const dosDate = (time, date) => new Date(((date >> 9) & 0x7f) + 1980, ((date >> 5) & 15) - 1, date & 31, time >> 11, (time >> 5) & 63, (time & 31) * 2);
685
+
686
+ async function readZip(path, out, job) {
687
+ const handle = await fsp.open(path, "r");
688
+ try {
689
+ const { size } = await handle.stat();
690
+ const members = [];
691
+ scanZip(fileReader(handle.fd, size), (entry, raw) => members.push({ ...entry, ...raw }));
692
+ for (const member of members) {
693
+ check(job);
694
+ if (member.encrypted) throw refusal("this archive is encrypted, and flea-qml cannot extract with a password yet");
695
+ const unixMode = member.madeBy === 3 ? member.external >>> 16 : 0,
696
+ type = unixMode & 0o170000,
697
+ mtime = member.mtime !== undefined ? member.mtime : dosDate(member.time, member.date);
698
+ if (member.dir || type === 0o040000) {
699
+ await out.folder(member.name, unixMode & 0o7777 || 0o755, mtime);
700
+ continue;
701
+ }
702
+ if (member.method !== 0 && member.method !== 8) throw refusal(`${member.name} uses a compression this port cannot read (method ${member.method})`);
703
+ const local = Buffer.alloc(30);
704
+ await handle.read(local, 0, 30, member.local);
705
+ if (local.readUInt32LE(0) !== 0x04034b50) throw refusal("the archive is damaged: a member header is missing");
706
+ const start = member.local + 30 + local.readUInt16LE(26) + local.readUInt16LE(28);
707
+ let crc = 0;
708
+ const data = (async function* () {
709
+ const stored = Readable.from(handleChunks(handle, start, member.compressed));
710
+ const raw = member.method === 8 ? through(stored, createInflateRaw()) : stored;
711
+ let total = 0;
712
+ for await (const chunk of raw) {
713
+ crc = crc32(chunk, crc);
714
+ total += chunk.length;
715
+ if (total > member.size) throw refusal(`the archive is damaged: ${member.name} is larger than it says`);
716
+ yield chunk;
717
+ }
718
+ if (total !== member.size || crc >>> 0 !== member.crc) throw refusal(`the archive is damaged: ${member.name} does not match its checksum`);
719
+ })();
720
+ if (type === 0o120000) {
721
+ const target = Buffer.concat(await Array.fromAsync(data)).toString();
722
+ await out.link(member.name, target);
723
+ } else await out.file(member.name, unixMode & 0o777 || 0o644, mtime, data);
724
+ }
725
+ } finally {
726
+ await handle.close();
727
+ }
728
+ }