Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ jobs:
ui: ${{ (github.event_name == 'workflow_dispatch' && inputs.release_artifacts == 'smoke') && 'false' || (startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && inputs.release_artifacts != 'smoke')) && 'true' || steps.filter.outputs.ui }}
compat: ${{ (github.event_name == 'workflow_dispatch' && inputs.release_artifacts == 'smoke') && 'false' || (startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && inputs.release_artifacts != 'smoke')) && 'true' || steps.filter.outputs.compat }}
docs: ${{ steps.filter.outputs.docs }}
nix: ${{ steps.filter.outputs.nix }}
workflow: ${{ steps.filter.outputs.workflow }}
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -211,6 +212,12 @@ jobs:
- 'scripts/tests/release-workflow.test.mjs'
- 'scripts/tests/resolve-release-version.test.mjs'
- 'packaging/homebrew/**'
nix:
- 'flake.nix'
- 'flake.lock'
- 'nix/**'
- 'udev/**'
- '.github/workflows/ci.yml'

# ── Workflow Lint ─────────────────────────────────────────────
workflow-lint:
Expand Down Expand Up @@ -941,6 +948,35 @@ jobs:
- name: Verify compatibility matrix is current
run: bun scripts/gen-compat.ts --check

# ── Nix Flake ───────────────────────────────────────────────────
nix:
name: Nix Flake
needs: changes
if: needs.changes.outputs.nix == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: cachix/install-nix-action@v31
with:
extra_nix_config: |
experimental-features = nix-command flakes
- name: Check flake formatting
run: nix run nixpkgs#nixfmt-tree -- --ci
- name: Build package and evaluate the NixOS module
run: nix flake check --print-build-logs
- name: Smoke-test the built package
run: |
set -euo pipefail
nix build .#hypercolor --print-build-logs
./result/bin/hypercolor --version
./result/bin/hypercolor-daemon --help >/dev/null
test -f ./result/lib/udev/rules.d/99-hypercolor.rules
test -f ./result/lib/udev/rules.d/70-hypercolor-input.rules
test -f ./result/share/hypercolor/ui/index.html
test -d ./result/share/hypercolor/effects/bundled

# ── Python Client ────────────────────────────────────────────────
python:
name: Python Client
Expand Down Expand Up @@ -2447,3 +2483,123 @@ jobs:
-m "hypercolor: update to ${VERSION}" \
-m "Update formula and cask from the published release checksums."
git push

# ── Update Nix Release Pin ───────────────────────────────────────
# Mirrors update-aur: the flake wraps the published tarballs, so the
# version and checksums in nix/release.json move after every stable
# release. Lands as a pull request so main keeps the review gate. GitHub
# never runs pull_request workflows for PRs opened with GITHUB_TOKEN, so
# the job builds the flake against the new pin itself before opening it.
update-nix:
if: >-
(github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
startsWith(github.ref, 'refs/tags/') &&
!contains(github.ref_name, '-')
needs: create-release
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v7
with:
ref: main

- name: Determine version
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

- name: Download release artifacts and compute checksums
id: checksums
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
mkdir -p release-artifacts
for platform in linux-amd64 linux-arm64; do
tarball="hypercolor-${VERSION}-${platform}.tar.gz"
gh release download "v${VERSION}" \
--repo "${{ github.repository }}" \
--pattern "${tarball}" \
--dir release-artifacts
sha=$(sha256sum "release-artifacts/${tarball}" | cut -d' ' -f1)
echo "sha256_${platform//-/_}=${sha}" >> "$GITHUB_OUTPUT"
echo " ${platform}: ${sha}"
done

- name: Write nix/release.json
env:
VERSION: ${{ steps.version.outputs.version }}
SHA_AMD64: ${{ steps.checksums.outputs.sha256_linux_amd64 }}
SHA_ARM64: ${{ steps.checksums.outputs.sha256_linux_arm64 }}
run: |
set -euo pipefail
jq -n \
--arg version "$VERSION" \
--arg amd64 "$SHA_AMD64" \
--arg arm64 "$SHA_ARM64" \
'{version: $version, sha256: {"x86_64-linux": $amd64, "aarch64-linux": $arm64}}' \
> nix/release.json
cat nix/release.json

- name: Check whether the pin moved
id: pin
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
if git diff --quiet -- nix/release.json; then
echo "nix/release.json already points at ${VERSION}; nothing to do."
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- uses: cachix/install-nix-action@v31
if: steps.pin.outputs.changed == 'true'
with:
extra_nix_config: |
experimental-features = nix-command flakes

- name: Build the flake against the new pin
if: steps.pin.outputs.changed == 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
nix flake check --print-build-logs
nix build .#hypercolor --print-build-logs
./result/bin/hypercolor --version | grep -F "${VERSION}"

- name: Open pull request
if: steps.pin.outputs.changed == 'true'
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
branch="ci/nix-release-${VERSION}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "${branch}"
git add nix/release.json
git commit -F - <<EOF
chore(nix): pin flake to release ${VERSION}

Automated after the v${VERSION} tarballs were published. The flake
wraps those artifacts, so the version and per-architecture
checksums in nix/release.json follow every stable release.
EOF
# The branch is bot-owned, so a rerun of the tag lane replaces it.
git push --force origin "${branch}"
existing="$(gh pr list --head "${branch}" --state open --json number --jq 'length')"
if [[ "${existing}" != "0" ]]; then
echo "Pull request already open for ${branch}; the force-push refreshed it."
exit 0
fi
gh pr create \
--base main \
--head "${branch}" \
--title "chore(nix): pin flake to release ${VERSION}" \
--body "Automated pin of the Nix flake to the v${VERSION} release tarballs. Checksums come straight from the published GitHub Release assets."
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,7 @@ site/out/
.agents/scheduled_tasks.lock
.agents/settings.local.json
drafts-browser.html

# Nix build outputs
result
result-*
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,10 @@ themselves.

Debian and Ubuntu users can install the `.deb` from the releases page. Arch users have
[`hypercolor-bin`](https://aur.archlinux.org/packages/hypercolor-bin) on the AUR, and the
Homebrew formula works on Linux too: `brew install hyperb1iss/tap/hypercolor`.
Homebrew formula works on Linux too: `brew install hyperb1iss/tap/hypercolor`. NixOS users
get a flake with a `services.hypercolor` module; `nix run github:hyperb1iss/hypercolor`
tries the CLI without installing anything (details on the
[download page](https://hyperb1iss.github.io/hypercolor/download/)).

Building from source instead:

Expand Down
5 changes: 5 additions & 0 deletions crates/hypercolor-daemon/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,11 @@ fn resolve_ui_dir(explicit: Option<PathBuf>) -> Option<PathBuf> {
.metadata()
.ok()
.and_then(|meta| meta.modified().ok())
// Reproducible package stores (Nix, Guix) normalize every mtime to
// one second past the Unix epoch, which would read as decades stale.
// Treat anything that early as an unknown build time rather than a
// rebuild nag on every boot.
.filter(|modified| *modified > std::time::UNIX_EPOCH + std::time::Duration::from_secs(1))
.and_then(|modified| modified.elapsed().ok());

let age_label = match age {
Expand Down
52 changes: 52 additions & 0 deletions docs/content/download.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,58 @@ The `hypercolor-bin` AUR package updates automatically on every tagged release:
yay -S hypercolor-bin
```

### NixOS and Nix

The repository is a flake that wraps the same release tarball and ships a
NixOS module. Try it without installing anything:

```bash
nix run github:hyperb1iss/hypercolor -- devices
```

On NixOS, add the flake as an input and enable the module. It installs the
package, the udev rules, the `i2c-dev` kernel module, and a hardened systemd
user service that starts the daemon with every graphical login:

```nix
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
inputs.hypercolor.url = "github:hyperb1iss/hypercolor";

outputs = { nixpkgs, hypercolor, ... }: {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
nixosConfigurations.rig = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
hypercolor.nixosModules.default
{ services.hypercolor.enable = true; }
];
};
};
}
```

Options live under `services.hypercolor`: `autoStart` (default `true`),
`logLevel`, `extraArgs`, `smbus.enable` (default `true`), and
`input.allDevices` (default `false`; grants every keyboard and mouse event
node to the seated user, which is a session-wide keylogging grant, so read the
description before turning it on). Screen-reactive effects on Wayland capture
through the desktop portal, so the module enables `xdg.portal` by default.
Log out and back in after the first rebuild so logind replays the device ACLs.

Outside NixOS, `nix profile install github:hyperb1iss/hypercolor` installs the
binaries, and the package ships a user unit with store paths already filled
in. systemd does not scan the Nix profile, so link the unit in and copy the
udev rules yourself:

```bash
mkdir -p ~/.config/systemd/user
ln -sf ~/.nix-profile/lib/systemd/user/hypercolor.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now hypercolor.service
sudo cp ~/.nix-profile/lib/udev/rules.d/*hypercolor*.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
```

## Windows

Download the NSIS installer (`Hypercolor_<version>_x64-setup.exe`) from the
Expand Down
27 changes: 27 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 94 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
{
description = "Hypercolor: open-source RGB lighting orchestration engine";

inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

outputs =
{ self, nixpkgs }:
let
systems = [
"x86_64-linux"
"aarch64-linux"
];
forEachSystem = nixpkgs.lib.genAttrs systems;
pkgsFor = system: nixpkgs.legacyPackages.${system};
in
{
overlays.default = final: _prev: {
hypercolor = final.callPackage ./nix/package.nix { };
};

packages = forEachSystem (
system:
let
hypercolor = (pkgsFor system).callPackage ./nix/package.nix { };
in
{
inherit hypercolor;
default = hypercolor;
}
);

nixosModules = {
hypercolor =
{ pkgs, ... }:
{
imports = [ ./nix/module.nix ];
services.hypercolor.package =
nixpkgs.lib.mkDefault
self.packages.${pkgs.stdenv.hostPlatform.system}.default;
};
default = self.nixosModules.hypercolor;
};

checks = forEachSystem (
system:
let
# Evaluate the module against a minimal host and build only the
# user unit, so `nix flake check` proves the options and the unit
# text without assembling a whole system closure.
host = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
self.nixosModules.default
{
services.hypercolor = {
enable = true;
input.allDevices = true;
extraArgs = [
"--log-level"
"debug"
];
};
fileSystems."/" = {
device = "/dev/null";
fsType = "ext4";
};
boot.loader.grub.enable = false;
system.stateVersion = "25.05";
}
];
};
pkgs = pkgsFor system;
hypercolor = self.packages.${system}.default;
unit = host.config.systemd.user.units."hypercolor.service".unit;
in
{
package = hypercolor;
module =
assert builtins.elem hypercolor host.config.services.udev.packages;
assert builtins.elem "i2c-dev" host.config.boot.kernelModules;
assert builtins.elem "default.target" host.config.systemd.user.services.hypercolor.wantedBy;
pkgs.runCommand "hypercolor-module-check" { } ''
unit=${unit}/hypercolor.service
grep -q -- "--ui-dir ${hypercolor}/share/hypercolor/ui" "$unit"
grep -q -- "--effects-dir ${hypercolor}/share/hypercolor/effects/bundled" "$unit"
grep -q -- "--log-level debug" "$unit"
grep -q "^ProtectSystem=strict" "$unit"
grep -q 'HYPERCOLOR_LOG=info' "$unit"
touch $out
'';
}
);
};
}
Loading
Loading