Skip to content

Enable creating "multi-watch" processes#7

Open
lucperkins wants to merge 5 commits intomainfrom
multi-watch
Open

Enable creating "multi-watch" processes#7
lucperkins wants to merge 5 commits intomainfrom
multi-watch

Conversation

@lucperkins
Copy link
Copy Markdown
Member

@lucperkins lucperkins commented Apr 24, 2026

This PR adds a new mkWatchMany function that enables you to instantiate multiple watchexec processes at the same time:

{
  watch-all = pkgs.lib.mkWatchMany {
    description = "Multiple watch tasks";
    aliases = [ "wa" ];
    watchers = [
      {
        command = "buf generate";
        paths = [
          "proto"
          "buf.gen.yaml"
        ];
        extensions = [
          "proto"
          "yaml"
        ];
      }
      {
        command = "cargo check";
        paths = [ "src" ];
        extensions = [ "rs" ];
      }
    ];
  };
}

Summary by CodeRabbit

  • New Features

    • Run multiple file watchers concurrently to multiplex development tasks (e.g., proto generation and Rust checks)
    • Example "watch-all" task demonstrating parallel watchers
  • Documentation

    • Guide added for configuring and running multiple watchers (examples and watcher options)
  • Chores

    • CI workflow added to validate flake configurations
  • Bug Fixes / UX

    • Greeting message adjusted to a more polite phrasing when a name is provided
  • Configuration

    • Added a toggleable TUI option for the process manager

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

Adds a concurrent watcher helper mkWatchMany, refactors the watch module to expose mkWatch and mkWatchMany, exports mkProcessTree earlier, documents mkWatchMany in README.md, adds an example watch-all task in example/flake.nix, tweaks an example RPC greeting, and introduces CI flake checks.

Changes

Cohort / File(s) Summary
Documentation
README.md
Adds usage docs and an example for the new mkWatchMany helper showing a watchers list and a dev entry.
Example flake & task
example/flake.nix
Adds taskRunners...proto.tasks.watch-all (wa) that composes two watchers (protobuf buf generate and Rust cargo check) and exposes overlays from inputs.flake-schemas.schemas.
Example source
example/src/bin/server.rs
Small change to RPC greeting string: "hello, {name}""hello there, {name}".
Library default export
lib/default.nix
Moves mkProcessTree earlier, re-exports mkProcessTree, and now imports and re-exports both mkWatch and mkWatchMany from ./watch.nix.
Watch module core
lib/watch.nix
Refactors to provide mkWatch and new mkWatchMany; centralizes command construction in mkWatchexecCmd; validates non-empty paths/watchers; builds per-watcher watchexec commands; composes processes with mkProcessTree and stable watcher names.
Process tree module
lib/process-tree.nix
Adds boolean module option tui and wires is_tui_disabled into generated runtime JSON based on !config.tui.
CI workflows
.github/workflows/checks.yaml, .github/workflows/flakehub-publish-rolling.yaml
Adds a GitHub Actions workflow to run nix flake check --all-systems for the root and ./example; minor whitespace formatting tweak in flakehub-publish-rolling.yaml.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant Task as Task Runner (flake task)
  participant Orchestrator as mkWatchMany
  participant ProcTree as mkProcessTree
  participant WatchA as watchexec (watcher-0)
  participant WatchB as watchexec (watcher-1)
  participant OS as OS (signals)

  User->>Task: run `watch-all` task
  Task->>Orchestrator: invoke mkWatchMany with `watchers` list
  Orchestrator->>WatchA: spawn watchexec --watch ... -- command A
  Orchestrator->>WatchB: spawn watchexec --watch ... -- command B
  Orchestrator->>ProcTree: register processes (watcher-0, watcher-1)
  Note right of Orchestrator: record PIDs and monitor
  OS-->>Orchestrator: INT/TERM signal
  Orchestrator->>WatchA: kill PID0
  Orchestrator->>WatchB: kill PID1
  Orchestrator->>WatchA: wait PID0
  Orchestrator->>WatchB: wait PID1
  Orchestrator-->>Task: exit with aggregated status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hop and spawn two watching friends,
Eyes on proto and Rusty bends;
I name their threads and catch the storm,
I kill, I wait, then keep them warm —
Hoppy watchers, tidy ends.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately describes the main change: enabling the creation of multi-watch processes via the new mkWatchMany function.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch multi-watch

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@example/flake.nix`:
- Around line 153-174: The watch-all attribute is using pkgs.lib.mkWatch but
supplies a watchers list (the mkWatchMany API); replace the call to mkWatch with
mkWatchMany so the watchers list is accepted, i.e. change the function name from
mkWatch to mkWatchMany in the watch-all block where the watchers, command,
paths, and extensions are defined so evaluation succeeds.

In `@lib/watch.nix`:
- Around line 91-95: taskModuleArgs is built by calling builtins.removeAttrs on
args but misses the exitMsg key, so exitMsg (which is consumed internally) will
be forwarded to the task module; update the removeAttrs call that builds
taskModuleArgs to include "exitMsg" in the list of removed attributes so exitMsg
is not passed into the task module and won't cause unexpected evaluation errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 13a0802c-1a52-47d4-8c39-f4de282606c0

📥 Commits

Reviewing files that changed from the base of the PR and between 9679e3e and 3805e3b.

📒 Files selected for processing (5)
  • README.md
  • example/flake.nix
  • example/src/bin/server.rs
  • lib/default.nix
  • lib/watch.nix

Comment thread example/flake.nix Outdated
Comment thread lib/watch.nix
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/watch.nix`:
- Around line 103-111: The shutdown() function currently interpolates exitMsg
directly into the shell script (echo "${exitMsg}" >&2) which can break the
generated shell if exitMsg contains quotes or metacharacters; change the code to
emit a shell-safe, quoted form of exitMsg instead of raw interpolation—either
call a shell-escaping helper or use printf '%q' (or equivalent
single-quote-with-embedded-quote escaping) when emitting exitMsg so the
shutdown() echo/write uses a safely quoted variable, and ensure pids handling
remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 920a6139-a845-4665-8d20-67c9ef4f56a9

📥 Commits

Reviewing files that changed from the base of the PR and between 3805e3b and c21cc8d.

📒 Files selected for processing (4)
  • .github/workflows/checks.yaml
  • .github/workflows/flakehub-publish-rolling.yaml
  • example/flake.nix
  • lib/watch.nix
✅ Files skipped from review due to trivial changes (1)
  • .github/workflows/flakehub-publish-rolling.yaml

Comment thread lib/watch.nix Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/watch.nix`:
- Around line 108-119: The code currently builds processes using processNameOf
and then passes them to builtins.listToAttrs which silently collapses duplicate
names; detect duplicates first by mapping each indexed entry through
processNameOf to a list of resolved names, check for any duplicates (same name
appearing more than once), and if found raise a clear error (e.g., via throw or
lib.error) listing the duplicated name(s) and their watcher indices; only
proceed to build processes with listToAttrs if no duplicates are detected.
Ensure you reference the existing processNameOf and processes logic when adding
the duplicate-checking step so the validation happens before listToAttrs is
invoked.
- Around line 34-36: The mkWatchexecCmd snippet that appends the "--debounce"
argument currently passes a unit-less number via (toString debounce); change it
to append the explicit milliseconds unit (e.g. "${toString debounce}ms" or
equivalent string concatenation) so the debounce value matches the pattern used
in lib/process-tree.nix (which uses "${toString w.debounce}ms") and avoids
future watchexec errors; update the array element built in lib.optionals
(debounce != null) to include the "ms" suffix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a4b11036-8b95-40cf-8d6b-1f5c62a9f0f2

📥 Commits

Reviewing files that changed from the base of the PR and between c21cc8d and cb309af.

📒 Files selected for processing (3)
  • lib/default.nix
  • lib/process-tree.nix
  • lib/watch.nix
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/default.nix

Comment thread lib/watch.nix
Comment on lines +34 to +36
++ lib.optionals (debounce != null) [
"--debounce"
(toString debounce)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, explore the repository structure
git ls-files | head -20

Repository: DeterminateSystems/up

Length of output: 480


🏁 Script executed:

# Find and read lib/watch.nix around lines 34-36
if [ -f lib/watch.nix ]; then
  wc -l lib/watch.nix
fi

Repository: DeterminateSystems/up

Length of output: 83


🏁 Script executed:

# Read the relevant section from lib/watch.nix
if [ -f lib/watch.nix ]; then
  sed -n '30,40p' lib/watch.nix
fi

Repository: DeterminateSystems/up

Length of output: 292


🏁 Script executed:

# Search for debounce usage in lib/process-tree.nix
if [ -f lib/process-tree.nix ]; then
  rg -n "debounce" lib/process-tree.nix -A 2 -B 2
fi

Repository: DeterminateSystems/up

Length of output: 412


🌐 Web query:

watchexec --debounce duration syntax milliseconds documentation

💡 Result:

The syntax for the --debounce option in watchexec is -d, --debounce , where specifies the time to wait for new events before taking action (default: 50ms). It accepts: - A unit-less value in milliseconds (e.g., 100 for 100ms). This is deprecated, will warn, and become an error in the future. - A time span value (e.g., 5sec, 20ms, 1min 30s, 30min). Example: watchexec -d 100 command or watchexec --debounce 500ms command. Setting to 0 is highly discouraged. This is from the official watchexec manual.

Citations:


Add explicit ms unit to --debounce argument.

Watchexec accepts unit-less debounce values as milliseconds but marks this form deprecated and will error on it in future versions. The lib/process-tree.nix path already uses "${toString w.debounce}ms", so this inconsistency makes the mkWatchexecCmd helper fragile and diverges from the established pattern.

🔧 Proposed fix
         ++ lib.optionals (debounce != null) [
           "--debounce"
-          (toString debounce)
+          "${toString debounce}ms"
         ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
++ lib.optionals (debounce != null) [
"--debounce"
(toString debounce)
+ lib.optionals (debounce != null) [
"--debounce"
"${toString debounce}ms"
]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/watch.nix` around lines 34 - 36, The mkWatchexecCmd snippet that appends
the "--debounce" argument currently passes a unit-less number via (toString
debounce); change it to append the explicit milliseconds unit (e.g. "${toString
debounce}ms" or equivalent string concatenation) so the debounce value matches
the pattern used in lib/process-tree.nix (which uses "${toString w.debounce}ms")
and avoids future watchexec errors; update the array element built in
lib.optionals (debounce != null) to include the "ms" suffix.

Comment thread lib/watch.nix
Comment on lines +108 to +119
# Process name: either user-supplied `name`, or `watcher-<index>`.
processNameOf = { i, watcher }: watcher.name or "watcher-${toString i}";

processes = lib.listToAttrs (
map (entry: {
name = processNameOf entry;
value = {
command = mkWatchexecCmd entry.watcher;
packages = [ entry.watcher.package ];
};
}) indexed
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject duplicate watcher names before building processes.

If two watchers resolve to the same name here, builtins.listToAttrs collapses the duplicate and only one process survives, so one watcher never starts. That can happen with repeated explicit names or with an explicit name colliding with the default watcher-<index> fallback. (releases.nixos.org)

🔧 Proposed fix
       # Process name: either user-supplied `name`, or `watcher-<index>`.
       processNameOf = { i, watcher }: watcher.name or "watcher-${toString i}";
 
-      processes = lib.listToAttrs (
-        map (entry: {
-          name = processNameOf entry;
-          value = {
-            command = mkWatchexecCmd entry.watcher;
-            packages = [ entry.watcher.package ];
-          };
-        }) indexed
-      );
+      processNames = map processNameOf indexed;
+
+      processes =
+        assert lib.assertMsg
+          (lib.length processNames == lib.length (lib.unique processNames))
+          "mkWatchMany: watcher names must be unique";
+        lib.listToAttrs (
+          map (entry: {
+            name = processNameOf entry;
+            value = {
+              command = mkWatchexecCmd entry.watcher;
+              packages = [ entry.watcher.package ];
+            };
+          }) indexed
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Process name: either user-supplied `name`, or `watcher-<index>`.
processNameOf = { i, watcher }: watcher.name or "watcher-${toString i}";
processes = lib.listToAttrs (
map (entry: {
name = processNameOf entry;
value = {
command = mkWatchexecCmd entry.watcher;
packages = [ entry.watcher.package ];
};
}) indexed
);
# Process name: either user-supplied `name`, or `watcher-<index>`.
processNameOf = { i, watcher }: watcher.name or "watcher-${toString i}";
processNames = map processNameOf indexed;
processes =
assert lib.assertMsg
(lib.length processNames == lib.length (lib.unique processNames))
"mkWatchMany: watcher names must be unique";
lib.listToAttrs (
map (entry: {
name = processNameOf entry;
value = {
command = mkWatchexecCmd entry.watcher;
packages = [ entry.watcher.package ];
};
}) indexed
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/watch.nix` around lines 108 - 119, The code currently builds processes
using processNameOf and then passes them to builtins.listToAttrs which silently
collapses duplicate names; detect duplicates first by mapping each indexed entry
through processNameOf to a list of resolved names, check for any duplicates
(same name appearing more than once), and if found raise a clear error (e.g.,
via throw or lib.error) listing the duplicated name(s) and their watcher
indices; only proceed to build processes with listToAttrs if no duplicates are
detected. Ensure you reference the existing processNameOf and processes logic
when adding the duplicate-checking step so the validation happens before
listToAttrs is invoked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant