From 637d4789f0c6f7c6dcf14e43cbdc5fb81d216141 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Fri, 24 Apr 2026 12:38:56 -0300 Subject: [PATCH 1/5] Add a function for spawning multiple watchexec commands --- example/flake.nix | 24 ++++++ example/src/bin/server.rs | 2 +- lib/default.nix | 2 +- lib/watch.nix | 170 +++++++++++++++++++++++++++----------- 4 files changed, 146 insertions(+), 52 deletions(-) diff --git a/example/flake.nix b/example/flake.nix index aa8431f..b573e24 100644 --- a/example/flake.nix +++ b/example/flake.nix @@ -149,6 +149,30 @@ aliases = [ "l" ]; command = "buf lint"; }; + + 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" ]; + } + ]; + }; + watch-gen = pkgs.lib.mkWatch { description = "Regenerate stubs on .proto change"; aliases = [ diff --git a/example/src/bin/server.rs b/example/src/bin/server.rs index 314d5b2..3fa02ec 100644 --- a/example/src/bin/server.rs +++ b/example/src/bin/server.rs @@ -24,7 +24,7 @@ impl GreeterService for Greeter { let message = if name.is_empty() { "hello, stranger".into() } else { - format!("hello, {name}") + format!("hello there, {name}") }; tracing::info!(%name, "say_hello"); Ok(Response::new(SayHelloResponse { message })) diff --git a/lib/default.nix b/lib/default.nix index 32765f9..b7b9bb4 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -96,5 +96,5 @@ in mkTool = import ./tool.nix { inherit lib mkScript pkgs; }; - mkWatch = import ./watch.nix { inherit lib pkgs; }; + inherit (import ./watch.nix { inherit lib pkgs; }) mkWatch mkWatchMany; } diff --git a/lib/watch.nix b/lib/watch.nix index 8511ea3..76c6bdd 100644 --- a/lib/watch.nix +++ b/lib/watch.nix @@ -3,56 +3,126 @@ pkgs, }: -{ - command, - paths ? [ "." ], - extensions ? [ ], - ignore ? [ ], - debounce ? null, - package ? pkgs.watchexec, - packages ? [ ], - ... -}@args: - -assert lib.assertMsg (paths != [ ]) "mkWatch: 'paths' must not be empty"; - let - taskModuleArgs = builtins.removeAttrs args [ - "command" - "paths" - "extensions" - "ignore" - "debounce" - "package" - "packages" - ]; - - watchexecPrefix = lib.escapeShellArgs ( - [ (lib.getExe package) ] - ++ lib.concatMap (p: [ - "--watch" - p - ]) paths - ++ lib.optionals (extensions != [ ]) [ - "--exts" - (lib.concatStringsSep "," extensions) - ] - ++ lib.concatMap (p: [ - "--ignore" - p - ]) ignore - ++ lib.optionals (debounce != null) [ - "--debounce" - (toString debounce) - ] - ++ [ "--" ] - ); - watchexecCmd = "${watchexecPrefix} ${command}"; + mkWatchexecCmd = + { + command, + paths ? [ "." ], + extensions ? [ ], + ignore ? [ ], + debounce ? null, + package ? pkgs.watchexec, + }: + assert lib.assertMsg (paths != [ ]) "mkWatchexecCmd: 'paths' must not be empty"; + let + prefix = lib.escapeShellArgs ( + [ (lib.getExe package) ] + ++ lib.concatMap (p: [ + "--watch" + p + ]) paths + ++ lib.optionals (extensions != [ ]) [ + "--exts" + (lib.concatStringsSep "," extensions) + ] + ++ lib.concatMap (p: [ + "--ignore" + p + ]) ignore + ++ lib.optionals (debounce != null) [ + "--debounce" + (toString debounce) + ] + ++ [ "--" ] + ); + in + "${prefix} ${command}"; + + mkWatch = + { + command, + paths ? [ "." ], + extensions ? [ ], + ignore ? [ ], + debounce ? null, + package ? pkgs.watchexec, + packages ? [ ], + ... + }@args: + let + taskModuleArgs = builtins.removeAttrs args [ + "command" + "paths" + "extensions" + "ignore" + "debounce" + "package" + "packages" + ]; + watchexecCmd = mkWatchexecCmd { + inherit + command + paths + extensions + ignore + debounce + package + ; + }; + in + taskModuleArgs + // { + raw = true; + skip = true; + packages = packages ++ [ package ]; + command = watchexecCmd; + }; + + mkWatchMany = + { + watchers, + package ? pkgs.watchexec, + packages ? [ ], + exitMsg ? "Shutting down", + ... + }@args: + assert lib.assertMsg (watchers != [ ]) "mkWatchMany: 'watchers' must not be empty"; + let + taskModuleArgs = builtins.removeAttrs args [ + "watchers" + "package" + "packages" + ]; + + # Resolve each watcher's package (explicit > shared default). + watchexecPkg = map (w: w // { package = w.package or package; }) watchers; + watcherCmds = map mkWatchexecCmd watchexecPkg; + watcherPackages = map (w: w.package) watchexecPkg; + + command = '' + pids=() + + shutdown() { + trap - INT TERM + echo "${exitMsg}" >&2 + kill "''${pids[@]}" 2>/dev/null + wait "''${pids[@]}" 2>/dev/null + } + trap shutdown INT TERM + + ${lib.concatMapStringsSep "\n" (c: "${c} & pids+=($!)") watcherCmds} + + wait + ''; + in + taskModuleArgs + // { + raw = true; + skip = true; + packages = lib.unique (packages ++ watcherPackages); + inherit command; + }; in -taskModuleArgs -// { - raw = true; - skip = true; - packages = packages ++ [ package ]; - command = watchexecCmd; +{ + inherit mkWatch mkWatchMany; } From 3805e3ba82332f8e19409b405bf02ff9197213d2 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Fri, 24 Apr 2026 14:06:42 -0300 Subject: [PATCH 2/5] Add documentation --- README.md | 23 +++++++++++++++++++++++ example/flake.nix | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 731c925..51f0fba 100644 --- a/README.md +++ b/README.md @@ -358,6 +358,29 @@ These attributes are available: | `package` | The watchexec package to use | `pkgs.watchexec` | | `excludeShellChecks` | [shellcheck] rules to disable in the command | `[ ]` | +There's also a function called `mkWatchMany` that enables you to run multiple watchexec commands at the same time by specifying a `watchers` list: + +```nix +{ + dev = pkgs.lib.mkWatchMany { + description = "Watch/build Rust and Protobuf"; + packages = with pkgs; [ buf cargo ]; + watchers = [ + { + command = "buf generate"; + extensions = [ "proto" ]; + paths = [ "proto" ]; + } + { + command = "cargo check"; + extensions = [ "rs" ]; + paths = [ "src" ]; + } + ]; + }; +} +``` + ## Environment variable sets There are two types of environment variable sets: **static** and **computed**. diff --git a/example/flake.nix b/example/flake.nix index b573e24..7de3500 100644 --- a/example/flake.nix +++ b/example/flake.nix @@ -150,7 +150,7 @@ command = "buf lint"; }; - watch-all = pkgs.lib.mkWatchMany { + watch-all = pkgs.lib.mkWatch { description = "Multiple watch tasks"; aliases = [ "wa" ]; watchers = [ From a258d7a451af516b07c505c32fd26a7c7cd5908f Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Fri, 24 Apr 2026 14:15:43 -0300 Subject: [PATCH 3/5] Add checks to CI --- .github/workflows/checks.yaml | 30 +++++++++++++++++++ .../workflows/flakehub-publish-rolling.yaml | 1 + example/flake.nix | 4 +-- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/checks.yaml diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml new file mode 100644 index 0000000..ae2636e --- /dev/null +++ b/.github/workflows/checks.yaml @@ -0,0 +1,30 @@ +name: Checks + +on: + pull_request: + push: + branches: + - main + +jobs: + checks: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - uses: DeterminateSystems/determinate-nix-action@v3 + + - uses: DeterminateSystems/flakehub-cache-action@v3 + + - name: Flake checks (root) + run: | + nix flake check --all-systems + + - name: Flake checks (example) + run: | + nix flake check --all-systems ./example diff --git a/.github/workflows/flakehub-publish-rolling.yaml b/.github/workflows/flakehub-publish-rolling.yaml index 61203af..40c385d 100644 --- a/.github/workflows/flakehub-publish-rolling.yaml +++ b/.github/workflows/flakehub-publish-rolling.yaml @@ -17,6 +17,7 @@ jobs: persist-credentials: false - uses: DeterminateSystems/determinate-nix-action@v3 + - uses: DeterminateSystems/flakehub-push@main with: name: DeterminateSystems/up diff --git a/example/flake.nix b/example/flake.nix index 7de3500..7407129 100644 --- a/example/flake.nix +++ b/example/flake.nix @@ -150,7 +150,7 @@ command = "buf lint"; }; - watch-all = pkgs.lib.mkWatch { + watch-all = pkgs.lib.mkWatchMany { description = "Multiple watch tasks"; aliases = [ "wa" ]; watchers = [ @@ -211,7 +211,7 @@ }; schemas = { - inherit (inputs.flake-schemas.schemas) devShells schemas; + inherit (inputs.flake-schemas.schemas) devShells overlays schemas; } // { inherit (inputs.up.exportedSchemas) processTrees taskRunners; From c21cc8dc607c0225486ef7e93251e9ee6aa9ee5b Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Fri, 24 Apr 2026 14:16:42 -0300 Subject: [PATCH 4/5] Add exitMsg arg to remove attrs block --- lib/watch.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/watch.nix b/lib/watch.nix index 76c6bdd..0ccdc17 100644 --- a/lib/watch.nix +++ b/lib/watch.nix @@ -92,6 +92,7 @@ let "watchers" "package" "packages" + "exitMsg" ]; # Resolve each watcher's package (explicit > shared default). From cb309afaf20ab61be752a81431c8c58ad8aa9ac8 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Fri, 24 Apr 2026 15:42:07 -0300 Subject: [PATCH 5/5] Use process-compose for multi-watch --- lib/default.nix | 24 +++++++++++--------- lib/process-tree.nix | 5 +++++ lib/watch.nix | 53 ++++++++++++++++++++++++++------------------ 3 files changed, 49 insertions(+), 33 deletions(-) diff --git a/lib/default.nix b/lib/default.nix index b7b9bb4..2ef8c6d 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -56,8 +56,20 @@ let }; taskModule = import ./task.nix { inherit lib mkScript pkgs; }; + + mkProcessTree = import ./process-tree.nix { + inherit + lib + mkScript + pkgs + processModule + taskModule + ; + }; in { + inherit mkProcessTree; + mkBenchmarkTask = import ./benchmark.nix { inherit lib @@ -75,16 +87,6 @@ in ]; }).config; - mkProcessTree = import ./process-tree.nix { - inherit - lib - mkScript - pkgs - processModule - taskModule - ; - }; - mkTaskRunner = import ./task-runner.nix { inherit lib @@ -96,5 +98,5 @@ in mkTool = import ./tool.nix { inherit lib mkScript pkgs; }; - inherit (import ./watch.nix { inherit lib pkgs; }) mkWatch mkWatchMany; + inherit (import ./watch.nix { inherit lib mkProcessTree pkgs; }) mkWatch mkWatchMany; } diff --git a/lib/process-tree.nix b/lib/process-tree.nix index bd34d4e..e54f041 100644 --- a/lib/process-tree.nix +++ b/lib/process-tree.nix @@ -23,6 +23,10 @@ let type = types.listOf types.str; default = [ ]; }; + tui = mkOption { + type = types.bool; + default = false; + }; package = mkOption { type = types.package; default = pkgs.process-compose; @@ -192,6 +196,7 @@ let { json = builtins.toJSON (stripNulls { inherit (config) log_level; + is_tui_disabled = !config.tui; log_location = "/tmp/pc-debug.log"; processes = lib.mapAttrs serializeProcess allProcesses; }); diff --git a/lib/watch.nix b/lib/watch.nix index 0ccdc17..3297004 100644 --- a/lib/watch.nix +++ b/lib/watch.nix @@ -1,11 +1,13 @@ { lib, + mkProcessTree, pkgs, }: let mkWatchexecCmd = { + name ? "watch", command, paths ? [ "." ], extensions ? [ ], @@ -80,48 +82,55 @@ let mkWatchMany = { + name ? "watch-all", watchers, package ? pkgs.watchexec, packages ? [ ], - exitMsg ? "Shutting down", ... }@args: assert lib.assertMsg (watchers != [ ]) "mkWatchMany: 'watchers' must not be empty"; let taskModuleArgs = builtins.removeAttrs args [ + "name" "watchers" "package" "packages" - "exitMsg" ]; - # Resolve each watcher's package (explicit > shared default). - watchexecPkg = map (w: w // { package = w.package or package; }) watchers; - watcherCmds = map mkWatchexecCmd watchexecPkg; - watcherPackages = map (w: w.package) watchexecPkg; + # Resolve each watcher's package and give it a stable process name. + indexed = lib.imap0 (i: w: { + inherit i; + watcher = w // { + package = w.package or package; + }; + }) watchers; - command = '' - pids=() + # Process name: either user-supplied `name`, or `watcher-`. + processNameOf = { i, watcher }: watcher.name or "watcher-${toString i}"; - shutdown() { - trap - INT TERM - echo "${exitMsg}" >&2 - kill "''${pids[@]}" 2>/dev/null - wait "''${pids[@]}" 2>/dev/null - } - trap shutdown INT TERM - - ${lib.concatMapStringsSep "\n" (c: "${c} & pids+=($!)") watcherCmds} - - wait - ''; + processes = lib.listToAttrs ( + map (entry: { + name = processNameOf entry; + value = { + command = mkWatchexecCmd entry.watcher; + packages = [ entry.watcher.package ]; + }; + }) indexed + ); in taskModuleArgs // { raw = true; skip = true; - packages = lib.unique (packages ++ watcherPackages); - inherit command; + command = + (mkProcessTree { + inherit + name + packages + processes + ; + }) + + "/bin/${name}"; }; in {