mirror of
https://github.com/moons-14/dotfiles.git
synced 2026-10-06 08:28:11 +09:00
Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec3770502d | ||
|
|
fda29cd08d | ||
|
|
db0a440da4 | ||
|
|
76f5dce9c0 | ||
|
|
3b61457718 | ||
|
|
c9f1584797 | ||
|
|
d51948c307 | ||
|
|
9a38e9f614 | ||
|
|
2cb7e76ca1 | ||
|
|
749410e032 | ||
|
|
d43f19c20a | ||
|
|
32a3067cb0 | ||
|
|
6e60bd8886 | ||
|
|
bcf7ef56cf | ||
|
|
63e2008423 | ||
|
|
3b8147ff46 | ||
|
|
c84f257149 | ||
|
|
6347292c1d | ||
|
|
2a82433754 | ||
|
|
847ee17b29 | ||
|
|
95f74aabcd | ||
|
|
717572ae24 | ||
|
|
8086c9a7f3 | ||
|
|
eba23455d4 | ||
|
|
b3b1031364 | ||
|
|
366ff54403 | ||
|
|
456e9946f4 | ||
|
|
c104404e5b | ||
|
|
cb41932fec | ||
|
|
fa4b7ca404 | ||
|
|
847d33e83b | ||
|
|
b2c700e1e4 | ||
|
|
c1540d8764 | ||
|
|
5b8c3b1415 | ||
|
|
6743569789 | ||
|
|
70253fc451 | ||
|
|
5107b80173 | ||
|
|
2bdea522cb | ||
|
|
c113d0d46d | ||
|
|
fa50be8feb | ||
|
|
33ebef4a1e | ||
|
|
94048ef8d5 | ||
|
|
c7627fa1b0 | ||
|
|
d68da620ac | ||
|
|
5f0df47775 | ||
|
|
119342e564 | ||
|
|
1f066e8937 | ||
|
|
0a440e3da8 | ||
|
|
bdf93ac6bb | ||
|
|
9ae5da4d30 | ||
|
|
872436b170 | ||
|
|
c14325e29c | ||
|
|
45bc66e25f | ||
|
|
78ee2d41ec | ||
|
|
0b1ae13048 | ||
|
|
458b0a2cdb | ||
|
|
01179f3dc6 | ||
|
|
42949fc06c | ||
|
|
a7f514c0f9 | ||
|
|
9bed1c3e25 | ||
|
|
791b6baa83 | ||
|
|
349f49e496 | ||
|
|
3166c12448 | ||
|
|
4c459e4aa7 | ||
|
|
9b9141dd84 | ||
|
|
16e3fda275 | ||
|
|
ca36b07839 |
@@ -1,13 +0,0 @@
|
||||
[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,15 +1,27 @@
|
||||
# Reference: https://github.com/ryoppippi/dotfiles/blob/main/.github/workflows/nix-build.yaml
|
||||
name: Build Linux checks
|
||||
description: Build every x86_64-linux flake check in parallel
|
||||
name: Check NixOS configurations
|
||||
description: Build every NixOS configuration and the Registry tests
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Build all Linux checks
|
||||
- name: Build every NixOS configuration
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
nix run .#nix-fast-build -- \
|
||||
--flake .#checks.x86_64-linux \
|
||||
--skip-cached \
|
||||
--no-nom \
|
||||
--no-link
|
||||
|
||||
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[@]}"
|
||||
|
||||
@@ -20,4 +20,3 @@ 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
|
||||
|
||||
+10
-163
@@ -1,50 +1,36 @@
|
||||
# Reference: https://github.com/ryoppippi/dotfiles/blob/main/.github/workflows/nix-build.yaml
|
||||
name: "CI: Nix"
|
||||
name: "CI: NixOS"
|
||||
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 }}
|
||||
@@ -52,153 +38,14 @@ concurrency:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
validate:
|
||||
name: Plan, lint, and evaluate
|
||||
check:
|
||||
name: Check all NixOS configurations
|
||||
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: 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'
|
||||
- name: Check NixOS configurations
|
||||
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,11 +12,9 @@ permissions:
|
||||
pull-requests: write
|
||||
jobs:
|
||||
update:
|
||||
name: Update and check Linux
|
||||
name: Update and validate flake inputs
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
outputs:
|
||||
changed: ${{ steps.update.outputs.changed }}
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
@@ -24,76 +22,18 @@ 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: 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
|
||||
- name: Check updated NixOS configurations
|
||||
if: steps.update.outputs.changed == 'true'
|
||||
uses: ./.github/actions/check-nixos
|
||||
- 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
|
||||
if: steps.update.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -105,5 +45,4 @@ jobs:
|
||||
body: |
|
||||
Automated update of `flake.lock`.
|
||||
|
||||
The updated inputs passed all-system evaluation and native builds of
|
||||
the Linux and Darwin check sets.
|
||||
The updated inputs passed the Registry tests and a build of every NixOS configuration.
|
||||
|
||||
+1
-7
@@ -1,6 +1,4 @@
|
||||
/*
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
!.gitignore
|
||||
!README.md
|
||||
@@ -8,10 +6,8 @@
|
||||
!AGENTS.md
|
||||
!/docs/
|
||||
|
||||
!/.codex/
|
||||
!/.github/
|
||||
!.github/
|
||||
!.gitea/
|
||||
!/.vscode/
|
||||
|
||||
!.envrc
|
||||
|
||||
@@ -19,9 +15,7 @@
|
||||
|
||||
!/flake.nix
|
||||
!/flake.lock
|
||||
!/opencode.json
|
||||
|
||||
!/scripts/
|
||||
!/shells/
|
||||
!/flake/
|
||||
!/overlays/
|
||||
|
||||
Vendored
-28
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"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 []"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -357,6 +357,7 @@ modules/profiles/
|
||||
│ ├── labwc/
|
||||
│ └── niri/
|
||||
├── networking/
|
||||
│ ├── homelab-cache-client/
|
||||
│ ├── tailscale-client/
|
||||
│ └── tailscale-subnet-router/
|
||||
├── platform/
|
||||
@@ -367,7 +368,11 @@ modules/profiles/
|
||||
│ ├── desktop/
|
||||
│ └── vm/
|
||||
├── workload/
|
||||
│ ├── camera/
|
||||
│ ├── development/
|
||||
│ ├── game/
|
||||
│ ├── machine-learning/
|
||||
│ ├── nix-builder/
|
||||
│ ├── personal/
|
||||
│ ├── server/
|
||||
│ └── remote-access/
|
||||
@@ -602,6 +607,7 @@ A host registry may use a specification like this:
|
||||
"security.secrets"
|
||||
"security.secure-boot"
|
||||
"security.tpm-storage"
|
||||
"workload.camera"
|
||||
"workload.development"
|
||||
"workload.personal"
|
||||
];
|
||||
@@ -624,6 +630,7 @@ A host registry may use a specification like this:
|
||||
"security.tpm-storage"
|
||||
"workload.development"
|
||||
"workload.game"
|
||||
"workload.machine-learning"
|
||||
"workload.personal"
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# moons14 dotfiles
|
||||
|
||||
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.
|
||||
My NixOS + Home Manager configurations build with flake.
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,119 @@
|
||||
# iOS Simulator 初期セットアップ
|
||||
|
||||
この手順は `m2` の macOS 環境で、stable Xcode と最新の stable iOS Simulator Runtime を使える状態にするためのもの。
|
||||
|
||||
## 前提
|
||||
|
||||
- `m2` が `workload.development` profile を有効にしていること
|
||||
- Mac App Store に Apple Account でサインイン済みであること
|
||||
- dotfiles を最新化していること
|
||||
|
||||
Xcode 本体は `applications.xcode` が Mac App Store 版を管理する。Simulator Runtime は Apple が管理する mutable state のため、Nix store には入れず専用 dev shell から導入する。
|
||||
|
||||
## 1. macOS 設定を反映する
|
||||
|
||||
リポジトリ直下で nix-darwin の設定を反映する。
|
||||
|
||||
```bash
|
||||
sudo darwin-rebuild switch --flake .#m2
|
||||
```
|
||||
|
||||
これにより `/Applications/Xcode.app` に stable Xcode がインストールされる。
|
||||
|
||||
Xcode のインストールで Mac App Store の認証エラーになる場合は、App Store を一度開いてサインイン状態を確認してから再実行する。
|
||||
|
||||
## 2. iOS Simulator Runtime を導入する
|
||||
|
||||
初回セットアップは次の1コマンドで行う。
|
||||
|
||||
```bash
|
||||
nix develop .#ios -c ios-simulator-install
|
||||
```
|
||||
|
||||
`ios-simulator-install` は次を順に実行する。
|
||||
|
||||
1. `/Applications/Xcode.app` が存在することを確認
|
||||
2. `xcode-select` の Developer Directory を stable Xcode に切り替え
|
||||
3. Xcode の first-launch components を導入
|
||||
4. 利用可能な新しい hardware support components を確認
|
||||
5. 選択中の Xcode に対応する最新の iOS Simulator Runtime をダウンロードしてインストール
|
||||
6. Xcode のバージョンとインストール済み Simulator Runtime を表示
|
||||
|
||||
途中で `sudo` の認証を求められる場合がある。
|
||||
|
||||
## 3. インストールを確認する
|
||||
|
||||
```bash
|
||||
xcodebuild -version
|
||||
xcode-select -p
|
||||
xcrun simctl list runtimes
|
||||
xcrun simctl list devices available
|
||||
```
|
||||
|
||||
`xcode-select -p` は次を指していること。
|
||||
|
||||
```text
|
||||
/Applications/Xcode.app/Contents/Developer
|
||||
```
|
||||
|
||||
`xcrun simctl list runtimes` に iOS runtime が表示されればセットアップ完了。
|
||||
|
||||
## 4. Simulator を起動する
|
||||
|
||||
```bash
|
||||
open -a Simulator
|
||||
```
|
||||
|
||||
Simulator の Device メニューから、インストール済み runtime で利用可能な iPhone を選択する。
|
||||
|
||||
## Runtime の更新
|
||||
|
||||
Xcode を stable の新しいバージョンへ更新した後は、同じコマンドを再実行する。
|
||||
|
||||
```bash
|
||||
nix develop .#ios -c ios-simulator-install
|
||||
```
|
||||
|
||||
Xcode の選択、first-launch components、hardware support、iOS Simulator Runtime の状態をまとめて更新できる。
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
### Xcode が見つからない
|
||||
|
||||
次のエラーが出る場合、先に nix-darwin の設定を反映する。
|
||||
|
||||
```text
|
||||
Xcode is not installed at /Applications/Xcode.app.
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo darwin-rebuild switch --flake .#m2
|
||||
```
|
||||
|
||||
### Simulator Runtime が見えない
|
||||
|
||||
まず runtime 一覧を確認する。
|
||||
|
||||
```bash
|
||||
xcrun simctl list runtimes
|
||||
```
|
||||
|
||||
iOS runtime がない場合は再度インストーラーを実行する。
|
||||
|
||||
```bash
|
||||
nix develop .#ios -c ios-simulator-install
|
||||
```
|
||||
|
||||
### Command Line Tools 側を参照している
|
||||
|
||||
```bash
|
||||
xcode-select -p
|
||||
```
|
||||
|
||||
が `/Library/Developer/CommandLineTools` を指している場合でも、`ios-simulator-install` が `/Applications/Xcode.app/Contents/Developer` へ切り替える。
|
||||
|
||||
手動で直す場合は次を実行する。
|
||||
|
||||
```bash
|
||||
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
|
||||
```
|
||||
Generated
+737
-380
File diff suppressed because it is too large
Load Diff
@@ -38,12 +38,6 @@
|
||||
url = "github:ghostty-org/ghostty";
|
||||
};
|
||||
|
||||
# Speech to text
|
||||
handy = {
|
||||
url = "github:cjpais/Handy";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Shell / Launcher
|
||||
vicinae.url = "github:vicinaehq/vicinae";
|
||||
|
||||
@@ -66,6 +60,11 @@
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
deploy-rs = {
|
||||
url = "github:serokell/deploy-rs";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Disk management
|
||||
disko = {
|
||||
url = "github:nix-community/disko";
|
||||
@@ -97,6 +96,13 @@
|
||||
url = "github:ilysenko/codex-desktop-linux";
|
||||
};
|
||||
|
||||
codex-session-usage.url = "github:moons-14/codex-session-usage";
|
||||
|
||||
skills = {
|
||||
url = "github:mattpocock/skills";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
# Index / Search
|
||||
nix-index-database = {
|
||||
url = "github:nix-community/nix-index-database";
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
{
|
||||
imports = [
|
||||
./deploy.nix
|
||||
./formatter.nix
|
||||
./git-hooks.nix
|
||||
./registry.nix
|
||||
./validation.nix
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
inputs,
|
||||
self,
|
||||
...
|
||||
}:
|
||||
{
|
||||
flake.deploy = {
|
||||
nodes.nix-builder = {
|
||||
hostname = "nix-builder";
|
||||
sshUser = "moons";
|
||||
user = "root";
|
||||
|
||||
interactiveSudo = true;
|
||||
remoteBuild = true;
|
||||
autoRollback = true;
|
||||
magicRollback = true;
|
||||
|
||||
profiles.system.path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.nix-builder;
|
||||
};
|
||||
};
|
||||
|
||||
perSystem =
|
||||
{ system, ... }:
|
||||
{
|
||||
apps.deploy = inputs.deploy-rs.apps.${system}.default;
|
||||
checks = inputs.deploy-rs.lib.${system}.deployChecks self.deploy;
|
||||
};
|
||||
}
|
||||
@@ -34,9 +34,7 @@
|
||||
];
|
||||
};
|
||||
|
||||
actionlint.enable = true;
|
||||
deadnix.enable = true;
|
||||
ruff.enable = true;
|
||||
statix.enable = true;
|
||||
shellcheck.enable = true;
|
||||
};
|
||||
|
||||
+7
-38
@@ -25,42 +25,11 @@ 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 // {
|
||||
inherit validationMetadata;
|
||||
};
|
||||
lib = dotfilesLib;
|
||||
};
|
||||
|
||||
perSystem =
|
||||
@@ -68,10 +37,11 @@ in
|
||||
let
|
||||
nixosChecks =
|
||||
lib.mapAttrs' (name: nixos: lib.nameValuePair "nixos-${name}" nixos.config.system.build.toplevel)
|
||||
(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
|
||||
);
|
||||
(
|
||||
lib.filterAttrs (
|
||||
_: nixos: nixos.pkgs.stdenv.hostPlatform.system == system
|
||||
) configurations.nixosConfigurations
|
||||
);
|
||||
in
|
||||
{
|
||||
checks = {
|
||||
@@ -79,7 +49,6 @@ in
|
||||
inherit inputs lib pkgs;
|
||||
};
|
||||
}
|
||||
// nixosChecks
|
||||
// darwinChecks;
|
||||
// nixosChecks;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
_: {
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
||||
+8
-32
@@ -1,47 +1,20 @@
|
||||
{
|
||||
nix-example = {
|
||||
nix-builder = {
|
||||
system = "x86_64-linux";
|
||||
stateVersion = "26.05";
|
||||
user = "moons";
|
||||
path = ./nix-example;
|
||||
path = ./nix-builder;
|
||||
|
||||
profiles = [
|
||||
"base"
|
||||
"interface.cli"
|
||||
"networking.tailscale-client"
|
||||
"platform.vm"
|
||||
"workload.development"
|
||||
"workload.nix-builder"
|
||||
"workload.remote-access"
|
||||
];
|
||||
};
|
||||
|
||||
ops = {
|
||||
system = "x86_64-linux";
|
||||
stateVersion = "26.05";
|
||||
user = "moons";
|
||||
path = ./ops;
|
||||
|
||||
profiles = [
|
||||
"base"
|
||||
"interface.cli"
|
||||
"platform.vm"
|
||||
"workload.remote-access"
|
||||
];
|
||||
};
|
||||
|
||||
internal-app-01 = {
|
||||
system = "x86_64-linux";
|
||||
stateVersion = "26.05";
|
||||
user = "moons";
|
||||
path = ./internal-app-01;
|
||||
|
||||
profiles = [
|
||||
"base"
|
||||
"interface.cli"
|
||||
"platform.vm"
|
||||
"workload.server"
|
||||
];
|
||||
};
|
||||
|
||||
installer = {
|
||||
system = "x86_64-linux";
|
||||
stateVersion = "26.05";
|
||||
@@ -91,6 +64,7 @@
|
||||
"workload.game"
|
||||
"workload.personal"
|
||||
];
|
||||
|
||||
};
|
||||
|
||||
galleria = {
|
||||
@@ -104,7 +78,7 @@
|
||||
"interface.cli"
|
||||
"interface.labwc"
|
||||
"interface.niri"
|
||||
"interface.wallpaperengine"
|
||||
"networking.tailscale-client"
|
||||
"platform.intel-nvidia-desktop"
|
||||
"security.secrets"
|
||||
"security.secure-boot"
|
||||
@@ -112,7 +86,9 @@
|
||||
"security.fingerprint"
|
||||
"workload.development"
|
||||
"workload.game"
|
||||
"workload.machine-learning"
|
||||
"workload.personal"
|
||||
"workload.camera"
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
...
|
||||
}:
|
||||
{
|
||||
# This desktop is permanently connected to AC power.
|
||||
systemd.user.services.swayidle.Service.Environment = [
|
||||
"SWAYIDLE_ASSUME_AC=1"
|
||||
];
|
||||
|
||||
services.kanshi = {
|
||||
enable = true;
|
||||
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
boot.initrd.luks.devices.cryptroot.device =
|
||||
"/dev/disk/by-partuuid/04295552-cbb8-4511-ac1e-1171ec20f8d1";
|
||||
|
||||
# Keep Windows data and recovery partitions out of UDisks-based file
|
||||
# managers. The shared EFI System Partition stays available as /boot.
|
||||
services.udev.extraRules = ''
|
||||
ENV{ID_PART_ENTRY_UUID}=="0480f887-d1f9-489d-b8fe-78549ced1938", ENV{UDISKS_IGNORE}="1"
|
||||
ENV{ID_PART_ENTRY_UUID}=="6c70041b-3f65-4eb1-8b08-18ed20001877", ENV{UDISKS_IGNORE}="1"
|
||||
'';
|
||||
|
||||
environment.systemPackages = with inputs.browser-previews.packages.${pkgs.system}; [
|
||||
google-chrome-beta
|
||||
];
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
users.users.root.openssh.authorizedKeys.keys = [
|
||||
"sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIKhxDkucmeCor6CKoXAua7DgDSzuXrZOtpdkyzQxz5+aAAAABHNzaDo= moons@moons14.com"
|
||||
"sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIN6hZJyng/5LgFKPjR6uZAd/00UkO0vN0uQOoIvfSELdAAAABHNzaDo= moons@moons14.com"
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPLwReAiwhXoO34S2+MrvqUhi8IWp4IzUq4OSp3niJdq"
|
||||
];
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
imports = [ ./hardware-configuration.nix ];
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
# nix-builder bootstrap
|
||||
|
||||
The host configuration can be built before its cache signing secret exists.
|
||||
Harmonia's socket remains stopped until SOPS installs the signing key at
|
||||
`/run/secrets/harmonia/signing-key`.
|
||||
|
||||
## Proxmox storage layout
|
||||
|
||||
The host configuration expects three filesystems. Keep the build scratch space
|
||||
separate from the store so a large build cannot fill the root filesystem.
|
||||
|
||||
| Mount point | Suggested size | Contents |
|
||||
| -------------------- | -------------- | ------------------------------- |
|
||||
| `/` | 48 GiB | NixOS and mutable system state |
|
||||
| `/var/lib/nix-build` | 192 GiB | Disposable build scratch space |
|
||||
| `/nix/store` | 1 TiB | Fleet closures and binary cache |
|
||||
|
||||
The build-server policy starts emergency store GC below 64 GiB free and aims
|
||||
for 128 GiB free. Persistent roots under `/var/lib/nix-fleet/roots` protect the
|
||||
latest fleet builds from that GC. It also limits Nix to two concurrent
|
||||
derivations while allowing each derivation to use every vCPU assigned to the
|
||||
VM.
|
||||
|
||||
For the two dedicated ext4 data filesystems, remove the default root-reserved
|
||||
blocks once after formatting; keep the root filesystem's reserve intact:
|
||||
|
||||
```bash
|
||||
sudo tune2fs -m 0 /dev/disk/by-label/nix-build
|
||||
sudo tune2fs -m 0 /dev/disk/by-label/nix-store
|
||||
```
|
||||
|
||||
## Initial deployment
|
||||
|
||||
Once the VM is reachable as `moons@nix-builder`, deploy it from the repository:
|
||||
|
||||
```bash
|
||||
nix run .#deploy -- .#nix-builder
|
||||
```
|
||||
|
||||
deploy-rs uses the target's `ssh-ng` store, so the system closure is built on
|
||||
the builder rather than copied from the laptop. Automatic and magic rollback
|
||||
remain enabled.
|
||||
|
||||
## Add the host SOPS recipient
|
||||
|
||||
After the VM has a stable SSH host key, derive its age recipient:
|
||||
|
||||
```bash
|
||||
ssh-keyscan -t ed25519 nix-builder 2>/dev/null | ssh-to-age
|
||||
```
|
||||
|
||||
Add the recipient to `.sops.yaml` and add a creation rule for
|
||||
`secrets/hosts/nix-builder/*.yaml`. The admin YubiKey recipient should remain in
|
||||
the same key group for recovery.
|
||||
|
||||
## Generate the cache signing key
|
||||
|
||||
Run this on a trusted Nix machine, preferably with the temporary files on a
|
||||
tmpfs:
|
||||
|
||||
```bash
|
||||
nix-store --generate-binary-cache-key \
|
||||
cache.app.homelabs.run-1 \
|
||||
harmonia.private \
|
||||
harmonia.public
|
||||
```
|
||||
|
||||
Create `secrets/hosts/nix-builder/system.yaml` with SOPS and store the complete
|
||||
contents of `harmonia.private` at `harmonia.signing-key`:
|
||||
|
||||
```yaml
|
||||
harmonia:
|
||||
signing-key: cache.app.homelabs.run-1:REDACTED
|
||||
```
|
||||
|
||||
Copy the complete contents of `harmonia.public` to
|
||||
`modules/systems/nix/homelab-cache/public-key`. The private plaintext file must
|
||||
not be committed or retained.
|
||||
|
||||
After committing both encrypted/public files, select
|
||||
`networking.homelab-cache-client` on each client host.
|
||||
|
||||
Redeploy the builder and verify the cache after installing the secret:
|
||||
|
||||
```bash
|
||||
nix run .#deploy -- .#nix-builder
|
||||
curl --fail http://nix-builder:5000/nix-cache-info
|
||||
```
|
||||
|
||||
## Normal operation
|
||||
|
||||
Run `fleet-build` on the builder to build and root every NixOS host, or pass a
|
||||
list of host names to build only those hosts. Run `fleet-deploy` with the normal
|
||||
deploy-rs target syntax when additional fleet nodes have been added to
|
||||
`flake/deploy.nix`:
|
||||
|
||||
```bash
|
||||
fleet-build
|
||||
fleet-build x1g13 galleria
|
||||
fleet-deploy .#nix-builder
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
fileSystems."/nix/store" = {
|
||||
device = "/dev/disk/by-label/nix-store";
|
||||
fsType = "ext4";
|
||||
|
||||
options = [
|
||||
"noatime"
|
||||
];
|
||||
|
||||
neededForBoot = true;
|
||||
};
|
||||
|
||||
fileSystems."/var/lib/nix-build" = {
|
||||
device = "/dev/disk/by-label/nix-build";
|
||||
fsType = "ext4";
|
||||
|
||||
options = [
|
||||
"noatime"
|
||||
];
|
||||
};
|
||||
|
||||
nix.settings.build-dir = "/var/lib/nix-build";
|
||||
|
||||
services.fstrim.enable = true;
|
||||
}
|
||||
+9
-5
@@ -1,8 +1,12 @@
|
||||
# Do not modify this file! It was generated by `nixos-generate-config` and may
|
||||
# be overwritten by future invocations.
|
||||
# Do not modify this file! It was generated by ‘nixos-generate-config’
|
||||
# and may be overwritten by future invocations. Please make changes
|
||||
# to /etc/nixos/configuration.nix instead.
|
||||
{ lib, modulesPath, ... }:
|
||||
|
||||
{
|
||||
imports = [ (modulesPath + "/profiles/qemu-guest.nix") ];
|
||||
imports = [
|
||||
(modulesPath + "/profiles/qemu-guest.nix")
|
||||
];
|
||||
|
||||
boot.initrd.availableKernelModules = [
|
||||
"ata_piix"
|
||||
@@ -17,12 +21,12 @@
|
||||
boot.extraModulePackages = [ ];
|
||||
|
||||
fileSystems."/" = {
|
||||
device = "/dev/disk/by-uuid/1b12ab98-2537-4207-a3f4-bb8ba7b53b00";
|
||||
device = "/dev/disk/by-uuid/49fc2e1c-7909-41cc-ac78-55b4d9a01e62";
|
||||
fsType = "ext4";
|
||||
};
|
||||
|
||||
fileSystems."/boot" = {
|
||||
device = "/dev/disk/by-uuid/8365-C778";
|
||||
device = "/dev/disk/by-uuid/40D4-ABBE";
|
||||
fsType = "vfat";
|
||||
options = [
|
||||
"fmask=0077"
|
||||
@@ -0,0 +1,19 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
hostSecrets = ../../secrets/hosts/nix-builder/system.yaml;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
./filesystem.nix
|
||||
./hardware-configuration.nix
|
||||
];
|
||||
|
||||
sops.secrets = lib.mkIf (builtins.pathExists hostSecrets) {
|
||||
"harmonia/signing-key" = {
|
||||
sopsFile = hostSecrets;
|
||||
restartUnits = [ "harmonia.service" ];
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.interfaces."tailscale0".allowedTCPPorts = [ 5000 ];
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
# Do not modify this file! It was generated by `nixos-generate-config` and may
|
||||
# be overwritten by future invocations.
|
||||
{ lib, modulesPath, ... }:
|
||||
{
|
||||
imports = [ (modulesPath + "/profiles/qemu-guest.nix") ];
|
||||
|
||||
boot.initrd.availableKernelModules = [
|
||||
"ata_piix"
|
||||
"uhci_hcd"
|
||||
"virtio_pci"
|
||||
"virtio_scsi"
|
||||
"sd_mod"
|
||||
"sr_mod"
|
||||
];
|
||||
boot.initrd.kernelModules = [ ];
|
||||
boot.kernelModules = [ ];
|
||||
boot.extraModulePackages = [ ];
|
||||
|
||||
fileSystems."/" = {
|
||||
device = "/dev/disk/by-uuid/69fa2193-1e4f-438a-8898-5de8a3f36e5b";
|
||||
fsType = "ext4";
|
||||
};
|
||||
|
||||
fileSystems."/boot" = {
|
||||
device = "/dev/disk/by-uuid/D09B-4277";
|
||||
fsType = "vfat";
|
||||
options = [
|
||||
"fmask=0077"
|
||||
"dmask=0077"
|
||||
];
|
||||
};
|
||||
|
||||
swapDevices = [ ];
|
||||
|
||||
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
imports = [ ./hardware-configuration.nix ];
|
||||
|
||||
networking = {
|
||||
useDHCP = false;
|
||||
|
||||
interfaces = {
|
||||
ens18 = {
|
||||
useDHCP = false;
|
||||
ipv4.addresses = [
|
||||
{
|
||||
address = "10.50.128.20";
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
ens19 = {
|
||||
useDHCP = false;
|
||||
ipv4.addresses = [
|
||||
{
|
||||
address = "10.50.7.101";
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
ens20 = {
|
||||
useDHCP = false;
|
||||
ipv4.routes = [
|
||||
{
|
||||
address = "10.50.64.0";
|
||||
prefixLength = 24;
|
||||
via = "10.50.82.1";
|
||||
}
|
||||
];
|
||||
ipv4.addresses = [
|
||||
{
|
||||
address = "10.50.82.10";
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
defaultGateway = {
|
||||
address = "10.50.128.1";
|
||||
interface = "ens18";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,37 @@
|
||||
_: {
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
chrome = pkgs.google-chrome.overrideAttrs (old: {
|
||||
nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ pkgs.makeWrapper ];
|
||||
|
||||
postFixup = (old.postFixup or "") + ''
|
||||
wrapProgram $out/bin/google-chrome-stable \
|
||||
--set LIBVA_DRIVER_NAME nvidia \
|
||||
--set NVD_BACKEND direct
|
||||
'';
|
||||
});
|
||||
|
||||
features = [
|
||||
"MiddleClickAutoscroll"
|
||||
"AcceleratedVideoDecoder"
|
||||
"AcceleratedVideoDecodeLinuxGL"
|
||||
"PlatformHEVCDecoderSupport"
|
||||
]
|
||||
++ lib.optional config.my.hardwares.nvidia.enable "VaapiOnNvidiaGPUs";
|
||||
in
|
||||
{
|
||||
programs.google-chrome = {
|
||||
enable = true;
|
||||
package = if config.my.hardwares.nvidia.enable then chrome else pkgs.google-chrome;
|
||||
|
||||
commandLineArgs = [
|
||||
"--enable-features=MiddleClickAutoscroll"
|
||||
"--enable-features=${lib.concatStringsSep "," features}"
|
||||
"--use-gl=angle"
|
||||
"--use-angle=gl"
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{ inputs, ... }:
|
||||
{
|
||||
_: {
|
||||
description = "Codex Desktop for Linux";
|
||||
|
||||
includes = [ "applications.codex" ];
|
||||
|
||||
imports.nixos = [
|
||||
inputs.codex-desktop-linux.nixosModules.default
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,15 +4,7 @@
|
||||
...
|
||||
}:
|
||||
{
|
||||
programs.codexDesktopLinux = {
|
||||
enable = true;
|
||||
cliPackage = inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.codex;
|
||||
remoteControl.enable = true;
|
||||
remoteMobileControl.enable = true;
|
||||
computerUseUi.enable = false;
|
||||
linuxFeatures = [
|
||||
"appshots"
|
||||
"open-target-discovery"
|
||||
];
|
||||
};
|
||||
environment.systemPackages = [
|
||||
inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.chatgpt
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
codexSessionUsage = inputs.codex-session-usage.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
in
|
||||
{
|
||||
home.packages = [ codexSessionUsage ];
|
||||
|
||||
xdg.dataFile = {
|
||||
"vicinae/scripts/codex-session-usage/start" = {
|
||||
executable = true;
|
||||
text = ''
|
||||
#!${lib.getExe pkgs.bash}
|
||||
# @vicinae.schemaVersion 1
|
||||
# @vicinae.title Start Codex Session Usage
|
||||
# @vicinae.description Start the local Codex session usage dashboard
|
||||
# @vicinae.mode compact
|
||||
# @vicinae.icon 📊
|
||||
# @vicinae.argument1 { "type": "text", "placeholder": "Port (optional)", "optional": true }
|
||||
|
||||
if [[ -z "$1" ]]; then
|
||||
exec ${lib.getExe codexSessionUsage} start
|
||||
fi
|
||||
|
||||
if [[ "$1" =~ ^[0-9]+$ ]] && (( 10#$1 >= 1 && 10#$1 <= 65535 )); then
|
||||
exec ${lib.getExe codexSessionUsage} start --port "$1"
|
||||
fi
|
||||
|
||||
printf '%s\n' 'Port must be an integer between 1 and 65535.' >&2
|
||||
exit 2
|
||||
'';
|
||||
};
|
||||
|
||||
"vicinae/scripts/codex-session-usage/stop" = {
|
||||
executable = true;
|
||||
text = ''
|
||||
#!${lib.getExe pkgs.bash}
|
||||
# @vicinae.schemaVersion 1
|
||||
# @vicinae.title Stop Codex Session Usage
|
||||
# @vicinae.description Stop the local Codex session usage dashboard
|
||||
# @vicinae.mode compact
|
||||
# @vicinae.icon 📊
|
||||
|
||||
exec ${lib.getExe codexSessionUsage} stop
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
model = "gpt-5.6-sol"
|
||||
model_reasoning_effort = "medium"
|
||||
|
||||
approval_policy = "on-request"
|
||||
approvals_reviewer = "auto_review"
|
||||
sandbox_mode = "workspace-write"
|
||||
web_search = "cached"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
network_access = false
|
||||
|
||||
[projects."/home/moons/dotfiles"]
|
||||
trust_level = "trusted"
|
||||
@@ -1,6 +1,36 @@
|
||||
{ inputs, pkgs, ... }:
|
||||
{
|
||||
home.packages = [
|
||||
inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.codex
|
||||
];
|
||||
config,
|
||||
inputs,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
configDirectory =
|
||||
if config.home.preferXdgDirectories then
|
||||
"${config.xdg.configHome}/codex"
|
||||
else
|
||||
"${config.home.homeDirectory}/.codex";
|
||||
configFile = "${configDirectory}/config.toml";
|
||||
in
|
||||
{
|
||||
programs.codex = {
|
||||
enable = true;
|
||||
package = inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.codex;
|
||||
|
||||
skills = {
|
||||
grilling = inputs.skills + "/skills/productivity/grilling";
|
||||
};
|
||||
};
|
||||
|
||||
# Keep the repository copy as an initial value. Codex may mutate the live
|
||||
# file between activations; each Home Manager switch resets it from here.
|
||||
home.activation.resetCodexConfig = {
|
||||
after = [ "writeBoundary" ];
|
||||
before = [ ];
|
||||
data = ''
|
||||
${pkgs.coreutils}/bin/mkdir -p ${lib.escapeShellArg configDirectory}
|
||||
${pkgs.coreutils}/bin/install -m 0600 ${./config.toml} ${lib.escapeShellArg configFile}
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
_: {
|
||||
{
|
||||
inputs,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
unstable = inputs.nixpkgs-unstable.legacyPackages.${pkgs.stdenv.hostPlatform.system};
|
||||
in
|
||||
{
|
||||
programs.vesktop = {
|
||||
enable = true;
|
||||
package = unstable.vesktop;
|
||||
|
||||
settings = {
|
||||
discordBranch = "stable";
|
||||
@@ -16,6 +25,10 @@ _: {
|
||||
|
||||
openLinksWithElectron = false;
|
||||
|
||||
# Keep WebRTC on the public interface selected by the default route.
|
||||
# Secondary private interfaces can otherwise stall voice at DTLS.
|
||||
webRTCIPHandlingPolicy = "default_public_interface_only";
|
||||
|
||||
spellCheckLanguages = [
|
||||
"ja-JP"
|
||||
"en-US"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
home.packages = [ pkgs.ffmpeg ];
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
{ inputs, system, ... }: {
|
||||
programs.ghostty = {
|
||||
systemd.enable = true;
|
||||
# Labwc's Close action correctly targets one xdg-toplevel, but Ghostty's
|
||||
# systemd service runs every window in one GTK single-instance process.
|
||||
# If that process exits while handling the request, every Ghostty window
|
||||
# disappears together. Keep each launcher invocation independent instead.
|
||||
systemd.enable = false;
|
||||
package = inputs.ghostty.packages.${system}.default;
|
||||
|
||||
settings.gtk-single-instance = false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
homebrew.casks = [ "handy" ];
|
||||
|
||||
launchd.agents.handy = {
|
||||
command = "/usr/bin/open -gja Handy --args --start-hidden";
|
||||
serviceConfig.RunAtLoad = true;
|
||||
};
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
{
|
||||
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" ];
|
||||
};
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
description = "Handy offline speech-to-text";
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
home.packages = [ pkgs.python314Packages.huggingface-hub ];
|
||||
}
|
||||
@@ -102,8 +102,6 @@ in
|
||||
(action "W-q" "Close")
|
||||
(action "W-f" "ToggleMaximize")
|
||||
(action "W-c" "Iconify")
|
||||
(execute "W-Tab" "window-overview")
|
||||
(execute "W-S-Tab" "window-overview --reverse")
|
||||
(action "W-Up" "Lower")
|
||||
(action "W-Down" "Raise")
|
||||
(action "W-Left" "NextWindow")
|
||||
@@ -123,14 +121,13 @@ in
|
||||
(execute "W-s" "noctalia msg panel-toggle launcher")
|
||||
(execute "W-e" "nautilus --new-window")
|
||||
(execute "W-l" "loginctl lock-session")
|
||||
(execute "W-v" "vicinae vicinae://launch/clipboard/history?toggle=true")
|
||||
(execute "W-v" "vicinae vicinae://launch/clipboard/history")
|
||||
(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")
|
||||
|
||||
@@ -6,16 +6,11 @@
|
||||
}:
|
||||
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;
|
||||
package = unstable.labwc;
|
||||
};
|
||||
|
||||
xdg.portal.config.labwc.default = [
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
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,167 +0,0 @@
|
||||
# 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
|
||||
@@ -1,57 +0,0 @@
|
||||
# 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[@]}"
|
||||
@@ -1,229 +0,0 @@
|
||||
{
|
||||
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" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
description = "Wallpaper Engine backgrounds for Linux";
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
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";
|
||||
}
|
||||
@@ -162,14 +162,6 @@ in
|
||||
"file://${config.home.homeDirectory}/Desktop Desktop"
|
||||
"file://${config.home.homeDirectory}/Pictures Pictures"
|
||||
"file://${config.home.homeDirectory}/Downloads Downloads"
|
||||
"file://${config.home.homeDirectory}/projects projects"
|
||||
"file://${config.home.homeDirectory}/Projects Projects"
|
||||
];
|
||||
|
||||
home.activation.createProjectsDirectory = {
|
||||
after = [ "writeBoundary" ];
|
||||
before = [ ];
|
||||
data = ''
|
||||
$DRY_RUN_CMD mkdir -p "$HOME/projects"
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ in
|
||||
|
||||
binds = keybindings // {
|
||||
# niri window or focus move
|
||||
"Mod+MouseMiddle".action.toggle-window-floating = [ ];
|
||||
|
||||
"Mod+Shift+Left" = {
|
||||
action.focus-monitor-left = [ ];
|
||||
hotkey-overlay.title = "Focus Monitor Left";
|
||||
@@ -24,6 +26,18 @@ in
|
||||
hotkey-overlay.title = "Focus Monitor Down";
|
||||
};
|
||||
|
||||
# Use the modifier combinations in the opposite direction from Niri's defaults.
|
||||
"Mod+WheelScrollDown".action.focus-column-right = [ ];
|
||||
"Mod+WheelScrollUp".action.focus-column-left = [ ];
|
||||
"Mod+Shift+WheelScrollDown" = {
|
||||
cooldown-ms = 150;
|
||||
action.focus-workspace-down = [ ];
|
||||
};
|
||||
"Mod+Shift+WheelScrollUp" = {
|
||||
cooldown-ms = 150;
|
||||
action.focus-workspace-up = [ ];
|
||||
};
|
||||
|
||||
"Mod+Shift+Ctrl+Left" = {
|
||||
action.move-window-to-monitor-left = [ ];
|
||||
hotkey-overlay.title = "Move Window to Monitor Left";
|
||||
@@ -41,24 +55,6 @@ 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"
|
||||
@@ -110,7 +106,7 @@ in
|
||||
"Mod+V" = {
|
||||
action.spawn = [
|
||||
"vicinae"
|
||||
"vicinae://launch/clipboard/history?toggle=true"
|
||||
"vicinae://launch/clipboard/history"
|
||||
];
|
||||
hotkey-overlay.title = "Clipboard History";
|
||||
};
|
||||
@@ -155,13 +151,6 @@ in
|
||||
];
|
||||
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 = [
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
description = "Fleet build and deploy command-line tools";
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
inputs,
|
||||
pkgs,
|
||||
primaryUser,
|
||||
...
|
||||
}:
|
||||
let
|
||||
system = pkgs.stdenv.hostPlatform.system;
|
||||
deployRs = inputs.deploy-rs.packages.${system}.default;
|
||||
|
||||
fleetBuild = pkgs.writeShellApplication {
|
||||
name = "fleet-build";
|
||||
runtimeInputs = [
|
||||
pkgs.jq
|
||||
pkgs.nix
|
||||
];
|
||||
text = ''
|
||||
flake_ref="''${FLAKE:-/home/${primaryUser}/dotfiles}"
|
||||
|
||||
if (( $# == 0 )); then
|
||||
# Keep this pipeline inside command substitution so pipefail and
|
||||
# writeShellApplication's errexit propagate evaluation failures.
|
||||
host_lines="$(
|
||||
nix eval --json "$flake_ref#nixosConfigurations" \
|
||||
--apply 'configs: builtins.attrNames configs' |
|
||||
jq -r '.[] | select(. != "installer")'
|
||||
)"
|
||||
if [[ -z "$host_lines" ]]; then
|
||||
echo "No deployable NixOS hosts found in $flake_ref" >&2
|
||||
exit 1
|
||||
fi
|
||||
mapfile -t hosts <<< "$host_lines"
|
||||
else
|
||||
hosts=("$@")
|
||||
fi
|
||||
|
||||
for host in "''${hosts[@]}"; do
|
||||
nix build \
|
||||
--out-link "/var/lib/nix-fleet/roots/build/$host" \
|
||||
"$flake_ref#nixosConfigurations.$host.config.system.build.toplevel"
|
||||
done
|
||||
'';
|
||||
};
|
||||
|
||||
fleetDeploy = pkgs.writeShellApplication {
|
||||
name = "fleet-deploy";
|
||||
runtimeInputs = [ deployRs ];
|
||||
text = ''
|
||||
cd "''${FLAKE:-/home/${primaryUser}/dotfiles}" || exit 1
|
||||
|
||||
exec deploy \
|
||||
--keep-result \
|
||||
--result-path /var/lib/nix-fleet/roots/deploy \
|
||||
"$@"
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
environment.systemPackages = [
|
||||
fleetBuild
|
||||
fleetDeploy
|
||||
];
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{ lib, pkgs }:
|
||||
let
|
||||
version = "0.46.0";
|
||||
version = "0.56.0";
|
||||
architecture =
|
||||
if pkgs.stdenv.hostPlatform.isx86_64 then
|
||||
"x86_64"
|
||||
@@ -10,8 +10,8 @@ let
|
||||
throw "CodexBar CLI is only packaged for x86_64-linux and aarch64-linux";
|
||||
hash =
|
||||
{
|
||||
x86_64 = "sha256-yMrOpu1WIv2sGVgvY9qf2XCoX2AXMSqR2b8bQDRU6os=";
|
||||
aarch64 = "sha256-pCp3NOlxFN6zeH67W04WGSPbUae+ktzR5vEunWqu00g=";
|
||||
x86_64 = "sha256-hzCnAyiizm8ZDfE1ONEP6S2Pdnskclm+XdDBBhEYVqE=";
|
||||
aarch64 = "sha256-vfcgDEFpORPymcRYmlqvsjhV4pU7lNC8Vd9FDPI9UjM=";
|
||||
}
|
||||
.${architecture};
|
||||
in
|
||||
|
||||
@@ -13,15 +13,6 @@ 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";
|
||||
@@ -51,7 +42,7 @@ in
|
||||
programs.noctalia = {
|
||||
enable = true;
|
||||
|
||||
package = noctaliaPatched;
|
||||
package = inputs.noctalia.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
|
||||
systemd.enable = true;
|
||||
|
||||
@@ -166,7 +157,7 @@ in
|
||||
network-connection = {
|
||||
type = "custom_button";
|
||||
glyph = "access-point";
|
||||
actions.left = "nm-connection-editor";
|
||||
actions.left = "exec nm-connection-editor";
|
||||
};
|
||||
tray = {
|
||||
type = "tray";
|
||||
@@ -187,7 +178,7 @@ in
|
||||
"google-chrome"
|
||||
"code"
|
||||
"dev.zed.Zed"
|
||||
"codex-desktop"
|
||||
"chatgpt"
|
||||
"vesktop"
|
||||
];
|
||||
show_all_outputs = true;
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
{
|
||||
description = "Noctalia Wayland desktop shell";
|
||||
|
||||
includes = [
|
||||
"applications.window-overview"
|
||||
];
|
||||
|
||||
imports = {
|
||||
nixos = [ inputs.noctalia.nixosModules.default ];
|
||||
home = [ inputs.noctalia.homeModules.default ];
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
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()) {
|
||||
@@ -1,26 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -2,5 +2,19 @@
|
||||
{
|
||||
home.packages = [
|
||||
inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.opencode
|
||||
inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.oh-my-opencode
|
||||
];
|
||||
|
||||
home.file.".omo/omo.jsonc".text = builtins.toJSON {
|
||||
agents = {
|
||||
sisyphus.model = "openai/gpt-5.6-sol";
|
||||
hephaestus.model = "openai/gpt-5.6-terra";
|
||||
prometheus.model = "openai/gpt-5.6-terra";
|
||||
oracle.model = "openai/gpt-5.6-terra";
|
||||
momus.model = "openai/gpt-5.6-terra";
|
||||
librarian.model = "openai/gpt-5.6-luna";
|
||||
explore.model = "openai/gpt-5.6-luna";
|
||||
atlas.model = "openai/gpt-5.6-luna";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ lib.mkMerge [
|
||||
};
|
||||
|
||||
extraConfig = ''
|
||||
Match exec "test -n \"$SSH_AUTH_SOCK\" && test -S \"$SSH_AUTH_SOCK\""
|
||||
IdentityAgent $SSH_AUTH_SOCK
|
||||
|
||||
Match exec "test -S %d/.1password/agent.sock"
|
||||
IdentityAgent %d/.1password/agent.sock
|
||||
'';
|
||||
|
||||
@@ -10,6 +10,8 @@ in
|
||||
programs.vicinae = {
|
||||
enable = true;
|
||||
|
||||
enableChromeIntegration = false;
|
||||
|
||||
settings = {
|
||||
font.size = 11;
|
||||
close_on_focus_loss = true;
|
||||
|
||||
@@ -8,9 +8,6 @@ let
|
||||
in
|
||||
{
|
||||
programs.vicinae = {
|
||||
|
||||
package = pkgs.vicinae;
|
||||
|
||||
systemd = {
|
||||
enable = true;
|
||||
autoStart = true;
|
||||
|
||||
@@ -118,7 +118,7 @@ let
|
||||
];
|
||||
userSettings = {
|
||||
"nix.enableLanguageServer" = true;
|
||||
"nix.serverPath" = "nixd";
|
||||
"nix.serverPath" = "nil";
|
||||
"nix.serverSettings" = {
|
||||
nixd = {
|
||||
formatting.command = [ "nixfmt" ];
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
windowOverview = pkgs.callPackage ../package.nix { };
|
||||
in
|
||||
{
|
||||
home.packages = [ windowOverview ];
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
description = "Fullscreen Wayland window overview with captured previews";
|
||||
|
||||
includes = [
|
||||
"systems.wayland"
|
||||
];
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
{
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -1,431 +0,0 @@
|
||||
--- 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 {
|
||||
@@ -1,336 +0,0 @@
|
||||
--- 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,8 @@
|
||||
{
|
||||
homebrew = {
|
||||
enable = true;
|
||||
masApps = {
|
||||
Xcode = 497799835;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
home.packages = [ pkgs.yt-dlp ];
|
||||
}
|
||||
@@ -4,8 +4,11 @@
|
||||
extensions = [
|
||||
"catppuccin"
|
||||
"nix"
|
||||
"dockerfile"
|
||||
];
|
||||
mutableUserSettings = false;
|
||||
mutableUserKeymaps = false;
|
||||
userKeymaps = import ./keymaps.nix;
|
||||
|
||||
userSettings = {
|
||||
agent = {
|
||||
@@ -36,6 +39,9 @@
|
||||
light = "Catppuccin Latte";
|
||||
dark = "Catppuccin Mocha";
|
||||
};
|
||||
edit_predictions = {
|
||||
provider = "copilot";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
context = "Editor && vim_mode == normal";
|
||||
bindings = {
|
||||
# This selected native-equivalent subset preserves Zed's Vim defaults for
|
||||
# K, gd, grr, gri, gra, gO, ]d, [d, zM, zR, [b, and ]b.
|
||||
"g r t" = "editor::GoToTypeDefinition";
|
||||
"space space" = "pane::AlternateFile";
|
||||
"space r n" = "editor::Rename";
|
||||
"space c a" = "editor::ToggleCodeActions";
|
||||
"space c f" = "editor::Format";
|
||||
"space d l" = "editor::Hover";
|
||||
"space e" = "project_panel::Toggle";
|
||||
"space t t" = "terminal_panel::Toggle";
|
||||
"space b c" = "pane::CloseActiveItem";
|
||||
"space f f" = "file_finder::Toggle";
|
||||
"space f g" = "pane::DeploySearch";
|
||||
"space f b" = "tab_switcher::ToggleAll";
|
||||
"space f s" = "outline::Toggle";
|
||||
"space f shift-s" = "project_symbols::Toggle";
|
||||
"space x x" = "diagnostics::Deploy";
|
||||
};
|
||||
}
|
||||
{
|
||||
# Keep normal-mode editor navigation out of terminals and other panels.
|
||||
context = "Editor && vim_mode == normal && !menu";
|
||||
bindings = {
|
||||
"ctrl-h" = "workspace::ActivatePaneLeft";
|
||||
"ctrl-j" = "workspace::ActivatePaneDown";
|
||||
"ctrl-k" = "workspace::ActivatePaneUp";
|
||||
"ctrl-l" = "workspace::ActivatePaneRight";
|
||||
"ctrl-left" = "workspace::ActivatePaneLeft";
|
||||
"ctrl-down" = "workspace::ActivatePaneDown";
|
||||
"ctrl-up" = "workspace::ActivatePaneUp";
|
||||
"ctrl-right" = "workspace::ActivatePaneRight";
|
||||
};
|
||||
}
|
||||
]
|
||||
@@ -5,10 +5,7 @@
|
||||
modesetting.enable = true;
|
||||
open = true;
|
||||
|
||||
powerManagement = {
|
||||
enable = true;
|
||||
kernelSuspendNotifier = true;
|
||||
};
|
||||
powerManagement.enable = true;
|
||||
|
||||
moduleParams.nvidia.NVreg_TemporaryFilePath = "/var/tmp";
|
||||
|
||||
|
||||
+14
-11
@@ -30,7 +30,6 @@ 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 |
|
||||
@@ -39,9 +38,13 @@ required on every supported host.
|
||||
| `platform.vm` | UEFI QEMU NixOS guest with NFS client support |
|
||||
| `workload.development` | NixOS, macOS with Home Manager |
|
||||
| `workload.game` | NixOS, macOS |
|
||||
| `workload.machine-learning` | NixOS with Home Manager |
|
||||
| `workload.nix-builder` | NixOS central build and binary-cache VM |
|
||||
| `workload.personal` | NixOS, macOS with Home Manager |
|
||||
| `workload.remote-access` | NixOS, macOS |
|
||||
| `workload.camera` | NixOS |
|
||||
| `workload.server` | NixOS, macOS with Home Manager |
|
||||
| `networking.homelab-cache-client` | NixOS, macOS with access to nix-builder |
|
||||
| `networking.tailscale-client` | NixOS, macOS |
|
||||
| `networking.tailscale-subnet-router` | NixOS |
|
||||
| `security.fingerprint` | NixOS, macOS |
|
||||
@@ -59,22 +62,22 @@ 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.machine-learning` provides the Hugging Face Hub CLI for hosts used
|
||||
to download and publish machine learning models and datasets.
|
||||
|
||||
`workload.nix-builder` provides the central build policy, persistent fleet GC
|
||||
roots, deploy-rs tooling, SOPS integration, and Harmonia binary cache. Network
|
||||
reachability and remote shell access remain independent host selections.
|
||||
|
||||
`networking.homelab-cache-client` adds the internal Harmonia substituter and
|
||||
its trusted public key. It requires the public key generated during
|
||||
`hosts/nix-builder/README.md` bootstrap.
|
||||
|
||||
`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.
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
publicShare = "$HOME/Public";
|
||||
templates = "$HOME/Templates";
|
||||
videos = "$HOME/Videos";
|
||||
projects = "$HOME/Projects";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"applications.vlc"
|
||||
"hardwares.graphics"
|
||||
"services.gvfs"
|
||||
"services.evremap"
|
||||
"services.polkit-gnome"
|
||||
"systems.audio"
|
||||
"systems.fonts"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
programs.noctalia.settings.wallpaper.enabled = false;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
description = "Wallpaper Engine backgrounds for Linux desktop sessions";
|
||||
|
||||
includes = [
|
||||
"applications.linux-wallpaperengine"
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
description = "Use the homelab Harmonia binary cache";
|
||||
|
||||
includes = [ "systems.nix.homelab-cache" ];
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
# every system sleep path; screen blanking while locked is handled by
|
||||
# services.swayidle in the graphical session.
|
||||
systemd.sleep.settings.Sleep = {
|
||||
AllowSuspend = true;
|
||||
AllowHibernation = false;
|
||||
AllowHybridSleep = false;
|
||||
AllowSuspendThenHibernate = false;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
sops.secrets."users/moons/hashedPassword" = {
|
||||
sopsFile = ../../../secrets/common/system.yaml;
|
||||
neededForUsers = true;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
description = "Camera capture and virtual camera tools";
|
||||
|
||||
includes = [ "systems.boot.v4l2loopback" ];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = with pkgs; [
|
||||
gphoto2
|
||||
ffmpeg
|
||||
v4l-utils
|
||||
];
|
||||
}
|
||||
@@ -3,11 +3,8 @@
|
||||
home.packages = with pkgs; [
|
||||
bind
|
||||
bun
|
||||
mcp-nixos
|
||||
nix-fast-build
|
||||
nix-tree
|
||||
nixd
|
||||
python312
|
||||
nil
|
||||
python314
|
||||
uv
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"applications.claude"
|
||||
"applications.codex"
|
||||
"applications.codex-desktop"
|
||||
"applications.codex-session-usage"
|
||||
"applications.docker"
|
||||
"applications.drawio"
|
||||
"applications.ghostty"
|
||||
@@ -13,6 +14,7 @@
|
||||
"applications.java"
|
||||
"applications.opencode"
|
||||
"applications.vscode"
|
||||
"applications.xcode"
|
||||
"applications.zed"
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
description = "Machine learning tools";
|
||||
|
||||
includes = [ "applications.huggingface-cli" ];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
description = "Central Nix builder, deploy controller, and binary cache";
|
||||
|
||||
includes = [
|
||||
"applications.nix-fleet"
|
||||
"services.harmonia"
|
||||
"systems.nix.build-server"
|
||||
"systems.sops"
|
||||
];
|
||||
}
|
||||
@@ -6,8 +6,8 @@
|
||||
"applications.activitywatch"
|
||||
"applications.chrome"
|
||||
"applications.discord"
|
||||
"applications.ffmpeg"
|
||||
"applications.gnome-text-editor"
|
||||
"applications.handy"
|
||||
"applications.kde"
|
||||
"applications.moonlight"
|
||||
"applications.slack"
|
||||
@@ -15,5 +15,6 @@
|
||||
"applications.obs"
|
||||
"applications.nani"
|
||||
"applications.thunderbird"
|
||||
"applications.yt-dlp"
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
services.evremap = {
|
||||
enable = true;
|
||||
|
||||
settings = {
|
||||
device_name = "VXE VXE Mouse 1K Dongle Mouse";
|
||||
|
||||
remap = [
|
||||
{
|
||||
input = [ "BTN_SIDE" ];
|
||||
output = [ "KEY_LEFTMETA" ];
|
||||
}
|
||||
{
|
||||
input = [ "BTN_EXTRA" ];
|
||||
output = [ "BTN_SIDE" ];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
description = "Harmonia binary cache backed by the local Nix store";
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
let
|
||||
signingKeyPath = "/run/secrets/harmonia/signing-key";
|
||||
in
|
||||
{
|
||||
services.harmonia.cache = {
|
||||
enable = true;
|
||||
signKeyPaths = [ signingKeyPath ];
|
||||
|
||||
settings = {
|
||||
bind = "0.0.0.0:5000";
|
||||
priority = 30;
|
||||
};
|
||||
};
|
||||
|
||||
# Keep activation usable while the host-specific SOPS secret is bootstrapped.
|
||||
# Once the secret exists, starting the socket also starts Harmonia on demand.
|
||||
systemd.sockets.harmonia.unitConfig.ConditionPathExists = signingKeyPath;
|
||||
systemd.services.harmonia.unitConfig.ConditionPathExists = signingKeyPath;
|
||||
}
|
||||
@@ -10,6 +10,8 @@ let
|
||||
brightnessState = "$XDG_RUNTIME_DIR/swayidle-brightness";
|
||||
|
||||
onBattery = pkgs.writeShellScript "swayidle-on-battery" ''
|
||||
[ "''${SWAYIDLE_ASSUME_AC:-0}" = 1 ] && exit 1
|
||||
|
||||
for supply in /sys/class/power_supply/*; do
|
||||
[ -f "$supply/type" ] || continue
|
||||
[ "$(< "$supply/type")" = "Battery" ] || continue
|
||||
|
||||
@@ -21,29 +21,5 @@ 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,6 +1,6 @@
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
boot.loader.systemd-boot.configurationLimit = lib.mkDefault 8;
|
||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||
boot.kernelPackages = pkgs.linuxPackages;
|
||||
programs.nix-ld.enable = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{ config, ... }:
|
||||
{
|
||||
boot = {
|
||||
extraModulePackages = [ config.boot.kernelPackages.v4l2loopback ];
|
||||
kernelModules = [ "v4l2loopback" ];
|
||||
extraModprobeConfig = ''
|
||||
options v4l2loopback devices=1 video_nr=42 card_label="LUMIX S9" exclusive_caps=1
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
description = "Central Nix build server policy and persistent fleet roots";
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{ primaryUser, ... }:
|
||||
let
|
||||
GiB = 1024 * 1024 * 1024;
|
||||
in
|
||||
{
|
||||
nix = {
|
||||
nrBuildUsers = 64;
|
||||
|
||||
settings = {
|
||||
# Limit concurrent derivations so build scratch and memory usage remain
|
||||
# bounded. Each derivation may still use every vCPU exposed to the VM.
|
||||
max-jobs = 2;
|
||||
cores = 0;
|
||||
|
||||
# Keep enough room for large desktop, browser, and CUDA closures.
|
||||
min-free = 64 * GiB;
|
||||
max-free = 128 * GiB;
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.nix-daemon.serviceConfig = {
|
||||
MemoryAccounting = true;
|
||||
MemoryMax = "90%";
|
||||
OOMScoreAdjust = 500;
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/nix-fleet 0750 ${primaryUser} users - -"
|
||||
"d /var/lib/nix-fleet/roots 0750 ${primaryUser} users - -"
|
||||
"d /var/lib/nix-fleet/roots/build 0750 ${primaryUser} users - -"
|
||||
"d /var/lib/nix-fleet/roots/deploy 0750 ${primaryUser} users - -"
|
||||
];
|
||||
}
|
||||
@@ -7,14 +7,18 @@
|
||||
"flakes"
|
||||
];
|
||||
|
||||
connect-timeout = 10;
|
||||
|
||||
extra-substituters = [
|
||||
"https://cache.nixos.org"
|
||||
"https://nix-community.cachix.org"
|
||||
"https://moons-dotfiles.cachix.org"
|
||||
"https://noctalia.cachix.org"
|
||||
"https://vicinae.cachix.org"
|
||||
"https://ghostty.cachix.org"
|
||||
"https://cache.numtide.com"
|
||||
"https://codex-desktop-linux.cachix.org"
|
||||
"https://cuda-maintainers.cachix.org"
|
||||
];
|
||||
|
||||
extra-trusted-public-keys = [
|
||||
@@ -23,8 +27,10 @@
|
||||
"moons-dotfiles.cachix.org-1:WHoroKiNScG2/dpxHHL1I0qVmvuQhJbEAP+DS2j9Rr0="
|
||||
"noctalia.cachix.org-1:pCOR47nnMEo5thcxNDtzWpOxNFQsBRglJzxWPp3dkU4="
|
||||
"vicinae.cachix.org-1:1kDrfienkGHPYbkpNj1mWTr7Fm1+zcenzgTizIcI3oc="
|
||||
"ghostty.cachix.org-1:QB389yTa6gTyneehvqG58y0WnHjQOqgnA+wBnpWWxns="
|
||||
"niks3.numtide.com-1:DTx8wZduET09hRmMtKdQDxNNthLQETkc/yaX7M4qK0g="
|
||||
"codex-desktop-linux.cachix.org-1:nX/xy6AdK9hQE24A8ALGjkCKj2ObFmcnemiL5Cid4nk="
|
||||
"cuda-maintainers.cachix.org-1:0dq3bujKpuEPMCX6U4WylrUDZ9JyUG0VpVZa7CNfq5E="
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
publicKeyFile = ./public-key;
|
||||
hasPublicKey = builtins.pathExists publicKeyFile;
|
||||
publicKey = if hasPublicKey then lib.removeSuffix "\n" (builtins.readFile publicKeyFile) else "";
|
||||
in
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = hasPublicKey;
|
||||
message = ''
|
||||
systems.nix.homelab-cache requires
|
||||
modules/systems/nix/homelab-cache/public-key
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
nix.settings = lib.mkIf hasPublicKey {
|
||||
extra-substituters = [ "http://nix-builder:5000" ];
|
||||
extra-trusted-public-keys = [ publicKey ];
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user