omakit 0.5.1 → 0.6.2

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 (94) hide show
  1. package/README.md +38 -45
  2. package/blocks/history.json +68 -0
  3. package/blocks/run/NOTICE +12 -0
  4. package/blocks/run/Run.qml +242 -0
  5. package/blocks/run/run-supervisor.py +522 -0
  6. package/blocks/store/NOTICE +12 -0
  7. package/blocks/store/Store.qml +157 -0
  8. package/blocks/store/store-helper.py +431 -0
  9. package/package.json +12 -5
  10. package/skills/omarchy-plugin-audit/SKILL.md +11 -5
  11. package/skills/omarchy-plugin-build/SKILL.md +164 -0
  12. package/skills/omarchy-plugin-check/SKILL.md +6 -3
  13. package/skills/omarchy-plugin-submit/SKILL.md +4 -1
  14. package/skills/omarchy-plugin-validation-watch/SKILL.md +5 -2
  15. package/skills/omarchy-plugin-weigh/SKILL.md +13 -2
  16. package/tests/fixtures/weigh/clean/Widget.qml +19 -0
  17. package/tests/fixtures/weigh/clean/manifest.json +9 -0
  18. package/tests/fixtures/weigh/clean/tests/harness.qml +7 -0
  19. package/tests/fixtures/weigh/idle-panel/Panel.qml +65 -0
  20. package/tests/fixtures/weigh/idle-panel/manifest.json +9 -0
  21. package/tests/fixtures/weigh/poller/Service.qml +50 -0
  22. package/tests/fixtures/weigh/poller/manifest.json +9 -0
  23. package/tests/fixtures/weigh/timer-180ms/Widget.qml +25 -0
  24. package/tests/fixtures/weigh/timer-180ms/manifest.json +9 -0
  25. package/tests/lab/run/harness/scenarios/controls.sh +6 -0
  26. package/tests/lab/run/harness/scenarios/envprobe.sh +10 -0
  27. package/tests/lab/run/harness/scenarios/forge.sh +11 -0
  28. package/tests/lab/run/harness/scenarios/holder.sh +5 -0
  29. package/tests/lab/run/harness/scenarios/orphan.sh +6 -0
  30. package/tests/lab/run/harness/scenarios/stall.sh +5 -0
  31. package/tests/lab/run/harness/scenarios/stubborn.sh +5 -0
  32. package/tests/lab/run/harness/scenarios/tree.sh +7 -0
  33. package/tests/lab/run/harness/shell.qml +84 -0
  34. package/tests/lab/run/report.py +217 -0
  35. package/tests/lab/run/suite.sh +106 -0
  36. package/tests/lab/store/harness/shell.qml +73 -0
  37. package/tests/lab/store/report.py +133 -0
  38. package/tests/lab/store/suite.sh +109 -0
  39. package/tests/parity/corpus.mjs +8 -3
  40. package/tests/parity/run.mjs +4 -4
  41. package/tools/audit/audit.mjs +17 -6
  42. package/tools/audit/git.mjs +3 -3
  43. package/tools/audit/report.mjs +31 -5
  44. package/tools/blocks/add.mjs +138 -0
  45. package/tools/blocks/commit.json +5 -0
  46. package/tools/blocks/record-commit.mjs +77 -0
  47. package/tools/blocks/registry.mjs +191 -0
  48. package/tools/blocks/stamp.mjs +61 -0
  49. package/tools/inspect/contract.mjs +36 -5
  50. package/tools/inspect/helpers.mjs +217 -0
  51. package/tools/inspect/inspect.mjs +68 -3
  52. package/tools/inspect/patterns.mjs +18 -3
  53. package/tools/inspect/processes.mjs +38 -5
  54. package/tools/inspect/report.mjs +18 -2
  55. package/tools/inspect/writes.mjs +22 -4
  56. package/tools/lab/guest.mjs +155 -0
  57. package/tools/lab/harness.sh +119 -0
  58. package/tools/lab/host.mjs +177 -0
  59. package/tools/lab/inspect.mjs +240 -0
  60. package/tools/lab/omarchy.gpg +13 -0
  61. package/tools/lab/patches/omarchy-iso-test.patch +351 -0
  62. package/tools/lab/paths.mjs +173 -0
  63. package/tools/lab/pin.json +42 -0
  64. package/tools/lab/pin.mjs +64 -0
  65. package/tools/lab/prune.mjs +68 -0
  66. package/tools/lab/qemu.mjs +153 -0
  67. package/tools/lab/qmp-cli.mjs +21 -0
  68. package/tools/lab/report.mjs +183 -0
  69. package/tools/lab/run.mjs +344 -0
  70. package/tools/lab/setup.mjs +430 -0
  71. package/tools/lab/suites/run.sh +35 -0
  72. package/tools/lab/suites/store.sh +41 -0
  73. package/tools/lab/suites/weigh.sh +196 -0
  74. package/tools/lab/suites.mjs +142 -0
  75. package/tools/lab/verify.mjs +134 -0
  76. package/tools/marketplace/README.md +38 -1
  77. package/tools/marketplace/banner.mjs +23 -2
  78. package/tools/marketplace/cli.mjs +449 -146
  79. package/tools/marketplace/completion-check.mjs +27 -1
  80. package/tools/marketplace/completion.mjs +32 -4
  81. package/tools/marketplace/doctor.mjs +47 -9
  82. package/tools/marketplace/github.mjs +52 -6
  83. package/tools/marketplace/local-transport.mjs +1 -1
  84. package/tools/marketplace/options.mjs +16 -5
  85. package/tools/marketplace/outcome.mjs +244 -0
  86. package/tools/marketplace/pin.mjs +178 -33
  87. package/tools/marketplace/setup.mjs +16 -15
  88. package/tools/marketplace/tree.mjs +1 -1
  89. package/tools/marketplace/upgrade.mjs +5 -5
  90. package/tools/marketplace/usage.mjs +116 -72
  91. package/tools/subject/resolve.mjs +19 -6
  92. package/tools/weigh/audit.mjs +47 -16
  93. package/tools/weigh/config.mjs +105 -24
  94. package/tools/weigh/list.mjs +10 -1
@@ -0,0 +1,164 @@
1
+ ---
2
+ name: omarchy-plugin-build
3
+ description: Build the process and state plumbing of an Omarchy Quattro plugin with omakit's Run and Store blocks instead of a bare QML Process or a FileView write. Use whenever plugin code starts a program (a Process block, execDetached, a helper script, a poll) or keeps a file of its own (state, a cache, remembered choices), when inspect shows a process-lifecycle, unbounded-buffering, environment-trust or file-and-state-boundary row, or when a review comment names a deadline, an output cap, PATH, the environment, an orphaned process, a symlink, /tmp, an atomic write or a permission. Adds tested, versioned files the plugin owns; posts nothing.
4
+ ---
5
+
6
+ # Build with the Run and Store blocks
7
+
8
+ This is the build job: copy the measured process and private-state plumbing
9
+ into the plugin before checking, tracking or proving anything around it.
10
+
11
+ ## Start every process through Run
12
+
13
+ Whenever plugin code starts a program, it goes through the Run block, never
14
+ through a bare `Process {`, `Quickshell.execDetached` or a shell string.
15
+ Add the block once, from the plugin's directory:
16
+
17
+ ```bash
18
+ omakit add run <path-to-the-plugin-repo>
19
+ ```
20
+
21
+ That writes `omakit/Run.qml`, `omakit/run-supervisor.py` and
22
+ `omakit/NOTICE` into the plugin's tree, each with a header naming the
23
+ block, its version, the MIT licence, the omakit commit and the body's
24
+ sha256, and nothing else. Commit them with the plugin: they are the
25
+ plugin's files now. Then, in the QML that starts the program:
26
+
27
+ ```qml
28
+ import "omakit"
29
+
30
+ Run {
31
+ id: catalog
32
+ command: [Quickshell.env("HOME") + "/.config/omarchy/plugins/<id>/catalog.sh", "--refresh"]
33
+ deadlineMs: 30000
34
+ onFinished: result => {
35
+ if (result.state === "ok") model.parse(result.stdout)
36
+ else status.text = result.state + (result.reason ? ": " + result.reason : "")
37
+ }
38
+ }
39
+ ```
40
+
41
+ and `catalog.start()` where the program should run. The rules Run holds,
42
+ and the count of review comments in one week that asked for each, are in
43
+ `docs/BLOCKS.md`. The ones that shape the call:
44
+
45
+ - `command[0]` is an absolute path. Omarchy's own commands are under
46
+ `Quickshell.env("HOME") + "/.local/share/omarchy/bin/"`; the plugin's
47
+ own helpers under its plugin directory; system tools under `/usr/bin/`.
48
+ A bare name is `spawn-failed` with the reason.
49
+ - `command` is argv. `["/usr/bin/bash", "-c", "..."]` is refused; write
50
+ the helper as a file and pass its path. `allowShellString: true` exists
51
+ for the one case that cannot be a file, on its own line, where a reviewer
52
+ sees it.
53
+ - The program sees `PATH=/usr/bin`, `HOME`, `LANG`, `XDG_RUNTIME_DIR` and
54
+ what `environment: ({ ... })` adds. Nothing else. A helper that needs a
55
+ variable gets it there, by name.
56
+ - `deadlineMs` is when the run ends whatever the program does; pick it for
57
+ the slow case and let `timeout` be the state that tells the user.
58
+ - `result.stdout` and `result.stderr` are bounded plain text with control
59
+ characters removed; bind them to a `Text` with `textFormat:
60
+ Text.PlainText`, never to rich text.
61
+ - `result.state` is one of `ok`, `exit`, `timeout`, `overflow`,
62
+ `cancelled`, `spawn-failed`, `supervisor-lost`, `python-missing`. Handle
63
+ `ok` and show the state word for the rest; every failure state carries
64
+ `reason` or `stderr` for the details.
65
+ - A `start()` on a live run cancels it and follows; destroying the
66
+ component ends the group. Do not add a `Timer` that kills, a
67
+ `Component.onDestruction` that signals, or a `StdioCollector`: the block
68
+ does those, and inspect knows it.
69
+
70
+ ## Never edit the block's files
71
+
72
+ `omakit inspect` recognises an unmodified block by the sha256 in its header
73
+ and lists it as one row that raises nothing; a modified copy is reported
74
+ as modified and read like any other file, and `omakit add run --update`
75
+ refuses to overwrite it. If the block lacks something, the fix belongs in
76
+ omakit; open an issue there, keep the copy unmodified, and put what the
77
+ plugin needs on the plugin's side of the API.
78
+
79
+ Update the copy when omakit ships a new block version:
80
+
81
+ ```bash
82
+ omakit add run <path-to-the-plugin-repo> --update
83
+ ```
84
+
85
+ ## Helpers that also run outside Run
86
+
87
+ A helper Run starts inherits the closed environment, and so does every
88
+ program the helper starts by bare name: `jq`, `find`, `curl` inside it
89
+ resolve in `/usr/bin` and nowhere else, which is what the review asks for
90
+ at that boundary. Nothing in the helper has to change for that.
91
+
92
+ A helper that also runs where Run did not start it (a hook Omarchy runs
93
+ from `hooks/`, a test, a terminal) starts with these three lines, its
94
+ own, on the plugin's side:
95
+
96
+ ```bash
97
+ PATH=/usr/bin
98
+ unset BASH_ENV ENV
99
+ set -euo pipefail
100
+ ```
101
+
102
+ ## Check the result
103
+
104
+ After adding the block and moving each site to it, run the check loop of
105
+ `skills/omarchy-plugin-check/SKILL.md`. `omakit inspect` should show one
106
+ `blocks` line, `run 0.2.1, 2 files, unmodified: no row of its own`, each
107
+ `Run {` site as a process with its deadline observed through the block,
108
+ and no process-lifecycle or unbounded-buffering row at those sites. A row
109
+ that remains names a site that still uses `Process` directly.
110
+
111
+ ## Keep state through Store
112
+
113
+ Whenever plugin code keeps a file of its own (remembered choices, a
114
+ cache it parses, anything under `~/.local/state` or `~/.cache`), it goes
115
+ through the Store block, never through a `FileView` write, a shell
116
+ redirect or a helper's own `open()`:
117
+
118
+ ```bash
119
+ omakit add store <path-to-the-plugin-repo>
120
+ ```
121
+
122
+ That writes `omakit/Store.qml` and `omakit/store-helper.py`, and the
123
+ run block if it is not there yet, since Store starts its helper through
124
+ Run. Then, in the QML that keeps the state:
125
+
126
+ ```qml
127
+ import "omakit"
128
+
129
+ Store {
130
+ id: memory
131
+ pluginId: "<the plugin's id>"
132
+ name: "memory.json"
133
+ schema: ({ type: "object", required: ["version"], properties: { version: { type: "integer", minimum: 1 } }, additionalProperties: false })
134
+ onFinished: result => {
135
+ if (result.op === "read" && result.state === "ok") apply(result.value)
136
+ else if (result.state !== "missing") status.text = result.state + ": " + result.reason
137
+ }
138
+ }
139
+ ```
140
+
141
+ `memory.read()`, `memory.write(value)` and `memory.remove()` queue and run
142
+ one at a time; each answers once through `finished` with `op`, `state`
143
+ and, for a read, `value`. The rules Store holds and the review comments
144
+ that asked for each are in `docs/BLOCKS.md`; the ones that shape the call:
145
+
146
+ - `pluginId` names the private directory, `$XDG_STATE_HOME/<pluginId>`
147
+ (or the cache base with `kind: "cache"`), created 0700; `name` is one
148
+ file in it. Nothing else of the plugin's touches that directory.
149
+ - Always give a `schema`. A read that departs from it is `invalid` with
150
+ the path that departs, and the plugin shows the state word instead of
151
+ using the value; a write is checked the same way before it starts.
152
+ - A write is one value of at most 64 KiB. A cache a helper downloads is
153
+ the helper's own transaction (`store-helper.py` is the same code,
154
+ importable); Store keeps the plugin's state.
155
+ - `result.state` is one of `ok`, `missing`, `invalid`, `refused`,
156
+ `overflow`, `failed`, `helper-failed`. Treat `missing` as a first run
157
+ and every other non-`ok` state as "show the reason, keep the defaults".
158
+ - Do not stat, test or read the file by path first; the block never does,
159
+ and a check before the open is what the review calls check-then-use.
160
+
161
+ `omakit inspect` lists an unmodified store block as one row and each
162
+ `Store {}` site as a write under a directory the plugin controls at mode
163
+ 0600; a `FileView` write or a shell redirect that remains names a site
164
+ that still keeps state on its own.
@@ -3,7 +3,10 @@ name: omarchy-plugin-check
3
3
  description: Check an Omarchy Quattro plugin while building or changing it, before committing or pushing. Use whenever you create, edit, refactor or test an Omarchy plugin, when asked whether a plugin is marketplace-ready, or before any push to its default branch. Runs the marketplace's own security baseline and submission checks locally, read-only, reports what the marketplace would refuse, and lists what the tree does (processes, hosts, writes, timers) as observations beside the review classes a human reviewer raises most.
4
4
  ---
5
5
 
6
- # Checking a plugin while you build it
6
+ # Check a plugin while building it
7
+
8
+ This is the check job: inspect the tree, run the marketplace's own baseline,
9
+ and run every submission check locally before a person sees the result.
7
10
 
8
11
  ## Run this the way you run a test suite
9
12
 
@@ -111,8 +114,8 @@ fetches a sparse read-only checkout of one marketplace commit under
111
114
 
112
115
  `submit --json` ends with `outcome`:
113
116
 
114
- - `ready`, exit 0: every blocking check passed. The plugin would be accepted
115
- by the marketplace's automated checks as it is now.
117
+ - `ready`, exit 0: every blocking check passed and the title and body were
118
+ produced. This is not review approval or a safety claim.
116
119
  - `refused`, exit 1: a blocking check failed. `blocking` lists the root
117
120
  causes; each check carries `detail`, `paths`, `remedy` and `why`.
118
121
  - `listed`, exit 0: the plugin is already listed by this repository.
@@ -3,7 +3,10 @@ name: omarchy-plugin-submit
3
3
  description: Submit an Omarchy Quattro plugin to the plugin marketplace on its owner's behalf. Use when asked to submit, list or publish a plugin to the Omarchy marketplace, or to check whether a plugin is ready to submit. Runs every pre-submission check, produces the exact issue title and body, and posts nothing.
4
4
  ---
5
5
 
6
- # Submitting an Omarchy plugin
6
+ # Check before submitting
7
+
8
+ Submission is the last part of the check job. The command produces the exact
9
+ title and body after the local checks, and the owner decides what happens next.
7
10
 
8
11
  ## The one thing to get right
9
12
 
@@ -3,7 +3,10 @@ name: omarchy-plugin-validation-watch
3
3
  description: Diagnose an Omarchy marketplace plugin submission that has gone quiet or is stuck waiting. Use when a submission issue has had no progress, when a reviewer asked for a fresh validation, or when fixes were pushed but nothing happened. Checks whether the validated commit has fallen behind the repository and names the one action that re-runs validation.
4
4
  ---
5
5
 
6
- # A submission that has gone quiet
6
+ # Track a submission that has gone quiet
7
+
8
+ This is one half of the track job: compare the commit the marketplace validated
9
+ with the repository's current commit, then name the next action without posting.
7
10
 
8
11
  ## The mechanism, first
9
12
 
@@ -23,7 +26,7 @@ comparisons were stale (62.8%), with 64 of 583 issues unknown. The older
23
26
  2026-09-12 sample found 68/93 readable issues stale (73.1%); that rate was
24
27
  not a measurement of all 464 issues in the queue.
25
28
 
26
- ## Check it
29
+ ## Run the check
27
30
 
28
31
  ```bash
29
32
  omakit watch https://github.com/omacom/omarchy-plugin-marketplace/issues/<number>
@@ -3,7 +3,11 @@ name: omarchy-plugin-weigh
3
3
  description: Weigh an Omarchy Quattro plugin on the shell, in CPU and child processes, by restarting the shell without it and with it. Use before a submission, after a change that adds a timer, a process or a file watcher, or when asked how heavy a plugin is. Restarts the person's shell, so it must never run without their explicit agreement.
4
4
  ---
5
5
 
6
- # What a plugin weighs on the shell
6
+ # Measure what a plugin weighs on the shell
7
+
8
+ Weigh supplies supporting evidence beside the four jobs. Prove remains the
9
+ disposable guest through `omakit lab prove`; weigh is the consented desktop A/B
10
+ measurement of CPU and child processes.
7
11
 
8
12
  ## The one thing to get right
9
13
 
@@ -34,7 +38,14 @@ one sentence naming what is missing, is how it refuses an Omarchy it cannot
34
38
  weigh on: no `omarchy-shell` (an install older than the Quattro shell), no
35
39
  `omarchy-restart-shell`, no readable version, a shell that does not answer
36
40
  `ping`, or an IPC target without the four methods it relies on. Report that
37
- sentence to the person as it is; do not work around it.
41
+ sentence to the person as it is; do not work around it. A
42
+ `shell.json.omakit-backup-<stamp>` already beside `shell.json` is refused
43
+ the same way (`backup-present`): an earlier measurement did not finish its
44
+ restore. Show the person the sentence and the restore it names; never copy,
45
+ move or remove that backup yourself, and never run the measurement in a
46
+ way that can be killed without notice (a tool timeout, a closing terminal):
47
+ a `SIGKILL` is the one exit that leaves `shell.json` on the measurement's
48
+ configuration.
38
49
 
39
50
  ## When to run it
40
51
 
@@ -0,0 +1,19 @@
1
+ import QtQuick
2
+
3
+ // Nothing here wakes up. The word "Timer" in this comment must not count,
4
+ // and neither must the string below.
5
+ Item {
6
+ id: root
7
+ property var bar: null
8
+ property string moduleName: ""
9
+ property var settings: ({})
10
+ property string label: "Timer { interval: 1 }"
11
+
12
+ implicitWidth: text.implicitWidth
13
+ implicitHeight: text.implicitHeight
14
+
15
+ Text {
16
+ id: text
17
+ text: "clean"
18
+ }
19
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "fixture.clean",
4
+ "name": "Fixture: clean",
5
+ "version": "0.0.1",
6
+ "description": "A bar widget that declares no timers, processes or watchers.",
7
+ "kinds": ["bar-widget"],
8
+ "entryPoints": { "barWidget": "Widget.qml" }
9
+ }
@@ -0,0 +1,7 @@
1
+ import QtQuick
2
+
3
+ // A test harness that ships with the plugin. The shell never loads it, so
4
+ // its timer must not count.
5
+ Item {
6
+ Timer { interval: 10; repeat: true; running: true }
7
+ }
@@ -0,0 +1,65 @@
1
+ import QtQuick
2
+ import Quickshell
3
+ import Quickshell.Io
4
+ import Quickshell.Hyprland
5
+
6
+ Item {
7
+ id: root
8
+ property var shell: null
9
+ property bool opened: false
10
+ property int brightness: 0
11
+ property string state: ""
12
+
13
+ function open(payloadJson) { opened = true; refresh() }
14
+ function close() { opened = false; retry.stop() }
15
+ function refresh() { stateProc.running = true }
16
+ function setBrightness(value) { brightness = value; brightnessDebounce.restart() }
17
+
18
+ // Runs only while the panel is open: conditional, zero declared wakeups.
19
+ Timer {
20
+ interval: 5000
21
+ running: root.opened
22
+ repeat: true
23
+ onTriggered: root.refresh()
24
+ }
25
+
26
+ // One-shot debounce: never a wakeup source on its own.
27
+ Timer {
28
+ id: brightnessDebounce
29
+ interval: 180
30
+ repeat: false
31
+ onTriggered: root.setBrightness(root.brightness)
32
+ }
33
+
34
+ // Declared off, started from code in two places.
35
+ Timer {
36
+ id: retry
37
+ interval: 1000
38
+ repeat: true
39
+ onTriggered: root.refresh()
40
+ }
41
+
42
+ Process {
43
+ id: stateProc
44
+ command: ["omarchy-monitor-state"]
45
+ stdout: StdioCollector { waitForEnd: true; onStreamFinished: root.state = String(text) }
46
+ }
47
+
48
+ FileView {
49
+ id: toggles
50
+ path: Quickshell.env("HOME") + "/.local/state/omarchy/toggles"
51
+ watchChanges: true
52
+ onFileChanged: { reload(); retry.start() }
53
+ }
54
+
55
+ Connections {
56
+ target: Hyprland
57
+ function onRawEvent(event) { retry.running = true }
58
+ }
59
+
60
+ // Quickshell's clock ticks at its precision, once a minute here.
61
+ SystemClock {
62
+ id: clock
63
+ precision: SystemClock.Minutes
64
+ }
65
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "fixture.idle-panel",
4
+ "name": "Fixture: idle when closed",
5
+ "version": "0.0.1",
6
+ "description": "A panel whose 5 s timer runs only while open, plus a debounce, a code-started timer and a once-a-minute SystemClock.",
7
+ "kinds": ["panel"],
8
+ "entryPoints": { "panel": "Panel.qml" }
9
+ }
@@ -0,0 +1,50 @@
1
+ import QtQuick
2
+ import Quickshell
3
+ import Quickshell.Io
4
+
5
+ QtObject {
6
+ id: root
7
+ property string loadavg: ""
8
+ property string helperPath: Quickshell.env("HOME") + "/.config/omarchy/plugins/fixture.poller/bin/helper"
9
+
10
+ // A poller: the timer restarts the process every 5 seconds (60 * 1000 / 12).
11
+ property Timer poll: Timer {
12
+ interval: 60 * 1000 / 12
13
+ repeat: true
14
+ running: true
15
+ triggeredOnStart: true
16
+ onTriggered: reader.running = true
17
+ }
18
+
19
+ property Process reader: Process {
20
+ id: reader
21
+ command: ["cat", "/proc/loadavg"]
22
+ stdout: StdioCollector {
23
+ waitForEnd: true
24
+ onStreamFinished: root.loadavg = String(text).trim()
25
+ }
26
+ }
27
+
28
+ // Long-lived: a watcher that stays up for the life of the service.
29
+ property Process watcher: Process {
30
+ id: watcher
31
+ command: [
32
+ "inotifywait", "-m", "-q",
33
+ "-e", "close_write",
34
+ "/tmp"
35
+ ]
36
+ running: true
37
+ stdout: SplitParser { onRead: function(line) { root.loadavg = line } }
38
+ }
39
+
40
+ // Dynamic: the executable is built from the plugin's own directory.
41
+ property Process helper: Process { id: helper; command: [root.helperPath, "status"]; running: true }
42
+
43
+ function refresh() {
44
+ helper.command = [root.helperPath, "refresh", "--now"]
45
+ helper.running = true
46
+ Quickshell.execDetached(["notify-send", "fixture", "refreshed"])
47
+ }
48
+
49
+ Component.onCompleted: Quickshell.execDetached([root.helperPath, "hello"])
50
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "fixture.poller",
4
+ "name": "Fixture: poller",
5
+ "version": "0.0.1",
6
+ "description": "A service that runs a command every 5 s, one long-lived watcher process, and one dynamic command.",
7
+ "kinds": ["service"],
8
+ "entryPoints": { "service": "Service.qml" }
9
+ }
@@ -0,0 +1,25 @@
1
+ import QtQuick
2
+
3
+ Item {
4
+ id: root
5
+ property var bar: null
6
+ property string moduleName: ""
7
+ property var settings: ({})
8
+ property int ticks: 0
9
+
10
+ implicitWidth: text.implicitWidth
11
+ implicitHeight: text.implicitHeight
12
+
13
+ Text {
14
+ id: text
15
+ text: String(root.ticks)
16
+ }
17
+
18
+ Timer {
19
+ id: tick
20
+ interval: 180
21
+ repeat: true
22
+ running: true
23
+ onTriggered: root.ticks++
24
+ }
25
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "fixture.timer-180ms",
4
+ "name": "Fixture: 180 ms timer",
5
+ "version": "0.0.1",
6
+ "description": "A bar widget with one repeating 180 ms timer, always running.",
7
+ "kinds": ["bar-widget"],
8
+ "entryPoints": { "barWidget": "Widget.qml" }
9
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/bash
2
+ # Output with C0, DEL, C1 and bidi controls between plain words; the result
3
+ # must carry the words and none of the controls.
4
+ printf 'a\033[31mb\177c\302\205d\342\200\256e\tf\ng\r\n'
5
+ printf 'stderr\033]0;title\007line\n' >&2
6
+ exit 0
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/bash
2
+ # Reports what a helper sees. env, sort, sed and wc are called by bare name
3
+ # on purpose: with shadow copies first in the session's PATH, a hit means the
4
+ # closed environment did not reach this line.
5
+ echo "PATH=$PATH"
6
+ echo "BASH_ENV=${BASH_ENV-unset}"
7
+ echo "PYTHONPATH=${PYTHONPATH-unset}"
8
+ echo "envcount=$(env | wc -l)"
9
+ env | sort | sed 's/^/env: /'
10
+ exit 0
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/bash
2
+ # A hostile program: same uid as its supervisor, so it can open the
3
+ # supervisor's stdout through /proc and write into the protocol pipe. It
4
+ # forges a leader line naming a group that is not its own and a result
5
+ # that says ok, then kills the supervisor and stays alive.
6
+ exec 3>"/proc/$PPID/fd/1"
7
+ printf '{"ev":"leader","pid":4194303,"pgid":4194303,"atMs":1}\n' >&3
8
+ printf '{"ev":"result","state":"ok","exitCode":0,"termSignal":null,"ms":1,"pgid":4194303,"survivors":0,"outBytes":6,"outLines":1,"errBytes":0,"errLines":0,"stdout":"forged\\u001b[31m\\n","stderr":"","signals":[]}\n' >&3
9
+ exec 3>&-
10
+ kill -9 "$PPID"
11
+ sleep 300
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/bash
2
+ # The leader exits at once; a descendant keeps stdout open for 300 s.
3
+ /usr/bin/sleep 300 &
4
+ echo "holder: leader exiting, descendant $! holds the pipe"
5
+ exit 0
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/bash
2
+ # A program that kills its own supervisor and keeps a tree alive: without
3
+ # the reaper the group would outlive the run it was reported lost in.
4
+ /usr/bin/sleep 300 &
5
+ kill -9 "$PPID"
6
+ sleep 300
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/bash
2
+ # A program that stops its supervisor: no result can come, the backstop
3
+ # has to end the run, and a start() queued behind it must still follow.
4
+ kill -STOP "$PPID"
5
+ sleep 300
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/bash
2
+ # Ignores TERM; the ignored disposition is inherited by every sleep it starts.
3
+ trap '' TERM
4
+ echo "stubborn: ignoring TERM"
5
+ while :; do /usr/bin/sleep 1; done
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/bash
2
+ # A leader that lives, with a child and a grandchild: the destroy, cancel
3
+ # and supersede scenarios must end all three.
4
+ /usr/bin/bash -c '/usr/bin/sleep 300 & wait' &
5
+ /usr/bin/sleep 300 &
6
+ echo "tree: leader $$ waiting"
7
+ wait
@@ -0,0 +1,84 @@
1
+ // The lab harness: a throwaway Quickshell configuration that loads the Run
2
+ // block from ./omakit (copied there by suite.sh from blocks/run/) and runs
3
+ // one scenario named by RUNLAB_SCENARIO, logging JSON events with a RUNLAB
4
+ // prefix for the driver to read. Never loaded into omarchy-shell.
5
+ import QtQuick
6
+ import Quickshell
7
+ import "omakit"
8
+
9
+ ShellRoot {
10
+ id: root
11
+ property string scenario: Quickshell.env("RUNLAB_SCENARIO")
12
+ property string base: Quickshell.env("RUNLAB_BASE")
13
+ property int keep: Number(Quickshell.env("RUNLAB_KEEP") || 65536)
14
+ property var table: ({
15
+ "producer": { command: ["/usr/bin/head", "-c", "1073741824", "/dev/zero"], deadlineMs: 60000, graceMs: 1000, maxBytes: 1048576 },
16
+ "producer-stream": { command: ["/usr/bin/head", "-c", "1073741824", "/dev/zero"], deadlineMs: 120000, graceMs: 1000, maxBytes: 2147483647 },
17
+ "holder": { command: ["/usr/bin/bash", base + "/scenarios/holder.sh"], deadlineMs: 10000, graceMs: 1000, maxBytes: 65536 },
18
+ "stubborn": { command: ["/usr/bin/bash", base + "/scenarios/stubborn.sh"], deadlineMs: 2000, graceMs: 1000, maxBytes: 65536 },
19
+ "hostile": { command: ["/usr/bin/bash", base + "/scenarios/envprobe.sh"], deadlineMs: 5000, graceMs: 1000, maxBytes: 65536 },
20
+ "destroy": { command: ["/usr/bin/bash", base + "/scenarios/tree.sh"], deadlineMs: 10000, graceMs: 1000, maxBytes: 65536, destroyAfterMs: 500 },
21
+ "cancel": { command: ["/usr/bin/bash", base + "/scenarios/tree.sh"], deadlineMs: 10000, graceMs: 1000, maxBytes: 65536, cancelAfterMs: 500 },
22
+ "supersede": { command: ["/usr/bin/bash", base + "/scenarios/tree.sh"], deadlineMs: 10000, graceMs: 1000, maxBytes: 65536, supersedeAfterMs: 500 },
23
+ "shell-string": { command: ["/usr/bin/bash", "-c", "echo never"], deadlineMs: 5000, graceMs: 1000, maxBytes: 65536 },
24
+ "relative": { command: ["bash", base + "/scenarios/envprobe.sh"], deadlineMs: 5000, graceMs: 1000, maxBytes: 65536 },
25
+ "missing": { command: ["/usr/bin/no-such-program-runlab"], deadlineMs: 5000, graceMs: 1000, maxBytes: 65536 },
26
+ "controls": { command: ["/usr/bin/bash", base + "/scenarios/controls.sh"], deadlineMs: 5000, graceMs: 1000, maxBytes: 65536 },
27
+ "concurrency": { command: ["/usr/bin/head", "-c", "1048576", "/dev/zero"], deadlineMs: 30000, graceMs: 1000, maxBytes: 2097152, count: 10 },
28
+ "forge": { command: ["/usr/bin/bash", base + "/scenarios/forge.sh"], deadlineMs: 5000, graceMs: 1000, maxBytes: 65536 },
29
+ "orphan": { command: ["/usr/bin/bash", base + "/scenarios/orphan.sh"], deadlineMs: 5000, graceMs: 1000, maxBytes: 65536 },
30
+ "stalled-supersede": { command: ["/usr/bin/bash", base + "/scenarios/stall.sh"], deadlineMs: 2000, graceMs: 500, maxBytes: 65536, supersedeAfterMs: 500 },
31
+ "destroy-early": { command: ["/usr/bin/bash", base + "/scenarios/tree.sh"], deadlineMs: 10000, graceMs: 1000, maxBytes: 65536, destroyBusyMs: 300 },
32
+ "shell-string-option": { command: ["/usr/bin/bash", "-o", "pipefail", "-c", "echo never"], deadlineMs: 5000, graceMs: 1000, maxBytes: 65536 },
33
+ "shell-string-wrapper": { command: ["/usr/bin/env", "bash", "-c", "echo never"], deadlineMs: 5000, graceMs: 1000, maxBytes: 65536 }
34
+ })
35
+ property var runs: []
36
+ property int finishedCount: 0
37
+
38
+ function log(o) { console.log("RUNLAB " + JSON.stringify(o)) }
39
+
40
+ property Component runComponent: Component { Run {} }
41
+
42
+ function create(spec, index) {
43
+ const object = root.runComponent.createObject(root, { command: spec.command, deadlineMs: spec.deadlineMs, graceMs: spec.graceMs, maxBytes: spec.maxBytes, keepBytes: root.keep })
44
+ object.finished.connect(result => { result.ev = "result"; result.index = index; root.finishedCount += 1; root.log(result); if (root.finishedCount === (spec.count || 1)) root.log({ ev: "all-finished", t: Date.now() }) })
45
+ return object
46
+ }
47
+
48
+ property Timer starter: Timer {
49
+ interval: 3000; running: true
50
+ onTriggered: {
51
+ const spec = root.table[root.scenario]
52
+ const count = spec.count || 1
53
+ const objects = []
54
+ for (let index = 0; index < count; index += 1) objects.push(root.create(spec, index))
55
+ root.runs = objects
56
+ root.log({ ev: "start", scenario: root.scenario, count: count, t: Date.now() })
57
+ for (const object of objects) object.start()
58
+ if (spec.destroyBusyMs) {
59
+ // The event loop is held after start(), so the supervisor has
60
+ // forked the program but this side has not read its leader line
61
+ // when the object is destroyed on the next turn.
62
+ const until = Date.now() + spec.destroyBusyMs
63
+ while (Date.now() < until) {}
64
+ root.log({ ev: "destroy", pgid: objects[0]._pgid, t: Date.now() })
65
+ objects[0].destroy(); root.runs = []
66
+ }
67
+ if (spec.destroyAfterMs) { root.later.mode = "destroy"; root.later.interval = spec.destroyAfterMs; root.later.start() }
68
+ if (spec.cancelAfterMs) { root.later.mode = "cancel"; root.later.interval = spec.cancelAfterMs; root.later.start() }
69
+ if (spec.supersedeAfterMs) { root.later.mode = "supersede"; root.later.interval = spec.supersedeAfterMs; root.later.start() }
70
+ }
71
+ }
72
+ property Timer later: Timer {
73
+ property string mode: ""
74
+ onTriggered: {
75
+ const object = root.runs[0]
76
+ root.log({ ev: mode, pgid: object._pgid, t: Date.now() })
77
+ if (mode === "destroy") { object.destroy(); root.runs = [] }
78
+ else if (mode === "cancel") object.cancel()
79
+ else if (mode === "supersede") { object.command = ["/usr/bin/bash", root.base + "/scenarios/envprobe.sh"]; object.start() }
80
+ }
81
+ }
82
+
83
+ Component.onCompleted: log({ ev: "ready", pid: Quickshell.processId, scenario: scenario })
84
+ }