nuspace 0.7.0__tar.gz → 0.7.2__tar.gz

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 (186) hide show
  1. {nuspace-0.7.0 → nuspace-0.7.2}/CHANGELOG.md +45 -2
  2. {nuspace-0.7.0 → nuspace-0.7.2}/PKG-INFO +6 -6
  3. {nuspace-0.7.0 → nuspace-0.7.2}/pyproject.toml +6 -6
  4. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/__init__.py +1 -1
  5. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/host/devices.py +3 -3
  6. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/host/layout.py +1 -1
  7. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/host/resources.py +0 -3
  8. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/host/stores.py +1 -1
  9. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/ops/plane.py +5 -2
  10. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/shapes/__init__.py +1 -1
  11. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/shapes/content.py +2 -2
  12. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/shapes/plane.py +13 -3
  13. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/shapes/store.py +1 -1
  14. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/specs/ops.py +4 -4
  15. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/utils/placement.py +8 -6
  16. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/__init__.py +23 -14
  17. nuspace-0.7.2/src/nuspace/web/desks/__init__.py +58 -0
  18. nuspace-0.7.2/src/nuspace/web/desks/feed.py +152 -0
  19. nuspace-0.7.2/src/nuspace/web/desks/interactions.py +90 -0
  20. nuspace-0.7.2/src/nuspace/web/desks/model.py +542 -0
  21. nuspace-0.7.2/src/nuspace/web/desks/ops.py +418 -0
  22. nuspace-0.7.2/src/nuspace/web/desks/pruner.py +223 -0
  23. nuspace-0.7.2/src/nuspace/web/desks/ref.py +43 -0
  24. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/device.py +48 -22
  25. nuspace-0.7.2/src/nuspace/web/home/__init__.py +56 -0
  26. nuspace-0.7.2/src/nuspace/web/home/common.py +103 -0
  27. nuspace-0.7.2/src/nuspace/web/home/history.py +141 -0
  28. nuspace-0.7.2/src/nuspace/web/home/indexer.py +341 -0
  29. nuspace-0.7.2/src/nuspace/web/home/now.py +360 -0
  30. nuspace-0.7.2/src/nuspace/web/home/runs.py +306 -0
  31. nuspace-0.7.2/src/nuspace/web/home/space.py +210 -0
  32. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/interactions.py +18 -7
  33. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/nav.py +11 -3
  34. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/ops.py +56 -107
  35. nuspace-0.7.2/src/nuspace/web/planes/__init__.py +214 -0
  36. {nuspace-0.7.0/src/nuspace/devices/web → nuspace-0.7.2/src/nuspace/web/planes}/search.py +190 -141
  37. nuspace-0.7.2/src/nuspace/web/planes/settings.py +211 -0
  38. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/ref.py +3 -3
  39. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/shapes.py +38 -8
  40. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/shell.py +8 -5
  41. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/sidebar/__init__.py +3 -10
  42. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/sidebar/feed.py +16 -48
  43. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/sidebar/interactions.py +4 -23
  44. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/sidebar/ref.py +5 -17
  45. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/stores.py +4 -3
  46. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/utils.py +7 -1
  47. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/viewer/__init__.py +3 -3
  48. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/viewer/feed.py +38 -16
  49. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/viewer/interactions.py +27 -3
  50. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/viewer/ref.py +2 -2
  51. {nuspace-0.7.0 → nuspace-0.7.2}/tests/conftest.py +1 -1
  52. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_cli.py +2 -3
  53. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_host.py +7 -6
  54. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_nav.py +19 -3
  55. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_session_pipe.py +1 -1
  56. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_shapes.py +11 -8
  57. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_specs.py +1 -1
  58. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_system_utils.py +1 -1
  59. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_telemetry.py +1 -1
  60. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_web.py +127 -45
  61. nuspace-0.7.2/tests/nuspace/test_web_desks.py +711 -0
  62. nuspace-0.7.2/tests/nuspace/test_web_home.py +763 -0
  63. nuspace-0.7.2/tests/nuspace/test_web_indexer.py +391 -0
  64. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_web_ops.py +33 -58
  65. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_web_planes.py +85 -144
  66. nuspace-0.7.2/tests/nuspace/test_web_pruner.py +406 -0
  67. nuspace-0.7.0/src/nuspace/devices/__init__.py +0 -10
  68. nuspace-0.7.0/src/nuspace/devices/web/home.py +0 -282
  69. nuspace-0.7.0/src/nuspace/devices/web/planes.py +0 -106
  70. nuspace-0.7.0/src/nuspace/devices/web/settings.py +0 -30
  71. {nuspace-0.7.0 → nuspace-0.7.2}/.gitignore +0 -0
  72. {nuspace-0.7.0 → nuspace-0.7.2}/.pre-commit-config.yaml +0 -0
  73. {nuspace-0.7.0 → nuspace-0.7.2}/LICENSE.md +0 -0
  74. {nuspace-0.7.0 → nuspace-0.7.2}/Makefile +0 -0
  75. {nuspace-0.7.0 → nuspace-0.7.2}/README.md +0 -0
  76. {nuspace-0.7.0 → nuspace-0.7.2}/examples/space.py +0 -0
  77. {nuspace-0.7.0 → nuspace-0.7.2}/pkgs/ts/nuspace-ui/python/nuspace_ui/build +0 -0
  78. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_branding/__init__.py +0 -0
  79. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_branding/header.py +0 -0
  80. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_branding/logo.py +0 -0
  81. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_branding/palette.py +0 -0
  82. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_cli/__init__.py +0 -0
  83. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_cli/__main__.py +0 -0
  84. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_cli/_installed.py +0 -0
  85. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_cli/_meta.py +0 -0
  86. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_cli/commands/__init__.py +0 -0
  87. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_cli/commands/init.py +0 -0
  88. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_cli/commands/run.py +0 -0
  89. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_cli/commands/telemetry.py +0 -0
  90. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_cli/commands/verse.py +0 -0
  91. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_cli/main.py +0 -0
  92. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_telemetry/__init__.py +0 -0
  93. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_telemetry/config.py +0 -0
  94. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_telemetry/send.py +0 -0
  95. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/_telemetry/usage.py +0 -0
  96. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/errors.py +0 -0
  97. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/host/__init__.py +0 -0
  98. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/host/interactions.py +0 -0
  99. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/host/lock.py +0 -0
  100. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/host/presets.py +0 -0
  101. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/host/space.py +0 -0
  102. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/__init__.py +0 -0
  103. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/backends/__init__.py +0 -0
  104. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/backends/async_.py +0 -0
  105. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/backends/base.py +0 -0
  106. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/backends/interactions.py +0 -0
  107. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/backends/mp.py +0 -0
  108. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/backends/pool.py +0 -0
  109. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/body.py +0 -0
  110. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/close.py +0 -0
  111. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/envs.py +0 -0
  112. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/interactions.py +0 -0
  113. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/kernel.py +0 -0
  114. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/out.py +0 -0
  115. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/reap.py +0 -0
  116. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/kernel/records.py +0 -0
  117. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/ops/__init__.py +0 -0
  118. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/ops/brackets.py +0 -0
  119. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/ops/cell.py +0 -0
  120. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/ops/cell_run.py +0 -0
  121. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/ops/interactions.py +0 -0
  122. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/ops/plane_run.py +0 -0
  123. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/ops/state.py +0 -0
  124. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/ops/worker.py +0 -0
  125. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/places.py +0 -0
  126. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/shapes/cell.py +0 -0
  127. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/shapes/cell_run.py +0 -0
  128. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/shapes/exec.py +0 -0
  129. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/shapes/plane_run.py +0 -0
  130. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/shapes/space.py +0 -0
  131. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/shapes/state.py +0 -0
  132. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/shapes/worker.py +0 -0
  133. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/specs/__init__.py +0 -0
  134. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/specs/registry.py +0 -0
  135. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/specs/specs.py +0 -0
  136. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/__init__.py +0 -0
  137. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/init/__init__.py +0 -0
  138. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/init/ops.py +0 -0
  139. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/init/plane.py +0 -0
  140. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/init/shapes.py +0 -0
  141. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/planes.py +0 -0
  142. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/reactions/__init__.py +0 -0
  143. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/reactions/ops.py +0 -0
  144. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/reactions/plane.py +0 -0
  145. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/reactions/shapes.py +0 -0
  146. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/reload/__init__.py +0 -0
  147. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/reload/plane.py +0 -0
  148. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/supervisor/__init__.py +0 -0
  149. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/supervisor/ops.py +0 -0
  150. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/supervisor/plane.py +0 -0
  151. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/system/supervisor/shapes.py +0 -0
  152. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/utils/__init__.py +0 -0
  153. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/utils/checks.py +0 -0
  154. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/utils/interactions.py +0 -0
  155. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/utils/policies.py +0 -0
  156. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/utils/reads.py +0 -0
  157. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/utils/tree.py +0 -0
  158. {nuspace-0.7.0 → nuspace-0.7.2}/src/nuspace/utils/typed.py +0 -0
  159. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/env.py +0 -0
  160. {nuspace-0.7.0/src/nuspace/devices → nuspace-0.7.2/src/nuspace}/web/session.py +0 -0
  161. {nuspace-0.7.0 → nuspace-0.7.2}/tests/_ext/nuspace_testing/__init__.py +0 -0
  162. {nuspace-0.7.0 → nuspace-0.7.2}/tests/_ext/pyproject.toml +0 -0
  163. {nuspace-0.7.0 → nuspace-0.7.2}/tests/_lib/_support/__init__.py +0 -0
  164. {nuspace-0.7.0 → nuspace-0.7.2}/tests/_lib/_support/actions.py +0 -0
  165. {nuspace-0.7.0 → nuspace-0.7.2}/tests/_lib/_support/contents.py +0 -0
  166. {nuspace-0.7.0 → nuspace-0.7.2}/tests/_lib/_support/crash_host.py +0 -0
  167. {nuspace-0.7.0 → nuspace-0.7.2}/tests/_lib/_support/kernel.py +0 -0
  168. {nuspace-0.7.0 → nuspace-0.7.2}/tests/_lib/_support/kernel_envs.py +0 -0
  169. {nuspace-0.7.0 → nuspace-0.7.2}/tests/_lib/_support/made.py +0 -0
  170. {nuspace-0.7.0 → nuspace-0.7.2}/tests/_lib/_support/probe.py +0 -0
  171. {nuspace-0.7.0 → nuspace-0.7.2}/tests/_lib/_support/specs.py +0 -0
  172. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_brackets.py +0 -0
  173. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_content.py +0 -0
  174. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_errors.py +0 -0
  175. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_exports.py +0 -0
  176. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_kernel.py +0 -0
  177. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_ops_cell.py +0 -0
  178. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_ops_cell_run.py +0 -0
  179. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_ops_plane.py +0 -0
  180. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_ops_plane_run.py +0 -0
  181. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_ops_state.py +0 -0
  182. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_ops_worker.py +0 -0
  183. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_packaging.py +0 -0
  184. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_places.py +0 -0
  185. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_system_ops.py +0 -0
  186. {nuspace-0.7.0 → nuspace-0.7.2}/tests/nuspace/test_system_planes.py +0 -0
@@ -2,13 +2,56 @@
2
2
 
3
3
  ## Packages shipped from this repo
4
4
 
5
- - **nuspace** (the runtime, the `nuspace` command) — 0.7.0
6
- - **nuspace-ui** (the compiled web bundle) — 0.7.0
5
+ - **nuspace** (the runtime, the `nuspace` command) — 0.7.2
6
+ - **nuspace-ui** (the compiled web bundle) — 0.7.2
7
7
 
8
8
  Below is the changelog for **nuspace** - the full commit stream. Newest first.
9
9
  The `nuspace-ui` wheel is the same source tree's vite output and is not tracked
10
10
  separately here.
11
11
 
12
+ ## 0.7.2 - 2026-11-13
13
+
14
+ - Release nuspace and nuspace-ui 0.7.2
15
+ - Home, settings and search follow the new layout model; plane links share one helper
16
+ - New planes get a default icon when created without one
17
+ - Home, settings and search planes get clean sectioned layouts with real tables
18
+ - Drops show the region they make, and single planes stack anywhere
19
+ - Sidebar states use the kit hover, press and selected washes in both themes
20
+ - Desks keep each item on its own, programs get item ops, cross-desk drags commit once
21
+ - The web device moves to nuspace.web, the devices package is gone
22
+ - Group the web device by area: desk ops under desks, shipped planes together
23
+
24
+ ## 0.7.1 - 2026-11-12
25
+
26
+ - Release nuspace and nuspace-ui 0.7.1
27
+ - Dragging the sidebar folds it straight to its rail and back, no gaps or doubles
28
+ - The sidebar folds to an icon rail, its bars line up with pane headers
29
+ - Desk folders list items and strips, save, reset, rename, close and reorder
30
+ - The sidebar is built from uniform sections and rows, nav follows the roles
31
+ - Panes drag by their header and show one not found pane for anything missing
32
+ - The canvas shows one plane or strip, with px column widths and drops
33
+ - Every desk item has its own url, links inside it remember where you went
34
+ - Desks hold plane items and strips that remember their saved and last plane
35
+ - Pins are gone, a Favorites desk takes their place
36
+ - Removing a plane takes it off every desk, even with no browser open
37
+ - Desks live in the web store and reach every tab live, with a seeded Favorites
38
+ - System planes home, search and settings are found by role, not fixed ids
39
+ - Opening a plane with no view or no plane says so instead of hanging
40
+ - A pane's bar shows the plane's breadcrumbs, with parents one click away
41
+ - Plane spacing reduced to one top pad, title gap and end run-off
42
+ - Search and spec setup no longer carry state over between runs
43
+ - Home shows space stats, a yearly heatmap, history, live runs and recent runs
44
+ - A background indexer counts planes and cells added per day so Home opens fast
45
+ - The sidebar lists installed specs under Specs instead of Extensions
46
+ - See, copy and edit a plane's ID and key, with its key shown under the title
47
+ - The cell menu always offers copying the key, with clearer ID and key icons
48
+ - See, copy, set and remove a cell's id and key from its controls
49
+ - An empty plane list shows just New plane, its shortcut always visible
50
+ - The boot screen says Booting your space instead of waking up
51
+ - Jump to Home from anywhere with Cmd+Shift+H, shown in the sidebar
52
+ - Reopened planes no longer flash their old cell output before rendering fresh
53
+ - New and empty planes put the cursor in the cell input right away
54
+
12
55
  ## 0.7.0 - 2026-11-10
13
56
 
14
57
  - Release nuspace and nuspace-ui 0.7.0 on nu 0.7.2, virtuals 0.2.2 and ui-kit 0.4.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: nuspace
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: Computational space for your data and tools.
5
5
  Project-URL: Repository, https://github.com/nustackdev/nuspace
6
6
  Author-email: Gor Arakelyan <gorarkln@gmail.com>
@@ -17,16 +17,16 @@ Classifier: Programming Language :: Python :: 3.13
17
17
  Requires-Python: >=3.10
18
18
  Requires-Dist: certifi>=2023.7.22
19
19
  Requires-Dist: click>=8.0
20
- Requires-Dist: nucore>=0.7.2
21
- Requires-Dist: nuspace-ui>=0.7.0
22
- Requires-Dist: nustd[kv,mp-pool,proxy,ui,valkey]>=0.7.2
23
- Requires-Dist: nuverse>=0.3.7
20
+ Requires-Dist: nucore>=0.7.4
21
+ Requires-Dist: nuspace-ui>=0.7.2
22
+ Requires-Dist: nustd[kv,mp-pool,proxy,ui,valkey]>=0.7.4
23
+ Requires-Dist: nuverse>=0.3.10
24
24
  Requires-Dist: rich-click>=1.6
25
25
  Requires-Dist: rich>=10.7
26
26
  Requires-Dist: typing-extensions>=4.7.0
27
27
  Requires-Dist: virtuals-py[redis]>=0.2.2
28
28
  Provides-Extra: all
29
- Requires-Dist: nustd[all]>=0.7.2; extra == 'all'
29
+ Requires-Dist: nustd[all]>=0.7.4; extra == 'all'
30
30
  Description-Content-Type: text/markdown
31
31
 
32
32
  <div align="center">
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nuspace"
7
- version = "0.7.0"
7
+ version = "0.7.2"
8
8
  description = "Computational space for your data and tools."
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-or-later"
@@ -14,17 +14,17 @@ dependencies = [
14
14
  # The fabrics a space runs on, nothing more: kv for the store, valkey for
15
15
  # its change notifications, ui for the browser, mp_pool for the workers,
16
16
  # proxy (invisibles) for the worker's session.
17
- "nucore>=0.7.2",
18
- "nustd[kv,ui,valkey,mp_pool,proxy]>=0.7.2",
17
+ "nucore>=0.7.4",
18
+ "nustd[kv,ui,valkey,mp_pool,proxy]>=0.7.4",
19
19
  # The store's change notifications run over virtuals' redis backend.
20
20
  "virtuals-py[redis]>=0.2.2",
21
21
  # The compiled web bundle. A separate distribution only because PyPI caps
22
22
  # project size and a SPA rebuilt on every UI tweak would eat the quota --
23
23
  # not optional. `pip install nuspace` gives you the browser.
24
- "nuspace-ui>=0.7.0",
24
+ "nuspace-ui>=0.7.2",
25
25
  # The everyday Planes and Cells. A plugin like any other, found through
26
26
  # its entry point, but every space ships with it.
27
- "nuverse>=0.3.7",
27
+ "nuverse>=0.3.10",
28
28
  "typing-extensions>=4.7.0",
29
29
  "click>=8.0",
30
30
  "rich>=10.7",
@@ -55,7 +55,7 @@ classifiers = [
55
55
  [project.optional-dependencies]
56
56
  # Every nustd fabric and kv backend, for cells that reach past the defaults.
57
57
  all = [
58
- "nustd[all]>=0.7.2",
58
+ "nustd[all]>=0.7.4",
59
59
  ]
60
60
 
61
61
  [project.urls]
@@ -15,7 +15,7 @@ The top level holds the everyday names only:
15
15
  - ``nuspace.specs`` (PlaneSpec, CellSpec, the registry, add_plane,
16
16
  add_cell) and ``nuspace.errors`` (what an op raises).
17
17
 
18
- ``nuspace.kernel``, ``nuspace.system.*``, ``nuspace.devices.web``,
18
+ ``nuspace.kernel``, ``nuspace.system.*``, ``nuspace.web``,
19
19
  ``nuspace.host`` and ``nuspace.utils`` stay namespaced.
20
20
 
21
21
  Workers import this package: nothing here may pull in a server.
@@ -56,11 +56,11 @@ class Device:
56
56
 
57
57
  def _web() -> Device:
58
58
  """The web device. Its stores and planes import no server; :func:`serve` does, when called."""
59
- from nuspace.devices.web.planes import ensure
60
- from nuspace.devices.web.stores import CONN_STORE, WEB_STORE
59
+ from nuspace.web.planes import ensure
60
+ from nuspace.web.stores import CONN_STORE, WEB_STORE
61
61
 
62
62
  def serve(*, registry: Registry, **options: object) -> tuple[nu.Nu, Mapping[str, EnvFactory]]:
63
- from nuspace.devices.web.device import serve_web
63
+ from nuspace.web.device import serve_web
64
64
 
65
65
  return serve_web(registry=registry, **options)
66
66
 
@@ -13,7 +13,7 @@ repository keeps ``.git/``::
13
13
  infra/valkey/ the notification server's dir, per open
14
14
 
15
15
  The store files are the store records' (:mod:`.stores`,
16
- :mod:`nuspace.devices.web.stores`). The Space store's file is the mark of a
16
+ :mod:`nuspace.web.stores`). The Space store's file is the mark of a
17
17
  space: ``~/.nuspace`` holds the user's config whether or not home is a space.
18
18
  """
19
19
 
@@ -126,9 +126,6 @@ class Warmed:
126
126
  Never the web device's server.
127
127
  """
128
128
  import nu.prog # noqa: F401 -- imported for the module cache
129
- import nuspace.devices.web.env
130
- import nuspace.kernel.body
131
- import nuspace.kernel.out # noqa: F401
132
129
  import nustd.ui # noqa: F401
133
130
 
134
131
  async def asetup(self, ctx: Context) -> None:
@@ -10,7 +10,7 @@ so stores never wake each other's subscribers. Each is declared as a
10
10
  store/state.db State nuspace.state what programs keep
11
11
 
12
12
  A device keeps its own stores beside these (the web device:
13
- :mod:`nuspace.devices.web.stores`).
13
+ :mod:`nuspace.web.stores`).
14
14
 
15
15
  A container never written is absent, and reading one (its length, its keys,
16
16
  whether it holds an id) raises. So each store's fixed containers are made
@@ -56,6 +56,9 @@ __all__ = [
56
56
  #: The prefixes an icon is spelled with, a lucide name or an emoji after it.
57
57
  ICON_PREFIXES = ("lucide:", "emoji:")
58
58
 
59
+ #: The icon a plane is made with when none is given.
60
+ DEFAULT_ICON = "lucide:file-text"
61
+
59
62
 
60
63
  def _icon_spelled(icon: nu.StrArg) -> None:
61
64
  """A literal icon is ``""`` or a prefix with something after it. A term is stored as it is.
@@ -138,7 +141,7 @@ def add(
138
141
  name: Display text only.
139
142
  key: An optional handle in key chars (letters, digits, ``_``, ``-``),
140
143
  case-sensitive, ``""`` for none.
141
- icon: ``lucide:<name>``, ``emoji:<char>`` or ``""``.
144
+ icon: ``lucide:<name>`` or ``emoji:<char>``. ``""``: :data:`DEFAULT_ICON`.
142
145
  backend: What its runs execute on. ``""``: it does not run.
143
146
  system: Protected: ``remove`` refuses it. Only a system plane takes
144
147
  a :data:`~nuspace.shapes.plane.RESERVED` id.
@@ -169,7 +172,7 @@ def add(
169
172
  return (
170
173
  row.name.set(name)
171
174
  >> row.key.set(key)
172
- >> row.icon.set(icon)
175
+ >> row.icon.set(icon or DEFAULT_ICON if isinstance(icon, str) else icon)
173
176
  >> row.parent.set(parent)
174
177
  >> row.version.set(0)
175
178
  >> row.cells.set(nu.Dict.create())
@@ -28,7 +28,7 @@ for a cell run's output.
28
28
 
29
29
  Each store is a :class:`Store` record (root, file, channel, init), declared
30
30
  by the host for the core stores. The web device keeps its own stores and
31
- shapes, in :mod:`nuspace.devices.web.shapes`. Core never reads them.
31
+ shapes, in :mod:`nuspace.web.shapes`. Core never reads them.
32
32
  """
33
33
 
34
34
  from .cell import Cell
@@ -13,7 +13,7 @@ Finding reads one kind's subtree, scope by scope, all bare reads:
13
13
  the items ``match`` holds for in one scope, :meth:`Content.find` both
14
14
  together. A search reads per kind, so it never touches another kind's
15
15
  items; the web device's search drives it a place at a time
16
- (:mod:`nuspace.devices.web.search`).
16
+ (:mod:`nuspace.web.planes.search`).
17
17
 
18
18
  Workers import this module: nothing here may pull in a server.
19
19
  """
@@ -219,7 +219,7 @@ class Content(Generic[S]):
219
219
  only the checks asked for.
220
220
  - :meth:`render`, :meth:`render_hit`: how an item draws in full and as a
221
221
  search hit. Bare ui chains, landed under wherever the drawing goes
222
- (:class:`~nuspace.devices.web.interactions.Drawing`).
222
+ (:class:`~nuspace.web.interactions.Drawing`).
223
223
  - :meth:`form`: the search form, generated from ``match``'s keywords.
224
224
  - :meth:`find`: every item ``match`` holds for, from every place.
225
225
 
@@ -16,10 +16,20 @@ ROOT = "root"
16
16
 
17
17
  #: Plane ids only nuspace's own planes take: ``plane.add`` refuses them
18
18
  #: unless ``system=True``. The system planes' (``nuspace.system``), then the
19
- #: web device's (``nuspace.devices.web.planes``): home, search, settings and
20
- #: the searches folder. Root's id and ``""`` are refused for every add.
19
+ #: web device's (``nuspace.web.planes``): the searches folder, the
20
+ #: home indexer and the desk pruner. Home, search and settings have minted
21
+ #: ids, found through the web device's roles. Root's id and ``""`` are
22
+ #: refused for every add.
21
23
  RESERVED = frozenset(
22
- {"init", "supervisor", "reload", "reactions", "home", "search", "settings", "searches"}
24
+ {
25
+ "init",
26
+ "supervisor",
27
+ "reload",
28
+ "reactions",
29
+ "searches",
30
+ "home_indexer",
31
+ "desk_pruner",
32
+ }
23
33
  )
24
34
 
25
35
 
@@ -1,7 +1,7 @@
1
1
  """Store: one store of a space, as a record: its root shape, its file, its channel, its init.
2
2
 
3
3
  The core stores are declared in :mod:`nuspace.host.stores`, a device's
4
- beside its shapes (the web device: :mod:`nuspace.devices.web.stores`). The
4
+ beside its shapes (the web device: :mod:`nuspace.web.stores`). The
5
5
  record sits here, with the roots, so the host and the devices both read it
6
6
  and neither imports the other for it. A store has no ops of its own: its
7
7
  init term is the one thing it runs, and the store's declaration builds it.
@@ -119,11 +119,11 @@ class _Tree:
119
119
 
120
120
 
121
121
  def _web_row(pid: nu.Str, spec: PlaneSpec) -> nu.Nu:
122
- """The plane's web row, from ``spec.web``: ``nuspace.devices.web.add``, the plane shows on the web.
122
+ """The plane's web row, from ``spec.web``: ``nuspace.web.add``, the plane shows on the web.
123
123
 
124
124
  Imported here, not at the top, so core loads without the web device.
125
125
  """
126
- from nuspace.devices.web import ops as web
126
+ from nuspace.web import ops as web
127
127
 
128
128
  flags = spec.web
129
129
  return web.add(
@@ -191,7 +191,7 @@ def add_plane(
191
191
  steps.append(nustd.kv.atomic(nu.Sequential(*tree.state)))
192
192
  if tree.web:
193
193
  steps.append(nustd.kv.atomic(nu.Sequential(*tree.web)))
194
- return nu.Frame(_Minted, nu.Sequential(*steps), ids={}, fabrics={})
194
+ return nu.Frame(_Minted, nu.Sequential(*steps), ids=nu.Dict.of(), fabrics=nu.Dict.of())
195
195
 
196
196
 
197
197
  def add_cell(
@@ -251,4 +251,4 @@ def add_cell(
251
251
  fabrics=fabrics,
252
252
  into=into,
253
253
  )
254
- return nu.Frame(_Minted, nu.Sequential(*first, made), ids={}, fabrics={})
254
+ return nu.Frame(_Minted, nu.Sequential(*first, made), ids=nu.Dict.of(), fabrics=nu.Dict.of())
@@ -1,8 +1,8 @@
1
1
  """Placing an id in an ordered list: before an anchor, after one, or at the end.
2
2
 
3
3
  The one spelling of "placement is ``before=`` / ``after=`` an id, neither
4
- means the end" that ops share. Generic: it works on any ``OrderedDictRef``
5
- of bool. The checks that make an anchor valid (it is there, it is a
4
+ means the end" that ops share. Generic: it works on any ``OrderedDictRef``,
5
+ of bool (membership, the default) or of values. The checks that make an anchor valid (it is there, it is a
6
6
  sibling) are the caller's, made before this runs.
7
7
  """
8
8
 
@@ -37,17 +37,19 @@ def place(
37
37
  *,
38
38
  before: nu.StrArg | None = None,
39
39
  after: nu.StrArg | None = None,
40
+ value: object = True,
40
41
  ) -> nu.Nu:
41
42
  """Put ``id`` into ``listed`` before or after an anchor, or at the end. One view call.
42
43
 
43
- ``id`` must not be listed yet: to move one, take it out first. No bracket.
44
+ ``value`` is what ``id`` holds, ``True`` for a membership list. ``id``
45
+ must not be listed yet: to move one, take it out first. No bracket.
44
46
 
45
47
  Raises:
46
48
  TypeError: Both ``before`` and ``after`` are given, at build.
47
49
  """
48
50
  anchor_of(before, after)
49
51
  if before is not None:
50
- return listed.insert_before(before, id)
52
+ return listed.insert_before(before, id, value)
51
53
  if after is not None:
52
- return listed.insert_after(after, id)
53
- return listed.set_item(id, True)
54
+ return listed.insert_after(after, id, value)
55
+ return listed.set_item(id, value)
@@ -1,27 +1,35 @@
1
- """``nuspace.devices.web``: the web device, the browser's way into the space, with its own stores.
1
+ """``nuspace.web``: the web device, the browser's way into the space, with its own stores.
2
2
 
3
3
  The sdk, what a program calls, is the ops (:mod:`.ops`), flat here::
4
4
 
5
- import nuspace.devices.web as web
5
+ import nuspace.web as web
6
6
 
7
7
  nuspace.atomic(web.add(plane, editable=True)) # put a plane on the web
8
- nuspace.atomic(web.pin(plane)) # pin it in the sidebar
9
8
  web.params(), web.on_params() # a cell's own pane params
10
9
  web.restart_cell(sid, cell) # run one cell again, brackets itself
11
10
  nuspace.atomic(web.link(row.link, plane)) # draw a link to a plane
11
+ nuspace.atomic(web.page(plane)) # {id, title, icon, href}: a PageLink table cell
12
+ web.url(plane, {"id": 3}) # its URL, plain python, for rows built outside a term
13
+ nuspace.atomic(web.set_role("home", plane)) # make a plane the home
12
14
 
13
- - :mod:`.shapes`: :class:`Web` (web planes, pins, recents) and :class:`Conn`
15
+ - :mod:`.shapes`: :class:`Web` (web planes, recents, desks, roles) and :class:`Conn`
14
16
  (browser tabs, their panes and sidebar folds); :mod:`.stores`: their
15
17
  files, channels and init terms.
16
- - :mod:`.ops`: the ops over them.
18
+ - :mod:`.ops`: the ops over them, all but the desks'.
19
+ - :mod:`.desks`: the desks: their ops (``web.desks.add_plane``, ...), the
20
+ browser node and feed, the desk pruner.
21
+ - :mod:`.home`: Home and the home indexer; :mod:`.planes`: every plane
22
+ the device ships with, made at open (search and settings live there).
17
23
  - :mod:`.nav`: the driver running what the panes show.
18
- - :mod:`.device`: the server and one arm per tab, the sidebar and viewer
19
- feeds under it, and :mod:`.planes` (home, settings, search) it ships with.
24
+ - :mod:`.device`: the server and one arm per tab, the sidebar, viewer and
25
+ desks feeds under it.
20
26
  Imported by the host only: it pulls in the web server.
21
27
 
22
28
  Safe for a worker to import: nothing imported here pulls in a server.
23
29
  """
24
30
 
31
+ from . import desks
32
+ from .interactions import url
25
33
  from .ops import (
26
34
  add,
27
35
  close_tab,
@@ -29,26 +37,26 @@ from .ops import (
29
37
  expand,
30
38
  href,
31
39
  link,
32
- move_pin,
33
40
  on_params,
34
41
  open_tab,
42
+ page,
35
43
  params,
36
- pin,
37
44
  restart_cell,
38
45
  restart_pane,
39
46
  set,
40
47
  set_pane_params,
41
48
  set_panes,
42
- unpin,
49
+ set_role,
43
50
  visit,
44
51
  )
45
- from .shapes import RECENTS_CAP, Conn, Pane, Tab, Web, WebPlane
52
+ from .shapes import RECENTS_CAP, ROLES, Conn, Pane, Tab, Web, WebPlane
46
53
  from .stores import CONN_STORE, WEB_STORE
47
54
 
48
55
 
49
56
  __all__ = [
50
57
  "CONN_STORE",
51
58
  "RECENTS_CAP",
59
+ "ROLES",
52
60
  "WEB_STORE",
53
61
  "Conn",
54
62
  "Pane",
@@ -58,19 +66,20 @@ __all__ = [
58
66
  "add",
59
67
  "close_tab",
60
68
  "collapse",
69
+ "desks",
61
70
  "expand",
62
71
  "href",
63
72
  "link",
64
- "move_pin",
65
73
  "on_params",
66
74
  "open_tab",
75
+ "page",
67
76
  "params",
68
- "pin",
69
77
  "restart_cell",
70
78
  "restart_pane",
71
79
  "set",
72
80
  "set_pane_params",
73
81
  "set_panes",
74
- "unpin",
82
+ "set_role",
83
+ "url",
75
84
  "visit",
76
85
  ]
@@ -0,0 +1,58 @@
1
+ """The desks: their ops, the tab's copy of the desks and the roles, and the pruner.
2
+
3
+ ``ops``
4
+ The desk ops a program calls, re-exported here::
5
+
6
+ import nuspace.web.desks as desks
7
+
8
+ nuspace.atomic(desks.add_plane(desk_id, plane))
9
+
10
+ ``model``
11
+ The desk shape in plain python, and the hosts running it.
12
+ ``ref``
13
+ The node the browser keeps, write only: it draws nothing.
14
+ ``interactions``
15
+ Everything that node says and hears, in the browser's words.
16
+ ``feed``
17
+ One arm per interaction. An event runs one op, a store change ships
18
+ the desks, the roles or the desk planes' titles again.
19
+ ``pruner``
20
+ The system plane taking removed planes off every desk.
21
+
22
+ The wire contract is ``task-167/contract.md`` in Go.
23
+ """
24
+
25
+ from nuspace.web.desks.ops import (
26
+ add_item,
27
+ add_plane,
28
+ create_desk,
29
+ delete_desk,
30
+ get_desk,
31
+ list_desks,
32
+ move_desk,
33
+ move_item,
34
+ remove_item,
35
+ rename_desk,
36
+ reset_item,
37
+ save_desk,
38
+ set_item,
39
+ write_items,
40
+ )
41
+
42
+
43
+ __all__ = [
44
+ "add_item",
45
+ "add_plane",
46
+ "create_desk",
47
+ "delete_desk",
48
+ "get_desk",
49
+ "list_desks",
50
+ "move_desk",
51
+ "move_item",
52
+ "remove_item",
53
+ "rename_desk",
54
+ "reset_item",
55
+ "save_desk",
56
+ "set_item",
57
+ "write_items",
58
+ ]
@@ -0,0 +1,152 @@
1
+ """The desks feed: one arm per interaction, all in parallel, for one tab. Host only.
2
+
3
+ - **browser to store.** A desk event runs its ops in one Web commit: a
4
+ create, a rename, a delete, a move, or ``items.write``, item writes on
5
+ one desk or more (a move across desks is one of these). A failure toasts
6
+ on the tab (:meth:`~nuspace.web.utils.Arms.event`) and writes nothing.
7
+ - **store to browser.** Any desk write (a desk created, renamed, moved or
8
+ deleted, an item set, moved or taken off, by any tab or program): every
9
+ desk ships again, whole, read back as ``{name, items}``, in sidebar
10
+ order, with that order as a list. A desk is a few hundred bytes, so
11
+ shipping all of them is cheaper than tracking which one moved. The roles
12
+ changed: the roles ship again.
13
+ - **titles.** The title and icon of every plane a desk links to, original
14
+ or recent, ship as their own write, so a tab names an item it neither
15
+ has open nor sees in its lazy tree. They ship again when the desks
16
+ change and when a plane's name or icon changes, the watches the
17
+ sidebar's tree uses. A plane with no row, or removed, is left out.
18
+
19
+ Level, with no re-checks, as the sidebar: a missed notification leaves
20
+ the tab stale until the next change.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import TYPE_CHECKING
26
+
27
+ import nu
28
+ import nuspace
29
+ from nuspace.ops.brackets import atomic
30
+ from nuspace.shapes.space import Space
31
+ from nuspace.web import ops as web
32
+ from nuspace.web.desks import interactions
33
+ from nuspace.web.desks import ops as desk_ops
34
+ from nuspace.web.desks.model import DeskPlanes, DesksView, Items
35
+ from nuspace.web.shapes import Web
36
+ from nuspace.web.utils import Arms, field_dict, field_dicts, field_str
37
+
38
+
39
+ if TYPE_CHECKING:
40
+ from collections.abc import Callable
41
+
42
+ from nustd.ui.core import Ref
43
+
44
+
45
+ __all__ = ["desks_feed", "titles"]
46
+
47
+
48
+ def _ship(read: nu.Nu, send: Callable[[nu.Dict], nu.Nu]) -> nu.Nu:
49
+ """``read`` in one snapshot, then ``send(value)`` to the browser."""
50
+ return nu.let(atomic(read), lambda held: send(nu.Dict(held)))
51
+
52
+
53
+ def titles() -> nu.Dict:
54
+ """``{plane_id: {title, icon}}`` for every plane a desk links to, original or recent. Bare read.
55
+
56
+ A title or icon the plane lacks is ``""``. A plane with no row, or
57
+ removed, is left out. Cost: one read of the desks, four reads a plane
58
+ on them.
59
+ """
60
+
61
+ def look(at: nu.Attr) -> nu.List:
62
+ plane = nu.Str(at)
63
+ row = Space.planes[plane]
64
+ return nu.List.of(
65
+ plane, nu.Dict.of(title=row.name.fallback(""), icon=row.icon.fallback(""))
66
+ )
67
+
68
+ planes = nu.List(DeskPlanes(Web.desks.extract()))
69
+ shown = planes.iter().filter(lambda p: nuspace.plane.exists(nu.Str(p)))
70
+ return nu.dict(shown.map(look).to_list())
71
+
72
+
73
+ def _create(ev: nu.Attr) -> nu.Nu:
74
+ """``desk.create``: the desk, last, then its items in their order."""
75
+ desk_id = field_str(ev, "desk_id")
76
+ desk = field_dict(ev, "desk")
77
+ made = desk_ops.create_desk(nu.Str(desk["name"]), id=desk_id)
78
+ return made >> desk_ops.write_items(desk_id, Items(desk["items"]))
79
+
80
+
81
+ def _move(ev: nu.Attr) -> nu.Nu:
82
+ """``desk.move``: in front of ``before``, last for ``""``."""
83
+ desk_id, before = field_str(ev, "desk_id"), field_str(ev, "before")
84
+ return nu.IfDo(
85
+ before == "", desk_ops.move_desk(desk_id), desk_ops.move_desk(desk_id, before=before)
86
+ )
87
+
88
+
89
+ def _write(ev: nu.Attr) -> nu.Nu:
90
+ """``items.write``: each desk's writes in turn, the caller's one commit around them all."""
91
+
92
+ def one(held: nu.Attr) -> nu.Nu:
93
+ w = nu.Dict(held)
94
+ return desk_ops.write_items(nu.Str(w["desk_id"]), w["sets"], w["deletes"], w["order"])
95
+
96
+ return nu.ForEachDo(field_dicts(ev, "writes"), one)
97
+
98
+
99
+ def desks_feed(desks: Ref, toasts: Ref) -> nu.Nu:
100
+ """The desks, the roles and the desk planes' titles of one tab, live, as one term. Built per tab, never ends.
101
+
102
+ Args:
103
+ desks: The shell's desks ref.
104
+ toasts: The tab's toast ref, where a failed event shows.
105
+ """
106
+ arms = Arms("desks", toasts)
107
+
108
+ def op(name: str, change: nu.Nu, run: Callable[[nu.Attr], nu.Nu]) -> nu.Nu:
109
+ return arms.event(name, change, lambda ev: atomic(run(ev)))
110
+
111
+ def send_desks(held: nu.Dict) -> nu.Nu:
112
+ return interactions.set_desks(desks, held, nu.list(held.keys()))
113
+
114
+ shipped_desks = _ship(DesksView(Web.desks.extract()), send_desks)
115
+ shipped_roles = _ship(Web.roles.extract(), lambda r: interactions.set_roles(desks, r))
116
+ shipped_titles = _ship(titles(), lambda t: interactions.set_titles(desks, t))
117
+ on_desks = atomic(Web.desks.on_change())
118
+ return nu.ParallelAsync(
119
+ arms.states("desks", [on_desks], shipped_desks),
120
+ arms.states(
121
+ "roles",
122
+ [atomic(Web.roles.on_children_change()), atomic(Web.roles.on_descendants_change("*"))],
123
+ shipped_roles,
124
+ ),
125
+ arms.states(
126
+ "titles",
127
+ [
128
+ atomic(Web.desks.on_change()),
129
+ atomic(Space.planes.on_descendants_change("*", "name")),
130
+ atomic(Space.planes.on_descendants_change("*", "icon")),
131
+ ],
132
+ shipped_titles,
133
+ ),
134
+ op("create", interactions.on_create_desk(desks), _create),
135
+ op(
136
+ "rename",
137
+ interactions.on_rename_desk(desks),
138
+ lambda ev: desk_ops.rename_desk(field_str(ev, "desk_id"), field_str(ev, "name")),
139
+ ),
140
+ op(
141
+ "delete",
142
+ interactions.on_delete_desk(desks),
143
+ lambda ev: desk_ops.delete_desk(field_str(ev, "desk_id")),
144
+ ),
145
+ op("move", interactions.on_move_desk(desks), _move),
146
+ op("write", interactions.on_write_items(desks), _write),
147
+ op(
148
+ "role",
149
+ interactions.on_set_role(desks),
150
+ lambda ev: web.set_role(field_str(ev, "role"), field_str(ev, "plane_id")),
151
+ ),
152
+ )