buninu-browser 0.3.0 → 0.5.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 (296) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +40 -4
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +152 -0
  5. package/frontends/wayland/casty/LICENSE +21 -0
  6. package/frontends/wayland/casty/bookmarks.js +32 -0
  7. package/frontends/wayland/casty/click.js +96 -0
  8. package/frontends/wayland/casty/commands.js +61 -0
  9. package/frontends/wayland/casty/config.js +45 -0
  10. package/frontends/wayland/casty/download-directory.js +28 -0
  11. package/frontends/wayland/casty/download.js +49 -0
  12. package/frontends/wayland/casty/hints.js +334 -0
  13. package/frontends/wayland/casty/keys.js +56 -0
  14. package/frontends/wayland/casty/page-commands.js +80 -0
  15. package/frontends/wayland/casty/prompt-width.js +148 -0
  16. package/frontends/wayland/casty/urlbar.js +606 -0
  17. package/frontends/wayland/client.js +88 -0
  18. package/frontends/wayland/clipboard.js +80 -0
  19. package/frontends/wayland/index.js +130 -0
  20. package/frontends/wayland/input.js +75 -0
  21. package/frontends/wayland/protocol.js +55 -0
  22. package/frontends/wayland/protocols/wayland.xml +3248 -0
  23. package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
  24. package/frontends/wayland/transport.js +120 -0
  25. package/frontends/wayland/urlbar.js +162 -0
  26. package/index.js +28 -1
  27. package/lib/audio/shared-connection.js +31 -0
  28. package/lib/cdp-server.js +15 -2
  29. package/lib/clipboard-command.js +10 -0
  30. package/lib/clipboard-powershell.js +22 -0
  31. package/lib/clipboard.js +37 -2
  32. package/lib/document-types.js +11 -0
  33. package/lib/headless-shell.js +54 -12
  34. package/lib/layout/text-layout.js +17 -9
  35. package/lib/paint/display-list.js +6 -1
  36. package/lib/qml/README.md +1427 -0
  37. package/lib/qml/builtins.js +771 -0
  38. package/lib/qml/canvas-dom.js +115 -0
  39. package/lib/qml/canvas.js +76 -0
  40. package/lib/qml/document.js +1199 -0
  41. package/lib/qml/fonts.js +69 -0
  42. package/lib/qml/format.js +9 -0
  43. package/lib/qml/host.js +352 -0
  44. package/lib/qml/images.js +199 -0
  45. package/lib/qml/launcher.js +132 -0
  46. package/lib/qml/loader.js +521 -0
  47. package/lib/qml/media-renderer.js +49 -0
  48. package/lib/qml/media.js +86 -0
  49. package/lib/qml/models.js +622 -0
  50. package/lib/qml/parser.js +381 -0
  51. package/lib/qml/qs +5 -0
  52. package/lib/qml/qscasty +3 -0
  53. package/lib/qml/qsheadless +3 -0
  54. package/lib/qml/qswayland +3 -0
  55. package/lib/qml/reactive.js +182 -0
  56. package/lib/qml/runtime.js +3603 -0
  57. package/lib/qml/shader/fragment.js +83 -0
  58. package/lib/qml/shader/qsb.js +27 -0
  59. package/lib/qml/shader/renderer.js +39 -0
  60. package/lib/qml/shader/runtime.js +61 -0
  61. package/lib/qml/shapes.js +251 -0
  62. package/lib/qml/shortcuts.js +65 -0
  63. package/lib/qml/text.js +109 -0
  64. package/lib/qml/textures.js +126 -0
  65. package/lib/qml/wayland-clipboard.js +32 -0
  66. package/lib/qml/window-constraints.js +16 -0
  67. package/lib/qml/worker-thread.js +70 -0
  68. package/lib/render-tree/index.js +16 -1
  69. package/lib/renderer/bindings.js +16 -1
  70. package/lib/renderer/canvas-2d.js +29 -0
  71. package/lib/renderer/document-selection.js +19 -0
  72. package/lib/renderer/host.js +45 -17
  73. package/lib/renderer/main.js +21 -6
  74. package/lib/renderer/page-realm.js +20 -10
  75. package/lib/renderer/page-renderer.js +497 -42
  76. package/lib/renderer/pointer-move.js +5 -3
  77. package/lib/renderer/render-interval.js +9 -0
  78. package/lib/renderer/text-input-paint.js +54 -0
  79. package/lib/renderer/text-input.js +57 -0
  80. package/lib/style/computed-style.js +10 -4
  81. package/package.json +1 -1
  82. package/test/audio-connection-lifetime.test.js +46 -0
  83. package/test/canvas-paint.test.js +8 -0
  84. package/test/clipboard.test.js +19 -0
  85. package/test/computed-style.test.js +12 -0
  86. package/test/document-selection.test.js +27 -0
  87. package/test/double-click.test.js +85 -0
  88. package/test/fixtures/canvas-damage.js +40 -0
  89. package/test/fixtures/fake-renderer.js +1 -0
  90. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  91. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  92. package/test/fixtures/flea-helpers/bin/bat +3 -0
  93. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  94. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  95. package/test/fixtures/flea-helpers/bin/glow +3 -0
  96. package/test/fixtures/flea-helpers/bin/qs +3 -0
  97. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  98. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  99. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  100. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  101. package/test/fixtures/flea-helpers/check.mjs +36 -0
  102. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  103. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  104. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  105. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  106. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  107. package/test/fixtures/omastorm-helpers/Dockerfile +35 -0
  108. package/test/fixtures/omastorm-helpers/check.mjs +56 -0
  109. package/test/fixtures/omastorm-helpers/launch.mjs +24 -0
  110. package/test/fixtures/omastorm-helpers/prepare.mjs +17 -0
  111. package/test/fixtures/omastorm-helpers/runtime.mjs +32 -0
  112. package/test/fixtures/pointer-paint-batching.js +23 -0
  113. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  114. package/test/fixtures/qml/flea/LICENSE +21 -0
  115. package/test/fixtures/qml/flea/README.md +25 -0
  116. package/test/fixtures/qml/flea/Theme.qml +8 -0
  117. package/test/fixtures/qml/flea/demo.qml +24 -0
  118. package/test/fixtures/qml/flea/qmldir +3 -0
  119. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  120. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  121. package/test/fixtures/qml/flea-theme/README.md +70 -0
  122. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  123. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  124. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  125. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  126. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  127. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  128. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  129. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  130. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  131. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  132. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  133. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  134. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  135. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  136. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  137. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  138. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  139. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  140. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  141. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  142. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  143. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  144. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  145. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  146. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  147. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  148. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  167. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  168. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  169. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  170. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  171. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  172. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  173. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  174. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  175. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  176. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  177. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  178. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  179. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  180. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  181. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  182. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  183. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  184. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  185. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  186. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  187. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  188. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  189. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  190. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  191. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  192. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  193. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  194. package/test/fixtures/qml/hello.qml +26 -0
  195. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  196. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  197. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  198. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  199. package/test/fixtures/qml/imports/logic/base.js +2 -0
  200. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  201. package/test/fixtures/qml/imports/logic/math.js +3 -0
  202. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  203. package/test/fixtures/qml/imports/main.qml +21 -0
  204. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  205. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  206. package/test/fixtures/qml/multiply.qml +61 -0
  207. package/test/fixtures/qml-canvas-dom.js +90 -0
  208. package/test/fixtures/qml-fonts/main.qml +10 -0
  209. package/test/fixtures/qml-locale/check.mjs +6 -0
  210. package/test/fixtures/qml-locale/main.qml +8 -0
  211. package/test/fixtures/qml-media-audio.js +39 -0
  212. package/test/fixtures/qml-shader/grab.qml +14 -0
  213. package/test/fixtures/qml-shader/gradient.frag +5 -0
  214. package/test/fixtures/qml-shader/main.qml +14 -0
  215. package/test/fixtures/qml-shader/texture-grab.qml +25 -0
  216. package/test/fixtures/qml-shader/texture-size.frag +5 -0
  217. package/test/fixtures/qml-shader-check.mjs +24 -0
  218. package/test/fixtures/title-no-paint.js +20 -0
  219. package/test/helpers/flea-sandbox.js +48 -0
  220. package/test/helpers/quickshell-fixture.js +125 -0
  221. package/test/html-clipboard-wayland.test.js +47 -0
  222. package/test/index.test.js +13 -0
  223. package/test/manual-casty-without-qml.js +87 -0
  224. package/test/manual-double-click.html +21 -0
  225. package/test/manual-flea-application.js +49 -0
  226. package/test/manual-flea-binary.js +62 -0
  227. package/test/manual-flea-qml.js +9 -0
  228. package/test/manual-flea-settings.js +40 -0
  229. package/test/manual-flea-theme.js +40 -0
  230. package/test/manual-mouse-buttons.html +18 -0
  231. package/test/manual-omastorm-engine.mjs +19 -0
  232. package/test/manual-omastorm-shaders.mjs +28 -0
  233. package/test/manual-omastorm-software.mjs +25 -0
  234. package/test/manual-pointer-scroll.html +43 -0
  235. package/test/manual-qml-removal.js +46 -0
  236. package/test/manual-without-qml.js +45 -0
  237. package/test/pointer-drag.test.js +104 -0
  238. package/test/qml-canvas-dom.test.js +37 -0
  239. package/test/qml-canvas.test.js +21 -0
  240. package/test/qml-clipboard-bridge.test.js +25 -0
  241. package/test/qml-clipboard-wayland.test.js +40 -0
  242. package/test/qml-connections-timing.test.js +95 -0
  243. package/test/qml-controls-window.test.js +182 -0
  244. package/test/qml-dynamic.test.js +401 -0
  245. package/test/qml-flea-app.test.js +146 -0
  246. package/test/qml-flea-backend.test.js +78 -0
  247. package/test/qml-flea-columns-backend.test.js +38 -0
  248. package/test/qml-flea-compile.test.js +78 -0
  249. package/test/qml-flea-launcher.test.js +88 -0
  250. package/test/qml-flea-markdown-backend.test.js +31 -0
  251. package/test/qml-flea-media.test.js +28 -0
  252. package/test/qml-flea-navigation-backend.test.js +35 -0
  253. package/test/qml-flea-navigation-timing.test.js +147 -0
  254. package/test/qml-flea-preview-backend.test.js +53 -0
  255. package/test/qml-flea-previews.test.js +163 -0
  256. package/test/qml-flea-rows.test.js +101 -0
  257. package/test/qml-flea-textures.test.js +59 -0
  258. package/test/qml-flea-view-switch.test.js +33 -0
  259. package/test/qml-flea-wayland.test.js +68 -0
  260. package/test/qml-flea.test.js +34 -0
  261. package/test/qml-fonts.test.js +31 -0
  262. package/test/qml-grid-multiline.test.js +48 -0
  263. package/test/qml-host.test.js +238 -0
  264. package/test/qml-imports.test.js +102 -0
  265. package/test/qml-inherited-handlers.test.js +51 -0
  266. package/test/qml-input-selection.test.js +66 -0
  267. package/test/qml-interactions.test.js +309 -0
  268. package/test/qml-language.test.js +167 -0
  269. package/test/qml-launcher.test.js +54 -0
  270. package/test/qml-layout-text.test.js +205 -0
  271. package/test/qml-locale.test.js +45 -0
  272. package/test/qml-media.test.js +63 -0
  273. package/test/qml-nonvisual-visible.test.js +19 -0
  274. package/test/qml-persisted-delegates.test.js +45 -0
  275. package/test/qml-quickshell-controls.test.js +65 -0
  276. package/test/qml-quickshell-settings.test.js +62 -0
  277. package/test/qml-repeater.test.js +72 -0
  278. package/test/qml-shader-runtime.test.js +6 -0
  279. package/test/qml-shortcuts.test.js +39 -0
  280. package/test/qml-socket.test.js +60 -0
  281. package/test/qml-software-shader.test.js +55 -0
  282. package/test/qml-style-updates.test.js +124 -0
  283. package/test/qml-textures.test.js +66 -0
  284. package/test/qml-views.test.js +332 -0
  285. package/test/qml-window-update.test.js +36 -0
  286. package/test/qml-worker.test.js +69 -0
  287. package/test/qml.test.js +83 -0
  288. package/test/render-interval.test.js +33 -0
  289. package/test/renderer-host.test.js +11 -0
  290. package/test/text-input-pointer.test.js +31 -0
  291. package/test/text-input-scroll.test.js +29 -0
  292. package/test/text-input.test.js +47 -0
  293. package/test/title-no-paint.test.js +6 -0
  294. package/test/wayland-frontend.test.js +83 -0
  295. package/test/wayland-scroll.test.js +40 -0
  296. package/test/wayland-urlbar.test.js +61 -0
@@ -0,0 +1,565 @@
1
+ pragma Singleton
2
+ import QtQuick
3
+ import Quickshell
4
+ import Quickshell.Io
5
+ import qs.Commons
6
+ import "js/Density.js" as Density
7
+ import "js/Format.js" as Format
8
+ import "js/Keymap.js" as Keymap
9
+ import "js/Settings.js" as Settings
10
+ import "js/TextSize.js" as TextSize
11
+ import "js/UiState.js" as UiState
12
+
13
+ // The per-user state that outlives a window, `~/.local/state/flea/ui.json`. Read once here with a
14
+ // blocking FileView so the first paint already has it, and never written from QML: every change
15
+ // goes back out through `flea --ui-state`, the one Rust path that takes the lock, validates each
16
+ // key, merges the caller's and renames a temp into place. main() settles this file before the
17
+ // window, so whenever that settle succeeded on a document it could read, what is read here has
18
+ // already been through that same validation; one it could not read is left alone and lands in the
19
+ // default shape UiState.fromFile answers with. There is no second settings file: the settings panel
20
+ // writes these same keys through the same patch. See AGENTS.md "The state file".
21
+ QtObject {
22
+ id: root
23
+
24
+ // The whole document, so a later section reads its own key without a second file read.
25
+ property var state: ({})
26
+
27
+ // Mirrors "columns" in src/uischema.rs, and is the only default this front end needs before the
28
+ // first frame: it is what a first launch draws, when there is no file to settle and none to read.
29
+ readonly property var defaultColumns: ["name", "size", "date"]
30
+
31
+ // Mirrors "menu"."hidden" in src/uischema.rs, for the same first launch: six ids this release's
32
+ // menu cannot build, plus Copy as and Open in terminal, which the SettingsMenus board ships
33
+ // switched off. Every id here is an action ui/js/Menu.js gives a row, or will give one.
34
+ // Mirrors src/uischema.rs DEFAULTS menu.hidden exactly; the two drifted once and a fresh
35
+ // ui.json then hid a row the shipped schema shows.
36
+ readonly property var defaultMenuHidden: ["delete", "openTerminal", "placeMenu", "runScript",
37
+ "moveto", "copyto", "properties", "permissions", "copyAs",
38
+ "pasteAs", "invertSelection", "extThumbs", "run", "openAsText"]
39
+
40
+ // ui.json names what is SHOWN. ui/Header.qml, ui/Row.qml and ui/ContextMenu.qml all ask the
41
+ // opposite question, so the inversion lives here once rather than at each of them.
42
+ readonly property var columns: Array.isArray(root.state.columns) ? root.state.columns : root.defaultColumns
43
+ readonly property var hiddenCols: {
44
+ var out = []
45
+ var optional = ["mode", "size", "date", "kind"]
46
+ for (var i = 0; i < optional.length; i++) {
47
+ if (root.columns.indexOf(optional[i]) < 0)
48
+ out.push(optional[i])
49
+ }
50
+ return out
51
+ }
52
+
53
+ // The Display section's text size, `display.textSize` in src/uischema.rs: {"mode":"system"}
54
+ // follows Omarchy and is the default, and an override pins one of TextSize.js's seven stops as
55
+ // {"mode":N}. ui/Theme.qml is the only consumer, and the monitor scale beside it in the panel is
56
+ // the compositor's alone.
57
+ readonly property var display: root.state.display || ({})
58
+ readonly property var textSize: TextSize.parse(root.display.textSize)
59
+
60
+ // Omarchy's own size, held here so the Ctrl+Shift chords and the panel's own rows step away
61
+ // from and back to one anchor rather than each resolving their own.
62
+ readonly property int omarchyBase: Style.font.baseSize
63
+
64
+ // The context-menu actions switched off in the settings panel's Menus section, by action id, and
65
+ // the section's only state: ui/js/Menu.js applyHidden is the consumer and ui/ContextMenu.qml the
66
+ // one caller that passes it in, and the panel's master row is derived from this set, not stored.
67
+ readonly property var menu: root.state.menu || ({})
68
+ // "Enable shelf" is one switch, so the Menus row is not a stored menu id at all.
69
+ // It reads the shelf's own boolean, which is what src/shelfplugin.rs installs the bar plugin on.
70
+ readonly property bool shelfEnabled: (root.state.shelf || ({})).enabled === true
71
+ readonly property var menuHidden: (Array.isArray(root.menu.hidden) ? root.menu.hidden
72
+ : root.defaultMenuHidden)
73
+ .filter(function (id) { return id !== "shelf" })
74
+ .concat(root.shelfEnabled ? [] : ["shelf"])
75
+
76
+ // The Menus section's "Show keyboard hints" row, `keyHints` in src/uischema.rs. It draws the key
77
+ // beside every menu row and the tip under an empty directory, and it binds no key of its own:
78
+ // every chord answers whether this is on or off.
79
+ // === true, not !== false: an absent key is off, which is what src/uischema.rs stores and what
80
+ // driveSize and trashCount beside it already read. The other way round drew hints from a state
81
+ // file that never named them.
82
+ readonly property bool keyHints: root.state.keyHints === true
83
+
84
+ // Code icons default on; only a saved boolean false disables them, matching schema healing.
85
+ readonly property bool langMarks: root.state.langMarks !== false
86
+ function toggleLangMarks() { root.changeKey("langMarks", !root.langMarks) }
87
+
88
+ // Absent reads off, the way keyHints beside it does.
89
+ readonly property bool highlightToday: root.state.highlightToday === true
90
+
91
+ // Settings > View > Open folders while dragging: absent reads off, like highlightToday.
92
+ readonly property bool openFoldersWhileDragging: root.state.openFoldersWhileDragging === true
93
+ // Rows compare one number and never build a Date.
94
+ property double todayStart: Format.dayStart(Date.now())
95
+
96
+ // One timer steps to the next local midnight while the switch is on, in minute steps so a suspend still lands the day within a minute of resume; rows never tick.
97
+ property var midnightTimer: Timer {
98
+ repeat: true
99
+ running: root.highlightToday
100
+ interval: Math.min(Format.MINUTE_MS, Format.msUntilMidnight(Date.now()))
101
+ onTriggered: {
102
+ root.todayStart = Format.dayStart(Date.now())
103
+ root.midnightTimer.interval = Math.min(Format.MINUTE_MS, Format.msUntilMidnight(Date.now()))
104
+ root.midnightTimer.running = root.highlightToday
105
+ }
106
+ }
107
+ onHighlightTodayChanged: {
108
+ root.todayStart = Format.dayStart(Date.now())
109
+ root.midnightTimer.interval = Math.min(Format.MINUTE_MS, Format.msUntilMidnight(Date.now()))
110
+ root.midnightTimer.running = root.highlightToday
111
+ }
112
+
113
+ // The listing's view, "view" in src/uischema.rs, which ui/Pane.qml draws for the first frame so
114
+ // the view the window was left on is the view the next launch opens on. A word this build cannot
115
+ // draw reads as the list, which covers both a hand edit made while the window is up and the
116
+ // stored "dual", a window shape and not a view a pane can be in. PR 97, DouglasdeMoura.
117
+ readonly property string view: Settings.contains(["list", "columns", "grid"], root.state.view)
118
+ ? root.state.view : "list"
119
+ // Per-folder views stay separate from the fallback above and off until opted in, so an existing ui.json keeps its global view.
120
+ readonly property bool rememberViewPerFolder: root.state.rememberViewPerFolder === true
121
+ readonly property var folderViews: root.state.folderViews || ({})
122
+ // The chooser's own last-used view (`pickerView` in src/uischema.rs); an undrawable word reads as the list.
123
+ readonly property string pickerView: Settings.contains(["list", "grid"], root.state.pickerView)
124
+ ? root.state.pickerView : "list"
125
+
126
+ // The Keys section's four-value chooser over the one generated key table, falling back to its
127
+ // first value, Default, which is what SettingsKeys.html says a missing or unknown name means.
128
+ readonly property string keysPreset: Settings.contains(Settings.PRESETS, root.state.keys)
129
+ ? root.state.keys : Settings.PRESETS[0]
130
+
131
+ // The generated table holds the live preset, so this is the whole wire between the stored value
132
+ // and every lookup: it fires on load and on a settings change alike, and rebinds in this process.
133
+ onKeysPresetChanged: Keymap.setPreset(root.keysPreset)
134
+
135
+ // A whole top-level ui.json key this window changed: the same value goes into the document and
136
+ // into the patch, because nothing else lives under it.
137
+ function changeKey(key, value) {
138
+ root.owe(key, UiState.withKey(root.state, key, value), UiState.withKey(root.unsaved, key, value))
139
+ }
140
+
141
+ // One leaf inside a group. The leaf merges into whatever else the group holds so the document
142
+ // keeps a sub-key a newer Flea left there, and the patch carries the leaf ALONE: a sub-key this
143
+ // Flea has no rule for is one src/uistate.rs refuses, and it refuses the whole patch with it.
144
+ function changeLeaf(key, leaf) {
145
+ root.owe(key, UiState.withGroup(root.state, key, leaf), UiState.withGroup(root.unsaved, key, leaf))
146
+ }
147
+
148
+ // Changed entries of a map key: the document draws the whole map, the patch owes those entries alone.
149
+ function changeMapEntries(key, entries, full) {
150
+ root.owe(key, UiState.withKey(root.state, key, full), UiState.withGroup(root.unsaved, key, entries))
151
+ }
152
+
153
+ // Both writers' last move: hold the new document, and when the named key is not the value it
154
+ // already held, owe the state file what changed and ask for a write. A setter that lands the
155
+ // value already on screen owes nothing, so a chord clamped at the end of its range writes nothing.
156
+ function owe(key, next, owed) {
157
+ var before = JSON.stringify(root.state[key])
158
+ if (JSON.stringify(next[key]) === before)
159
+ return
160
+ root.state = next
161
+ root.unsaved = owed
162
+ root.saveStatus = "Saving…"
163
+ root.save()
164
+ }
165
+
166
+ // RailAdditions rule 4: ctrl-b remembers the rail the way the view is remembered, so this is a
167
+ // state and not a setting; the width rule in ui/Pane.qml hides it without writing anything.
168
+ readonly property bool railHidden: (root.state.places || ({})).rail === "hidden"
169
+ // Directive 74: hiding on a narrow window is its own switch and it ships off, so the rail stays
170
+ // where it is at every width until somebody asks for the other behaviour.
171
+ readonly property bool railAutoHide: (root.state.places || ({})).autoHide === true
172
+ function toggleRail() { root.changeLeaf("places", { rail: root.railHidden ? "shown" : "hidden" }) }
173
+
174
+ readonly property var preview: root.state.preview || ({})
175
+ readonly property bool previewColumn: root.preview.column !== false
176
+ readonly property bool previewAutomatic: root.preview.loadOn !== "manual"
177
+ // `previewFacts` in src/uischema.rs: the tall kinds' one compact line, or the full table `i` opens.
178
+ readonly property string previewFacts: root.state.previewFacts === "full" ? "full" : "compact"
179
+ readonly property string thumbnailMode: root.preview.thumbnails || "media"
180
+ readonly property string thumbnailSize: root.preview.thumbSize || "medium"
181
+ // Huge 192 and Largest 256 decode nothing new: the shared cache already holds 256.
182
+ readonly property int thumbnailPixels: Density.thumbPixelsFor(root.thumbnailSize)
183
+ readonly property bool ctrlZoom: root.preview.ctrlZoom !== false
184
+ // `preview.volume` in src/uischema.rs, 0 to 100: the level ui/MediaSound.qml plays every player at, 100 being full scale.
185
+ readonly property int mediaVolume: root.preview.volume !== undefined ? root.preview.volume : 100
186
+ function setMediaVolume(level) { root.changeLeaf("preview", { volume: level }) }
187
+ // `preview.width` in src/uischema.rs: the Columns view's last column as a whole percent of the area, 0 for the default share.
188
+ readonly property int previewWidth: root.preview.width || 0
189
+ function setPreviewWidth(percent) { root.changeLeaf("preview", { width: percent }) }
190
+ // Quick Look's w: long lines wrap in a code or text file; off until pressed, and remembered.
191
+ readonly property bool previewWrap: root.preview.wrap === true
192
+ readonly property string density: root.state.density || "compact"
193
+ readonly property string addressBar: root.state.addressBar || "breadcrumb"
194
+ readonly property bool hyprlandIcons: root.display.hyprlandIcons === true
195
+ property string saveStatus: "Saved"
196
+
197
+ // Settings > View > Opening. ui/js/Startup.js is what turns these into a path; nothing else reads
198
+ // them, so the panel and the two openers can never disagree about where a window or a tab begins.
199
+ readonly property string startFolder: root.state.startFolder || ""
200
+
201
+ // Settings > Places > Trash. The sweep reads both; ui/TrashHost.qml is its only driver.
202
+ readonly property bool trashAutoEmpty: root.state.trashAutoEmpty === true
203
+ readonly property int trashSweptOn: root.state.trashSweptOn || 0
204
+
205
+ // Settings > About's "Check automatically", `updates.autoCheck` in src/uischema.rs, on until switched off.
206
+ readonly property bool updateAutoCheck: (root.state.updates || ({})).autoCheck !== false
207
+
208
+ // Only a successful sweep records its day, so failures retry on the next launch.
209
+ function recordTrashSweep(day) {
210
+ root.changeKey("trashSweptOn", day)
211
+ }
212
+
213
+ // Choosing the panel's current folder also selects the folder start mode.
214
+ function setStartFolder(path) {
215
+ root.changeKey("startIn", "folder")
216
+ root.changeKey("startFolder", String(path || ""))
217
+ }
218
+
219
+ // Navigation owes only changed keys, then writes the last folder and ordered tab strip together.
220
+ function rememberNavigation(path, tabs) {
221
+ var next = root.state
222
+ var owed = root.unsaved
223
+ var values = { lastPath: String(path || ""), lastTabs: tabs }
224
+ for (var key in values) {
225
+ if (JSON.stringify(next[key]) === JSON.stringify(values[key])) continue
226
+ next = UiState.withKey(next, key, values[key])
227
+ owed = UiState.withKey(owed, key, values[key])
228
+ }
229
+ if (next === root.state) return
230
+ root.state = next
231
+ root.unsaved = owed
232
+ root.saveStatus = "Saving…"
233
+ root.save()
234
+ }
235
+
236
+ // Setting ids name either one top-level key or one leaf of an existing group.
237
+ function changeSetting(id, value) {
238
+ // The rail is remembered as a word, so its own Places row writes that word and not a boolean.
239
+ if (id === "places.rail") { root.changeLeaf("places", { rail: value ? "shown" : "hidden" }); return }
240
+ var parts = id.split(".")
241
+ if (parts.length === 1) {
242
+ root.changeKey(id, value)
243
+ } else {
244
+ var leaf = {}
245
+ leaf[parts[1]] = value
246
+ root.changeLeaf(parts[0], leaf)
247
+ }
248
+ }
249
+
250
+ // The Display controls and text-size chords share these writers.
251
+ function setTextSize(next) {
252
+ root.changeLeaf("display", { textSize: TextSize.parse(next) })
253
+ }
254
+
255
+ function followTextSize() {
256
+ root.setTextSize(TextSize.follow())
257
+ }
258
+
259
+ function stepTextSize(direction) {
260
+ root.setTextSize(TextSize.stepped(root.textSize, root.omarchyBase, direction))
261
+ }
262
+
263
+ function toggleTextFollow() {
264
+ root.setTextSize(TextSize.following(root.textSize)
265
+ ? TextSize.pin(root.textSize, root.omarchyBase) : TextSize.follow())
266
+ }
267
+
268
+ // The Menus section's own two writers. Both write the hidden set alone, because a group's master is Settings.masterState of that set rather than a value of its own.
269
+ function setMenuHidden(hidden) {
270
+ root.changeLeaf("menu", { hidden: hidden.filter(function (id) { return id !== "shelf" }) })
271
+ }
272
+
273
+ function toggleMenuAction(id) {
274
+ if (id === "shelf") { root.changeLeaf("shelf", { enabled: !root.shelfEnabled }); return }
275
+ root.setMenuHidden(Settings.toggleId(root.menuHidden, id))
276
+ }
277
+
278
+ // A master that turns its whole group on turns the shelf on with it, and the same the other way.
279
+ function toggleMenuGroup(ids) {
280
+ if (ids.indexOf("shelf") >= 0)
281
+ root.changeLeaf("shelf", { enabled: Settings.masterState(root.menuHidden, ids) !== "all" })
282
+ root.setMenuHidden(Settings.toggleMaster(root.menuHidden, ids))
283
+ }
284
+
285
+ function toggleKeyHints() {
286
+ root.changeKey("keyHints", !root.keyHints)
287
+ }
288
+
289
+ function setKeysPreset(name) {
290
+ root.changeKey("keys", name)
291
+ }
292
+
293
+ // Flipped by ui/Pane.qml's onChosen, when a header-menu row answers "col:<key>".
294
+ function toggleColumn(key) {
295
+ var shown = root.columns.slice()
296
+ var at = shown.indexOf(key)
297
+ if (at >= 0)
298
+ shown.splice(at, 1)
299
+ else
300
+ shown.push(key)
301
+ root.changeKey("columns", shown)
302
+ }
303
+
304
+ // A patch flea refused, or a state file it could not write. The pane turns it into the status
305
+ // bar's one sentence: the change is on screen and the file does not have it.
306
+ signal saveFailed()
307
+ signal favouritesReadFailed(string message)
308
+ property string favouritesReadError: ""
309
+
310
+ // The settings on screen are not the ones on disk: main() left a ui.json it cannot read as a
311
+ // JSON object exactly as the operator wrote it, or the read below failed outright. Either way
312
+ // what is drawn is the shipped defaults, and ui/PaneWire.qml is where that is said once. Only
313
+ // ever set true, because the read that would clear it is the one that could not be taken.
314
+ property bool unreadable: false
315
+ property bool initialReadComplete: false
316
+
317
+ // What this window has changed and no write has landed for yet, in the shape of a ui.json patch.
318
+ // A write that lands takes its own settings out of it leaf by leaf, so a refused one keeps its
319
+ // settings for the next patch to carry and a stored one is never carried again.
320
+ property var unsaved: ({})
321
+
322
+ // ui/js/UiState.js's book: the newest patch a writer landed, what the running writer carries,
323
+ // what waits behind it. A save that would change nothing writes nothing.
324
+ property var writeBook: UiState.book()
325
+
326
+ // Only what this window has changed, because src/uistate.rs merges a patch key by key: a key
327
+ // left out is one the file keeps, so saving a text size here cannot write this window's own read
328
+ // of `keys` over a change another window or the CLI made after that read. It is every change no
329
+ // writer has stored yet rather than the newest one alone, so a refused patch's settings ride out
330
+ // again on the next one; a stored patch's are gone from it before the next one is built.
331
+ function patch() {
332
+ return JSON.stringify(root.unsaved)
333
+ }
334
+
335
+ function save() {
336
+ root.writeRevision += 1
337
+ var next = UiState.asked(root.writeBook, root.patch())
338
+ root.writeBook = next
339
+ if (next.start.length > 0)
340
+ root.run(next.start)
341
+ }
342
+
343
+ function run(patch) {
344
+ patcher.command = [Quickshell.env("FLEA_BIN") || "flea", "--ui-state", patch]
345
+ patcher.running = true
346
+ }
347
+
348
+ // The read is taken here and not in the FileView's onLoaded, which was measured on the box
349
+ // arriving after the first property read; blockLoading is what makes text() answer inside this
350
+ // call, so the stored columns are in the first frame instead of replacing it.
351
+ Component.onCompleted: { root.load(stateFile.text()); root.initialReadComplete = true }
352
+
353
+ function load(text) {
354
+ var read = UiState.fromFile(text)
355
+ root.state = read.state
356
+ if (read.unreadable)
357
+ root.unreadable = true
358
+ }
359
+ function syncFavourites(text) {
360
+ var read = UiState.refreshedFavourites(root.state, text)
361
+ if (read.error && read.error !== root.favouritesReadError) root.favouritesReadFailed(read.error)
362
+ root.favouritesReadError = read.error
363
+ if (read.state !== root.state) root.state = read.state
364
+ return read.error.length === 0
365
+ }
366
+
367
+ // A change another window saved: the settled document with no argument, merged and validated, writes nothing.
368
+ property string settleMode: ""
369
+ // Local saves and acknowledgements invalidate older snapshots, even after their owed keys clear.
370
+ property double writeRevision: 0
371
+ property double settleRevision: 0
372
+ property bool settleDirty: false
373
+ property string pruneInflight: ""
374
+ property string pruneQueued: ""
375
+ property var settleAnswer: ({})
376
+ function applyShared(text) {
377
+ if (!UiState.parsesAsObject(text))
378
+ return
379
+ if (UiState.settleBusy(settler.running, root.settleMode, root.settleAnswer)) {
380
+ root.settleDirty = true
381
+ return
382
+ }
383
+ root.settleStart("apply")
384
+ }
385
+ // One starter for both settle kinds, so a prune never inherits a stale exit or text.
386
+ function settleStart(mode) {
387
+ // A half-landed settle is still pending, so starting over it would lose the half already here.
388
+ if (UiState.settleBusy(settler.running, root.settleMode, root.settleAnswer))
389
+ return
390
+ root.settleMode = mode
391
+ root.settleRevision = root.writeRevision
392
+ root.settleAnswer = {}
393
+ settler.command = [Quickshell.env("FLEA_BIN") || "flea", "--ui-state"]
394
+ settler.running = true
395
+ }
396
+ // A settler's exit and its collected text land in either order, so only a whole answer reaches settleDone.
397
+ function settleLanded(half) {
398
+ if (UiState.whole(root.settleAnswer))
399
+ return
400
+ root.settleAnswer = UiState.landed(root.settleAnswer, half)
401
+ if (!UiState.whole(root.settleAnswer))
402
+ return
403
+ root.settleDone(root.settleAnswer.code, root.settleAnswer.text)
404
+ }
405
+ // A settle's end spends a queued prune before a dirty re-read; the dirty flag survives the prune so the re-read still runs after it.
406
+ function settleNext() {
407
+ // Keep the prune queued until the process is idle and both reply halves have landed.
408
+ if (UiState.settleBusy(settler.running, root.settleMode, root.settleAnswer))
409
+ return
410
+ var next = UiState.settleNext(root.settleDirty, root.pruneQueued)
411
+ if (next === "prune") {
412
+ var queued = root.pruneQueued
413
+ root.pruneQueued = ""
414
+ root.pruneInflight = queued
415
+ root.settleStart("prune")
416
+ } else if (next === "apply") {
417
+ root.settleDirty = false
418
+ root.applyShared(stateFile.text())
419
+ }
420
+ }
421
+ function settleDone(exitCode, out) {
422
+ var mode = root.settleMode
423
+ root.settleMode = ""
424
+ if (mode === "prune") {
425
+ if (exitCode !== 0) { root.pruneFailed(); return }
426
+ var settled = null
427
+ try {
428
+ settled = JSON.parse(out)
429
+ } catch (e) {
430
+ root.pruneFailed()
431
+ return
432
+ }
433
+ if (!UiState.parsesAsObject(stateFile.text())) { root.pruneFailed(); return }
434
+ root.unsaved = UiState.dropInvalid(root.unsaved, root.pruneInflight)
435
+ root.state = UiState.revertedState(root.state, root.pruneInflight, settled)
436
+ root.finishPrune(false)
437
+ return
438
+ }
439
+ if (root.settleRevision !== root.writeRevision) {
440
+ root.settleDirty = true
441
+ root.settleNext()
442
+ return
443
+ }
444
+ if (exitCode !== 0) { root.settleNext(); return }
445
+ if (!UiState.parsesAsObject(out)) { root.settleNext(); return }
446
+ if (!UiState.parsesAsObject(stateFile.text())) { root.settleNext(); return }
447
+ var merged = UiState.applyExternal(root.state, root.unsaved, out)
448
+ if (merged.changed)
449
+ root.state = merged.state
450
+ root.syncFavourites(stateFile.text())
451
+ root.settleNext()
452
+ }
453
+ // One tail for both prune endings: end the settle, then save what is still owed.
454
+ function finishPrune(failed) {
455
+ root.pruneInflight = ""
456
+ root.writeBook = { saved: root.writeBook.saved, inflight: "", pending: "", start: "" }
457
+ if (failed)
458
+ root.saveFailed()
459
+ root.settleNext()
460
+ if (JSON.stringify(root.unsaved) !== "{}") {
461
+ root.saveStatus = "Saving…"
462
+ root.save()
463
+ }
464
+ }
465
+ // A prune settle that never answered still drops what the schema refused, so no save strands behind a phantom writer.
466
+ function pruneFailed() {
467
+ root.unsaved = UiState.dropInvalid(root.unsaved, root.pruneInflight)
468
+ root.finishPrune(true)
469
+ }
470
+ function refreshFavourites() {
471
+ stateFile.reload()
472
+ // blockLoading covers only the first read; a reload otherwise returns the previous document.
473
+ stateFile.waitForJob()
474
+ if (!stateFile.loaded || root.favouritesReadError.length > 0) return false
475
+ var text = stateFile.text()
476
+ return root.syncFavourites(text)
477
+ }
478
+
479
+ // blockLoading, because the first list draws from this: an async read would paint one column
480
+ // set and correct it. printErrors off, because a missing file is what a first launch looks like.
481
+ property var store: FileView {
482
+ id: stateFile
483
+ path: (Quickshell.env("XDG_STATE_HOME") && Quickshell.env("XDG_STATE_HOME").length > 0
484
+ ? Quickshell.env("XDG_STATE_HOME") : Quickshell.env("HOME") + "/.local/state") + "/flea/ui.json"
485
+ blockLoading: true
486
+ watchChanges: true
487
+ onFileChanged: reload()
488
+ onLoaded: {
489
+ root.favouritesReadError = ""
490
+ if (root.initialReadComplete) {
491
+ root.applyShared(text())
492
+ }
493
+ root.syncFavourites(text())
494
+ }
495
+ printErrors: false
496
+ // A file that is not there is a first launch and says nothing; anything else is a file this
497
+ // window could not read and is about to draw the defaults over, which is the unchecked-read
498
+ // defect ui/NetworkDialog.qml already carried once and must not be repeated here.
499
+ onLoadFailed: function (error) {
500
+ if (error === FileViewError.FileNotFound && !root.initialReadComplete) return
501
+ if (!root.initialReadComplete) root.unreadable = true
502
+ root.favouritesReadError = UiState.FAVOURITES_UNREAD
503
+ root.favouritesReadFailed(root.favouritesReadError)
504
+ }
505
+ }
506
+
507
+ // The writer answered, with its own status or with 2 for one that never started: the same refusal
508
+ // to the pane and the same retry to the book, because neither reached the file. A refusal never
509
+ // leaves a key owed forever: the owed keys the settled document disagrees with are dropped once,
510
+ // said once in the status line, so a later valid write applies again in the same process.
511
+ function wrote(exitCode) {
512
+ // Taken out before the patch below is built, so a writer queued behind this one launches with
513
+ // what is still owed and not with the settings this one has just stored.
514
+ root.saveStatus = exitCode === 0 ? "Saved"
515
+ : "Could not save settings · changes apply to this session only"
516
+ if (exitCode === 0) {
517
+ root.writeRevision += 1
518
+ root.unsaved = UiState.acknowledged(root.unsaved, root.writeBook.inflight)
519
+ }
520
+ var failedPatch = root.writeBook.inflight
521
+ var next = UiState.exited(root.writeBook, exitCode, root.patch())
522
+ root.writeBook = next
523
+ if (next.failed)
524
+ root.saveFailed()
525
+ if (exitCode !== 0 && UiState.isPatchInvalid(failedPatch)) {
526
+ // A refused patch prunes behind a pending settle instead of hijacking that settle's mode.
527
+ var ask = UiState.pruneAsk(UiState.settleBusy(settler.running, root.settleMode, root.settleAnswer), failedPatch)
528
+ if (ask.queue.length > 0) {
529
+ root.pruneQueued = ask.queue
530
+ return
531
+ }
532
+ root.pruneInflight = ask.start
533
+ root.settleStart("prune")
534
+ return
535
+ }
536
+ if (next.start.length > 0)
537
+ root.run(next.start)
538
+ }
539
+
540
+ property var writer: Process {
541
+ id: patcher
542
+ onExited: function (exitCode, exitStatus) { root.wrote(exitCode) }
543
+ // Measured on Quickshell 0.3.1: a Process that cannot start its program emits no exited at
544
+ // all, only running going false, and a real exit clears the book before its own false
545
+ // arrives, so this fires for the writer that never ran and never for one that did.
546
+ onRunningChanged: {
547
+ if (!patcher.running && root.writeBook.inflight.length > 0)
548
+ root.wrote(2)
549
+ }
550
+ }
551
+
552
+ property var settler: Process {
553
+ id: settler
554
+ stdout: StdioCollector { id: settleOut; waitForEnd: true; onStreamFinished: root.settleLanded({ text: settleOut.text }) }
555
+ onExited: function (exitCode, exitStatus) { root.settleLanded({ code: exitCode }) }
556
+ // A settler that never started raises no exited, only running going false; the joined halves tell it from a real exit the way the writer's own rule does.
557
+ onRunningChanged: {
558
+ if (root.settleMode.length > 0 && UiState.neverRan(root.settleAnswer, settler.running))
559
+ root.settleLanded(UiState.NEVER_RAN)
560
+ // onExited may land both halves while running is true; the idle transition resumes its queued work.
561
+ if (!settler.running && root.settleMode.length === 0)
562
+ root.settleNext()
563
+ }
564
+ }
565
+ }