mirror of
https://github.com/moons-14/dotfiles.git
synced 2026-10-06 03:08:11 +09:00
Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99878e741b | ||
|
|
b9a4bf72fa | ||
|
|
6d223029df | ||
|
|
e37a0b9e41 | ||
|
|
102393819d | ||
|
|
26e59ad5f7 | ||
|
|
d191f06cc7 | ||
|
|
483d343f48 | ||
|
|
5ebcbb4abf | ||
|
|
f8fd8a3d99 | ||
|
|
16742d2fd7 | ||
|
|
15da7affaa | ||
|
|
548647fec7 | ||
|
|
bcbd08c225 | ||
|
|
a0ae83d24e | ||
|
|
33e09f8e93 | ||
|
|
eff32fddcd | ||
|
|
8ce3a6082a | ||
|
|
a8246261ad | ||
|
|
8b51b5c55f | ||
|
|
e24d85da56 | ||
|
|
01ead9a385 | ||
|
|
328f11d0ed | ||
|
|
d877ffc76c | ||
|
|
fe40adaeee | ||
|
|
991dde5305 | ||
|
|
96ce4d768c | ||
|
|
30b1480e50 | ||
|
|
71011d7bd1 | ||
|
|
9cced59e58 | ||
|
|
20a601402e | ||
|
|
1b4a5fa5a2 | ||
|
|
5fb55f2e6a | ||
|
|
2a3f7ee6ff | ||
|
|
247db71f2d | ||
|
|
a44a83a587 | ||
|
|
1f1d46ea2a | ||
|
|
29c4815b88 | ||
|
|
28a46d9990 |
@@ -0,0 +1,13 @@
|
||||
[mcp_servers.nixos]
|
||||
command = "mcp-nixos"
|
||||
startup_timeout_sec = 30
|
||||
tool_timeout_sec = 60
|
||||
required = false
|
||||
|
||||
[mcp_servers.github]
|
||||
url = "https://api.githubcopilot.com/mcp/"
|
||||
bearer_token_env_var = "GITHUB_PERSONAL_ACCESS_TOKEN"
|
||||
http_headers = { X-MCP-Readonly = "true", X-MCP-Toolsets = "repos,pull_requests,actions" }
|
||||
startup_timeout_sec = 30
|
||||
tool_timeout_sec = 60
|
||||
required = false
|
||||
@@ -1,27 +1,15 @@
|
||||
# Reference: https://github.com/ryoppippi/dotfiles/blob/main/.github/workflows/nix-build.yaml
|
||||
name: Check NixOS configurations
|
||||
description: Build every NixOS configuration and the Registry tests
|
||||
name: Build Linux checks
|
||||
description: Build every x86_64-linux flake check in parallel
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Build every NixOS configuration
|
||||
- name: Build all Linux checks
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
mapfile -t hosts < <(
|
||||
nix eval --raw .#nixosConfigurations \
|
||||
--apply 'configs: builtins.concatStringsSep "\n" (builtins.attrNames configs)'
|
||||
)
|
||||
|
||||
installables=(.#checks.x86_64-linux.registry)
|
||||
for host in "${hosts[@]}"; do
|
||||
installables+=(".#nixosConfigurations.${host}.config.system.build.toplevel")
|
||||
done
|
||||
|
||||
nix build \
|
||||
--keep-going \
|
||||
--no-link \
|
||||
--print-build-logs \
|
||||
--show-trace \
|
||||
"${installables[@]}"
|
||||
nix run .#nix-fast-build -- \
|
||||
--flake .#checks.x86_64-linux \
|
||||
--skip-cached \
|
||||
--no-nom \
|
||||
--no-link
|
||||
|
||||
@@ -20,3 +20,4 @@ runs:
|
||||
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.lock') }}
|
||||
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}-
|
||||
gc-max-store-size-linux: 4G
|
||||
gc-max-store-size-macos: 4G
|
||||
|
||||
+163
-10
@@ -1,36 +1,50 @@
|
||||
# Reference: https://github.com/ryoppippi/dotfiles/blob/main/.github/workflows/nix-build.yaml
|
||||
name: "CI: NixOS"
|
||||
name: "CI: Nix"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- .gitignore
|
||||
- AGENTS.md
|
||||
- README.md
|
||||
- "docs/**"
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- ".codex/**"
|
||||
- ".github/**"
|
||||
- ".vscode/**"
|
||||
- "flake/**"
|
||||
- "hosts/**"
|
||||
- "libs/**"
|
||||
- "modules/**"
|
||||
- "opencode.json"
|
||||
- "overlays/**"
|
||||
- "scripts/**"
|
||||
- "shells/**"
|
||||
- "skills/**"
|
||||
- "tests/**"
|
||||
- ".github/actions/check-nixos/**"
|
||||
- ".github/actions/setup-nix/**"
|
||||
- ".github/workflows/nixos.yaml"
|
||||
pull_request:
|
||||
paths:
|
||||
- .gitignore
|
||||
- AGENTS.md
|
||||
- README.md
|
||||
- "docs/**"
|
||||
- flake.nix
|
||||
- flake.lock
|
||||
- ".codex/**"
|
||||
- ".github/**"
|
||||
- ".vscode/**"
|
||||
- "flake/**"
|
||||
- "hosts/**"
|
||||
- "libs/**"
|
||||
- "modules/**"
|
||||
- "opencode.json"
|
||||
- "overlays/**"
|
||||
- "scripts/**"
|
||||
- "shells/**"
|
||||
- "skills/**"
|
||||
- "tests/**"
|
||||
- ".github/actions/check-nixos/**"
|
||||
- ".github/actions/setup-nix/**"
|
||||
- ".github/workflows/nixos.yaml"
|
||||
workflow_dispatch:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -38,14 +52,153 @@ concurrency:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
check:
|
||||
name: Check all NixOS configurations
|
||||
validate:
|
||||
name: Plan, lint, and evaluate
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
outputs:
|
||||
build_linux: ${{ steps.plan.outputs.build_linux }}
|
||||
build_darwin: ${{ steps.plan.outputs.build_darwin }}
|
||||
nix_validation: ${{ steps.plan.outputs.nix_validation }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nix
|
||||
uses: ./.github/actions/setup-nix
|
||||
- name: Check NixOS configurations
|
||||
- name: Plan validation
|
||||
id: plan
|
||||
env:
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
PUSH_BASE_SHA: ${{ github.event.before }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
case "${{ github.event_name }}" in
|
||||
pull_request)
|
||||
plan="$(nix run .#check -- plan --base "$PR_BASE_SHA" --json)"
|
||||
;;
|
||||
push)
|
||||
plan="$(nix run .#check -- plan --base "$PUSH_BASE_SHA" --json)"
|
||||
;;
|
||||
*)
|
||||
plan="$(nix run .#check -- plan --all-files --all-hosts --json)"
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '%s\n' "$plan"
|
||||
nix_validation="$(jq -r '.requiresNixValidation' <<<"$plan")"
|
||||
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
build_linux="$(jq -r '.nativeBuildSystems["x86_64-linux"] // false' <<<"$plan")"
|
||||
build_darwin="$(jq -r '.nativeBuildSystems["aarch64-darwin"] // false' <<<"$plan")"
|
||||
else
|
||||
build_linux="$nix_validation"
|
||||
build_darwin="$nix_validation"
|
||||
fi
|
||||
|
||||
{
|
||||
echo "nix_validation=$nix_validation"
|
||||
echo "build_linux=$build_linux"
|
||||
echo "build_darwin=$build_darwin"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Run fast checks
|
||||
env:
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
PUSH_BASE_SHA: ${{ github.event.before }}
|
||||
shell: bash
|
||||
run: |
|
||||
set +e
|
||||
set -uo pipefail
|
||||
case "${{ github.event_name }}" in
|
||||
pull_request)
|
||||
nix run .#check -- fast --base "$PR_BASE_SHA"
|
||||
;;
|
||||
push)
|
||||
nix run .#check -- fast --base "$PUSH_BASE_SHA"
|
||||
;;
|
||||
*)
|
||||
nix run .#check -- fast --all-files
|
||||
;;
|
||||
esac
|
||||
status=$?
|
||||
if (( status != 0 )); then
|
||||
git diff -- .
|
||||
exit "$status"
|
||||
fi
|
||||
- name: Evaluate configurations
|
||||
if: steps.plan.outputs.nix_validation == 'true' || github.event_name == 'workflow_dispatch'
|
||||
env:
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
PUSH_BASE_SHA: ${{ github.event.before }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "${{ github.event_name }}" in
|
||||
pull_request)
|
||||
nix run .#check -- eval --base "$PR_BASE_SHA"
|
||||
;;
|
||||
push)
|
||||
nix run .#check -- eval --base "$PUSH_BASE_SHA" --all-systems
|
||||
;;
|
||||
*)
|
||||
nix run .#check -- eval --all-hosts --all-systems
|
||||
;;
|
||||
esac
|
||||
build-linux:
|
||||
name: Build Linux checks
|
||||
needs: validate
|
||||
if: needs.validate.outputs.build_linux == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nix
|
||||
uses: ./.github/actions/setup-nix
|
||||
- name: Build affected Linux checks
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
nix run .#check -- build --base "$PR_BASE_SHA"
|
||||
- name: Build all Linux checks
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: ./.github/actions/check-nixos
|
||||
build-darwin:
|
||||
name: Build Darwin checks
|
||||
needs: validate
|
||||
if: needs.validate.outputs.build_darwin == 'true'
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nix
|
||||
uses: ./.github/actions/setup-nix
|
||||
- name: Build affected Darwin checks
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
nix run .#check -- build --base "$PR_BASE_SHA"
|
||||
- name: Build all Darwin checks
|
||||
if: github.event_name != 'pull_request'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
nix run .#nix-fast-build -- \
|
||||
--flake .#checks.aarch64-darwin \
|
||||
--skip-cached \
|
||||
--no-nom \
|
||||
--no-link
|
||||
|
||||
@@ -12,9 +12,11 @@ permissions:
|
||||
pull-requests: write
|
||||
jobs:
|
||||
update:
|
||||
name: Update and validate flake inputs
|
||||
name: Update and check Linux
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
timeout-minutes: 180
|
||||
outputs:
|
||||
changed: ${{ steps.update.outputs.changed }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
@@ -22,18 +24,76 @@ jobs:
|
||||
uses: ./.github/actions/setup-nix
|
||||
- name: Update flake inputs
|
||||
id: update
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
nix flake update
|
||||
if git diff --quiet -- flake.lock; then
|
||||
echo 'changed=false' >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo 'changed=true' >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Check updated NixOS configurations
|
||||
- name: Evaluate every flake system
|
||||
if: steps.update.outputs.changed == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
nix flake check \
|
||||
--no-build \
|
||||
--all-systems \
|
||||
--keep-going \
|
||||
--show-trace
|
||||
- name: Build Linux checks
|
||||
if: steps.update.outputs.changed == 'true'
|
||||
uses: ./.github/actions/check-nixos
|
||||
- name: Create update pull request
|
||||
- name: Upload updated lock file
|
||||
if: steps.update.outputs.changed == 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: flake-lock
|
||||
path: flake.lock
|
||||
if-no-files-found: error
|
||||
check-darwin:
|
||||
name: Check Darwin
|
||||
needs: update
|
||||
if: needs.update.outputs.changed == 'true'
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 180
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- name: Download updated lock file
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: flake-lock
|
||||
path: .
|
||||
- name: Setup Nix
|
||||
uses: ./.github/actions/setup-nix
|
||||
- name: Build Darwin checks
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
nix run .#nix-fast-build -- \
|
||||
--flake .#checks.aarch64-darwin \
|
||||
--skip-cached \
|
||||
--no-nom \
|
||||
--no-link
|
||||
pull-request:
|
||||
name: Create update pull request
|
||||
needs:
|
||||
- update
|
||||
- check-darwin
|
||||
if: needs.update.outputs.changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- name: Download validated lock file
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: flake-lock
|
||||
path: .
|
||||
- name: Create update pull request
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -45,4 +105,5 @@ jobs:
|
||||
body: |
|
||||
Automated update of `flake.lock`.
|
||||
|
||||
The updated inputs passed the Registry tests and a build of every NixOS configuration.
|
||||
The updated inputs passed all-system evaluation and native builds of
|
||||
the Linux and Darwin check sets.
|
||||
|
||||
+8
-1
@@ -1,4 +1,6 @@
|
||||
/*
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
!.gitignore
|
||||
!README.md
|
||||
@@ -6,8 +8,10 @@
|
||||
!AGENTS.md
|
||||
!/docs/
|
||||
|
||||
!.github/
|
||||
!/.codex/
|
||||
!/.github/
|
||||
!.gitea/
|
||||
!/.vscode/
|
||||
|
||||
!.envrc
|
||||
|
||||
@@ -15,7 +19,9 @@
|
||||
|
||||
!/flake.nix
|
||||
!/flake.lock
|
||||
!/opencode.json
|
||||
|
||||
!/scripts/
|
||||
!/shells/
|
||||
!/flake/
|
||||
!/overlays/
|
||||
@@ -30,3 +36,4 @@
|
||||
!/modules/
|
||||
|
||||
!/tests/
|
||||
!/skills/
|
||||
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"nix.enableLanguageServer": true,
|
||||
"nix.serverPath": "nixd",
|
||||
"nix.serverSettings": {
|
||||
"nixd": {
|
||||
"formatting": {
|
||||
"command": ["nixfmt"]
|
||||
},
|
||||
"nixpkgs": {
|
||||
"expr": "import (builtins.getFlake (builtins.toString ./.)).inputs.nixpkgs { }"
|
||||
},
|
||||
"options": {
|
||||
"nixos": {
|
||||
"expr": "(builtins.getFlake (builtins.toString ./.)).nixosConfigurations.galleria.options"
|
||||
},
|
||||
"home-manager-nixos": {
|
||||
"expr": "(builtins.getFlake (builtins.toString ./.)).nixosConfigurations.galleria.options.home-manager.users.type.getSubOptions []"
|
||||
},
|
||||
"darwin": {
|
||||
"expr": "(builtins.getFlake (builtins.toString ./.)).darwinConfigurations.m2.options"
|
||||
},
|
||||
"home-manager-darwin": {
|
||||
"expr": "(builtins.getFlake (builtins.toString ./.)).darwinConfigurations.m2.options.home-manager.users.type.getSubOptions []"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,11 @@ makes any statement here stale, update `AGENTS.md` in the same change.
|
||||
| `overlays/` | Package replacements and additions |
|
||||
| `shells/` | Development shells |
|
||||
| `flake/` | Supporting flake outputs such as formatters, checks, and Git hooks |
|
||||
| `skills/` | Repository-specific Codex workflows that enforce this contract for recurring changes |
|
||||
|
||||
Before adding or materially extending an application or service, read and
|
||||
follow `skills/add-application-or-service/SKILL.md`. `AGENTS.md` remains the
|
||||
authoritative contract when the skill and repository ever disagree.
|
||||
|
||||
Use **unit** as the generic internal term for a Registry-managed component and
|
||||
**profile** for a unit that composes multiple units. Do not introduce a
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# moons14 dotfiles
|
||||
|
||||
My NixOS + Home Manager configurations build with flake.
|
||||
My NixOS, nix-darwin, and Home Manager configurations built with flakes.
|
||||
|
||||
See [Coding-agent validation](docs/coding-agents.md) for the non-activating,
|
||||
change-aware validation workflow used by Codex, OpenCode, and CI.
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# Coding-agent workflow
|
||||
|
||||
This repository exposes deterministic validation and narrowly scoped research
|
||||
tools for Codex, OpenCode, and editor agents. Live system activation is outside
|
||||
this workflow.
|
||||
|
||||
## Validation commands
|
||||
|
||||
Use task-owned paths during the edit loop:
|
||||
|
||||
```console
|
||||
nix run .#check -- plan --paths modules/applications/example/home.nix --json
|
||||
nix run .#check -- fast --paths modules/applications/example/home.nix
|
||||
nix run .#check -- eval --paths modules/applications/example/home.nix
|
||||
nix run .#check -- build --paths modules/applications/example/home.nix
|
||||
nix run .#check -- all --paths modules/applications/example/home.nix
|
||||
```
|
||||
|
||||
For a committed pull-request range, replace `--paths ...` with
|
||||
`--base <base-sha>`. `full` is reserved for CI, scheduled maintenance, or an
|
||||
explicit repository-wide audit:
|
||||
|
||||
```console
|
||||
nix run .#check -- full
|
||||
```
|
||||
|
||||
The stages have distinct meanings:
|
||||
|
||||
- `plan` maps changed paths to Registry units, reverse `meta.includes`
|
||||
dependencies, real hosts, and compatible build systems.
|
||||
- `fast` parses changed Nix files, validates JSON, TOML, Python, and Agent Skill
|
||||
frontmatter, checks whitespace, and runs configured hooks only for the
|
||||
selected files.
|
||||
- `eval` instantiates affected NixOS and nix-darwin configurations. Flake-wide,
|
||||
validation-tool, shell, overlay, and test changes additionally evaluate every
|
||||
flake system.
|
||||
- `build` realizes affected configurations supported by the current platform
|
||||
with no result link. Incompatible targets remain evaluation-only until a
|
||||
matching runner handles them.
|
||||
- `all` performs task-scoped file checks, all-system evaluation, and compatible
|
||||
targeted builds.
|
||||
- `full` runs all-file hooks, all-system evaluation, and every check for the
|
||||
current platform through `nix-fast-build`.
|
||||
|
||||
The validation app constructs a filtered temporary `path:` flake from the
|
||||
committed `HEAD` tree and overlays only task-owned changed paths. This isolates
|
||||
unrelated worktree changes, makes new untracked Registry fragments visible to
|
||||
Nix without staging them, and avoids copying ignored state such as `.direnv`.
|
||||
|
||||
None of these commands runs `nh os switch`, `nixos-rebuild switch`,
|
||||
`darwin-rebuild switch`, `home-manager switch`, or another activation command.
|
||||
The user performs activation separately.
|
||||
|
||||
## Agent Skills
|
||||
|
||||
Read `AGENTS.md` first. Use the repository skills as follows:
|
||||
|
||||
- `validate-nix-change` controls validation scope and evidence.
|
||||
- `debug-nix-failure` classifies parse, evaluation, build, test, activation-log,
|
||||
and runtime failures before proposing a correction.
|
||||
- `test-nixos-service` adds a `pkgs.testers.runNixOSTest` check when a build
|
||||
cannot prove service behavior.
|
||||
- `update-flake-input` limits lock-file updates and validates them without
|
||||
activating a host.
|
||||
- `add-application-or-service` preserves Registry ownership and delegates
|
||||
validation to `validate-nix-change`.
|
||||
|
||||
## MCP and language-server setup
|
||||
|
||||
The development workload installs `mcp-nixos`, `nixd`, `nix-fast-build`, and
|
||||
`nix-tree`.
|
||||
|
||||
Project-local Codex and OpenCode configuration exposes:
|
||||
|
||||
- `mcp-nixos` for current NixOS, Home Manager, nix-darwin, package, and Nix
|
||||
documentation queries;
|
||||
- GitHub's remote MCP endpoint for Codex and OpenCode in read-only mode,
|
||||
restricted to repository, pull-request, and Actions toolsets.
|
||||
|
||||
Set `GITHUB_PERSONAL_ACCESS_TOKEN` in the launching environment when GitHub MCP
|
||||
access is needed. Do not commit the token or put it in a Nix expression because
|
||||
that would expose it through source control or the Nix store.
|
||||
|
||||
The workspace VS Code settings use `nixd` and expose option sets for the
|
||||
`galleria` NixOS configuration, its integrated Home Manager configuration, the
|
||||
`m2` nix-darwin configuration, and its integrated Home Manager configuration.
|
||||
Generated
+248
-80
@@ -91,6 +91,32 @@
|
||||
}
|
||||
},
|
||||
"bun2nix": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts_2",
|
||||
"import-tree": "import-tree",
|
||||
"nixpkgs": [
|
||||
"handy",
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems_4",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770895533,
|
||||
"narHash": "sha256-v3QaK9ugy9bN9RXDnjw0i2OifKmz2NnKM82agtqm/UY=",
|
||||
"owner": "nix-community",
|
||||
"repo": "bun2nix",
|
||||
"rev": "c843f477b15f51151f8c6bcc886954699440a6e1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "2.0.8",
|
||||
"repo": "bun2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"bun2nix_2": {
|
||||
"inputs": {
|
||||
"flake-parts": [
|
||||
"llm-agents",
|
||||
@@ -129,11 +155,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1785771258,
|
||||
"narHash": "sha256-4qwZF7xxLBokf6gfIBkymFMQiJ36fbJ9SPkOAp5VI4I=",
|
||||
"lastModified": 1785831553,
|
||||
"narHash": "sha256-9q7WKbhq6bOGUdeo9Q/e3PmapJew0b2LCpaCio1X17k=",
|
||||
"owner": "ilysenko",
|
||||
"repo": "codex-desktop-linux",
|
||||
"rev": "ab314923b5bf9b786a16cee67605d9eea2433d84",
|
||||
"rev": "7166d1153fd99647fb080605c8b2a8f22b50b08f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -292,6 +318,24 @@
|
||||
}
|
||||
},
|
||||
"flake-parts_2": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1769996383,
|
||||
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts_3": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"llm-agents",
|
||||
@@ -312,7 +356,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts_3": {
|
||||
"flake-parts_4": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"nixvim",
|
||||
@@ -333,7 +377,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts_4": {
|
||||
"flake-parts_5": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"stylix",
|
||||
@@ -419,11 +463,11 @@
|
||||
"zon2nix": "zon2nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1785770443,
|
||||
"narHash": "sha256-VZ9UpfgG5+Dy17eTLJ8ryV5e7Ki0/0JTA5MhWnfSriw=",
|
||||
"lastModified": 1785853205,
|
||||
"narHash": "sha256-rHvUeXh4druPSNPg2EMvY9F+av/NSQzjNLsf7s6lNnI=",
|
||||
"owner": "ghostty-org",
|
||||
"repo": "ghostty",
|
||||
"rev": "863fc9531ae0b8e09b7103a9089dfc00319a7d9c",
|
||||
"rev": "594ee212bc3c048ffa06ba90623eaf207a4d145c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -470,6 +514,27 @@
|
||||
"type": "gitlab"
|
||||
}
|
||||
},
|
||||
"handy": {
|
||||
"inputs": {
|
||||
"bun2nix": "bun2nix",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1785763469,
|
||||
"narHash": "sha256-YHfOchlPBKbBbeoaT1amrjKBHJTYMXmcR1Gm31E7TLE=",
|
||||
"owner": "cjpais",
|
||||
"repo": "Handy",
|
||||
"rev": "b1b2d9f9072e55902a46ca6cc655e5893ed7a91d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cjpais",
|
||||
"repo": "Handy",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"home-manager": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -533,6 +598,21 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"import-tree": {
|
||||
"locked": {
|
||||
"lastModified": 1763762820,
|
||||
"narHash": "sha256-ZvYKbFib3AEwiNMLsejb/CWs/OL/srFQ8AogkebEPF0=",
|
||||
"owner": "vic",
|
||||
"repo": "import-tree",
|
||||
"rev": "3c23749d8013ec6daa1d7255057590e9ca726646",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "vic",
|
||||
"repo": "import-tree",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"lanzaboote": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
@@ -558,18 +638,18 @@
|
||||
},
|
||||
"llm-agents": {
|
||||
"inputs": {
|
||||
"bun2nix": "bun2nix",
|
||||
"flake-parts": "flake-parts_2",
|
||||
"bun2nix": "bun2nix_2",
|
||||
"flake-parts": "flake-parts_3",
|
||||
"nixpkgs": "nixpkgs_4",
|
||||
"systems": "systems_4",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
"systems": "systems_5",
|
||||
"treefmt-nix": "treefmt-nix_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1785753922,
|
||||
"narHash": "sha256-cRnp7DSvT4BwRI04YSbA0Ocn1iY6+fN9KTfnsADHm3I=",
|
||||
"lastModified": 1785826862,
|
||||
"narHash": "sha256-UOPf9uQHGoNqEMZ2kii8DqlPBd5dYR6uERzdU8wdroQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "llm-agents.nix",
|
||||
"rev": "8a86f04f5cfbffc5b5d0b38ac24795ab4909b46b",
|
||||
"rev": "fd0508ef842609ad0c6f5af46c6f572214efebd9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -578,11 +658,29 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nani-translate-linux": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_5"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1785857772,
|
||||
"narHash": "sha256-JMEQH9x8Pb9cHW4JbWfFBcqxsh4JkPDR5k8dCnphUTU=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "9d6fec627a620841c27c7e67e8e7b5719ce9b465",
|
||||
"revCount": 8,
|
||||
"type": "git",
|
||||
"url": "https://github.com/zunoser/nani-translate-linux.git"
|
||||
},
|
||||
"original": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/zunoser/nani-translate-linux.git"
|
||||
}
|
||||
},
|
||||
"niri-flake": {
|
||||
"inputs": {
|
||||
"niri-stable": "niri-stable",
|
||||
"niri-unstable": "niri-unstable",
|
||||
"nixpkgs": "nixpkgs_5",
|
||||
"nixpkgs": "nixpkgs_6",
|
||||
"nixpkgs-stable": "nixpkgs-stable",
|
||||
"xwayland-satellite-stable": "xwayland-satellite-stable",
|
||||
"xwayland-satellite-unstable": "xwayland-satellite-unstable"
|
||||
@@ -698,7 +796,7 @@
|
||||
},
|
||||
"nixos-hardware": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_6"
|
||||
"nixpkgs": "nixpkgs_7"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1785232496,
|
||||
@@ -718,7 +816,7 @@
|
||||
"nixos-wsl": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat_4",
|
||||
"nixpkgs": "nixpkgs_7"
|
||||
"nixpkgs": "nixpkgs_8"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1784642409,
|
||||
@@ -765,6 +863,21 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib_2": {
|
||||
"locked": {
|
||||
"lastModified": 1769909678,
|
||||
"narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "72716169fe93074c333e8d0173151350670b824c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-libxml2": {
|
||||
"locked": {
|
||||
"lastModified": 1751274312,
|
||||
@@ -814,6 +927,19 @@
|
||||
}
|
||||
},
|
||||
"nixpkgs_10": {
|
||||
"locked": {
|
||||
"lastModified": 1785571196,
|
||||
"narHash": "sha256-xwSqxTsama0YTtS78+4YyCm6jJg09v78QWfP1cAyoVA=",
|
||||
"rev": "148bab9c1c3c53136ecb44a6ea356a0ed5b39b06",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1045728.148bab9c1c3c/nixexprs.tar.xz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
|
||||
}
|
||||
},
|
||||
"nixpkgs_11": {
|
||||
"locked": {
|
||||
"lastModified": 1770107345,
|
||||
"narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=",
|
||||
@@ -829,7 +955,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_11": {
|
||||
"nixpkgs_12": {
|
||||
"locked": {
|
||||
"lastModified": 1772542754,
|
||||
"narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=",
|
||||
@@ -845,7 +971,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_12": {
|
||||
"nixpkgs_13": {
|
||||
"locked": {
|
||||
"lastModified": 1778869304,
|
||||
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
|
||||
@@ -892,11 +1018,11 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1785602060,
|
||||
"narHash": "sha256-z7D96eESRM4CPV/XtwpwFn8IDdfLAmxz6lVWrGYXvR4=",
|
||||
"lastModified": 1785747939,
|
||||
"narHash": "sha256-D740uKsMbgsfK2oaDenJLLPIZfq7W0/g4KN/Fls8eKs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a5cbcfe954791221bfffe2307f7d1a1bf61a871e",
|
||||
"rev": "104240a772428cc2e20d8fd86c9ddbb886bbaff2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -907,6 +1033,22 @@
|
||||
}
|
||||
},
|
||||
"nixpkgs_5": {
|
||||
"locked": {
|
||||
"lastModified": 1785454630,
|
||||
"narHash": "sha256-LQy14TZp77TwbQf40gg1V3jo8FwJG0jGDkAH+zRHqg8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1559d3daa3ecc813a650b79375ea61b6741b8746",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_6": {
|
||||
"locked": {
|
||||
"lastModified": 1785692966,
|
||||
"narHash": "sha256-vUfIeBEfpbAfZ5zjgIkYk7eHBeVfCYVjLbWnMkseYnk=",
|
||||
@@ -922,7 +1064,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_6": {
|
||||
"nixpkgs_7": {
|
||||
"locked": {
|
||||
"lastModified": 1767892417,
|
||||
"narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=",
|
||||
@@ -935,7 +1077,7 @@
|
||||
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
|
||||
}
|
||||
},
|
||||
"nixpkgs_7": {
|
||||
"nixpkgs_8": {
|
||||
"locked": {
|
||||
"lastModified": 1783776592,
|
||||
"narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=",
|
||||
@@ -951,13 +1093,13 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_8": {
|
||||
"nixpkgs_9": {
|
||||
"locked": {
|
||||
"lastModified": 1785599192,
|
||||
"narHash": "sha256-dg4RTtDxnXY13UkJNdhmgTUTl0n/IJBlCigfO7nutZw=",
|
||||
"lastModified": 1785734586,
|
||||
"narHash": "sha256-ODZkEK9Gy50yg6h98u7KkitZ3oc/uuTFK00bh1CRdNA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6d65bfc1bcef2ef39a239d38e577e92a89fb0f07",
|
||||
"rev": "531670d871c0e29724a02f3cbcac170adc65b58c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -967,26 +1109,13 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_9": {
|
||||
"locked": {
|
||||
"lastModified": 1785571196,
|
||||
"narHash": "sha256-xwSqxTsama0YTtS78+4YyCm6jJg09v78QWfP1cAyoVA=",
|
||||
"rev": "148bab9c1c3c53136ecb44a6ea356a0ed5b39b06",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1045728.148bab9c1c3c/nixexprs.tar.xz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
|
||||
}
|
||||
},
|
||||
"nixvim": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts_3",
|
||||
"flake-parts": "flake-parts_4",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems_5"
|
||||
"systems": "systems_6"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1782919967,
|
||||
@@ -1005,14 +1134,14 @@
|
||||
},
|
||||
"noctalia": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_9"
|
||||
"nixpkgs": "nixpkgs_10"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1785767538,
|
||||
"narHash": "sha256-9foQ31PSKpIu8UWGalt+4G5o4TU21g4a1qwMwJmeOy4=",
|
||||
"lastModified": 1785809412,
|
||||
"narHash": "sha256-QbFIduUhpNIucJy/WcwRxsvTQocnwIXlSwnYEjmGMSQ=",
|
||||
"owner": "noctalia-dev",
|
||||
"repo": "noctalia",
|
||||
"rev": "50f0f095053a4b5c10874c4e7080f55dc15d3b38",
|
||||
"rev": "e41c99439605df6d2fa62409139f96ded6ac7345",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -1077,24 +1206,26 @@
|
||||
"flake-parts": "flake-parts",
|
||||
"ghostty": "ghostty",
|
||||
"git-hooks-nix": "git-hooks-nix",
|
||||
"handy": "handy",
|
||||
"home-manager": "home-manager_2",
|
||||
"lanzaboote": "lanzaboote",
|
||||
"llm-agents": "llm-agents",
|
||||
"nani-translate-linux": "nani-translate-linux",
|
||||
"niri-flake": "niri-flake",
|
||||
"nix-darwin": "nix-darwin",
|
||||
"nix-hazkey": "nix-hazkey",
|
||||
"nix-index-database": "nix-index-database",
|
||||
"nixos-hardware": "nixos-hardware",
|
||||
"nixos-wsl": "nixos-wsl",
|
||||
"nixpkgs": "nixpkgs_8",
|
||||
"nixpkgs": "nixpkgs_9",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||
"nixvim": "nixvim",
|
||||
"noctalia": "noctalia",
|
||||
"services-flake": "services-flake",
|
||||
"sops-nix": "sops-nix",
|
||||
"stylix": "stylix",
|
||||
"systems": "systems_7",
|
||||
"treefmt-nix": "treefmt-nix_2",
|
||||
"systems": "systems_8",
|
||||
"treefmt-nix": "treefmt-nix_3",
|
||||
"vicinae": "vicinae",
|
||||
"vicinae-extensions": "vicinae-extensions"
|
||||
}
|
||||
@@ -1157,7 +1288,7 @@
|
||||
},
|
||||
"soulver-cpp": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_12",
|
||||
"nixpkgs": "nixpkgs_13",
|
||||
"nixpkgs-libxml2": "nixpkgs-libxml2"
|
||||
},
|
||||
"locked": {
|
||||
@@ -1181,24 +1312,24 @@
|
||||
"base16-helix": "base16-helix",
|
||||
"base16-vim": "base16-vim",
|
||||
"firefox-gnome-theme": "firefox-gnome-theme",
|
||||
"flake-parts": "flake-parts_4",
|
||||
"flake-parts": "flake-parts_5",
|
||||
"gnome-shell": "gnome-shell",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"nur": "nur",
|
||||
"systems": "systems_6",
|
||||
"systems": "systems_7",
|
||||
"tinted-kitty": "tinted-kitty",
|
||||
"tinted-schemes": "tinted-schemes",
|
||||
"tinted-tmux": "tinted-tmux",
|
||||
"tinted-zed": "tinted-zed"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1785680312,
|
||||
"narHash": "sha256-e26BuraMqnara0VXVk9nsQLgGC+8nvEgYNc6odv5Yi8=",
|
||||
"lastModified": 1785794750,
|
||||
"narHash": "sha256-OqIrGVL7AX462ISyJFAqjbqTc1RZlBkVHCa4dwPEZU4=",
|
||||
"owner": "nix-community",
|
||||
"repo": "stylix",
|
||||
"rev": "04b284060ffa638f080ab1b2d05cf1f236217995",
|
||||
"rev": "cb5eb3a7343faba61fd694fac8040a326485a339",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -1222,6 +1353,21 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_10": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
@@ -1284,6 +1430,21 @@
|
||||
}
|
||||
},
|
||||
"systems_6": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_7": {
|
||||
"locked": {
|
||||
"lastModified": 1774449309,
|
||||
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
|
||||
@@ -1299,21 +1460,6 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_7": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_8": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
@@ -1409,6 +1555,28 @@
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"handy",
|
||||
"bun2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770228511,
|
||||
"narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "337a4fe074be1042a35086f15481d763b8ddc0e7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix_2": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"llm-agents",
|
||||
@@ -1429,9 +1597,9 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix_2": {
|
||||
"treefmt-nix_3": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_10"
|
||||
"nixpkgs": "nixpkgs_11"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1785360170,
|
||||
@@ -1449,16 +1617,16 @@
|
||||
},
|
||||
"vicinae": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_11",
|
||||
"nixpkgs": "nixpkgs_12",
|
||||
"soulver-cpp": "soulver-cpp",
|
||||
"systems": "systems_8"
|
||||
"systems": "systems_9"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1785733880,
|
||||
"narHash": "sha256-Rv4rVFSvxF7ViOQOrv+N8IEvweS5MCqUM2fnaQVIR+w=",
|
||||
"lastModified": 1785823793,
|
||||
"narHash": "sha256-iGKyp+peQzXI0T9LzYmRHr2tevkbhF5GfzDlVldVm2Q=",
|
||||
"owner": "vicinaehq",
|
||||
"repo": "vicinae",
|
||||
"rev": "61bd7cd5c9cdd6d8df6e6d34531beb344219ff71",
|
||||
"rev": "b3130be7b480817d2255b4781476a91593267afb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -1473,7 +1641,7 @@
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems_9",
|
||||
"systems": "systems_10",
|
||||
"vicinae": "vicinae_2"
|
||||
},
|
||||
"locked": {
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
url = "github:ghostty-org/ghostty";
|
||||
};
|
||||
|
||||
# Speech to text
|
||||
handy = {
|
||||
url = "github:cjpais/Handy";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Shell / Launcher
|
||||
vicinae.url = "github:vicinaehq/vicinae";
|
||||
|
||||
@@ -104,6 +110,9 @@
|
||||
url = "github:nix-community/browser-previews";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
nani-translate-linux.url = "git+https://github.com/zunoser/nani-translate-linux.git";
|
||||
|
||||
};
|
||||
|
||||
outputs =
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
./formatter.nix
|
||||
./git-hooks.nix
|
||||
./registry.nix
|
||||
./validation.nix
|
||||
];
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
];
|
||||
};
|
||||
|
||||
actionlint.enable = true;
|
||||
deadnix.enable = true;
|
||||
ruff.enable = true;
|
||||
statix.enable = true;
|
||||
shellcheck.enable = true;
|
||||
};
|
||||
|
||||
+38
-7
@@ -25,11 +25,42 @@ let
|
||||
else
|
||||
{ };
|
||||
configurations = dotfilesLib.hosts.mkConfigurations hostSpecs;
|
||||
|
||||
validationMetadata = {
|
||||
schemaVersion = 1;
|
||||
|
||||
hosts = lib.mapAttrs (
|
||||
name: spec:
|
||||
let
|
||||
isDarwin = lib.hasSuffix "-darwin" spec.system;
|
||||
in
|
||||
{
|
||||
inherit (spec) system user;
|
||||
kind = if isDarwin then "darwin" else "nixos";
|
||||
homeManager = spec.homeManager or true;
|
||||
selectedUnits = dotfilesLib.hosts.selectedUnits spec;
|
||||
buildAttr =
|
||||
if isDarwin then
|
||||
"darwinConfigurations.${name}.system"
|
||||
else
|
||||
"nixosConfigurations.${name}.config.system.build.toplevel";
|
||||
}
|
||||
) hostSpecs;
|
||||
|
||||
units = lib.mapAttrs (_id: unit: {
|
||||
inherit (unit) id relativePath;
|
||||
directory = "modules/${lib.concatStringsSep "/" unit.relativePath}";
|
||||
includes = unit.meta.includes;
|
||||
fragments = builtins.attrNames (lib.filterAttrs (_: value: value != null) unit.fragments);
|
||||
}) dotfilesLib.registry.units;
|
||||
};
|
||||
in
|
||||
{
|
||||
flake = {
|
||||
inherit (configurations) darwinConfigurations nixosConfigurations;
|
||||
lib = dotfilesLib;
|
||||
lib = dotfilesLib // {
|
||||
inherit validationMetadata;
|
||||
};
|
||||
};
|
||||
|
||||
perSystem =
|
||||
@@ -37,11 +68,10 @@ in
|
||||
let
|
||||
nixosChecks =
|
||||
lib.mapAttrs' (name: nixos: lib.nameValuePair "nixos-${name}" nixos.config.system.build.toplevel)
|
||||
(
|
||||
lib.filterAttrs (
|
||||
_: nixos: nixos.pkgs.stdenv.hostPlatform.system == system
|
||||
) configurations.nixosConfigurations
|
||||
);
|
||||
(lib.filterAttrs (name: _: hostSpecs.${name}.system == system) configurations.nixosConfigurations);
|
||||
darwinChecks = lib.mapAttrs' (name: darwin: lib.nameValuePair "darwin-${name}" darwin.system) (
|
||||
lib.filterAttrs (name: _: hostSpecs.${name}.system == system) configurations.darwinConfigurations
|
||||
);
|
||||
in
|
||||
{
|
||||
checks = {
|
||||
@@ -49,6 +79,7 @@ in
|
||||
inherit inputs lib pkgs;
|
||||
};
|
||||
}
|
||||
// nixosChecks;
|
||||
// nixosChecks
|
||||
// darwinChecks;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
_: {
|
||||
perSystem =
|
||||
{ pkgs, config, ... }:
|
||||
let
|
||||
script = pkgs.writeText "dotfiles-check.py" (builtins.readFile ../scripts/dotfiles-check.py);
|
||||
dotfilesCheck = pkgs.writeShellApplication {
|
||||
name = "dotfiles-check";
|
||||
runtimeInputs = [
|
||||
pkgs.git
|
||||
pkgs.nix
|
||||
pkgs.python3
|
||||
];
|
||||
text = ''
|
||||
exec python3 ${script} "$@"
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
apps = {
|
||||
check = {
|
||||
type = "app";
|
||||
program = "${dotfilesCheck}/bin/dotfiles-check";
|
||||
};
|
||||
nix-fast-build = {
|
||||
type = "app";
|
||||
program = "${pkgs.nix-fast-build}/bin/nix-fast-build";
|
||||
};
|
||||
};
|
||||
|
||||
packages = {
|
||||
dotfiles-check = dotfilesCheck;
|
||||
inherit (pkgs) nix-fast-build;
|
||||
};
|
||||
|
||||
devShells.validation = config.pre-commit.devShell;
|
||||
|
||||
checks = {
|
||||
validation-tool =
|
||||
pkgs.runCommand "dotfiles-check-self-test"
|
||||
{
|
||||
nativeBuildInputs = [ dotfilesCheck ];
|
||||
}
|
||||
''
|
||||
dotfiles-check self-test
|
||||
touch "$out"
|
||||
'';
|
||||
|
||||
dotnix-shell = config.devShells.dotnix;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -104,6 +104,7 @@
|
||||
"interface.cli"
|
||||
"interface.labwc"
|
||||
"interface.niri"
|
||||
"interface.wallpaperengine"
|
||||
"platform.intel-nvidia-desktop"
|
||||
"security.secrets"
|
||||
"security.secure-boot"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
homebrew.casks = [ "activitywatch" ];
|
||||
|
||||
launchd.agents.activitywatch = {
|
||||
command = "/usr/bin/open -gja ActivityWatch";
|
||||
serviceConfig.RunAtLoad = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
services.activitywatch = {
|
||||
enable = true;
|
||||
|
||||
watchers.aw-awatcher = {
|
||||
package = pkgs.awatcher;
|
||||
executable = "awatcher";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
description = "ActivityWatch automated time tracker";
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
{ pkgs, ... }:
|
||||
{ inputs, pkgs, ... }:
|
||||
{
|
||||
|
||||
services.hazkey.enable = true;
|
||||
services.hazkey = {
|
||||
enable = true;
|
||||
server.package =
|
||||
inputs.nix-hazkey.packages.${pkgs.stdenv.hostPlatform.system}.hazkey-server.override
|
||||
{ enableVulkan = true; };
|
||||
};
|
||||
|
||||
i18n.inputMethod = {
|
||||
enable = true;
|
||||
|
||||
@@ -6,6 +6,7 @@ _: {
|
||||
theme = "dracula";
|
||||
background-blur-radius = 20;
|
||||
background-opacity = 0.9;
|
||||
background-opacity-cells = true;
|
||||
font-family = "BlexMono Nerd Font Mono";
|
||||
mouse-hide-while-typing = true;
|
||||
window-decoration = "auto";
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
homebrew.casks = [ "handy" ];
|
||||
|
||||
launchd.agents.handy = {
|
||||
command = "/usr/bin/open -gja Handy --args --start-hidden";
|
||||
serviceConfig.RunAtLoad = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
config,
|
||||
inputs,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
handy = inputs.handy.packages.${pkgs.stdenv.hostPlatform.system}.handy;
|
||||
settingsFile = "${config.xdg.configHome}/com.pais.handy/settings_store.json";
|
||||
in
|
||||
{
|
||||
home.packages = [
|
||||
handy
|
||||
pkgs.wtype
|
||||
];
|
||||
|
||||
# Handy has no disabled-shortcut setting. Empty bindings are rejected before
|
||||
# registration, leaving niri and labwc as the sole owners of Ctrl+Space.
|
||||
home.activation.disableHandyGlobalShortcuts = {
|
||||
after = [ "writeBoundary" ];
|
||||
before = [ ];
|
||||
data = ''
|
||||
settingsFile=${lib.escapeShellArg settingsFile}
|
||||
mkdir -p "$(dirname "$settingsFile")"
|
||||
|
||||
if [ -f "$settingsFile" ]; then
|
||||
${lib.getExe pkgs.jq} \
|
||||
'.settings.bindings.transcribe.current_binding = ""
|
||||
| .settings.bindings.transcribe_with_post_process.current_binding = ""' \
|
||||
"$settingsFile" > "$settingsFile.tmp"
|
||||
else
|
||||
${lib.getExe pkgs.jq} -n '
|
||||
{
|
||||
settings: {
|
||||
bindings: {
|
||||
transcribe: {
|
||||
id: "transcribe",
|
||||
name: "Transcribe",
|
||||
description: "Converts your speech into text.",
|
||||
default_binding: "ctrl+space",
|
||||
current_binding: ""
|
||||
},
|
||||
transcribe_with_post_process: {
|
||||
id: "transcribe_with_post_process",
|
||||
name: "Transcribe with Post-Processing",
|
||||
description: "Converts your speech into text and applies AI post-processing.",
|
||||
default_binding: "ctrl+shift+space",
|
||||
current_binding: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
' > "$settingsFile.tmp"
|
||||
fi
|
||||
|
||||
mv "$settingsFile.tmp" "$settingsFile"
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.user.services.handy = {
|
||||
Unit = {
|
||||
Description = "Handy speech-to-text";
|
||||
After = [ "graphical-session.target" ];
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
ExecStart = "${lib.getExe handy} --start-hidden";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
description = "Handy offline speech-to-text";
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
{ lib, pkgs, ... }:
|
||||
let
|
||||
screenshot = import ./screenshot.nix { inherit pkgs; };
|
||||
|
||||
systemctl = lib.getExe' pkgs.systemd "systemctl";
|
||||
|
||||
keybind = key: actionAttrs: {
|
||||
@@ -62,8 +60,6 @@ let
|
||||
};
|
||||
in
|
||||
{
|
||||
home.packages = [ screenshot ];
|
||||
|
||||
wayland.windowManager.labwc = {
|
||||
enable = true;
|
||||
# NixOS owns the package so Home Manager only generates the user config.
|
||||
@@ -106,8 +102,8 @@ in
|
||||
(action "W-q" "Close")
|
||||
(action "W-f" "ToggleMaximize")
|
||||
(action "W-c" "Iconify")
|
||||
(action "W-Tab" "NextWindow")
|
||||
(action "W-S-Tab" "PreviousWindow")
|
||||
(execute "W-Tab" "window-overview")
|
||||
(execute "W-S-Tab" "window-overview --reverse")
|
||||
(action "W-Up" "Lower")
|
||||
(action "W-Down" "Raise")
|
||||
(action "W-Left" "NextWindow")
|
||||
@@ -117,9 +113,9 @@ in
|
||||
(snapToEdge "W-C-Up" "up")
|
||||
(snapToEdge "W-C-Down" "down")
|
||||
(confirmAction "W-S-e" "Exit labwc?" "Exit")
|
||||
(execute "Print" "labwc-screenshot region")
|
||||
(execute "C-Print" "labwc-screenshot output")
|
||||
(execute "A-Print" "labwc-screenshot all")
|
||||
(execute "Print" "screenshot region")
|
||||
(execute "C-Print" "screenshot output")
|
||||
(execute "A-Print" "screenshot all")
|
||||
|
||||
# spawn applications (sync with niri modules/applications/niri/home.nix)
|
||||
(execute "W-t" "ghostty")
|
||||
@@ -128,8 +124,13 @@ in
|
||||
(execute "W-e" "nautilus --new-window")
|
||||
(execute "W-l" "loginctl lock-session")
|
||||
(execute "W-v" "vicinae vicinae://launch/clipboard/history?toggle=true")
|
||||
(execute "W-j" "nani-translate-primary")
|
||||
(execute "W-S-j" "nani-translate-ocr")
|
||||
(execute "W-C-j" "${lib.getExe' pkgs.xdg-utils "xdg-open"} naniapp://translate")
|
||||
(execute "W-space" "ghostty +toggle-quick-terminal")
|
||||
(execute "W-p" "wdisplays")
|
||||
(execute "W-z" "wl-find-cursor -c 0xCCFF453A -s 160 -d 1200")
|
||||
(execute "C-space" "handy --toggle-transcription")
|
||||
|
||||
# Function keys (sync with niri modules/applications/niri/home.nix)
|
||||
(execute "XF86AudioRaiseVolume" "noctalia msg volume-up")
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
description = "labwc Wayland stacking compositor";
|
||||
|
||||
includes = [ "systems.wayland" ];
|
||||
includes = [
|
||||
"systems.wayland"
|
||||
"applications.screenshot"
|
||||
"applications.wl-find-cursor"
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
{
|
||||
inputs,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
unstable = inputs.nixpkgs-unstable.legacyPackages.${pkgs.stdenv.hostPlatform.system};
|
||||
labwc = unstable.labwc.overrideAttrs (oldAttrs: {
|
||||
patches = (oldAttrs.patches or [ ]) ++ [
|
||||
./patches/remove-ext-workspace-output-on-destroy.patch
|
||||
];
|
||||
});
|
||||
in
|
||||
{
|
||||
programs.labwc = {
|
||||
enable = true;
|
||||
package = labwc;
|
||||
};
|
||||
|
||||
xdg.portal.config.labwc.default = [
|
||||
"wlr"
|
||||
"gtk"
|
||||
];
|
||||
|
||||
# xdg-desktop-portal-wlr implements screencasting for wlroots compositors.
|
||||
# Keep it with Labwc: Niri uses xdg-desktop-portal-gnome instead.
|
||||
xdg.portal.wlr = {
|
||||
enable = true;
|
||||
settings.screencast = {
|
||||
chooser_type = "dmenu";
|
||||
chooser_cmd = "${pkgs.fuzzel}/bin/fuzzel --dmenu";
|
||||
};
|
||||
};
|
||||
|
||||
# NixOS decides whether a graphical session manages graphical-session.target
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
From: Codex <[email protected]>
|
||||
Subject: [PATCH] output: detach destroyed outputs from protocol handles
|
||||
|
||||
The DRM backend destroys and recreates outputs when a session is paused and
|
||||
resumed. Remove the output from the ext-workspace group and the foreign
|
||||
toplevel handles before wlroots finishes it. Both protocols attach bind
|
||||
listeners to the output; leaving either listener behind makes
|
||||
wlr_output_finish() abort.
|
||||
|
||||
---
|
||||
src/foreign-toplevel/foreign.c | 14 ++++++++++++++
|
||||
src/output.c | 6 ++++++
|
||||
include/foreign-toplevel/foreign.h | 2 ++
|
||||
3 files changed, 22 insertions(+)
|
||||
|
||||
diff --git a/src/output.c b/src/output.c
|
||||
index 2eab8ec..6f6e7ef 100644
|
||||
--- a/src/output.c
|
||||
+++ b/src/output.c
|
||||
@@ -27,8 +27,9 @@
|
||||
#include "common/macros.h"
|
||||
#include "common/mem.h"
|
||||
#include "common/scene-helpers.h"
|
||||
#include "common/string-helpers.h"
|
||||
#include "config/rcxml.h"
|
||||
+#include "foreign-toplevel/foreign.h"
|
||||
#include "labwc.h"
|
||||
#include "layers.h"
|
||||
#include "node.h"
|
||||
@@ -276,7 +277,15 @@ handle_output_destroy(struct wl_listener *listener, void *data)
|
||||
struct output *output = wl_container_of(listener, output, destroy);
|
||||
struct seat *seat = &server.seat;
|
||||
regions_evacuate_output(output);
|
||||
regions_destroy(seat, &output->regions);
|
||||
+ wlr_ext_workspace_group_handle_v1_output_leave(
|
||||
+ server.workspaces.ext_group, output->wlr_output);
|
||||
+
|
||||
+ struct view *view;
|
||||
+ wl_list_for_each(view, &server.views, link) {
|
||||
+ foreign_toplevel_remove_output(view->foreign_toplevel, output->wlr_output);
|
||||
+ }
|
||||
+
|
||||
if (seat->overlay.active.output == output) {
|
||||
overlay_finish(seat);
|
||||
}
|
||||
@@ -297,7 +305,6 @@ handle_output_destroy(struct wl_listener *listener, void *data)
|
||||
output->workspace_osd = NULL;
|
||||
}
|
||||
|
||||
- struct view *view;
|
||||
wl_list_for_each(view, &server.views, link) {
|
||||
if (view->output == output) {
|
||||
view_on_output_destroy(view);
|
||||
diff --git a/include/foreign-toplevel/foreign.h b/include/foreign-toplevel/foreign.h
|
||||
index 69a340a..d8ec9b4 100644
|
||||
--- a/include/foreign-toplevel/foreign.h
|
||||
+++ b/include/foreign-toplevel/foreign.h
|
||||
@@ -3,11 +3,14 @@
|
||||
#define LABWC_FOREIGN_TOPLEVEL_H
|
||||
|
||||
struct view;
|
||||
+struct wlr_output;
|
||||
struct foreign_toplevel;
|
||||
|
||||
struct foreign_toplevel *foreign_toplevel_create(struct view *view);
|
||||
void foreign_toplevel_set_parent(struct foreign_toplevel *toplevel,
|
||||
struct foreign_toplevel *parent);
|
||||
+void foreign_toplevel_remove_output(struct foreign_toplevel *toplevel,
|
||||
+ struct wlr_output *output);
|
||||
void foreign_toplevel_destroy(struct foreign_toplevel *toplevel);
|
||||
|
||||
#endif /* LABWC_FOREIGN_TOPLEVEL_H */
|
||||
diff --git a/src/foreign-toplevel/foreign.c b/src/foreign-toplevel/foreign.c
|
||||
index 7f1ec8d..0f628a3 100644
|
||||
--- a/src/foreign-toplevel/foreign.c
|
||||
+++ b/src/foreign-toplevel/foreign.c
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
#include "foreign-toplevel/foreign.h"
|
||||
+#include <wlr/types/wlr_foreign_toplevel_management_v1.h>
|
||||
#include <assert.h>
|
||||
#include "common/mem.h"
|
||||
@@ -34,6 +34,18 @@ foreign_toplevel_set_parent(struct foreign_toplevel *toplevel, struct foreign_to
|
||||
parent ? &parent->wlr_toplevel : NULL);
|
||||
}
|
||||
|
||||
+void
|
||||
+foreign_toplevel_remove_output(struct foreign_toplevel *toplevel,
|
||||
+ struct wlr_output *output)
|
||||
+{
|
||||
+ if (!toplevel || !toplevel->wlr_toplevel.handle) {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ wlr_foreign_toplevel_handle_v1_output_leave(
|
||||
+ toplevel->wlr_toplevel.handle, output);
|
||||
+}
|
||||
+
|
||||
void
|
||||
foreign_toplevel_destroy(struct foreign_toplevel *toplevel)
|
||||
{
|
||||
--
|
||||
2.51.0
|
||||
@@ -1,50 +0,0 @@
|
||||
{ pkgs }:
|
||||
|
||||
pkgs.writeShellApplication {
|
||||
name = "labwc-screenshot";
|
||||
|
||||
runtimeInputs = with pkgs; [
|
||||
coreutils
|
||||
grim
|
||||
slurp
|
||||
wl-clipboard
|
||||
xdg-user-dirs
|
||||
];
|
||||
|
||||
text = ''
|
||||
mode="''${1:-region}"
|
||||
|
||||
pictures_dir="$(xdg-user-dir PICTURES)"
|
||||
if [[ -z "$pictures_dir" ]]; then
|
||||
pictures_dir="$HOME/Pictures"
|
||||
fi
|
||||
|
||||
output_dir="$pictures_dir/Screenshots"
|
||||
output_file="$output_dir/Screenshot from $(date '+%Y-%m-%d %H-%M-%S').png"
|
||||
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
case "$mode" in
|
||||
region)
|
||||
geometry="$(slurp)" || exit 0
|
||||
grim -g "$geometry" "$output_file"
|
||||
;;
|
||||
|
||||
output)
|
||||
geometry="$(slurp -o)" || exit 0
|
||||
grim -g "$geometry" "$output_file"
|
||||
;;
|
||||
|
||||
all)
|
||||
grim "$output_file"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown screenshot mode: $mode" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
wl-copy --type image/png < "$output_file"
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
# shellcheck shell=bash
|
||||
|
||||
set -o errexit -o nounset -o pipefail
|
||||
|
||||
playlist_file="$1"
|
||||
default_id="$2"
|
||||
configuration_id="$3"
|
||||
shift 3
|
||||
|
||||
state_dir="${XDG_RUNTIME_DIR:?}/linux-wallpaperengine"
|
||||
state_file="$state_dir/current"
|
||||
|
||||
usage() {
|
||||
printf '%s\n' \
|
||||
'Usage: wallpaperengine-wallpaper COMMAND [ID]' \
|
||||
'' \
|
||||
'Commands:' \
|
||||
' select ID Select a declaratively configured ID for this session' \
|
||||
' next Select the next configured ID' \
|
||||
' previous Select the previous configured ID' \
|
||||
' list List configured IDs and mark the selected one' \
|
||||
' current Print the selected ID' \
|
||||
' restart Restart the selected wallpaper' \
|
||||
' stop Stop Wallpaper Engine for this session'
|
||||
}
|
||||
|
||||
load_playlist() {
|
||||
mapfile -t wallpaper_ids < <(awk '/^[0-9]+$/ && !seen[$0]++' "$playlist_file")
|
||||
if [ "${#wallpaper_ids[@]}" -eq 0 ]; then
|
||||
echo "wallpaperengine-wallpaper: no wallpaper IDs are configured in settings.nix" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
contains_id() {
|
||||
local candidate="$1"
|
||||
local id
|
||||
|
||||
for id in "${wallpaper_ids[@]}"; do
|
||||
[ "$id" = "$candidate" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
current_id() {
|
||||
local saved_configuration=""
|
||||
local saved_id=""
|
||||
local -a saved_state=()
|
||||
|
||||
if [ -s "$state_file" ]; then
|
||||
mapfile -t saved_state <"$state_file"
|
||||
saved_configuration="${saved_state[0]:-}"
|
||||
saved_id="${saved_state[1]:-}"
|
||||
fi
|
||||
|
||||
if [ "$saved_configuration" = "$configuration_id" ] && contains_id "$saved_id"; then
|
||||
printf '%s\n' "$saved_id"
|
||||
else
|
||||
printf '%s\n' "$default_id"
|
||||
fi
|
||||
}
|
||||
|
||||
select_id() {
|
||||
local id="$1"
|
||||
|
||||
if ! contains_id "$id"; then
|
||||
echo "wallpaperengine-wallpaper: ID is not declared in settings.nix: $id" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$state_dir"
|
||||
printf '%s\n%s\n' "$configuration_id" "$id" >"$state_file"
|
||||
systemctl --user restart linux-wallpaperengine.service
|
||||
}
|
||||
|
||||
cycle() {
|
||||
local step="$1"
|
||||
local current
|
||||
local index=0
|
||||
local next_index
|
||||
|
||||
current=$(current_id)
|
||||
for i in "${!wallpaper_ids[@]}"; do
|
||||
if [ "${wallpaper_ids[$i]}" = "$current" ]; then
|
||||
index="$i"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
next_index=$(((index + step + ${#wallpaper_ids[@]}) % ${#wallpaper_ids[@]}))
|
||||
select_id "${wallpaper_ids[$next_index]}"
|
||||
}
|
||||
|
||||
load_playlist
|
||||
|
||||
command="${1:-}"
|
||||
if [ -z "$command" ]; then
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
shift
|
||||
|
||||
case "$command" in
|
||||
select)
|
||||
[ "$#" -eq 1 ] || {
|
||||
usage >&2
|
||||
exit 2
|
||||
}
|
||||
select_id "$1"
|
||||
;;
|
||||
next)
|
||||
[ "$#" -eq 0 ] || {
|
||||
usage >&2
|
||||
exit 2
|
||||
}
|
||||
cycle 1
|
||||
;;
|
||||
previous)
|
||||
[ "$#" -eq 0 ] || {
|
||||
usage >&2
|
||||
exit 2
|
||||
}
|
||||
cycle -1
|
||||
;;
|
||||
list)
|
||||
[ "$#" -eq 0 ] || {
|
||||
usage >&2
|
||||
exit 2
|
||||
}
|
||||
current=$(current_id)
|
||||
for id in "${wallpaper_ids[@]}"; do
|
||||
if [ "$id" = "$current" ]; then
|
||||
printf '* %s\n' "$id"
|
||||
else
|
||||
printf ' %s\n' "$id"
|
||||
fi
|
||||
done
|
||||
;;
|
||||
current)
|
||||
[ "$#" -eq 0 ] || {
|
||||
usage >&2
|
||||
exit 2
|
||||
}
|
||||
current_id
|
||||
;;
|
||||
restart)
|
||||
[ "$#" -eq 0 ] || {
|
||||
usage >&2
|
||||
exit 2
|
||||
}
|
||||
systemctl --user restart linux-wallpaperengine.service
|
||||
;;
|
||||
stop)
|
||||
[ "$#" -eq 0 ] || {
|
||||
usage >&2
|
||||
exit 2
|
||||
}
|
||||
systemctl --user stop linux-wallpaperengine.service
|
||||
;;
|
||||
help | -h | --help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,57 @@
|
||||
# shellcheck shell=bash
|
||||
|
||||
set -o errexit -o nounset -o pipefail
|
||||
|
||||
playlist_file="$1"
|
||||
default_id="$2"
|
||||
configuration_id="$3"
|
||||
assets_dir="$4"
|
||||
workshop_dir="$5"
|
||||
scaling="$6"
|
||||
shift 6
|
||||
|
||||
state_file="${XDG_RUNTIME_DIR:?}/linux-wallpaperengine/current"
|
||||
wallpaper_id="$default_id"
|
||||
saved_configuration=""
|
||||
saved_id=""
|
||||
saved_state=()
|
||||
outputs=()
|
||||
|
||||
if [ -s "$state_file" ]; then
|
||||
mapfile -t saved_state <"$state_file"
|
||||
saved_configuration="${saved_state[0]:-}"
|
||||
saved_id="${saved_state[1]:-}"
|
||||
|
||||
if [ "$saved_configuration" = "$configuration_id" ] &&
|
||||
awk -v id="$saved_id" '$0 == id { found = 1 } END { exit !found }' "$playlist_file"; then
|
||||
wallpaper_id="$saved_id"
|
||||
fi
|
||||
fi
|
||||
|
||||
wallpaper_path="$workshop_dir/$wallpaper_id"
|
||||
if [ ! -d "$wallpaper_path" ]; then
|
||||
echo "linux-wallpaperengine: missing declared wallpaper: $wallpaper_path" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$assets_dir" ]; then
|
||||
echo "linux-wallpaperengine: missing Wallpaper Engine assets: $assets_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mapfile -t outputs < <(wlr-randr --json | jq -r '.[] | select(.enabled == true) | .name')
|
||||
if [ "${#outputs[@]}" -eq 0 ]; then
|
||||
echo "linux-wallpaperengine: no enabled Wayland outputs were detected" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
engine_args=("$@" --assets-dir "$assets_dir")
|
||||
for output in "${outputs[@]}"; do
|
||||
engine_args+=(
|
||||
--screen-root "$output"
|
||||
--bg "$wallpaper_path"
|
||||
--scaling "$scaling"
|
||||
)
|
||||
done
|
||||
|
||||
exec linux-wallpaperengine "${engine_args[@]}"
|
||||
@@ -0,0 +1,229 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
settings = import ../settings.nix;
|
||||
inherit (settings)
|
||||
assetsDirectory
|
||||
fps
|
||||
mute
|
||||
scaling
|
||||
selectedWallpaperId
|
||||
switchIntervalSeconds
|
||||
wallpaperIds
|
||||
workshopDirectory
|
||||
;
|
||||
|
||||
resolveHomePath =
|
||||
path:
|
||||
if lib.hasPrefix "~/" path then
|
||||
"${config.home.homeDirectory}/${lib.removePrefix "~/" path}"
|
||||
else if lib.hasPrefix "/" path then
|
||||
path
|
||||
else
|
||||
"${config.home.homeDirectory}/${path}";
|
||||
|
||||
hasWallpapers = wallpaperIds != [ ];
|
||||
defaultWallpaperId =
|
||||
if selectedWallpaperId != null then
|
||||
selectedWallpaperId
|
||||
else if hasWallpapers then
|
||||
lib.head wallpaperIds
|
||||
else
|
||||
"";
|
||||
configurationId = builtins.hashString "sha256" (builtins.toJSON settings);
|
||||
resolvedAssetsDirectory = resolveHomePath assetsDirectory;
|
||||
resolvedWorkshopDirectory = resolveHomePath workshopDirectory;
|
||||
playlist = pkgs.writeText "linux-wallpaperengine-playlist" (
|
||||
lib.concatMapStringsSep "\n" (id: id) wallpaperIds + "\n"
|
||||
);
|
||||
engineArguments = lib.optional mute "--silent" ++ [
|
||||
"--fps"
|
||||
(toString fps)
|
||||
];
|
||||
|
||||
controller = pkgs.writeShellApplication {
|
||||
name = "wallpaperengine-wallpaper";
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.gawk
|
||||
pkgs.systemd
|
||||
];
|
||||
text = ''
|
||||
exec ${lib.getExe pkgs.bash} ${./controller.sh} \
|
||||
${lib.escapeShellArg playlist} \
|
||||
${lib.escapeShellArg defaultWallpaperId} \
|
||||
${lib.escapeShellArg configurationId} \
|
||||
"$@"
|
||||
'';
|
||||
};
|
||||
|
||||
mkVicinaeScript =
|
||||
{
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
command,
|
||||
mode ? "compact",
|
||||
message ? null,
|
||||
}:
|
||||
{
|
||||
name = "vicinae/scripts/wallpaper-engine/${name}";
|
||||
value = {
|
||||
executable = true;
|
||||
text = ''
|
||||
#!${lib.getExe pkgs.bash}
|
||||
# @vicinae.schemaVersion 1
|
||||
# @vicinae.title ${title}
|
||||
# @vicinae.description ${description}
|
||||
# @vicinae.mode ${mode}
|
||||
# @vicinae.icon 🖼️
|
||||
|
||||
${lib.getExe controller} ${lib.escapeShellArg command}
|
||||
${lib.optionalString (message != null) "printf '%s\\n' ${lib.escapeShellArg message}"}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
launcher = pkgs.writeShellApplication {
|
||||
name = "linux-wallpaperengine-launcher";
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.gawk
|
||||
pkgs.jq
|
||||
pkgs.linux-wallpaperengine
|
||||
pkgs.wlr-randr
|
||||
];
|
||||
text = ''
|
||||
exec ${lib.getExe pkgs.bash} ${./launcher.sh} \
|
||||
${lib.escapeShellArg playlist} \
|
||||
${lib.escapeShellArg defaultWallpaperId} \
|
||||
${lib.escapeShellArg configurationId} \
|
||||
${lib.escapeShellArg resolvedAssetsDirectory} \
|
||||
${lib.escapeShellArg resolvedWorkshopDirectory} \
|
||||
${lib.escapeShellArg scaling} \
|
||||
${lib.escapeShellArgs engineArguments}
|
||||
'';
|
||||
};
|
||||
in
|
||||
assert lib.assertMsg (lib.all (
|
||||
id: builtins.isString id && builtins.match "[0-9]+" id != null
|
||||
) wallpaperIds) "linux-wallpaperengine: wallpaperIds must contain only numeric strings";
|
||||
assert lib.assertMsg (
|
||||
lib.unique wallpaperIds == wallpaperIds
|
||||
) "linux-wallpaperengine: wallpaperIds must not contain duplicates";
|
||||
assert lib.assertMsg (
|
||||
selectedWallpaperId == null || builtins.elem selectedWallpaperId wallpaperIds
|
||||
) "linux-wallpaperengine: selectedWallpaperId must be null or a member of wallpaperIds";
|
||||
assert lib.assertMsg (
|
||||
switchIntervalSeconds == null || (builtins.isInt switchIntervalSeconds && switchIntervalSeconds > 0)
|
||||
) "linux-wallpaperengine: switchIntervalSeconds must be null or a positive integer";
|
||||
assert lib.assertMsg (
|
||||
builtins.isInt fps && fps > 0
|
||||
) "linux-wallpaperengine: fps must be a positive integer";
|
||||
assert lib.assertMsg (
|
||||
builtins.isString assetsDirectory && assetsDirectory != ""
|
||||
) "linux-wallpaperengine: assetsDirectory must be a non-empty string";
|
||||
assert lib.assertMsg (
|
||||
builtins.isString workshopDirectory && workshopDirectory != ""
|
||||
) "linux-wallpaperengine: workshopDirectory must be a non-empty string";
|
||||
assert lib.assertMsg (builtins.elem scaling [
|
||||
"default"
|
||||
"fill"
|
||||
"fit"
|
||||
"stretch"
|
||||
]) "linux-wallpaperengine: scaling must be default, fill, fit, or stretch";
|
||||
{
|
||||
home.packages = [
|
||||
controller
|
||||
pkgs.linux-wallpaperengine
|
||||
];
|
||||
|
||||
xdg.dataFile = builtins.listToAttrs [
|
||||
(mkVicinaeScript {
|
||||
name = "reload";
|
||||
title = "Reload Wallpaper Engine";
|
||||
description = "Restart the active Wallpaper Engine background";
|
||||
command = "restart";
|
||||
message = "Wallpaper Engine reloaded";
|
||||
})
|
||||
(mkVicinaeScript {
|
||||
name = "next";
|
||||
title = "Next Wallpaper Engine Wallpaper";
|
||||
description = "Switch to the next configured Wallpaper Engine background";
|
||||
command = "next";
|
||||
message = "Switched to the next Wallpaper Engine wallpaper";
|
||||
})
|
||||
(mkVicinaeScript {
|
||||
name = "previous";
|
||||
title = "Previous Wallpaper Engine Wallpaper";
|
||||
description = "Switch to the previous configured Wallpaper Engine background";
|
||||
command = "previous";
|
||||
message = "Switched to the previous Wallpaper Engine wallpaper";
|
||||
})
|
||||
(mkVicinaeScript {
|
||||
name = "list";
|
||||
title = "List Wallpaper Engine Wallpapers";
|
||||
description = "Show the configured Wallpaper Engine backgrounds";
|
||||
command = "list";
|
||||
mode = "fullOutput";
|
||||
})
|
||||
(mkVicinaeScript {
|
||||
name = "current";
|
||||
title = "Current Wallpaper Engine Wallpaper";
|
||||
description = "Show the active Wallpaper Engine background";
|
||||
command = "current";
|
||||
})
|
||||
(mkVicinaeScript {
|
||||
name = "stop";
|
||||
title = "Stop Wallpaper Engine";
|
||||
description = "Stop Wallpaper Engine for this session";
|
||||
command = "stop";
|
||||
message = "Wallpaper Engine stopped";
|
||||
})
|
||||
];
|
||||
|
||||
systemd.user.services = lib.optionalAttrs hasWallpapers {
|
||||
linux-wallpaperengine = {
|
||||
Unit = {
|
||||
Description = "Linux Wallpaper Engine";
|
||||
After = [ "graphical-session.target" ];
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
ExecStart = lib.getExe launcher;
|
||||
Restart = "on-abnormal";
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
|
||||
linux-wallpaperengine-switch = {
|
||||
Unit.Description = "Switch Linux Wallpaper Engine wallpaper";
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${lib.getExe controller} next";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.user.timers =
|
||||
lib.optionalAttrs
|
||||
(hasWallpapers && builtins.length wallpaperIds > 1 && switchIntervalSeconds != null)
|
||||
{
|
||||
linux-wallpaperengine-switch = {
|
||||
Unit = {
|
||||
Description = "Periodically switch Linux Wallpaper Engine wallpaper";
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
};
|
||||
Timer = {
|
||||
OnActiveSec = "${toString switchIntervalSeconds}s";
|
||||
OnUnitActiveSec = "${toString switchIntervalSeconds}s";
|
||||
Unit = "linux-wallpaperengine-switch.service";
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
description = "Wallpaper Engine backgrounds for Linux";
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
assetsDirectory = "~/.local/share/Steam/steamapps/common/wallpaper_engine/assets";
|
||||
workshopDirectory = "~/.local/share/Steam/steamapps/workshop/content/431960";
|
||||
|
||||
wallpaperIds = [
|
||||
"2833810156"
|
||||
"2834355367"
|
||||
"2836628501"
|
||||
"2845095254"
|
||||
"2859848175"
|
||||
"2861661119"
|
||||
"2982896307"
|
||||
];
|
||||
|
||||
# null selects the first ID above.
|
||||
selectedWallpaperId = null;
|
||||
# Set a number such as 300 to rotate through multiple IDs.
|
||||
switchIntervalSeconds = 300;
|
||||
|
||||
fps = 30;
|
||||
mute = true;
|
||||
scaling = "fill";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
homebrew = {
|
||||
enable = true;
|
||||
casks = [ "nani" ];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
tessdataBest = pkgs.runCommand "tessdata-best-jpn-eng-chi-sim" { } ''
|
||||
mkdir -p "$out"
|
||||
ln -s ${
|
||||
pkgs.fetchurl {
|
||||
url = "https://github.com/tesseract-ocr/tessdata_best/raw/main/jpn.traineddata";
|
||||
hash = "sha256-Nr35rII/WRHmJMMNBVPokLirx8MaZbPvFNqUNljEC3k=";
|
||||
}
|
||||
} "$out/jpn.traineddata"
|
||||
ln -s ${
|
||||
pkgs.fetchurl {
|
||||
url = "https://github.com/tesseract-ocr/tessdata_best/raw/main/eng.traineddata";
|
||||
hash = "sha256-goCu0Hgv4nJXpo6hD+fvMkyg+Nhb0v0UXRwrVgvLZro=";
|
||||
}
|
||||
} "$out/eng.traineddata"
|
||||
ln -s ${
|
||||
pkgs.fetchurl {
|
||||
url = "https://github.com/tesseract-ocr/tessdata_best/raw/main/chi_sim.traineddata";
|
||||
hash = "sha256-T+8tEwbI6HYW1NPkxsZ/r11EvjNCKQz48vD246p+c1s=";
|
||||
}
|
||||
} "$out/chi_sim.traineddata"
|
||||
'';
|
||||
|
||||
tesseract = pkgs.tesseract5.override {
|
||||
tessdata = tessdataBest;
|
||||
};
|
||||
|
||||
naniTranslatePrimary = pkgs.writeShellApplication {
|
||||
name = "nani-translate-primary";
|
||||
|
||||
runtimeInputs = with pkgs; [
|
||||
jq
|
||||
wl-clipboard
|
||||
xdg-utils
|
||||
];
|
||||
|
||||
text = ''
|
||||
selected_text="$(wl-paste --primary --no-newline)" || exit 0
|
||||
[ -n "$selected_text" ] || exit 0
|
||||
encoded_text="$(printf '%s' "$selected_text" | jq -sRr @uri)"
|
||||
exec xdg-open "naniapp://translate?source=$encoded_text"
|
||||
'';
|
||||
};
|
||||
|
||||
naniTranslateOcr = pkgs.writeShellApplication {
|
||||
name = "nani-translate-ocr";
|
||||
|
||||
runtimeInputs = with pkgs; [
|
||||
grim
|
||||
jq
|
||||
slurp
|
||||
tesseract
|
||||
xdg-utils
|
||||
];
|
||||
|
||||
text = ''
|
||||
geometry="$(slurp)" || exit 0
|
||||
captured_text="$(
|
||||
grim -g "$geometry" - \
|
||||
| tesseract stdin stdout -l jpn+eng+chi_sim --oem 1 --psm 6
|
||||
)"
|
||||
[ -n "$captured_text" ] || exit 0
|
||||
encoded_text="$(printf '%s' "$captured_text" | jq -sRr @uri)"
|
||||
exec xdg-open "naniapp://translate?source=$encoded_text"
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
programs.naniTranslateLinux.enable = true;
|
||||
|
||||
xdg.mimeApps.defaultApplications."x-scheme-handler/naniapp" = "nani.desktop";
|
||||
|
||||
home.packages = [
|
||||
naniTranslatePrimary
|
||||
naniTranslateOcr
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{ inputs, ... }:
|
||||
{
|
||||
description = "Nani Translate application";
|
||||
|
||||
imports.home = [
|
||||
inputs.nani-translate-linux.homeManagerModules.default
|
||||
];
|
||||
}
|
||||
@@ -25,6 +25,7 @@ in
|
||||
systemd.user.sessionVariables.NAUTILUS_4_EXTENSION_DIR = nautilusExtensionDir;
|
||||
|
||||
xdg.dataFile."nautilus-python/extensions/open-in-editor.py".text = ''
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from gi.repository import GObject, Nautilus
|
||||
@@ -75,6 +76,12 @@ in
|
||||
|
||||
return paths
|
||||
|
||||
@staticmethod
|
||||
def get_working_directory(paths):
|
||||
path = paths[0]
|
||||
|
||||
return path if os.path.isdir(path) else os.path.dirname(path)
|
||||
|
||||
@staticmethod
|
||||
def launch(_item, command):
|
||||
subprocess.Popen(
|
||||
@@ -101,6 +108,22 @@ in
|
||||
|
||||
items.append(item)
|
||||
|
||||
item = Nautilus.MenuItem(
|
||||
name=f"OpenInEditor::{context}::ghostty",
|
||||
label="Ghostty で開く",
|
||||
)
|
||||
|
||||
item.connect(
|
||||
"activate",
|
||||
self.launch,
|
||||
[
|
||||
"${lib.getExe pkgs.ghostty}",
|
||||
f"--working-directory={self.get_working_directory(paths)}",
|
||||
],
|
||||
)
|
||||
|
||||
items.append(item)
|
||||
|
||||
return items
|
||||
|
||||
def get_file_items(self, files):
|
||||
|
||||
@@ -5,4 +5,38 @@
|
||||
NH_FLAKE = "${config.home.homeDirectory}/dotfiles";
|
||||
NH_SHOW_ACTIVATION_LOGS = "1";
|
||||
};
|
||||
|
||||
programs.zsh.initContent = ''
|
||||
export SUDO_PROMPT=$'\a[sudo] authenticate for %u: '
|
||||
|
||||
nh() {
|
||||
local notify=false
|
||||
local argument
|
||||
|
||||
case "$1:$2" in
|
||||
os:switch | os:build) notify=true ;;
|
||||
esac
|
||||
|
||||
for argument in "$@"; do
|
||||
if [[ "$argument" == "--update" ]]; then
|
||||
notify=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
command nh "$@"
|
||||
local status=$?
|
||||
|
||||
if [[ "$notify" == true ]]; then
|
||||
printf '\a'
|
||||
if ((status == 0)); then
|
||||
print -P "%F{green}nh completed%f"
|
||||
else
|
||||
print -P "%F{red}nh failed (exit $status)%f"
|
||||
fi
|
||||
fi
|
||||
|
||||
return "$status"
|
||||
}
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# niri のデフォルトキーバインド。編集しないこと。
|
||||
{
|
||||
"Mod+Shift+Slash".action.show-hotkey-overlay = { };
|
||||
|
||||
|
||||
@@ -41,6 +41,37 @@ in
|
||||
hotkey-overlay.title = "Move Window to Monitor Down";
|
||||
};
|
||||
|
||||
"Mod+Tab" = {
|
||||
repeat = false;
|
||||
action.spawn = [
|
||||
"window-overview"
|
||||
"--hold"
|
||||
];
|
||||
hotkey-overlay.title = "Window Overview: Next";
|
||||
};
|
||||
"Mod+Shift+Tab" = {
|
||||
repeat = false;
|
||||
action.spawn = [
|
||||
"window-overview"
|
||||
"--hold"
|
||||
"--reverse"
|
||||
];
|
||||
hotkey-overlay.title = "Window Overview: Previous";
|
||||
};
|
||||
|
||||
"Print".action.spawn = [
|
||||
"screenshot"
|
||||
"region"
|
||||
];
|
||||
"Ctrl+Print".action.spawn = [
|
||||
"screenshot"
|
||||
"output"
|
||||
];
|
||||
"Alt+Print".action.spawn = [
|
||||
"screenshot"
|
||||
"all"
|
||||
];
|
||||
|
||||
# spawn applications (sync with labwc modules/applications/labwc/home.nix)
|
||||
"Mod+T" = {
|
||||
action.spawn = "ghostty";
|
||||
@@ -83,6 +114,24 @@ in
|
||||
];
|
||||
hotkey-overlay.title = "Clipboard History";
|
||||
};
|
||||
"Mod+J" = {
|
||||
repeat = false;
|
||||
action.spawn = [ "nani-translate-primary" ];
|
||||
hotkey-overlay.title = "Translate Primary Selection";
|
||||
};
|
||||
"Mod+Shift+J" = {
|
||||
repeat = false;
|
||||
action.spawn = [ "nani-translate-ocr" ];
|
||||
hotkey-overlay.title = "OCR and Translate with Nani";
|
||||
};
|
||||
"Mod+Ctrl+J" = {
|
||||
repeat = false;
|
||||
action.spawn = [
|
||||
(lib.getExe' pkgs.xdg-utils "xdg-open")
|
||||
"naniapp://translate"
|
||||
];
|
||||
hotkey-overlay.title = "Open Nani Translate";
|
||||
};
|
||||
"Mod+Space" = {
|
||||
action.spawn = [
|
||||
"ghostty"
|
||||
@@ -94,6 +143,25 @@ in
|
||||
action.spawn = "wdisplays";
|
||||
hotkey-overlay.title = "Display Settings: wdisplays";
|
||||
};
|
||||
"Mod+Z" = {
|
||||
action.spawn = [
|
||||
"wl-find-cursor"
|
||||
"-c"
|
||||
"0xCCFF453A"
|
||||
"-s"
|
||||
"160"
|
||||
"-d"
|
||||
"1200"
|
||||
];
|
||||
hotkey-overlay.title = "Find Cursor";
|
||||
};
|
||||
"Ctrl+Space" = {
|
||||
action.spawn = [
|
||||
"handy"
|
||||
"--toggle-transcription"
|
||||
];
|
||||
hotkey-overlay.title = "Toggle Handy Transcription";
|
||||
};
|
||||
|
||||
# Function keys (sync with labwc modules/applications/labwc/home.nix)
|
||||
"XF86AudioRaiseVolume".action.spawn = [
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
{
|
||||
description = "niri Wayland compositor";
|
||||
|
||||
includes = [ "systems.wayland" ];
|
||||
includes = [
|
||||
"systems.wayland"
|
||||
"applications.screenshot"
|
||||
"applications.wl-find-cursor"
|
||||
];
|
||||
|
||||
imports = {
|
||||
nixos = [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -12,6 +13,15 @@ let
|
||||
--replace-fail "@codexbarPath@" "${lib.getExe codexbar}"
|
||||
'';
|
||||
|
||||
noctaliaPatched =
|
||||
inputs.noctalia.packages.${pkgs.stdenv.hostPlatform.system}.default.overrideAttrs
|
||||
(oldAttrs: {
|
||||
patches = (oldAttrs.patches or [ ]) ++ [
|
||||
./patches/window-switcher-labwc.patch
|
||||
./patches/taskbar-overview-hook.patch
|
||||
];
|
||||
});
|
||||
|
||||
wallpapers = pkgs.fetchFromGitHub {
|
||||
owner = "moons-14";
|
||||
repo = "wallpapers";
|
||||
@@ -41,6 +51,8 @@ in
|
||||
programs.noctalia = {
|
||||
enable = true;
|
||||
|
||||
package = noctaliaPatched;
|
||||
|
||||
systemd.enable = true;
|
||||
|
||||
settings = {
|
||||
@@ -171,10 +183,11 @@ in
|
||||
"org.gnome.Nautilus"
|
||||
"org.gnome.TextEditor"
|
||||
"com.mitchellh.ghostty"
|
||||
"thunderbird"
|
||||
"google-chrome"
|
||||
"code"
|
||||
"dev.zed.Zed"
|
||||
"codex-desktop.desktop"
|
||||
"codex-desktop"
|
||||
"vesktop"
|
||||
];
|
||||
show_all_outputs = true;
|
||||
@@ -193,7 +206,7 @@ in
|
||||
};
|
||||
|
||||
wallpaper = {
|
||||
enabled = true;
|
||||
enabled = lib.mkDefault true;
|
||||
directory = "~/.wallpapers";
|
||||
fill_mode = "crop";
|
||||
automation = {
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
{
|
||||
description = "Noctalia Wayland desktop shell";
|
||||
|
||||
includes = [
|
||||
"applications.window-overview"
|
||||
];
|
||||
|
||||
imports = {
|
||||
nixos = [ inputs.noctalia.nixosModules.default ];
|
||||
home = [ inputs.noctalia.homeModules.default ];
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
diff --git a/src/shell/bar/widgets/taskbar_widget.cpp b/src/shell/bar/widgets/taskbar_widget.cpp
|
||||
--- a/src/shell/bar/widgets/taskbar_widget.cpp
|
||||
+++ b/src/shell/bar/widgets/taskbar_widget.cpp
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "compositors/workspace_backend.h"
|
||||
#include "config/config_service.h"
|
||||
#include "core/deferred_call.h"
|
||||
+#include "core/process/process.h"
|
||||
#include "i18n/i18n.h"
|
||||
#include "render/core/color.h"
|
||||
#include "render/core/renderer.h"
|
||||
@@ -36,6 +37,18 @@
|
||||
|
||||
namespace {
|
||||
|
||||
+ [[nodiscard]] bool launchTaskbarOverview(const std::string& appId) {
|
||||
+ constexpr const char* command = "noctalia-taskbar-overview";
|
||||
+ if (appId.empty() || !process::commandExists(command)) {
|
||||
+ return false;
|
||||
+ }
|
||||
+ return process::runAsync({
|
||||
+ command,
|
||||
+ "--app-id",
|
||||
+ appId.c_str(),
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
// Integer centering; optional odd spare pixel on the end side (right/bottom).
|
||||
[[nodiscard]] float centeredOffset(float extent, float content, float inset = 0.0F, bool oddSpareOnEnd = true) {
|
||||
const float inner = std::max(0.0F, extent - inset * 2.0F);
|
||||
@@ -373,6 +386,12 @@ void TaskbarWidget::activateOrLaunchPinned(const TaskModel& task) {
|
||||
return;
|
||||
}
|
||||
|
||||
+ const std::string overviewAppId =
|
||||
+ !task.appId.empty() ? task.appId : (!task.desktopEntryId.empty() ? task.desktopEntryId : task.idLower);
|
||||
+ if (launchTaskbarOverview(overviewAppId)) {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
const std::string cycleKey = !task.desktopEntryId.empty() ? task.desktopEntryId : task.idLower;
|
||||
std::size_t& cursor = m_groupedAppCycleCursor[cycleKey];
|
||||
if (cursor >= windows.size()) {
|
||||
@@ -0,0 +1,26 @@
|
||||
diff --git a/src/shell/switcher/window_switcher.cpp b/src/shell/switcher/window_switcher.cpp
|
||||
--- a/src/shell/switcher/window_switcher.cpp
|
||||
+++ b/src/shell/switcher/window_switcher.cpp
|
||||
@@ -286,12 +286,19 @@ indexLiveToplevelsByWindowId(const CompositorPlatform& platform, std::unordered_
|
||||
continue;
|
||||
}
|
||||
|
||||
- const auto mappedId = platform.compositorWindowIdForToplevelInfo(info);
|
||||
- if (!mappedId.has_value() || mappedId->empty()) {
|
||||
- continue;
|
||||
+ std::string key;
|
||||
+ if (const auto mappedId = platform.compositorWindowIdForToplevelInfo(info);
|
||||
+ mappedId.has_value() && !mappedId->empty()) {
|
||||
+ key = canonicalWindowId(*mappedId);
|
||||
}
|
||||
- const std::string key = canonicalWindowId(*mappedId);
|
||||
+
|
||||
+ // Generic wlroots compositors such as labwc do not provide a
|
||||
+ // compositor-specific window ID. The wlr toplevel handle is stable
|
||||
+ // for the lifetime of the window and is sufficient for switcher identity.
|
||||
+ if (key.empty() && wlrHandle != 0) {
|
||||
+ key = "toplevel:" + std::to_string(wlrHandle);
|
||||
+ }
|
||||
if (key.empty()) {
|
||||
continue;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
home.packages = [
|
||||
(pkgs.writeShellApplication {
|
||||
name = "screenshot";
|
||||
|
||||
runtimeInputs = with pkgs; [
|
||||
coreutils
|
||||
grim
|
||||
slurp
|
||||
wl-clipboard
|
||||
xdg-user-dirs
|
||||
];
|
||||
|
||||
text = ''
|
||||
mode="''${1:-region}"
|
||||
|
||||
pictures_dir="$(xdg-user-dir PICTURES)"
|
||||
if [[ -z "$pictures_dir" ]]; then
|
||||
pictures_dir="$HOME/Pictures"
|
||||
fi
|
||||
|
||||
output_dir="$pictures_dir/Screenshots"
|
||||
output_file="$output_dir/Screenshot from $(date '+%Y-%m-%d %H-%M-%S').png"
|
||||
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
case "$mode" in
|
||||
region)
|
||||
geometry="$(slurp)" || exit 0
|
||||
grim -g "$geometry" "$output_file"
|
||||
;;
|
||||
|
||||
output)
|
||||
geometry="$(slurp -o)" || exit 0
|
||||
grim -g "$geometry" "$output_file"
|
||||
;;
|
||||
|
||||
all)
|
||||
grim "$output_file"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown screenshot mode: $mode" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
wl-copy --type image/png < "$output_file"
|
||||
'';
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
description = "Wayland screenshot command";
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
programs.thunderbird = {
|
||||
enable = true;
|
||||
package = pkgs.thunderbird;
|
||||
|
||||
languagePacks = [ "ja" ];
|
||||
|
||||
policies = {
|
||||
DisableTelemetry = true;
|
||||
DisableAppUpdate = true;
|
||||
InAppNotification_Disabled = true;
|
||||
};
|
||||
|
||||
settings = {
|
||||
"mailnews.start_page.enabled" = false;
|
||||
"mail.shell.checkDefaultClient" = false;
|
||||
};
|
||||
|
||||
profiles.default = {
|
||||
isDefault = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
_: {
|
||||
xdg.mimeApps = {
|
||||
enable = true;
|
||||
|
||||
defaultApplications = {
|
||||
"x-scheme-handler/mailto" = "thunderbird.desktop";
|
||||
"message/rfc822" = "thunderbird.desktop";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -118,7 +118,7 @@ let
|
||||
];
|
||||
userSettings = {
|
||||
"nix.enableLanguageServer" = true;
|
||||
"nix.serverPath" = "nil";
|
||||
"nix.serverPath" = "nixd";
|
||||
"nix.serverSettings" = {
|
||||
nixd = {
|
||||
formatting.command = [ "nixfmt" ];
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
windowOverview = pkgs.callPackage ../package.nix { };
|
||||
in
|
||||
{
|
||||
home.packages = [ windowOverview ];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
description = "Fullscreen Wayland window overview with captured previews";
|
||||
|
||||
includes = [
|
||||
"systems.wayland"
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
lib,
|
||||
fetchCrate,
|
||||
libgbm,
|
||||
libglvnd,
|
||||
libxkbcommon,
|
||||
jq,
|
||||
makeWrapper,
|
||||
pkg-config,
|
||||
rustPlatform,
|
||||
wayland,
|
||||
wl-clipboard,
|
||||
}:
|
||||
rustPlatform.buildRustPackage rec {
|
||||
pname = "window-overview";
|
||||
version = "1.5.0";
|
||||
|
||||
src = fetchCrate {
|
||||
pname = "wlr-chooser";
|
||||
inherit version;
|
||||
hash = "sha256-Jb59m1z+2G5istcbC0sg9jRVcC/bQh/OY0ny9OdTsdw=";
|
||||
};
|
||||
|
||||
patches = [
|
||||
./patches/window-overview.patch
|
||||
./patches/niri-backend.patch
|
||||
];
|
||||
|
||||
cargoHash = "sha256-KBLgtS8ULrmsOf6BN5SlkVQyXB12utvr/KonnKnCTCM=";
|
||||
|
||||
nativeBuildInputs = [
|
||||
makeWrapper
|
||||
pkg-config
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
libgbm
|
||||
libglvnd
|
||||
libxkbcommon
|
||||
wayland
|
||||
];
|
||||
|
||||
cargoBuildFlags = [
|
||||
"--bin"
|
||||
"wlr-switcher"
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
mv "$out/bin/wlr-switcher" "$out/bin/.window-overview-wrapped"
|
||||
makeWrapper "$out/bin/.window-overview-wrapped" "$out/bin/window-overview" \
|
||||
--add-flags "--layout grid" \
|
||||
--prefix PATH : "${
|
||||
lib.makeBinPath [
|
||||
jq
|
||||
wl-clipboard
|
||||
]
|
||||
}" \
|
||||
--prefix LD_LIBRARY_PATH : "${
|
||||
lib.makeLibraryPath [
|
||||
libgbm
|
||||
libglvnd
|
||||
]
|
||||
}:/run/opengl-driver/lib"
|
||||
ln -s window-overview "$out/bin/noctalia-taskbar-overview"
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Fullscreen Wayland window overview with captured previews";
|
||||
homepage = "https://github.com/sjourdois/wlr-utils";
|
||||
license = with lib.licenses; [
|
||||
asl20
|
||||
mit
|
||||
];
|
||||
mainProgram = "window-overview";
|
||||
platforms = lib.platforms.linux;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
--- a/src/chooser_cli.rs
|
||||
+++ b/src/chooser_cli.rs
|
||||
@@ -81,6 +81,7 @@
|
||||
initial_cycle: None,
|
||||
snapshot: false,
|
||||
cycle_socket: None,
|
||||
+ niri_backend: false,
|
||||
};
|
||||
|
||||
match run_overlay(opts, t0) {
|
||||
--- a/src/lib.rs
|
||||
+++ b/src/lib.rs
|
||||
@@ -65,7 +65,14 @@
|
||||
// must connect, enumerate, and open sessions before any thumbnail appears.
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let snapshot = opts.snapshot;
|
||||
- std::thread::spawn(move || ui::capture_thread(tx, snapshot));
|
||||
+ let niri_backend = opts.niri_backend;
|
||||
+ std::thread::spawn(move || {
|
||||
+ if niri_backend {
|
||||
+ ui::niri_capture_thread(tx);
|
||||
+ } else {
|
||||
+ ui::capture_thread(tx, snapshot);
|
||||
+ }
|
||||
+ });
|
||||
shell::tlog(t0, "capture-thread spawned");
|
||||
|
||||
let out: ui::Outcome = Arc::new(Mutex::new(None));
|
||||
--- a/src/switcher_cli.rs
|
||||
+++ b/src/switcher_cli.rs
|
||||
@@ -88,6 +88,7 @@
|
||||
let t0 = Instant::now();
|
||||
let cli = Cli::parse();
|
||||
i18n::init();
|
||||
+ let niri_backend = std::env::var_os("NIRI_SOCKET").is_some();
|
||||
|
||||
if cli.doctor {
|
||||
if let Err(e) = wlr_capture::doctor::report("wlr-switcher", env!("CARGO_PKG_VERSION")) {
|
||||
@@ -129,32 +130,50 @@
|
||||
initial_cycle: Some(!cli.reverse),
|
||||
snapshot: true,
|
||||
cycle_socket: Some(cycle_socket),
|
||||
+ niri_backend,
|
||||
};
|
||||
|
||||
// Pre-flight: wlr-switcher switches *windows*, which need the foreign-toplevel
|
||||
// capture source (wlroots >= 0.20 / Sway >= 1.12). On older compositors connect()
|
||||
// now succeeds for screen-only capture, but there are no windows to offer — so say
|
||||
// so clearly and exit, instead of showing an empty dimmed overlay (issue #1).
|
||||
- match wl::Client::connect() {
|
||||
- Ok(client) if !client.can_capture_windows() => {
|
||||
- eprintln!("{}", tr!("capture-no-window"));
|
||||
- std::process::exit(2);
|
||||
- }
|
||||
- Ok(_) => {}
|
||||
- Err(e) => {
|
||||
- eprintln!("{}", tr!("error", error = format!("{e:#}")));
|
||||
- std::process::exit(2);
|
||||
+ if !niri_backend {
|
||||
+ match wl::Client::connect() {
|
||||
+ Ok(client) if !client.can_capture_windows() => {
|
||||
+ eprintln!("{}", tr!("capture-no-window"));
|
||||
+ std::process::exit(2);
|
||||
+ }
|
||||
+ Ok(_) => {}
|
||||
+ Err(e) => {
|
||||
+ eprintln!("{}", tr!("error", error = format!("{e:#}")));
|
||||
+ std::process::exit(2);
|
||||
+ }
|
||||
}
|
||||
}
|
||||
|
||||
match run_overlay(opts, t0) {
|
||||
Ok(Some(sel)) => {
|
||||
// Focus the picked window (outputs aren't focusable, so ignore them).
|
||||
- if sel.is_window
|
||||
- && let Err(e) = wl::activate_window(&sel.app_id, &sel.title, sel.dup_index)
|
||||
- {
|
||||
- eprintln!("{}", tr!("error", error = format!("{e:#}")));
|
||||
- std::process::exit(2);
|
||||
+ if sel.is_window {
|
||||
+ let result: anyhow::Result<()> = if niri_backend {
|
||||
+ let output = std::process::Command::new("niri")
|
||||
+ .args(["msg", "action", "focus-window", "--id", &sel.identifier])
|
||||
+ .output();
|
||||
+ match output {
|
||||
+ Ok(output) if output.status.success() => Ok(()),
|
||||
+ Ok(output) => Err(anyhow::anyhow!(
|
||||
+ "{}",
|
||||
+ String::from_utf8_lossy(&output.stderr).trim()
|
||||
+ )),
|
||||
+ Err(e) => Err(e.into()),
|
||||
+ }
|
||||
+ } else {
|
||||
+ wl::activate_window(&sel.app_id, &sel.title, sel.dup_index).map_err(Into::into)
|
||||
+ };
|
||||
+ if let Err(e) = result {
|
||||
+ eprintln!("{}", tr!("error", error = format!("{e:#}")));
|
||||
+ std::process::exit(2);
|
||||
+ }
|
||||
}
|
||||
}
|
||||
Ok(None) => std::process::exit(1), // cancelled
|
||||
--- a/src/ui.rs
|
||||
+++ b/src/ui.rs
|
||||
@@ -6,7 +6,9 @@
|
||||
|
||||
use crate::tr;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
+use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
+use std::process::{Command, Stdio};
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -366,6 +368,288 @@
|
||||
}
|
||||
}
|
||||
|
||||
+#[derive(Clone)]
|
||||
+struct NiriWindow {
|
||||
+ id: String,
|
||||
+ app_id: String,
|
||||
+ title: String,
|
||||
+}
|
||||
+
|
||||
+enum ClipboardSnapshot {
|
||||
+ Empty,
|
||||
+ Content { mime: String, data: Vec<u8> },
|
||||
+ Unavailable,
|
||||
+}
|
||||
+
|
||||
+fn save_clipboard() -> ClipboardSnapshot {
|
||||
+ let Ok(types) = Command::new("wl-paste").arg("--list-types").output() else {
|
||||
+ return ClipboardSnapshot::Unavailable;
|
||||
+ };
|
||||
+ if !types.status.success() {
|
||||
+ return ClipboardSnapshot::Empty;
|
||||
+ }
|
||||
+ let types = String::from_utf8_lossy(&types.stdout);
|
||||
+ let offered: Vec<&str> = types.lines().filter(|line| !line.is_empty()).collect();
|
||||
+ let Some(mime) = offered
|
||||
+ .iter()
|
||||
+ .copied()
|
||||
+ .find(|mime| *mime == "text/plain;charset=utf-8")
|
||||
+ .or_else(|| offered.first().copied())
|
||||
+ else {
|
||||
+ return ClipboardSnapshot::Empty;
|
||||
+ };
|
||||
+ let Ok(data) = Command::new("wl-paste")
|
||||
+ .args(["--type", mime])
|
||||
+ .output()
|
||||
+ else {
|
||||
+ return ClipboardSnapshot::Unavailable;
|
||||
+ };
|
||||
+ if !data.status.success() {
|
||||
+ return ClipboardSnapshot::Unavailable;
|
||||
+ }
|
||||
+ ClipboardSnapshot::Content {
|
||||
+ mime: mime.to_owned(),
|
||||
+ data: data.stdout,
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+fn restore_clipboard(snapshot: ClipboardSnapshot) {
|
||||
+ match snapshot {
|
||||
+ ClipboardSnapshot::Empty => {
|
||||
+ let _ = Command::new("wl-copy").arg("--clear").status();
|
||||
+ }
|
||||
+ ClipboardSnapshot::Content { mime, data } => {
|
||||
+ let Ok(mut copy) = Command::new("wl-copy")
|
||||
+ .args(["--type", &mime])
|
||||
+ .stdin(Stdio::piped())
|
||||
+ .spawn()
|
||||
+ else {
|
||||
+ return;
|
||||
+ };
|
||||
+ if let Some(mut stdin) = copy.stdin.take() {
|
||||
+ let _ = stdin.write_all(&data);
|
||||
+ }
|
||||
+ let _ = copy.wait();
|
||||
+ }
|
||||
+ ClipboardSnapshot::Unavailable => {}
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/// Decode one field emitted by jq's `@tsv` formatter. It escapes the only
|
||||
+/// characters that would otherwise make the line-oriented transport ambiguous.
|
||||
+fn unescape_tsv(value: &str) -> String {
|
||||
+ let mut decoded = String::with_capacity(value.len());
|
||||
+ let mut chars = value.chars();
|
||||
+ while let Some(ch) = chars.next() {
|
||||
+ if ch != '\\' {
|
||||
+ decoded.push(ch);
|
||||
+ continue;
|
||||
+ }
|
||||
+ match chars.next() {
|
||||
+ Some('t') => decoded.push('\t'),
|
||||
+ Some('r') => decoded.push('\r'),
|
||||
+ Some('n') => decoded.push('\n'),
|
||||
+ Some('\\') => decoded.push('\\'),
|
||||
+ Some(other) => {
|
||||
+ decoded.push('\\');
|
||||
+ decoded.push(other);
|
||||
+ }
|
||||
+ None => decoded.push('\\'),
|
||||
+ }
|
||||
+ }
|
||||
+ decoded
|
||||
+}
|
||||
+
|
||||
+/// Ask niri for its toplevel list. niri does not implement the
|
||||
+/// ext-image-copy-capture window protocol, so its IPC is the source of both the
|
||||
+/// stable window id and the metadata used by this backend.
|
||||
+fn niri_windows() -> Result<Vec<NiriWindow>, String> {
|
||||
+ let response = Command::new("niri")
|
||||
+ .args(["msg", "-j", "windows"])
|
||||
+ .output()
|
||||
+ .map_err(|e| format!("could not run niri msg: {e}"))?;
|
||||
+ if !response.status.success() {
|
||||
+ return Err(String::from_utf8_lossy(&response.stderr).trim().to_owned());
|
||||
+ }
|
||||
+
|
||||
+ let mut jq = Command::new("jq")
|
||||
+ .args([
|
||||
+ "-r",
|
||||
+ ".[] | [(.id | tostring), (.app_id // \"\"), (.title // \"\")] | @tsv",
|
||||
+ ])
|
||||
+ .stdin(Stdio::piped())
|
||||
+ .stdout(Stdio::piped())
|
||||
+ .stderr(Stdio::piped())
|
||||
+ .spawn()
|
||||
+ .map_err(|e| format!("could not run jq: {e}"))?;
|
||||
+ jq.stdin
|
||||
+ .take()
|
||||
+ .ok_or_else(|| String::from("jq stdin was unavailable"))?
|
||||
+ .write_all(&response.stdout)
|
||||
+ .map_err(|e| format!("could not pass niri window list to jq: {e}"))?;
|
||||
+ let output = jq
|
||||
+ .wait_with_output()
|
||||
+ .map_err(|e| format!("could not read jq output: {e}"))?;
|
||||
+ if !output.status.success() {
|
||||
+ return Err(String::from_utf8_lossy(&output.stderr).trim().to_owned());
|
||||
+ }
|
||||
+
|
||||
+ let text = String::from_utf8(output.stdout)
|
||||
+ .map_err(|e| format!("niri window list was not UTF-8: {e}"))?;
|
||||
+ let mut windows = Vec::new();
|
||||
+ for line in text.lines() {
|
||||
+ let mut fields = line.splitn(3, '\t');
|
||||
+ let Some(id) = fields.next() else { continue };
|
||||
+ let Some(app_id) = fields.next() else { continue };
|
||||
+ let Some(title) = fields.next() else { continue };
|
||||
+ windows.push(NiriWindow {
|
||||
+ id: id.to_owned(),
|
||||
+ app_id: unescape_tsv(app_id),
|
||||
+ title: unescape_tsv(title),
|
||||
+ });
|
||||
+ }
|
||||
+ windows.sort_by(|a, b| {
|
||||
+ a.app_id
|
||||
+ .to_lowercase()
|
||||
+ .cmp(&b.app_id.to_lowercase())
|
||||
+ .then_with(|| a.title.to_lowercase().cmp(&b.title.to_lowercase()))
|
||||
+ });
|
||||
+ Ok(windows)
|
||||
+}
|
||||
+
|
||||
+fn niri_window_source(w: &NiriWindow, dup_index: usize) -> Source {
|
||||
+ let is_system = w.app_id.is_empty();
|
||||
+ let (title, subtitle) = if is_system {
|
||||
+ (w.title.clone(), String::new())
|
||||
+ } else {
|
||||
+ (w.app_id.clone(), w.title.clone())
|
||||
+ };
|
||||
+ Source {
|
||||
+ key: w.id.clone(),
|
||||
+ token: format!("Window: {}", w.id),
|
||||
+ filter: format!("{} {}", w.app_id, w.title).to_lowercase(),
|
||||
+ title,
|
||||
+ subtitle,
|
||||
+ is_window: true,
|
||||
+ is_system,
|
||||
+ app_id: w.app_id.clone(),
|
||||
+ win_title: w.title.clone(),
|
||||
+ dup_index,
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/// Capture a static overview through niri's IPC. This is deliberately separate
|
||||
+/// from the generic Wayland backend: niri can render any toplevel by id (even an
|
||||
+/// occluded one), but does not advertise ext-image-copy-capture.
|
||||
+pub fn niri_capture_thread(tx: Sender<Msg>) {
|
||||
+ let windows = match niri_windows() {
|
||||
+ Ok(windows) => windows,
|
||||
+ Err(e) => {
|
||||
+ eprintln!("niri overview backend: {e}");
|
||||
+ return;
|
||||
+ }
|
||||
+ };
|
||||
+
|
||||
+ let mut dup: HashMap<(String, String), usize> = HashMap::new();
|
||||
+ let mut current = Vec::with_capacity(windows.len());
|
||||
+ for window in windows {
|
||||
+ let e = dup
|
||||
+ .entry((window.app_id.clone(), window.title.clone()))
|
||||
+ .or_insert(0);
|
||||
+ let source = niri_window_source(&window, *e);
|
||||
+ *e += 1;
|
||||
+ current.push((source, window));
|
||||
+ }
|
||||
+ if tx
|
||||
+ .send(Msg::Sources(
|
||||
+ current.iter().map(|(source, _)| source.clone()).collect(),
|
||||
+ ))
|
||||
+ .is_err()
|
||||
+ {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ let capture_dir = wlr_capture::paths::runtime_dir().join(format!(
|
||||
+ "window-overview-niri-{}",
|
||||
+ std::process::id()
|
||||
+ ));
|
||||
+ if let Err(e) = std::fs::create_dir_all(&capture_dir) {
|
||||
+ eprintln!("niri overview backend: could not create capture directory: {e}");
|
||||
+ return;
|
||||
+ }
|
||||
+ // niri's screenshot action also sets the clipboard. Preserve the existing
|
||||
+ // selection so opening the overview does not unexpectedly replace it.
|
||||
+ let clipboard = save_clipboard();
|
||||
+
|
||||
+ for (source, window) in current {
|
||||
+ if let Some(path) = icons::resolve(&window.app_id)
|
||||
+ && let Some((w, h, rgba)) = icons::load(&path, 128)
|
||||
+ && tx
|
||||
+ .send(Msg::Icon {
|
||||
+ key: source.key.clone(),
|
||||
+ w: w as usize,
|
||||
+ h: h as usize,
|
||||
+ rgba,
|
||||
+ })
|
||||
+ .is_err()
|
||||
+ {
|
||||
+ break;
|
||||
+ }
|
||||
+
|
||||
+ let path = capture_dir.join(format!("{}.png", window.id));
|
||||
+ let command = Command::new("niri")
|
||||
+ .args(["msg", "action", "screenshot-window", "--id"])
|
||||
+ .arg(&window.id)
|
||||
+ .args(["--write-to-disk", "true", "--show-pointer", "false", "--path"])
|
||||
+ .arg(&path)
|
||||
+ .output();
|
||||
+ let Ok(output) = command else {
|
||||
+ continue;
|
||||
+ };
|
||||
+ if !output.status.success() {
|
||||
+ eprintln!(
|
||||
+ "niri overview backend: could not capture window {}: {}",
|
||||
+ window.id,
|
||||
+ String::from_utf8_lossy(&output.stderr).trim()
|
||||
+ );
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ // niri encodes PNGs off the compositor thread, after acknowledging the
|
||||
+ // IPC action. Wait briefly for that completion without delaying the UI.
|
||||
+ let mut bytes = None;
|
||||
+ for _ in 0..200 {
|
||||
+ match std::fs::read(&path) {
|
||||
+ Ok(data) => {
|
||||
+ bytes = Some(data);
|
||||
+ break;
|
||||
+ }
|
||||
+ Err(_) => std::thread::sleep(Duration::from_millis(10)),
|
||||
+ }
|
||||
+ }
|
||||
+ let Some(bytes) = bytes else { continue };
|
||||
+ let Ok(image) = image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) else {
|
||||
+ continue;
|
||||
+ };
|
||||
+ let image = image.into_rgba8();
|
||||
+ let (w, h, rgba) = thumbnail_rgba(image.width(), image.height(), image.into_raw());
|
||||
+ if tx
|
||||
+ .send(Msg::Thumb {
|
||||
+ key: source.key,
|
||||
+ w,
|
||||
+ h,
|
||||
+ rgba,
|
||||
+ })
|
||||
+ .is_err()
|
||||
+ {
|
||||
+ break;
|
||||
+ }
|
||||
+ let _ = std::fs::remove_file(path);
|
||||
+ }
|
||||
+ let _ = std::fs::remove_dir(capture_dir);
|
||||
+ restore_clipboard(clipboard);
|
||||
+}
|
||||
+
|
||||
/// Cheap content fingerprint of a frame (subsampled FNV-1a), to tell whether a
|
||||
/// capture actually changed between rounds — used by the headless bench.
|
||||
fn quick_hash(rgba: &[u8]) -> u64 {
|
||||
@@ -515,11 +799,14 @@
|
||||
|
||||
/// Downscale a capture to a thumbnail (max side `THUMB_MAX`), never upscaling.
|
||||
fn thumbnail(img: wl::CapturedImage) -> (usize, usize, Vec<u8>) {
|
||||
- let (w, h) = (img.width, img.height);
|
||||
+ thumbnail_rgba(img.width, img.height, img.rgba)
|
||||
+}
|
||||
+
|
||||
+fn thumbnail_rgba(w: u32, h: u32, rgba: Vec<u8>) -> (usize, usize, Vec<u8>) {
|
||||
let scale = (THUMB_MAX as f32 / w as f32)
|
||||
.min(THUMB_MAX as f32 / h as f32)
|
||||
.min(1.0);
|
||||
- let src = match image::RgbaImage::from_raw(w, h, img.rgba) {
|
||||
+ let src = match image::RgbaImage::from_raw(w, h, rgba) {
|
||||
Some(s) => s,
|
||||
None => return (0, 0, Vec::new()),
|
||||
};
|
||||
@@ -557,6 +844,9 @@
|
||||
pub snapshot: bool,
|
||||
/// Receives next/previous commands from repeated compositor keybind launches.
|
||||
pub cycle_socket: Option<std::os::unix::net::UnixDatagram>,
|
||||
+ /// Use niri's IPC for window enumeration and snapshots instead of the
|
||||
+ /// ext-image-copy-capture protocol that niri does not implement.
|
||||
+ pub niri_backend: bool,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
@@ -0,0 +1,336 @@
|
||||
--- a/src/chooser_cli.rs
|
||||
+++ b/src/chooser_cli.rs
|
||||
@@ -79,0 +80,4 @@
|
||||
+ app_id_filter: None,
|
||||
+ initial_cycle: None,
|
||||
+ snapshot: false,
|
||||
+ cycle_socket: None,
|
||||
--- a/src/lib.rs
|
||||
+++ b/src/lib.rs
|
||||
@@ -33,3 +33,3 @@
|
||||
-/// Returns the held lock file (keep it alive), or `None` if another instance owns
|
||||
-/// it — sway processes its own keybinding even over our exclusive keyboard grab,
|
||||
-/// so re-pressing the bind would otherwise stack overlays.
|
||||
+/// Returns the held lock and command socket for the first instance. Later
|
||||
+/// invocations forward their requested cycle direction through the socket and
|
||||
+/// return `None`, which also supports compositors that keep handling the binding.
|
||||
@@ -36 +36,3 @@
|
||||
-pub fn acquire_switch_lock() -> Option<std::fs::File> {
|
||||
+pub fn acquire_switch_lock(
|
||||
+ forward: bool,
|
||||
+) -> Option<(std::fs::File, std::os::unix::net::UnixDatagram)> {
|
||||
@@ -38,0 +41 @@
|
||||
+ let socket_path = dir.join("wlr-switcher.sock");
|
||||
@@ -45,2 +49,10 @@
|
||||
- flock(&f, FlockOperation::NonBlockingLockExclusive).ok()?;
|
||||
- Some(f)
|
||||
+ if flock(&f, FlockOperation::NonBlockingLockExclusive).is_err() {
|
||||
+ let socket = std::os::unix::net::UnixDatagram::unbound().ok()?;
|
||||
+ let command = if forward { b"next" } else { b"prev" };
|
||||
+ let _ = socket.send_to(command, socket_path);
|
||||
+ return None;
|
||||
+ }
|
||||
+ let _ = std::fs::remove_file(&socket_path);
|
||||
+ let socket = std::os::unix::net::UnixDatagram::bind(socket_path).ok()?;
|
||||
+ socket.set_nonblocking(true).ok()?;
|
||||
+ Some((f, socket))
|
||||
@@ -56 +68,2 @@
|
||||
- std::thread::spawn(move || ui::capture_thread(tx));
|
||||
+ let snapshot = opts.snapshot;
|
||||
+ std::thread::spawn(move || ui::capture_thread(tx, snapshot));
|
||||
--- a/src/shell.rs
|
||||
+++ b/src/shell.rs
|
||||
@@ -617,0 +618,3 @@
|
||||
+ Keysym::plus | Keysym::KP_Add => Key::Plus,
|
||||
+ Keysym::equal => Key::Equals,
|
||||
+ Keysym::minus | Keysym::KP_Subtract => Key::Minus,
|
||||
--- a/src/switcher_cli.rs
|
||||
+++ b/src/switcher_cli.rs
|
||||
@@ -75,0 +76,6 @@
|
||||
+ /// Show only windows whose Wayland app-id exactly matches this value.
|
||||
+ #[arg(long)]
|
||||
+ app_id: Option<String>,
|
||||
+ /// Select the previous window when the overview first opens.
|
||||
+ #[arg(long)]
|
||||
+ reverse: bool,
|
||||
@@ -96,2 +102,2 @@
|
||||
- let _lock = match acquire_switch_lock() {
|
||||
- Some(lock) => lock,
|
||||
+ let (_lock, cycle_socket) = match acquire_switch_lock(!cli.reverse) {
|
||||
+ Some(instance) => instance,
|
||||
@@ -121,0 +128,4 @@
|
||||
+ app_id_filter: cli.app_id,
|
||||
+ initial_cycle: Some(!cli.reverse),
|
||||
+ snapshot: true,
|
||||
+ cycle_socket: Some(cycle_socket),
|
||||
--- a/src/ui.rs
|
||||
+++ b/src/ui.rs
|
||||
@@ -8,0 +9 @@
|
||||
+use std::path::PathBuf;
|
||||
@@ -41,0 +43,32 @@
|
||||
+const DEFAULT_OVERVIEW_SCALE: f32 = 0.85;
|
||||
+const MIN_OVERVIEW_SCALE: f32 = 0.55;
|
||||
+const MAX_OVERVIEW_SCALE: f32 = 1.0;
|
||||
+
|
||||
+fn overview_scale_path() -> Option<PathBuf> {
|
||||
+ if let Some(path) = std::env::var_os("XDG_STATE_HOME") {
|
||||
+ return Some(PathBuf::from(path).join("window-overview/scale"));
|
||||
+ }
|
||||
+ std::env::var_os("HOME")
|
||||
+ .map(PathBuf::from)
|
||||
+ .map(|path| path.join(".local/state/window-overview/scale"))
|
||||
+}
|
||||
+
|
||||
+fn load_overview_scale() -> f32 {
|
||||
+ overview_scale_path()
|
||||
+ .and_then(|path| std::fs::read_to_string(path).ok())
|
||||
+ .and_then(|value| value.trim().parse::<f32>().ok())
|
||||
+ .unwrap_or(DEFAULT_OVERVIEW_SCALE)
|
||||
+ .clamp(MIN_OVERVIEW_SCALE, MAX_OVERVIEW_SCALE)
|
||||
+}
|
||||
+
|
||||
+fn save_overview_scale(scale: f32) {
|
||||
+ let Some(path) = overview_scale_path() else {
|
||||
+ return;
|
||||
+ };
|
||||
+ let Some(parent) = path.parent() else {
|
||||
+ return;
|
||||
+ };
|
||||
+ if std::fs::create_dir_all(parent).is_ok() {
|
||||
+ let _ = std::fs::write(path, format!("{scale:.2}\n"));
|
||||
+ }
|
||||
+}
|
||||
@@ -177 +210 @@
|
||||
-pub fn capture_thread(tx: Sender<Msg>) {
|
||||
+pub fn capture_thread(tx: Sender<Msg>, snapshot: bool) {
|
||||
@@ -189,0 +223 @@
|
||||
+ let mut captured: HashSet<String> = HashSet::new();
|
||||
@@ -236,0 +271 @@
|
||||
+ captured.retain(|key| present.contains(key.as_str()));
|
||||
@@ -255 +290 @@
|
||||
- if sessions.contains_key(&s.key) {
|
||||
+ if sessions.contains_key(&s.key) || (snapshot && captured.contains(&s.key)) {
|
||||
@@ -295 +330,3 @@
|
||||
- let Some(key) = by_id.get(&id) else { continue };
|
||||
+ let Some(key) = by_id.get(&id).cloned() else {
|
||||
+ continue;
|
||||
+ };
|
||||
@@ -313,0 +351,6 @@
|
||||
+ if snapshot {
|
||||
+ captured.insert(key.clone());
|
||||
+ sessions.remove(&key);
|
||||
+ by_id.remove(&id);
|
||||
+ client.close_session(&id);
|
||||
+ }
|
||||
@@ -508,0 +552,8 @@
|
||||
+ /// Exact Wayland app-id filter, used by taskbar launches.
|
||||
+ pub app_id_filter: Option<String>,
|
||||
+ /// Initial cycle direction. `None` leaves the first item selected.
|
||||
+ pub initial_cycle: Option<bool>,
|
||||
+ /// Capture each window once instead of continuously refreshing previews.
|
||||
+ pub snapshot: bool,
|
||||
+ /// Receives next/previous commands from repeated compositor keybind launches.
|
||||
+ pub cycle_socket: Option<std::os::unix::net::UnixDatagram>,
|
||||
@@ -533,0 +585,5 @@
|
||||
+ app_id_filter: Option<String>,
|
||||
+ initial_forward: bool,
|
||||
+ overview_scale: f32,
|
||||
+ cycle_socket: Option<std::os::unix::net::UnixDatagram>,
|
||||
+ queued_cycles: isize,
|
||||
@@ -567 +623 @@
|
||||
- pending_initial_select: false,
|
||||
+ pending_initial_select: opts.initial_cycle.is_some(),
|
||||
@@ -571,0 +628,5 @@
|
||||
+ app_id_filter: opts.app_id_filter,
|
||||
+ initial_forward: opts.initial_cycle.unwrap_or(true),
|
||||
+ overview_scale: load_overview_scale(),
|
||||
+ cycle_socket: opts.cycle_socket,
|
||||
+ queued_cycles: 0,
|
||||
@@ -604 +665,4 @@
|
||||
- return; // nothing to cycle yet; keep the pending initial jump
|
||||
+ self.queued_cycles = self
|
||||
+ .queued_cycles
|
||||
+ .saturating_add(if forward { 1 } else { -1 });
|
||||
+ return;
|
||||
@@ -639,0 +704,12 @@
|
||||
+ loop {
|
||||
+ let command = self.cycle_socket.as_ref().and_then(|socket| {
|
||||
+ let mut buf = [0_u8; 8];
|
||||
+ socket.recv(&mut buf).ok().map(|len| (buf, len))
|
||||
+ });
|
||||
+ match command {
|
||||
+ Some((buf, len)) if &buf[..len] == b"next" => self.cycle(true),
|
||||
+ Some((buf, len)) if &buf[..len] == b"prev" => self.cycle(false),
|
||||
+ Some(_) => {}
|
||||
+ None => break,
|
||||
+ }
|
||||
+ }
|
||||
@@ -698,0 +775,5 @@
|
||||
+ .filter(|s| {
|
||||
+ self.app_id_filter
|
||||
+ .as_ref()
|
||||
+ .is_none_or(|app_id| s.app_id.eq_ignore_ascii_case(app_id))
|
||||
+ })
|
||||
@@ -713,2 +793,0 @@
|
||||
- // Exposé covers the whole screen: dim almost to opaque so the real windows
|
||||
- // behind are hidden (a client can't move them; this hides them instead).
|
||||
@@ -716 +795 @@
|
||||
- c[3] = c[3].max(0.96);
|
||||
+ c[3] = 0.5;
|
||||
@@ -734 +813,5 @@
|
||||
- self.selected = if n > 1 { 1 } else { 0 };
|
||||
+ self.selected = if n > 1 {
|
||||
+ if self.initial_forward { 1 } else { n - 1 }
|
||||
+ } else {
|
||||
+ 0
|
||||
+ };
|
||||
@@ -737,0 +821,8 @@
|
||||
+ if self.queued_cycles != 0 {
|
||||
+ let n = self.visible().len();
|
||||
+ if n > 0 {
|
||||
+ self.selected = (self.selected as isize + self.queued_cycles)
|
||||
+ .rem_euclid(n as isize) as usize;
|
||||
+ self.queued_cycles = 0;
|
||||
+ }
|
||||
+ }
|
||||
@@ -745 +836 @@
|
||||
- let (esc, next, prev, enter) = ctx.input(|i| {
|
||||
+ let (esc, next, prev, enter, zoom_in, zoom_out, wheel_zoom) = ctx.input(|i| {
|
||||
@@ -746,0 +838,8 @@
|
||||
+ let wheel_zoom = i
|
||||
+ .events
|
||||
+ .iter()
|
||||
+ .filter_map(|event| match event {
|
||||
+ egui::Event::MouseWheel { delta, .. } => Some(delta.y),
|
||||
+ _ => None,
|
||||
+ })
|
||||
+ .sum::<f32>();
|
||||
@@ -755,0 +855,3 @@
|
||||
+ i.key_pressed(egui::Key::Plus) || i.key_pressed(egui::Key::Equals),
|
||||
+ i.key_pressed(egui::Key::Minus),
|
||||
+ wheel_zoom,
|
||||
@@ -771,0 +874,12 @@
|
||||
+ if self.view == View::Grid && (zoom_in || zoom_out || wheel_zoom != 0.0) {
|
||||
+ let delta = if zoom_in {
|
||||
+ 0.05
|
||||
+ } else if zoom_out {
|
||||
+ -0.05
|
||||
+ } else {
|
||||
+ wheel_zoom.signum() * 0.05
|
||||
+ };
|
||||
+ self.overview_scale =
|
||||
+ (self.overview_scale + delta).clamp(MIN_OVERVIEW_SCALE, MAX_OVERVIEW_SCALE);
|
||||
+ save_overview_scale(self.overview_scale);
|
||||
+ }
|
||||
@@ -1020 +1134,5 @@
|
||||
- let area = ctx.content_rect().shrink(24.0);
|
||||
+ let full_area = ctx.content_rect().shrink(24.0);
|
||||
+ let area = egui::Rect::from_center_size(
|
||||
+ full_area.center(),
|
||||
+ full_area.size() * self.overview_scale,
|
||||
+ );
|
||||
@@ -1048,0 +1167 @@
|
||||
+ let selected = *i == self.selected;
|
||||
@@ -1062,9 +1181,3 @@
|
||||
- rect.size() * (0.86 + 0.14 * ease),
|
||||
- );
|
||||
- self.paint_expose_tile(
|
||||
- ui,
|
||||
- s,
|
||||
- scaled,
|
||||
- *i == self.selected,
|
||||
- resp.hovered(),
|
||||
- ease,
|
||||
+ rect.size()
|
||||
+ * (0.86 + 0.14 * ease)
|
||||
+ * if selected { 1.04 } else { 1.0 },
|
||||
@@ -1071,0 +1185 @@
|
||||
+ self.paint_expose_tile(ui, s, scaled, selected, resp.hovered(), ease);
|
||||
@@ -1077,0 +1192,8 @@
|
||||
+ ui.painter().text(
|
||||
+ egui::pos2(full_area.right(), full_area.top()),
|
||||
+ egui::Align2::RIGHT_TOP,
|
||||
+ format!("− {:.0}% +", self.overview_scale * 100.0),
|
||||
+ egui::FontId::proportional(14.0),
|
||||
+ self.theme.text_dim,
|
||||
+ );
|
||||
+
|
||||
@@ -1107 +1229,14 @@
|
||||
- let white = egui::Color32::WHITE.gamma_multiply(a);
|
||||
+ let accent = if s.is_window {
|
||||
+ t.window_accent
|
||||
+ } else {
|
||||
+ t.screen_accent
|
||||
+ };
|
||||
+ let content_alpha = if selected || hovered { 1.0 } else { 0.55 };
|
||||
+ let white = egui::Color32::WHITE.gamma_multiply(a * content_alpha);
|
||||
+ if selected {
|
||||
+ p.rect_filled(
|
||||
+ rect.expand(8.0),
|
||||
+ radius + 8.0,
|
||||
+ fade(accent).gamma_multiply(0.35),
|
||||
+ );
|
||||
+ }
|
||||
@@ -1127,0 +1263,7 @@
|
||||
+ if !selected && !hovered {
|
||||
+ p.rect_filled(
|
||||
+ rect,
|
||||
+ radius,
|
||||
+ egui::Color32::from_black_alpha(72).gamma_multiply(a),
|
||||
+ );
|
||||
+ }
|
||||
@@ -1136 +1278,5 @@
|
||||
- egui::Color32::from_black_alpha(160).gamma_multiply(a),
|
||||
+ if selected {
|
||||
+ fade(accent).gamma_multiply(0.9)
|
||||
+ } else {
|
||||
+ egui::Color32::from_black_alpha(160).gamma_multiply(a)
|
||||
+ },
|
||||
@@ -1175,9 +1321,22 @@
|
||||
- let accent = if s.is_window {
|
||||
- t.window_accent
|
||||
- } else {
|
||||
- t.screen_accent
|
||||
- };
|
||||
- let (sw, col) = if selected {
|
||||
- (3.0, accent)
|
||||
- } else if hovered {
|
||||
- (2.0, accent)
|
||||
+ if selected {
|
||||
+ p.rect_stroke(
|
||||
+ rect.expand(4.0),
|
||||
+ radius + 4.0,
|
||||
+ egui::Stroke::new(3.0, fade(egui::Color32::WHITE)),
|
||||
+ egui::StrokeKind::Inside,
|
||||
+ );
|
||||
+ p.rect_stroke(
|
||||
+ rect,
|
||||
+ radius,
|
||||
+ egui::Stroke::new(6.0, fade(accent)),
|
||||
+ egui::StrokeKind::Inside,
|
||||
+ );
|
||||
+ let marker = egui::pos2(rect.right() - 18.0, rect.top() + 18.0);
|
||||
+ p.circle_filled(marker, 13.0, fade(accent));
|
||||
+ p.text(
|
||||
+ marker,
|
||||
+ egui::Align2::CENTER_CENTER,
|
||||
+ "✓",
|
||||
+ egui::FontId::proportional(18.0),
|
||||
+ fade(egui::Color32::WHITE),
|
||||
+ );
|
||||
@@ -1185,8 +1344,7 @@
|
||||
- (1.0, t.thumb)
|
||||
- };
|
||||
- p.rect_stroke(
|
||||
- rect,
|
||||
- radius,
|
||||
- egui::Stroke::new(sw, fade(col)),
|
||||
- egui::StrokeKind::Inside,
|
||||
- );
|
||||
+ p.rect_stroke(
|
||||
+ rect,
|
||||
+ radius,
|
||||
+ egui::Stroke::new(if hovered { 2.0 } else { 1.0 }, fade(accent)),
|
||||
+ egui::StrokeKind::Inside,
|
||||
+ );
|
||||
+ }
|
||||
@@ -0,0 +1,7 @@
|
||||
{ inputs, pkgs, ... }:
|
||||
let
|
||||
unstable = inputs.nixpkgs-unstable.legacyPackages.${pkgs.stdenv.hostPlatform.system};
|
||||
in
|
||||
{
|
||||
home.packages = [ unstable.wl-find-cursor ];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
description = "Wayland cursor locator";
|
||||
}
|
||||
@@ -30,6 +30,7 @@ required on every supported host.
|
||||
| `interface.linux-desktop` | NixOS with Home Manager |
|
||||
| `interface.labwc` | NixOS with Home Manager |
|
||||
| `interface.niri` | NixOS with Home Manager |
|
||||
| `interface.wallpaperengine` | NixOS with Home Manager |
|
||||
| `platform.nixos` | NixOS |
|
||||
| `platform.desktop` | Physical NixOS desktop |
|
||||
| `platform.intel-nvidia-desktop` | Intel/NVIDIA physical NixOS desktop |
|
||||
@@ -58,11 +59,26 @@ selects labwc. A daily-use macOS development machine can combine
|
||||
`interface.macos`, `workload.development`, and `workload.personal`. Hardware
|
||||
support does not implicitly select an interface or workload.
|
||||
|
||||
`interface.wallpaperengine` is an opt-in Linux desktop appearance profile. It
|
||||
enables Wallpaper Engine and disables Noctalia's wallpaper surface, so hosts
|
||||
without this profile retain Noctalia's configured wallpaper.
|
||||
|
||||
`workload.personal` provides Pear Desktop on both NixOS and macOS. Home Manager
|
||||
enables performance improvements, synced lyrics, tracker blocking, the album
|
||||
color theme, and custom output-device selection while preserving user-owned
|
||||
settings such as the selected device.
|
||||
|
||||
`workload.personal` also provides Handy for offline speech-to-text. It starts
|
||||
hidden when the graphical session begins. On niri and labwc, `Ctrl+Space` is
|
||||
owned by the compositor and invokes Handy's `--toggle-transcription` command;
|
||||
Handy's own shortcut bindings are disabled on NixOS so it does not monitor
|
||||
keyboard events under Wayland. Handy uses the Homebrew cask and starts at login
|
||||
on macOS.
|
||||
|
||||
`workload.personal` provides ActivityWatch for local activity tracking. On
|
||||
NixOS, its server and Wayland-compatible watcher run as Home Manager user
|
||||
services. On macOS, the Homebrew cask starts at login.
|
||||
|
||||
On NixOS, `workload.game` provides Steam with Valve Proton and Proton-GE,
|
||||
Protontricks, Wayland-compatible Steam Input, GameMode, Gamescope, and MangoHud.
|
||||
Enabling Steam also activates the 32-bit graphics and PipeWire support required
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
{
|
||||
environment.systemPackages = with pkgs; [
|
||||
alacritty
|
||||
grim
|
||||
slurp
|
||||
wf-recorder
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{ pkgs, ... }: {
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
home.packages = [
|
||||
pkgs.playerctl
|
||||
];
|
||||
|
||||
xdg.userDirs = {
|
||||
enable = true;
|
||||
createDirectories = true;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"applications.gtk"
|
||||
"applications.loupe"
|
||||
"applications.nautilus"
|
||||
"applications.nani"
|
||||
"applications.papers"
|
||||
"applications.qalculate-gtk"
|
||||
"applications.resources"
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
{
|
||||
environment.systemPackages = with pkgs; [
|
||||
alacritty
|
||||
grim
|
||||
slurp
|
||||
wf-recorder
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
programs.noctalia.settings.wallpaper.enabled = false;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
description = "Wallpaper Engine backgrounds for Linux desktop sessions";
|
||||
|
||||
includes = [
|
||||
"applications.linux-wallpaperengine"
|
||||
];
|
||||
}
|
||||
@@ -3,7 +3,10 @@
|
||||
home.packages = with pkgs; [
|
||||
bind
|
||||
bun
|
||||
nil
|
||||
mcp-nixos
|
||||
nix-fast-build
|
||||
nix-tree
|
||||
nixd
|
||||
python312
|
||||
uv
|
||||
];
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
|
||||
includes = [
|
||||
"applications.1password"
|
||||
"applications.activitywatch"
|
||||
"applications.chrome"
|
||||
"applications.discord"
|
||||
"applications.gnome-text-editor"
|
||||
"applications.handy"
|
||||
"applications.kde"
|
||||
"applications.moonlight"
|
||||
"applications.slack"
|
||||
"applications.zoom"
|
||||
"applications.obs"
|
||||
"applications.nani"
|
||||
"applications.thunderbird"
|
||||
];
|
||||
}
|
||||
|
||||
@@ -21,5 +21,29 @@ in
|
||||
pulse.enable = true;
|
||||
jack.enable = true;
|
||||
wireplumber.enable = true;
|
||||
|
||||
extraConfig.pipewire."90-echo-cancel.conf" = {
|
||||
"context.modules" = [
|
||||
{
|
||||
name = "libpipewire-module-echo-cancel";
|
||||
|
||||
args = {
|
||||
"library.name" = "aec/libspa-aec-webrtc";
|
||||
|
||||
"monitor.mode" = true;
|
||||
|
||||
"capture.props" = {
|
||||
"node.name" = "echo_cancel_capture";
|
||||
"node.description" = "Echo Cancel Capture";
|
||||
};
|
||||
|
||||
"source.props" = {
|
||||
"node.name" = "echo_cancel_source";
|
||||
"node.description" = "Echo Cancelled Microphone";
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,98 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
networking.nameservers = lib.mkDefault [
|
||||
"1.1.1.1"
|
||||
"1.0.0.1"
|
||||
let
|
||||
dohPort = 5300;
|
||||
|
||||
dohLocalUpstream = "127.0.0.1#${toString dohPort}";
|
||||
|
||||
internalDns = [
|
||||
"10.50.80.53"
|
||||
"10.50.80.54"
|
||||
|
||||
# "fd00:50:80::53"
|
||||
# "fd00:50:80::54"
|
||||
];
|
||||
|
||||
services.resolved.enable = lib.mkDefault false;
|
||||
security.pki.certificateFiles = [ ./root_ca.crt ];
|
||||
internalZones = [
|
||||
"app.homelabs.run"
|
||||
];
|
||||
|
||||
internalDnsServers = lib.concatMap (zone: map (dns: "/${zone}/${dns}") internalDns) internalZones;
|
||||
in
|
||||
{
|
||||
services.resolved.enable = false;
|
||||
|
||||
networking.networkmanager.dns = "none";
|
||||
|
||||
services.dnscrypt-proxy = {
|
||||
enable = true;
|
||||
|
||||
upstreamDefaults = true;
|
||||
|
||||
settings = {
|
||||
listen_addresses = [
|
||||
"127.0.0.1:${toString dohPort}"
|
||||
];
|
||||
|
||||
server_names = [
|
||||
"cloudflare"
|
||||
"cloudflare-ipv6"
|
||||
];
|
||||
|
||||
ipv4_servers = true;
|
||||
ipv6_servers = true;
|
||||
|
||||
dnscrypt_servers = false;
|
||||
doh_servers = true;
|
||||
odoh_servers = false;
|
||||
|
||||
cache = false;
|
||||
|
||||
block_ipv6 = false;
|
||||
|
||||
ignore_system_dns = true;
|
||||
|
||||
bootstrap_resolvers = [
|
||||
"9.9.9.11:53"
|
||||
"149.112.112.11:53"
|
||||
"[2620:fe::11]:53"
|
||||
"[2620:fe::fe:11]:53"
|
||||
];
|
||||
|
||||
netprobe_address = "1.1.1.1:443";
|
||||
|
||||
timeout = 5000;
|
||||
keepalive = 30;
|
||||
};
|
||||
};
|
||||
|
||||
services.dnsmasq = {
|
||||
enable = true;
|
||||
|
||||
resolveLocalQueries = true;
|
||||
|
||||
settings = {
|
||||
no-resolv = true;
|
||||
|
||||
local-service = "host";
|
||||
|
||||
server = [ dohLocalUpstream ] ++ internalDnsServers;
|
||||
|
||||
all-servers = true;
|
||||
|
||||
cache-size = 10000;
|
||||
|
||||
dns-loop-detect = true;
|
||||
domain-needed = true;
|
||||
bogus-priv = true;
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.dnsmasq = {
|
||||
wants = [ "dnscrypt-proxy.service" ];
|
||||
after = [ "dnscrypt-proxy.service" ];
|
||||
};
|
||||
|
||||
security.pki.certificateFiles = [
|
||||
./root_ca.crt
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"nixos": {
|
||||
"type": "local",
|
||||
"command": ["mcp-nixos"],
|
||||
"enabled": true,
|
||||
"timeout": 30000
|
||||
},
|
||||
"github": {
|
||||
"type": "remote",
|
||||
"url": "https://api.githubcopilot.com/mcp/",
|
||||
"enabled": true,
|
||||
"oauth": false,
|
||||
"headers": {
|
||||
"Authorization": "Bearer {env:GITHUB_PERSONAL_ACCESS_TOKEN}",
|
||||
"X-MCP-Readonly": "true",
|
||||
"X-MCP-Toolsets": "repos,pull_requests,actions"
|
||||
},
|
||||
"timeout": 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+526
@@ -0,0 +1,526 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Change-aware, non-activating validation for this Nix dotfiles repository."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections import defaultdict, deque
|
||||
from contextlib import contextmanager
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import tomllib
|
||||
from typing import Any, Iterator, Sequence
|
||||
|
||||
GLOBAL_FILES = {"flake.nix", "flake.lock", "hosts/default.nix"}
|
||||
GLOBAL_PREFIXES = ("flake/", "libs/", "overlays/")
|
||||
FULL_EVAL_PREFIXES = GLOBAL_PREFIXES + ("scripts/", "shells/", "tests/")
|
||||
FULL_BUILD_PREFIXES = GLOBAL_PREFIXES + ("scripts/", "shells/", "tests/")
|
||||
DOC_SUFFIXES = (".md", ".png", ".jpg", ".jpeg", ".webp")
|
||||
|
||||
|
||||
class ValidationError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def command_text(command: Sequence[str]) -> str:
|
||||
return " ".join(json.dumps(part) if any(c.isspace() for c in part) else part for part in command)
|
||||
|
||||
|
||||
def run(
|
||||
command: Sequence[str],
|
||||
*,
|
||||
cwd: Path,
|
||||
capture: bool = False,
|
||||
check: bool = True,
|
||||
input_text: str | None = None,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
print(f"+ {command_text(command)}", file=sys.stderr)
|
||||
return subprocess.run(
|
||||
list(command),
|
||||
cwd=cwd,
|
||||
check=check,
|
||||
text=True,
|
||||
input=input_text,
|
||||
stdout=subprocess.PIPE if capture else None,
|
||||
stderr=subprocess.PIPE if capture else None,
|
||||
)
|
||||
|
||||
|
||||
def git(root: Path, *args: str, check: bool = True) -> str:
|
||||
return run(["git", *args], cwd=root, capture=True, check=check).stdout
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
return Path(run(["git", "rev-parse", "--show-toplevel"], cwd=Path.cwd(), capture=True).stdout.strip()).resolve()
|
||||
|
||||
|
||||
def normalize(root: Path, raw: str) -> str:
|
||||
candidate = Path(raw)
|
||||
absolute = candidate.resolve() if candidate.is_absolute() else (root / candidate).resolve()
|
||||
try:
|
||||
return absolute.relative_to(root).as_posix()
|
||||
except ValueError as error:
|
||||
raise ValidationError(f"path escapes repository root: {raw}") from error
|
||||
|
||||
|
||||
def nonempty_lines(value: str) -> list[str]:
|
||||
return [line for line in value.splitlines() if line]
|
||||
|
||||
|
||||
def tracked_paths(root: Path) -> set[str]:
|
||||
return set(nonempty_lines(git(root, "ls-files")))
|
||||
|
||||
|
||||
def untracked_paths(root: Path) -> set[str]:
|
||||
return set(nonempty_lines(git(root, "ls-files", "--others", "--exclude-standard")))
|
||||
|
||||
|
||||
def expand_paths(root: Path, raw_paths: Sequence[str], available: set[str]) -> list[str]:
|
||||
result: set[str] = set()
|
||||
for raw in raw_paths:
|
||||
relative = normalize(root, raw)
|
||||
target = root / relative
|
||||
if target.is_dir():
|
||||
prefix = f"{relative.rstrip('/')}/" if relative else ""
|
||||
result.update(path for path in available if path.startswith(prefix))
|
||||
else:
|
||||
result.add(relative)
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def collect_paths(root: Path, args: argparse.Namespace) -> tuple[list[str], set[str]]:
|
||||
tracked = tracked_paths(root)
|
||||
untracked = untracked_paths(root)
|
||||
if args.paths:
|
||||
paths = expand_paths(root, args.paths, tracked | untracked)
|
||||
elif args.all_files:
|
||||
paths = sorted(tracked)
|
||||
elif args.base:
|
||||
paths = nonempty_lines(
|
||||
git(root, "diff", "--name-only", "--no-renames", "--diff-filter=ACMRTUXBD", f"{args.base}...HEAD", "--")
|
||||
)
|
||||
else:
|
||||
paths = nonempty_lines(git(root, "diff", "--name-only", "--no-renames", "--diff-filter=ACMRTUXBD", "HEAD", "--"))
|
||||
paths += sorted(untracked)
|
||||
return sorted(set(paths)), untracked
|
||||
|
||||
|
||||
def remove_path(path: Path) -> None:
|
||||
if path.is_symlink() or path.is_file():
|
||||
path.unlink()
|
||||
elif path.is_dir():
|
||||
shutil.rmtree(path)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def flake_reference(root: Path, changed: Sequence[str]) -> Iterator[str]:
|
||||
"""Create a clean HEAD snapshot and overlay only task-owned worktree paths."""
|
||||
with tempfile.TemporaryDirectory(prefix="dotfiles-flake-") as temporary:
|
||||
source = Path(temporary) / "source"
|
||||
source.mkdir()
|
||||
archive = subprocess.run(
|
||||
["git", "archive", "--format=tar", "HEAD"],
|
||||
cwd=root,
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout
|
||||
with tarfile.open(fileobj=io.BytesIO(archive), mode="r:") as tar:
|
||||
tar.extractall(source, filter="data")
|
||||
for relative in changed:
|
||||
src, dst = root / relative, source / relative
|
||||
remove_path(dst)
|
||||
if src.is_symlink():
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
dst.symlink_to(os.readlink(src))
|
||||
elif src.is_file():
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
yield f"path:{source}"
|
||||
|
||||
|
||||
def load_metadata(root: Path, reference: str) -> dict[str, Any]:
|
||||
result = run(
|
||||
["nix", "eval", "--json", "--show-trace", f"{reference}#lib.validationMetadata"],
|
||||
cwd=root,
|
||||
capture=True,
|
||||
)
|
||||
metadata = json.loads(result.stdout)
|
||||
if metadata.get("schemaVersion") != 1:
|
||||
raise ValidationError("unsupported validation metadata schema")
|
||||
return metadata
|
||||
|
||||
|
||||
def reverse_dependencies(units: dict[str, Any]) -> dict[str, set[str]]:
|
||||
result: dict[str, set[str]] = defaultdict(set)
|
||||
for unit_id, unit in units.items():
|
||||
for dependency in unit.get("includes", []):
|
||||
result[dependency].add(unit_id)
|
||||
return result
|
||||
|
||||
|
||||
def users_of(unit_id: str, reverse: dict[str, set[str]]) -> set[str]:
|
||||
result, queue = {unit_id}, deque([unit_id])
|
||||
while queue:
|
||||
for dependent in reverse.get(queue.popleft(), set()):
|
||||
if dependent not in result:
|
||||
result.add(dependent)
|
||||
queue.append(dependent)
|
||||
return result
|
||||
|
||||
|
||||
def owner_for(path: str, units: dict[str, Any]) -> str | None:
|
||||
matches = []
|
||||
for unit_id, unit in units.items():
|
||||
directory = unit["directory"].rstrip("/")
|
||||
if path == directory or path.startswith(f"{directory}/"):
|
||||
matches.append((len(directory), unit_id))
|
||||
return max(matches)[1] if matches else None
|
||||
|
||||
|
||||
def path_classes(path: str, unit: dict[str, Any]) -> set[str]:
|
||||
directory = unit["directory"].rstrip("/")
|
||||
relative = path[len(directory) :].lstrip("/")
|
||||
if relative == "nixos.nix" or relative == "home/nixos.nix":
|
||||
return {"nixos"}
|
||||
if relative == "darwin.nix" or relative == "home/darwin.nix":
|
||||
return {"darwin"}
|
||||
if relative == "meta.nix":
|
||||
return {"nixos", "darwin"}
|
||||
if relative in {"common.nix", "home.nix", "home/common.nix"}:
|
||||
return {"nixos", "darwin"}
|
||||
fragments = set(unit.get("fragments", []))
|
||||
classes: set[str] = set()
|
||||
if fragments & {"common", "nixos", "home", "homeCommon", "homeNixos"}:
|
||||
classes.add("nixos")
|
||||
if fragments & {"common", "darwin", "home", "homeCommon", "homeDarwin"}:
|
||||
classes.add("darwin")
|
||||
return classes or {"nixos", "darwin"}
|
||||
|
||||
|
||||
def is_docs_only(path: str) -> bool:
|
||||
return path.endswith(DOC_SUFFIXES)
|
||||
|
||||
|
||||
def plan(metadata: dict[str, Any], paths: Sequence[str], *, all_hosts: bool = False) -> dict[str, Any]:
|
||||
hosts: dict[str, Any] = metadata["hosts"]
|
||||
units: dict[str, Any] = metadata["units"]
|
||||
reverse = reverse_dependencies(units)
|
||||
affected_units: set[str] = set()
|
||||
unit_classes: dict[str, set[str]] = defaultdict(set)
|
||||
affected_hosts = {"nixos": set(), "darwin": set()}
|
||||
global_change = all_hosts
|
||||
requires_full_eval = False
|
||||
requires_full_build = False
|
||||
requires_nix = False
|
||||
|
||||
for path in paths:
|
||||
if path in GLOBAL_FILES or path.startswith(GLOBAL_PREFIXES):
|
||||
global_change = True
|
||||
requires_nix = True
|
||||
if path.startswith(FULL_EVAL_PREFIXES) or path in GLOBAL_FILES:
|
||||
requires_full_eval = True
|
||||
if path.startswith(FULL_BUILD_PREFIXES) or path in {"flake.nix", "flake.lock"}:
|
||||
requires_full_build = True
|
||||
if path.endswith(".nix") or path == "flake.lock":
|
||||
requires_nix = True
|
||||
if path == "scripts/dotfiles-check.py":
|
||||
requires_nix = requires_full_eval = requires_full_build = True
|
||||
|
||||
if path.startswith("hosts/") and path != "hosts/default.nix":
|
||||
parts = PurePosixPath(path).parts
|
||||
if len(parts) > 1 and parts[1] in hosts:
|
||||
affected_hosts[hosts[parts[1]]["kind"]].add(parts[1])
|
||||
requires_nix = True
|
||||
|
||||
owner = owner_for(path, units)
|
||||
if owner:
|
||||
classes = path_classes(path, units[owner])
|
||||
for unit_id in users_of(owner, reverse):
|
||||
affected_units.add(unit_id)
|
||||
unit_classes[unit_id].update(classes)
|
||||
requires_nix = requires_nix or not is_docs_only(path)
|
||||
elif path.startswith("modules/") and not is_docs_only(path):
|
||||
global_change = True
|
||||
requires_nix = True
|
||||
|
||||
if global_change:
|
||||
for name, host in hosts.items():
|
||||
affected_hosts[host["kind"]].add(name)
|
||||
else:
|
||||
for name, host in hosts.items():
|
||||
selected = set(host["selectedUnits"])
|
||||
if any(unit_id in selected and host["kind"] in unit_classes[unit_id] for unit_id in affected_units):
|
||||
affected_hosts[host["kind"]].add(name)
|
||||
|
||||
synthetic = {"nixos": set(), "darwin": set()}
|
||||
for unit_id in affected_units:
|
||||
for kind in unit_classes[unit_id]:
|
||||
if not any(unit_id in hosts[name]["selectedUnits"] for name in affected_hosts[kind]):
|
||||
synthetic[kind].add(unit_id)
|
||||
|
||||
systems = sorted({host["system"] for host in hosts.values()})
|
||||
native_systems = {system: False for system in systems}
|
||||
if requires_full_build:
|
||||
native_systems = {system: requires_nix for system in systems}
|
||||
else:
|
||||
for kind, names in affected_hosts.items():
|
||||
for name in names:
|
||||
native_systems[hosts[name]["system"]] = True
|
||||
for kind, unit_ids in synthetic.items():
|
||||
if unit_ids:
|
||||
candidates = sorted(host["system"] for host in hosts.values() if host["kind"] == kind)
|
||||
if candidates:
|
||||
native_systems[candidates[0]] = True
|
||||
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"paths": sorted(paths),
|
||||
"affectedUnits": sorted(affected_units),
|
||||
"affectedHosts": {kind: sorted(names) for kind, names in affected_hosts.items()},
|
||||
"syntheticUnits": {kind: sorted(names) for kind, names in synthetic.items()},
|
||||
"requiresNixValidation": requires_nix,
|
||||
"requiresFullEvaluation": requires_full_eval,
|
||||
"requiresFullNativeBuild": requires_full_build,
|
||||
"nativeBuildSystems": native_systems,
|
||||
}
|
||||
|
||||
|
||||
def render_plan(value: dict[str, Any], reference: str) -> None:
|
||||
print(f"flake: {reference}")
|
||||
print("paths:", ", ".join(value["paths"]) or "(none)")
|
||||
print("units:", ", ".join(value["affectedUnits"]) or "(none)")
|
||||
for kind in ("nixos", "darwin"):
|
||||
print(f"{kind} hosts:", ", ".join(value["affectedHosts"][kind]) or "(none)")
|
||||
print(f"{kind} synthetic:", ", ".join(value["syntheticUnits"][kind]) or "(none)")
|
||||
print("full evaluation:", value["requiresFullEvaluation"])
|
||||
print("full native build:", value["requiresFullNativeBuild"])
|
||||
|
||||
|
||||
def validate_skill(path: Path) -> None:
|
||||
text = path.read_text()
|
||||
if not text.startswith("---\n"):
|
||||
raise ValidationError(f"missing Skill frontmatter: {path}")
|
||||
try:
|
||||
frontmatter = text.split("---\n", 2)[1]
|
||||
name = next(line.split(":", 1)[1].strip() for line in frontmatter.splitlines() if line.startswith("name:"))
|
||||
except (IndexError, StopIteration) as error:
|
||||
raise ValidationError(f"invalid Skill frontmatter: {path}") from error
|
||||
if name != path.parent.name:
|
||||
raise ValidationError(f"Skill name {name!r} does not match directory {path.parent.name!r}")
|
||||
|
||||
|
||||
def static_checks(root: Path, paths: Sequence[str], untracked: set[str]) -> None:
|
||||
for relative in paths:
|
||||
path = root / relative
|
||||
if not path.is_file():
|
||||
continue
|
||||
if relative.endswith(".nix"):
|
||||
run(["nix-instantiate", "--parse", str(path)], cwd=root, capture=True)
|
||||
elif relative.endswith(".json"):
|
||||
json.loads(path.read_text())
|
||||
elif relative.endswith(".toml"):
|
||||
tomllib.loads(path.read_text())
|
||||
elif relative.endswith(".py"):
|
||||
compile(path.read_text(), relative, "exec")
|
||||
if path.name == "SKILL.md":
|
||||
validate_skill(path)
|
||||
if relative in untracked:
|
||||
for number, line in enumerate(path.read_text(errors="replace").splitlines(), 1):
|
||||
if line.rstrip() != line:
|
||||
raise ValidationError(f"trailing whitespace: {relative}:{number}")
|
||||
|
||||
diff_paths = [path for path in paths if path not in untracked]
|
||||
if diff_paths:
|
||||
run(["git", "diff", "--check", "HEAD", "--", *diff_paths], cwd=root)
|
||||
|
||||
|
||||
def run_fast(root: Path, reference: str, paths: Sequence[str], untracked: set[str], *, all_files: bool) -> None:
|
||||
static_checks(root, paths, untracked)
|
||||
command = ["nix", "develop", f"{reference}#validation", "--command", "pre-commit", "run"]
|
||||
command += ["--all-files"] if all_files else (["--files", *paths] if paths else [])
|
||||
if paths or all_files:
|
||||
run(command, cwd=root)
|
||||
|
||||
|
||||
def selection_module_expr(unit_id: str) -> str:
|
||||
quoted = json.dumps(unit_id)
|
||||
return f"(flake.lib.registry.mkSelectionModule [ {quoted} ])"
|
||||
|
||||
|
||||
def synthetic_expr(reference: str, host_name: str, host: dict[str, Any], unit_id: str, *, drv_path: bool) -> str:
|
||||
selection = selection_module_expr(unit_id)
|
||||
modules = [selection]
|
||||
if host.get("homeManager", True):
|
||||
user = json.dumps(host["user"])
|
||||
modules.append(f"{{ home-manager.users.{user}.imports = [ {selection} ]; }}")
|
||||
base = f"flake.{('darwinConfigurations' if host['kind'] == 'darwin' else 'nixosConfigurations')}.{json.dumps(host_name)}"
|
||||
output = "extended.system" if host["kind"] == "darwin" else "extended.config.system.build.toplevel"
|
||||
if drv_path:
|
||||
output += ".drvPath"
|
||||
return f'''let
|
||||
flake = builtins.getFlake {json.dumps(reference)};
|
||||
base = {base};
|
||||
extended = base.extendModules {{ modules = [ {' '.join(modules)} ]; }};
|
||||
in {output}'''
|
||||
|
||||
|
||||
def representative_host(metadata: dict[str, Any], kind: str, current_system: str | None = None) -> tuple[str, dict[str, Any]] | None:
|
||||
candidates = [(name, host) for name, host in metadata["hosts"].items() if host["kind"] == kind]
|
||||
if current_system:
|
||||
candidates = [item for item in candidates if item[1]["system"] == current_system]
|
||||
return sorted(candidates)[0] if candidates else None
|
||||
|
||||
|
||||
def host_drv_attr(reference: str, host: dict[str, Any]) -> str:
|
||||
return f"{reference}#{host['buildAttr']}.drvPath"
|
||||
|
||||
|
||||
def run_eval(
|
||||
root: Path,
|
||||
reference: str,
|
||||
metadata: dict[str, Any],
|
||||
value: dict[str, Any],
|
||||
*,
|
||||
all_systems: bool,
|
||||
) -> None:
|
||||
if all_systems or value["requiresFullEvaluation"]:
|
||||
run(["nix", "flake", "check", reference, "--no-build", "--all-systems", "--keep-going", "--show-trace"], cwd=root)
|
||||
return
|
||||
for kind in ("nixos", "darwin"):
|
||||
for name in value["affectedHosts"][kind]:
|
||||
run(["nix", "eval", "--raw", "--show-trace", host_drv_attr(reference, metadata["hosts"][name])], cwd=root)
|
||||
representative = representative_host(metadata, kind)
|
||||
if representative:
|
||||
name, host = representative
|
||||
for unit_id in value["syntheticUnits"][kind]:
|
||||
run(["nix", "eval", "--raw", "--impure", "--show-trace", "--expr", synthetic_expr(reference, name, host, unit_id, drv_path=True)], cwd=root)
|
||||
|
||||
|
||||
def current_system(root: Path) -> str:
|
||||
return run(["nix", "eval", "--raw", "--impure", "--expr", "builtins.currentSystem"], cwd=root, capture=True).stdout.strip()
|
||||
|
||||
|
||||
def run_full_native(root: Path, reference: str, system: str) -> None:
|
||||
run(
|
||||
["nix", "run", f"{reference}#nix-fast-build", "--", "--flake", f"{reference}#checks.{system}", "--skip-cached", "--no-nom", "--no-link"],
|
||||
cwd=root,
|
||||
)
|
||||
|
||||
|
||||
def run_build(root: Path, reference: str, metadata: dict[str, Any], value: dict[str, Any]) -> None:
|
||||
system = current_system(root)
|
||||
if value["requiresFullNativeBuild"]:
|
||||
run_full_native(root, reference, system)
|
||||
return
|
||||
|
||||
installables: list[str] = []
|
||||
for kind in ("nixos", "darwin"):
|
||||
for name in value["affectedHosts"][kind]:
|
||||
host = metadata["hosts"][name]
|
||||
if host["system"] == system:
|
||||
installables.append(f"{reference}#{host['buildAttr']}")
|
||||
else:
|
||||
print(f"skip incompatible build: {name} ({host['system']})", file=sys.stderr)
|
||||
for check in ("registry", "validation-tool"):
|
||||
installables.append(f"{reference}#checks.{system}.{check}")
|
||||
if installables:
|
||||
run(["nix", "build", "--no-link", "--keep-going", "--print-build-logs", *sorted(set(installables))], cwd=root)
|
||||
|
||||
for kind in ("nixos", "darwin"):
|
||||
representative = representative_host(metadata, kind, system)
|
||||
if representative:
|
||||
name, host = representative
|
||||
for unit_id in value["syntheticUnits"][kind]:
|
||||
run(["nix", "build", "--no-link", "--impure", "--expr", synthetic_expr(reference, name, host, unit_id, drv_path=False)], cwd=root)
|
||||
|
||||
|
||||
def self_test() -> None:
|
||||
metadata = {
|
||||
"schemaVersion": 1,
|
||||
"units": {
|
||||
"applications.foo": {"directory": "modules/applications/foo", "includes": [], "fragments": ["home", "homeNixos"]},
|
||||
"applications.dormant": {"directory": "modules/applications/dormant", "includes": [], "fragments": ["home"]},
|
||||
"profiles.workload.dev": {"directory": "modules/profiles/workload/dev", "includes": ["applications.foo"], "fragments": ["meta"]},
|
||||
},
|
||||
"hosts": {
|
||||
"linux": {"kind": "nixos", "system": "x86_64-linux", "user": "test", "homeManager": True, "selectedUnits": ["profiles.workload.dev"], "buildAttr": "nixosConfigurations.linux.config.system.build.toplevel"},
|
||||
"mac": {"kind": "darwin", "system": "aarch64-darwin", "user": "test", "homeManager": True, "selectedUnits": ["profiles.workload.dev"], "buildAttr": "darwinConfigurations.mac.system"},
|
||||
},
|
||||
}
|
||||
nixos = plan(metadata, ["modules/applications/foo/home/nixos.nix"])
|
||||
assert nixos["affectedHosts"] == {"nixos": ["linux"], "darwin": []}
|
||||
assert nixos["affectedUnits"] == ["applications.foo", "profiles.workload.dev"]
|
||||
common = plan(metadata, ["modules/applications/foo/home.nix"])
|
||||
assert common["affectedHosts"] == {"nixos": ["linux"], "darwin": ["mac"]}
|
||||
dormant = plan(metadata, ["modules/applications/dormant/home.nix"])
|
||||
assert dormant["syntheticUnits"] == {"nixos": ["applications.dormant"], "darwin": ["applications.dormant"]}
|
||||
global_value = plan(metadata, ["flake.nix"])
|
||||
assert global_value["requiresFullEvaluation"] and global_value["requiresFullNativeBuild"]
|
||||
docs = plan(metadata, ["modules/profiles/README.md"])
|
||||
assert not docs["requiresNixValidation"]
|
||||
print("dotfiles-check self-test passed")
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
result = argparse.ArgumentParser(description=__doc__)
|
||||
result.add_argument("command", choices=("plan", "fast", "eval", "build", "all", "full", "self-test"))
|
||||
result.add_argument("--paths", nargs="+", help="Explicit task-owned repository paths")
|
||||
result.add_argument("--base", help="Compare BASE...HEAD")
|
||||
result.add_argument("--all-files", action="store_true", help="Check every tracked file")
|
||||
result.add_argument("--all-hosts", action="store_true", help="Validate every registered host")
|
||||
result.add_argument("--all-systems", action="store_true", help="Evaluate every flake system")
|
||||
result.add_argument("--json", action="store_true", help="Emit the plan as JSON")
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parser().parse_args()
|
||||
if args.command == "self-test":
|
||||
self_test()
|
||||
return 0
|
||||
selectors = sum(bool(value) for value in (args.paths, args.base, args.all_files))
|
||||
if selectors > 1:
|
||||
raise ValidationError("use only one of --paths, --base, or --all-files")
|
||||
if args.command == "full":
|
||||
if selectors or args.all_hosts or args.all_systems:
|
||||
raise ValidationError("full is exhaustive and accepts no scope flags")
|
||||
args.all_files = args.all_hosts = args.all_systems = True
|
||||
|
||||
root = repo_root()
|
||||
paths, untracked = collect_paths(root, args)
|
||||
with flake_reference(root, paths) as reference:
|
||||
metadata = load_metadata(root, reference)
|
||||
value = plan(metadata, paths, all_hosts=args.all_hosts)
|
||||
if args.command == "plan":
|
||||
print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True)) if args.json else render_plan(value, reference)
|
||||
return 0
|
||||
if args.command in {"fast", "all", "full"}:
|
||||
run_fast(root, reference, paths, untracked, all_files=args.all_files)
|
||||
if args.command in {"eval", "all", "full"} and value["requiresNixValidation"]:
|
||||
run_eval(root, reference, metadata, value, all_systems=args.all_systems or args.command in {"all", "full"})
|
||||
if args.command in {"build", "all"} and value["requiresNixValidation"]:
|
||||
run_build(root, reference, metadata, value)
|
||||
if args.command == "full":
|
||||
run_full_native(root, reference, current_system(root))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except ValidationError as error:
|
||||
print(f"error: {error}", file=sys.stderr)
|
||||
raise SystemExit(2) from error
|
||||
except subprocess.CalledProcessError as error:
|
||||
if error.stdout:
|
||||
print(error.stdout, file=sys.stderr, end="")
|
||||
if error.stderr:
|
||||
print(error.stderr, file=sys.stderr, end="")
|
||||
raise SystemExit(error.returncode) from error
|
||||
@@ -3,15 +3,22 @@ _: {
|
||||
{
|
||||
pkgs,
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
devShells.dotnix = pkgs.mkShell {
|
||||
packages = [
|
||||
config.treefmt.build.wrapper
|
||||
config.packages.dotfiles-check
|
||||
|
||||
pkgs.actionlint
|
||||
pkgs.git
|
||||
pkgs.gitleaks
|
||||
pkgs.mcp-nixos
|
||||
pkgs.nix-fast-build
|
||||
pkgs.nix-tree
|
||||
pkgs.nixd
|
||||
pkgs.pre-commit
|
||||
|
||||
# sops-nix / age
|
||||
@@ -22,6 +29,8 @@ _: {
|
||||
# YubiKey for sops editing
|
||||
pkgs.age-plugin-yubikey
|
||||
pkgs.yubikey-manager
|
||||
]
|
||||
++ lib.optionals (lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.pcsc-tools) [
|
||||
pkgs.pcsc-tools
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
name: add-application-or-service
|
||||
description: Add, install, configure, or enable an application or long-running service in this NixOS, nix-darwin, and Home Manager flake while preserving its Registry architecture and quality bar. Use for new GUI or CLI applications, packages, daemons, background services, application-service pairs, cross-platform installations, profile adoption, or substantial extensions to an existing application or service unit.
|
||||
---
|
||||
|
||||
# Add Application or Service
|
||||
|
||||
Add the smallest complete Registry unit change that has a clear owner, an
|
||||
explicit dependency path, and evidence that every affected host class
|
||||
evaluates. Treat `AGENTS.md` as the authoritative repository contract; never
|
||||
replace it with generic Nix conventions.
|
||||
|
||||
## Follow the workflow
|
||||
|
||||
### 1. Establish the baseline
|
||||
|
||||
1. Read `AGENTS.md` completely before editing.
|
||||
2. Run `git status --short`. Preserve all pre-existing user changes and identify
|
||||
which later diffs belong to this task.
|
||||
3. Translate the request into observable outcomes: package or program, desired
|
||||
configuration, supported host classes, required daemon or permissions, and
|
||||
the profile or user intent that should select it.
|
||||
4. Inspect the nearest existing units, relevant profiles, `hosts/default.nix`,
|
||||
and Registry implementation. Prefer repository evidence over memory.
|
||||
5. Verify current package names, module options, external module exports, and
|
||||
Homebrew cask names from the locked inputs or authoritative upstream
|
||||
documentation. Do not guess an option path.
|
||||
6. Read [references/review-checklist.md](references/review-checklist.md) before
|
||||
choosing files or dependencies.
|
||||
|
||||
### 2. Choose ownership before code
|
||||
|
||||
Classify each concern independently:
|
||||
|
||||
- Put the user-facing program and its settings in
|
||||
`modules/applications/<name>/`.
|
||||
- Put a daemon, long-running process, firewall rule, permission, or user/group
|
||||
membership in `modules/services/<name>/`.
|
||||
- Split an application and independently meaningful daemon into two units.
|
||||
Let the application include the service only when the service is a technical
|
||||
requirement of that application.
|
||||
- Put adoption of otherwise independent units in the narrowest coherent
|
||||
`modules/profiles/` composition.
|
||||
- Use another documented owner when the request is actually a system,
|
||||
hardware, user, overlay, or host concern. Do not force it into an application
|
||||
or service directory merely because this skill was invoked.
|
||||
|
||||
Choose only the reserved fragments that contain real configuration. Use
|
||||
`common.nix`, `nixos.nix`, and `darwin.nix` for system-side configuration; use
|
||||
`home.nix` or `home/{common,nixos,darwin}.nix` for Home Manager. Use `meta.nix`
|
||||
only for description, fully qualified `includes`, and external module imports.
|
||||
|
||||
Before editing, formulate a short implementation contract containing:
|
||||
|
||||
- the unit ID and owner;
|
||||
- each file to create or change and why;
|
||||
- technical dependencies versus profile-level choices;
|
||||
- supported and affected host classes;
|
||||
- the evaluations or builds that will prove the change.
|
||||
|
||||
Rework the design if an ordinary addition appears to require Registry changes,
|
||||
new global `specialArgs`, `_module.args`, direct host selection, or an overlay.
|
||||
Use those mechanisms only with concrete evidence that the documented extension
|
||||
points cannot express the requirement.
|
||||
|
||||
### 3. Implement the minimum complete change
|
||||
|
||||
1. Return configuration directly from every reserved fragment. Do not add
|
||||
top-level `imports`, `options`, or `config`, and do not reproduce Registry
|
||||
`mkEnableOption`, `cfg`, or `mkIf` boilerplate.
|
||||
2. Put upstream NixOS, nix-darwin, or Home Manager modules in
|
||||
`meta.imports.<class>`. Import ordinary helper files explicitly from the
|
||||
fragment that uses them.
|
||||
3. Declare unit-to-unit technical dependencies only through fully qualified
|
||||
`meta.includes`. Never enable another unit by assigning its
|
||||
`my.<path>.enable` option inside a fragment.
|
||||
4. Add an independent application or service to an existing coherent profile,
|
||||
or create a justified profile when no existing one expresses the user
|
||||
intent. Do not use `hosts/default.nix` application or unit escape hatches for
|
||||
normal composition.
|
||||
5. Keep cross-platform purpose shared and installation differences in the
|
||||
owning unit. Do not create thin `*-linux` profiles.
|
||||
6. Use existing module arguments and standard options. Do not inject a
|
||||
dependency through global arguments, Registry internals, import ordering, or
|
||||
`lib.mkForce`. Use explicit module priorities only when a real ownership
|
||||
boundary requires them and make that reason visible in the code or handoff.
|
||||
7. Avoid speculative abstraction. Create a helper only when it separates
|
||||
meaningful configuration or prevents real duplication. Do not add empty
|
||||
fragments, compatibility aliases, unused options, redundant comments, or
|
||||
copied boilerplate.
|
||||
8. Update `modules/profiles/README.md`, `AGENTS.md`, profile selections, or
|
||||
other contract documentation whenever the change makes an existing
|
||||
statement stale. Do not edit them performatively when their meaning remains
|
||||
accurate.
|
||||
|
||||
### 4. Prove the change
|
||||
|
||||
Invoke the `validate-nix-change` skill and use the task-owned files as its
|
||||
explicit path set. At minimum:
|
||||
|
||||
1. Inspect `nix run .#check -- plan --paths <task-path>... --json` and confirm
|
||||
the reported units, host classes, and real hosts are correct.
|
||||
2. Run `nix run .#check -- fast --paths <task-path>...` during the edit loop.
|
||||
3. Inspect the complete task diff for accidental files, duplication, leaked
|
||||
secrets, forced values, direct enable assignments, and unrelated rewrites.
|
||||
4. Run `nix run .#check -- all --paths <task-path>...` after the structure is
|
||||
complete. This evaluates every flake system and builds affected targets for
|
||||
the current platform without activation.
|
||||
5. Verify selection as well as syntax: confirm that the expected package,
|
||||
program, service, group, cask, or external module appears in the resulting
|
||||
configuration.
|
||||
6. Add a `pkgs.testers.runNixOSTest` check through the `test-nixos-service`
|
||||
skill when service startup or another runtime contract cannot be proved by
|
||||
evaluation and a system build.
|
||||
|
||||
Use `nix run .#check -- full` only for CI, scheduled maintenance, or an explicit
|
||||
repository-wide audit. These commands never activate the live system. Do not
|
||||
run `nh os switch`, `nixos-rebuild switch`, `darwin-rebuild switch`,
|
||||
`home-manager switch`, or an equivalent activation command as validation.
|
||||
|
||||
If a command is unavailable, blocked by the environment, or fails for a
|
||||
pre-existing reason, invoke the `debug-nix-failure` skill, diagnose it, and
|
||||
report the exact gap. Never silently skip a required check or weaken the
|
||||
implementation to make a check pass.
|
||||
|
||||
### 5. Audit before completion
|
||||
|
||||
Reject the change until all of the following are true:
|
||||
|
||||
- Every line has one clear owner and is required by the requested behavior.
|
||||
- Every dependency is either technical and declared in `meta.includes`, or a
|
||||
user choice owned by a profile.
|
||||
- The unit is reachable from the intended profile or has an explicit reason to
|
||||
remain independently selectable.
|
||||
- No host, Registry, flake root, global argument, or overlay was changed as a
|
||||
shortcut.
|
||||
- Reserved fragments, metadata, and profile documentation satisfy the current
|
||||
repository contract.
|
||||
- Validation covers every affected host class and all failures are resolved or
|
||||
explicitly reported.
|
||||
|
||||
Conclude with the owner and selection rationale, affected hosts or profiles,
|
||||
validation commands and results, and any manual activation or runtime check
|
||||
that remains. Do not claim runtime behavior that was only evaluated.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Add Application or Service"
|
||||
short_description: "Add clean, validated Registry units"
|
||||
default_prompt: "Use $add-application-or-service to add an application or service cleanly and verify every affected host class."
|
||||
@@ -0,0 +1,132 @@
|
||||
# Application and Service Review Checklist
|
||||
|
||||
Use this reference during design and again during final review.
|
||||
|
||||
## Ownership and fragment matrix
|
||||
|
||||
| Concern | Owner | Typical fragment |
|
||||
| -------------------------------------------------------- | ----------------------------------- | ----------------------------------------- |
|
||||
| User-facing GUI, CLI, editor, or compositor | `modules/applications/<name>/` | `home*.nix`, `nixos.nix`, or `darwin.nix` |
|
||||
| User-scoped package and program settings | Application unit | `home.nix` or `home/<class>.nix` |
|
||||
| macOS Homebrew package or cask | Owning application or service | `darwin.nix` |
|
||||
| Daemon or long-running service | `modules/services/<name>/` | `nixos.nix` or `darwin.nix` |
|
||||
| Firewall, group, permission, or service account | Owning service | `nixos.nix` or `darwin.nix` |
|
||||
| Upstream module defining options | Owning unit metadata | `meta.nix` under `imports.<class>` |
|
||||
| Technical prerequisite unit | Owning unit metadata | Fully qualified `meta.includes` |
|
||||
| A set of independent tools chosen for one purpose | Narrowest coherent profile | Profile `meta.includes` |
|
||||
| Machine fact such as UUID, monitor ID, or static address | `hosts/<name>/` | Normal host module |
|
||||
| Missing or replaced package | `overlays/` only after proving need | Overlay definition |
|
||||
|
||||
Use `home/common.nix` when Home Manager configuration is truly shared between
|
||||
NixOS and Darwin. Use `home/nixos.nix` or `home/darwin.nix` for class-specific
|
||||
Home Manager behavior. Root `common.nix` is system-side and never Home Manager.
|
||||
Do not create an unused counterpart for symmetry.
|
||||
|
||||
## Dependency review
|
||||
|
||||
For every edge from unit A to unit B, answer these questions:
|
||||
|
||||
1. Does A fail to work without B? If yes, put the fully qualified ID of B in
|
||||
A's `meta.includes`.
|
||||
2. Are A and B merely useful together for a particular workflow? If yes, let a
|
||||
profile include both.
|
||||
3. Does the dependency exist only on one host? Keep the machine fact in the
|
||||
host, but keep reusable behavior in its unit or profile.
|
||||
4. Is a new `specialArgs`, `_module.args`, Registry field, or direct
|
||||
`my.*.enable` assignment being proposed? Reject it unless the repository's
|
||||
normal module and `meta.includes` mechanisms provably cannot model the
|
||||
requirement.
|
||||
5. Would adding the dependency make the depended-on application select a
|
||||
compositor, desktop, personal workload, or unrelated tool? Reverse or remove
|
||||
the edge; application metadata contains technical requirements, not taste.
|
||||
|
||||
Accept no circular dependency, shortened unit ID, duplicate include, stale
|
||||
unit ID, or ordering-dependent override.
|
||||
|
||||
## Minimality and code-quality review
|
||||
|
||||
Reject any of these patterns:
|
||||
|
||||
- Empty or placeholder reserved fragments.
|
||||
- Hand-written enable options or guards already generated by the Registry.
|
||||
- Top-level `imports`, `options`, or `config` in a configuration fragment.
|
||||
- External module imports hidden in a guarded configuration fragment.
|
||||
- Helper files assumed to be auto-imported.
|
||||
- A helper abstraction used once without reducing meaningful complexity.
|
||||
- Configuration duplicated across fragments when a shared fragment can express
|
||||
it cleanly.
|
||||
- Direct host application or unit selection where a profile expresses the
|
||||
concern.
|
||||
- A new flake input or overlay when the locked package set already provides the
|
||||
package and required module.
|
||||
- Global argument injection for a value owned by one unit.
|
||||
- `lib.mkForce` used to win an ordering fight instead of resolving ownership.
|
||||
- Secret material, generated state, machine IDs, or mutable user preferences
|
||||
committed as reusable configuration.
|
||||
- Comments that repeat the code, compatibility aliases, dead options, or
|
||||
opportunistic unrelated cleanup.
|
||||
|
||||
Prefer standard upstream module options over hand-written service definitions.
|
||||
Prefer existing repository arguments and helpers over new plumbing. Preserve
|
||||
user-owned mutable state unless the requested policy explicitly owns it.
|
||||
|
||||
## Validation matrix
|
||||
|
||||
Use the `validate-nix-change` skill and run checks from the repository root.
|
||||
Keep exact results for the handoff. The validation app never switches or
|
||||
activates a live system.
|
||||
|
||||
### Always
|
||||
|
||||
1. Run `nix run .#check -- plan --paths <task-path>... --json` and inspect the
|
||||
affected units and hosts.
|
||||
2. Run `nix run .#check -- fast --paths <task-path>...` during implementation.
|
||||
3. Review `git status --short`, `git diff --stat`, and the complete task diff.
|
||||
4. Run `nix run .#check -- all --paths <task-path>...` before handoff. It runs
|
||||
all-system evaluation and compatible targeted builds without activation.
|
||||
5. Reserve `nix run .#check -- full` for CI, scheduled maintenance, or an
|
||||
explicit repository-wide audit.
|
||||
|
||||
Do not run `nh os switch`, `nixos-rebuild switch`, `darwin-rebuild switch`,
|
||||
`home-manager switch`, or an equivalent activation command.
|
||||
|
||||
### NixOS or Home Manager on NixOS
|
||||
|
||||
- Confirm that the validation plan includes each affected real NixOS host.
|
||||
- Build affected NixOS configurations with `--no-link` through the validation
|
||||
app when the current system supports them.
|
||||
- Inspect the resulting option that proves selection: for example
|
||||
`environment.systemPackages`, the user's `home.packages`,
|
||||
`systemd.services`, `users.users.<name>.extraGroups`, or the upstream
|
||||
`programs`/`services` option.
|
||||
|
||||
### nix-darwin or Home Manager on Darwin
|
||||
|
||||
- Confirm that every affected Darwin host is evaluated even when running on
|
||||
Linux.
|
||||
- Inspect `homebrew.casks` or `homebrew.brews` for Homebrew-backed additions.
|
||||
- Evaluate the relevant Home Manager program or package option.
|
||||
- Build a Darwin configuration only on a compatible Darwin runner or builder;
|
||||
otherwise report the build as an explicit platform gap.
|
||||
|
||||
### Profiles and cross-platform changes
|
||||
|
||||
- Confirm transitive selection through `meta.includes`, not only direct
|
||||
mentions. The validation plan computes reverse dependency closure.
|
||||
- Evaluate every real host selecting the changed profile.
|
||||
- Evaluate both host classes for a cross-platform profile, even if only one
|
||||
current fragment changed.
|
||||
- Re-read `modules/profiles/README.md` and `hosts/default.nix` for stale meaning,
|
||||
compatibility, or role statements.
|
||||
- If no real host selects the new unit, construct a non-persistent evaluation
|
||||
that enables it or explain why the unit is intentionally dormant. Do not add
|
||||
a fake host or permanent direct selection as a test harness.
|
||||
|
||||
### Runtime-dependent behavior
|
||||
|
||||
Evaluation and builds cannot prove GUI appearance, credentials, network access,
|
||||
hardware behavior, successful daemon interaction, or reboot state. For
|
||||
reusable NixOS behavior, use the `test-nixos-service` skill and add a
|
||||
`pkgs.testers.runNixOSTest` check. State the precise manual post-activation check
|
||||
needed for physical hardware or external systems. Never describe evaluation as
|
||||
a runtime test.
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: debug-nix-failure
|
||||
description: Diagnose failures from parsing, Nix module evaluation, derivation builds, flake checks, NixOS tests, or Home Manager activation logs without changing the live system. Use when `nix run .#check`, `nix flake check`, `nix build`, CI, or a user-provided activation log fails.
|
||||
---
|
||||
|
||||
# Debug a Nix Failure
|
||||
|
||||
Classify the failure before changing code. Preserve the original command,
|
||||
complete error, first causal frame, and affected attribute. Never run a live
|
||||
switch or activation to reproduce a validation failure.
|
||||
|
||||
## Identify the failing layer
|
||||
|
||||
- **Parse or format:** syntax location, malformed string, unmatched delimiter,
|
||||
or formatter-owned rewrite.
|
||||
- **Static analysis:** dead binding, suspicious expression, ShellCheck finding,
|
||||
secret scan, or workflow lint.
|
||||
- **Module evaluation:** missing option, wrong type, assertion, infinite
|
||||
recursion, conflicting definitions, Registry selection, or unsupported host
|
||||
class.
|
||||
- **Derivation instantiation/build:** missing dependency, hash mismatch, patch
|
||||
failure, compiler/test failure, sandbox violation, or unsupported platform.
|
||||
- **NixOS test:** failed unit, timeout, command assertion, network readiness, or
|
||||
reboot state.
|
||||
- **Activation/runtime:** filesystem conflict, activation script, systemd unit,
|
||||
hardware, credential, or external-service behavior. Diagnose only from logs
|
||||
supplied by the user unless they explicitly request a non-switch inspection
|
||||
command.
|
||||
|
||||
## Reproduce the narrowest failing operation
|
||||
|
||||
Start with the stage and paths reported by the validation app:
|
||||
|
||||
```sh
|
||||
nix run .#check -- plan --paths <task-path>... --json
|
||||
nix run .#check -- fast --paths <task-path>...
|
||||
nix run .#check -- eval --paths <task-path>...
|
||||
nix run .#check -- build --paths <task-path>...
|
||||
```
|
||||
|
||||
For a single attribute, use `nix eval --show-trace` on its `drvPath` before a
|
||||
build. For a failed derivation, retain `--print-build-logs` and inspect
|
||||
`nix log <drv-path>` when the summary omits the causal lines.
|
||||
|
||||
## Read traces selectively
|
||||
|
||||
1. Find the first repository-owned frame or option path.
|
||||
2. Separate the immediate failure from wrapper frames in `modules.nix`,
|
||||
`lib.evalModules`, or flake-parts.
|
||||
3. Inspect the option declaration and every definition contributing to it.
|
||||
4. Confirm package and option names against locked inputs, not memory.
|
||||
5. Check whether the failure reproduces on the base revision before calling it
|
||||
task-owned.
|
||||
|
||||
Do not respond to a type or ownership error with import-order changes,
|
||||
`lib.mkForce`, global arguments, or an overlay unless repository evidence shows
|
||||
that those mechanisms are the correct owner.
|
||||
|
||||
## Finish with a bounded diagnosis
|
||||
|
||||
Report the failing layer, root cause, minimal correction, rerun command, and any
|
||||
remaining platform or runtime gap. Include enough of the error to identify it,
|
||||
but do not paste large unrelated logs.
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: test-nixos-service
|
||||
description: Add or extend a non-activating NixOS VM or container test for service startup, sockets, timers, permissions, firewall behavior, reboot state, and inter-service dependencies. Use when evaluation and a system build cannot prove the requested runtime behavior.
|
||||
---
|
||||
|
||||
# Test NixOS Runtime Behavior
|
||||
|
||||
Prefer `pkgs.testers.runNixOSTest` for reusable NixOS behavior that can be
|
||||
proved without the user's physical machine. Do not activate the host
|
||||
configuration and do not substitute a live `nh os switch` for a deterministic
|
||||
test.
|
||||
|
||||
## Define the observable contract
|
||||
|
||||
List the runtime facts that must hold, such as:
|
||||
|
||||
- a systemd unit reaches `active`;
|
||||
- a socket or port is listening;
|
||||
- a timer triggers its service;
|
||||
- a user can or cannot read a file;
|
||||
- a group membership grants access;
|
||||
- a firewall permits one path and blocks another;
|
||||
- state survives a reboot;
|
||||
- one service waits for another dependency.
|
||||
|
||||
Exclude behavior that requires physical GPU, fingerprint, audio, display,
|
||||
Secure Boot, TPM, private credentials, or an external provider unless the test
|
||||
can model it explicitly.
|
||||
|
||||
## Implement the smallest useful machine
|
||||
|
||||
Create a test under `tests/` and expose it through `checks.<system>`. Import the
|
||||
owning module or Registry selection instead of copying its implementation into
|
||||
the test. Use only the packages, users, files, and network peers required by the
|
||||
contract.
|
||||
|
||||
Typical shape:
|
||||
|
||||
```nix
|
||||
pkgs.testers.runNixOSTest {
|
||||
name = "service-name";
|
||||
|
||||
nodes.machine = {
|
||||
# Enable the owning unit or import the module under test.
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.start()
|
||||
machine.wait_for_unit("service-name.service")
|
||||
machine.succeed("systemctl is-active service-name.service")
|
||||
'';
|
||||
}
|
||||
```
|
||||
|
||||
Use `wait_for_unit`, `wait_for_open_port`, `succeed`, `fail`, and explicit
|
||||
reboots to express outcomes. Avoid arbitrary sleeps when a readiness condition
|
||||
exists.
|
||||
|
||||
## Validate and report
|
||||
|
||||
Run the targeted test through its flake check, then run the repository
|
||||
validation app for the task paths. Report the test attribute and assertions
|
||||
that passed. State clearly which hardware or external behavior remains outside
|
||||
the VM/container model.
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: update-flake-input
|
||||
description: Update one or more pinned flake inputs with bounded lock-file changes and non-activating Linux and Darwin validation. Use for dependency refreshes, input-specific updates, automated lock-file pull requests, or diagnosing a regression introduced by flake.lock.
|
||||
---
|
||||
|
||||
# Update a Flake Input
|
||||
|
||||
Keep the update scope explicit and treat `flake.lock` as generated dependency
|
||||
state. Never activate a host as part of this workflow; do not run `nh os switch`,
|
||||
`nixos-rebuild switch`, `darwin-rebuild switch`, `home-manager switch`, or an
|
||||
equivalent command.
|
||||
|
||||
## Bound the update
|
||||
|
||||
1. Read `AGENTS.md`, inspect `git status --short`, and preserve unrelated work.
|
||||
2. Record the input names and the behavior or version change being requested.
|
||||
3. Prefer an input-specific update:
|
||||
|
||||
```sh
|
||||
nix flake update <input-name>
|
||||
```
|
||||
|
||||
Use an unrestricted `nix flake update` only when the task explicitly requests a
|
||||
full refresh. Do not hand-edit lock nodes.
|
||||
|
||||
## Audit the lock diff
|
||||
|
||||
Inspect the complete `flake.lock` diff. Confirm that changed nodes are the
|
||||
requested inputs or unavoidable followers and that source owners, repositories,
|
||||
reference types, and hashes remain expected. Investigate unexpected node
|
||||
replacement, disappearing followers, or a large transitive graph rewrite before
|
||||
validation.
|
||||
|
||||
## Validate without activation
|
||||
|
||||
Run the common validation workflow against the lock file:
|
||||
|
||||
```sh
|
||||
nix run .#check -- plan --paths flake.lock --json
|
||||
nix run .#check -- fast --paths flake.lock
|
||||
nix run .#check -- eval --paths flake.lock
|
||||
nix run .#check -- build --paths flake.lock
|
||||
```
|
||||
|
||||
A lock-file change requires full evaluation and the complete native check set.
|
||||
Linux and Darwin builds must run on compatible runners. The scheduled update
|
||||
workflow uploads the candidate lock file, builds Linux and Darwin checks, and
|
||||
creates a pull request only after both pass.
|
||||
|
||||
When a failure appears only after the update, invoke `debug-nix-failure`, compare
|
||||
the failing derivation or option with the base lock, and narrow the responsible
|
||||
input before adding an override or patch.
|
||||
|
||||
## Report the result
|
||||
|
||||
List requested and transitively changed inputs, validation commands and native
|
||||
platform results, any package or option migration, and remaining manual runtime
|
||||
checks. Evaluation or a native build is not activation.
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
name: validate-nix-change
|
||||
description: Plan and run efficient, non-activating validation for edits to this NixOS, nix-darwin, and Home Manager flake. Use after changing Nix modules, hosts, profiles, overlays, flake outputs, tests, scripts, CI, or agent configuration; before handing off a task; or when deciding which real hosts must be evaluated or built.
|
||||
---
|
||||
|
||||
# Validate a Nix Change
|
||||
|
||||
Use the repository validation app as the source of truth for change impact and
|
||||
validation commands. It derives affected hosts from Registry ownership,
|
||||
`meta.includes`, host selections, fragment class, and host-local paths.
|
||||
|
||||
Never activate a live configuration as part of this workflow. Do not run
|
||||
`nh os switch`, `nixos-rebuild switch`, `darwin-rebuild switch`,
|
||||
`home-manager switch`, or an equivalent activation command. The user owns live
|
||||
activation separately.
|
||||
|
||||
## Establish the validation scope
|
||||
|
||||
1. Read `AGENTS.md` and run `git status --short` before editing.
|
||||
2. Preserve unrelated user changes. Track the paths owned by the current task,
|
||||
including newly created untracked files.
|
||||
3. Inspect the plan before expensive checks:
|
||||
|
||||
```sh
|
||||
nix run .#check -- plan --paths <task-path>... --json
|
||||
```
|
||||
|
||||
When validating a committed pull-request range, use:
|
||||
|
||||
```sh
|
||||
nix run .#check -- plan --base <base-sha> --json
|
||||
```
|
||||
|
||||
The app automatically uses a `path:` flake reference when task paths are
|
||||
untracked, so newly created Registry fragments are visible to Nix without
|
||||
staging them.
|
||||
|
||||
## Run checks in increasing cost order
|
||||
|
||||
### Fast edit loop
|
||||
|
||||
After each coherent edit, parse Nix files, validate project JSON, TOML, and
|
||||
skill frontmatter, check whitespace, and run the configured hooks only for
|
||||
task-owned files:
|
||||
|
||||
```sh
|
||||
nix run .#check -- fast --paths <task-path>...
|
||||
```
|
||||
|
||||
Do not replace this with `pre-commit run --all-files` during the edit loop.
|
||||
Unrelated repository files must not become part of the task merely because an
|
||||
existing check fails elsewhere.
|
||||
|
||||
### Evaluation
|
||||
|
||||
After the implementation is structurally complete, evaluate every affected
|
||||
NixOS and Darwin derivation plus the supporting checks without realizing or
|
||||
activating them. Flake-wide paths additionally evaluate every flake system:
|
||||
|
||||
```sh
|
||||
nix run .#check -- eval --paths <task-path>...
|
||||
```
|
||||
|
||||
This proves module evaluation, option types, assertions, Registry selection,
|
||||
and derivation instantiation. It does not prove a successful build or runtime
|
||||
behavior.
|
||||
|
||||
### Compatible builds
|
||||
|
||||
Build affected configurations for the current platform with no result link:
|
||||
|
||||
```sh
|
||||
nix run .#check -- build --paths <task-path>...
|
||||
```
|
||||
|
||||
The app reports incompatible targets as evaluated but skipped for native build.
|
||||
A Darwin target must be built by a compatible Darwin runner or builder; a Linux
|
||||
evaluation is not a Darwin build.
|
||||
|
||||
### Final task validation
|
||||
|
||||
Before handoff, run the cumulative task check. It applies file checks only to
|
||||
task-owned paths, evaluates every flake system, and builds affected native
|
||||
targets:
|
||||
|
||||
```sh
|
||||
nix run .#check -- all --paths <task-path>...
|
||||
```
|
||||
|
||||
Use `--all-hosts` only when a deliberate audit must report every registered
|
||||
host as affected. Use the exhaustive command for CI, scheduled maintenance, or
|
||||
an explicit repository-wide audit:
|
||||
|
||||
```sh
|
||||
nix run .#check -- full
|
||||
```
|
||||
|
||||
`full` runs hooks over every tracked file and builds every check for the current
|
||||
platform through `nix-fast-build`.
|
||||
|
||||
## Add runtime tests when needed
|
||||
|
||||
Evaluation and builds do not prove service startup, socket behavior, firewall
|
||||
rules, users and groups, permissions, reboot behavior, or network interaction.
|
||||
For reusable NixOS behavior, invoke the `test-nixos-service` skill and add a
|
||||
`pkgs.testers.runNixOSTest` check. Hardware, credentials, GUI appearance, and
|
||||
external services may still require a precisely described manual check after
|
||||
the user activates the configuration.
|
||||
|
||||
## Diagnose failures by layer
|
||||
|
||||
Invoke the `debug-nix-failure` skill when a stage fails. Fix the first failing
|
||||
layer before running a more expensive one. Do not hide a pre-existing failure,
|
||||
weaken an assertion, add `lib.mkForce`, or skip a required host merely to make
|
||||
the task appear green.
|
||||
|
||||
## Report evidence precisely
|
||||
|
||||
Conclude with:
|
||||
|
||||
- task-owned paths;
|
||||
- affected units and hosts reported by `plan`;
|
||||
- each command run and its result;
|
||||
- which targets were parsed, evaluated, built, or runtime-tested;
|
||||
- any compatible-platform or manual-runtime gap.
|
||||
|
||||
Never describe evaluation as a build, a build as activation, or a VM test as
|
||||
proof of hardware-specific behavior.
|
||||
Reference in New Issue
Block a user