139 changed files with 3758 additions and 2299 deletions
+13
View File
@@ -0,0 +1,13 @@
[mcp_servers.nixos]
command = "mcp-nixos"
startup_timeout_sec = 30
tool_timeout_sec = 60
required = false
[mcp_servers.github]
url = "https://api.githubcopilot.com/mcp/"
bearer_token_env_var = "GITHUB_PERSONAL_ACCESS_TOKEN"
http_headers = { X-MCP-Readonly = "true", X-MCP-Toolsets = "repos,pull_requests,actions" }
startup_timeout_sec = 30
tool_timeout_sec = 60
required = false
+8 -20
View File
@@ -1,27 +1,15 @@
# Reference: https://github.com/ryoppippi/dotfiles/blob/main/.github/workflows/nix-build.yaml
name: Check NixOS configurations
description: Build every NixOS configuration and the Registry tests
name: Build Linux checks
description: Build every x86_64-linux flake check in parallel
runs:
using: composite
steps:
- name: Build every NixOS configuration
- name: Build all Linux checks
shell: bash
run: |
set -euo pipefail
mapfile -t hosts < <(
nix eval --raw .#nixosConfigurations \
--apply 'configs: builtins.concatStringsSep "\n" (builtins.attrNames configs)'
)
installables=(.#checks.x86_64-linux.registry)
for host in "${hosts[@]}"; do
installables+=(".#nixosConfigurations.${host}.config.system.build.toplevel")
done
nix build \
--keep-going \
--no-link \
--print-build-logs \
--show-trace \
"${installables[@]}"
nix run .#nix-fast-build -- \
--flake .#checks.x86_64-linux \
--skip-cached \
--no-nom \
--no-link
+1
View File
@@ -20,3 +20,4 @@ runs:
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.lock') }}
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}-
gc-max-store-size-linux: 4G
gc-max-store-size-macos: 4G
+163 -10
View File
@@ -1,36 +1,50 @@
# Reference: https://github.com/ryoppippi/dotfiles/blob/main/.github/workflows/nix-build.yaml
name: "CI: NixOS"
name: "CI: Nix"
on:
push:
branches:
- main
paths:
- .gitignore
- AGENTS.md
- README.md
- "docs/**"
- flake.nix
- flake.lock
- ".codex/**"
- ".github/**"
- ".vscode/**"
- "flake/**"
- "hosts/**"
- "libs/**"
- "modules/**"
- "opencode.json"
- "overlays/**"
- "scripts/**"
- "shells/**"
- "skills/**"
- "tests/**"
- ".github/actions/check-nixos/**"
- ".github/actions/setup-nix/**"
- ".github/workflows/nixos.yaml"
pull_request:
paths:
- .gitignore
- AGENTS.md
- README.md
- "docs/**"
- flake.nix
- flake.lock
- ".codex/**"
- ".github/**"
- ".vscode/**"
- "flake/**"
- "hosts/**"
- "libs/**"
- "modules/**"
- "opencode.json"
- "overlays/**"
- "scripts/**"
- "shells/**"
- "skills/**"
- "tests/**"
- ".github/actions/check-nixos/**"
- ".github/actions/setup-nix/**"
- ".github/workflows/nixos.yaml"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -38,14 +52,153 @@ concurrency:
permissions:
contents: read
jobs:
check:
name: Check all NixOS configurations
validate:
name: Plan, lint, and evaluate
runs-on: ubuntu-latest
timeout-minutes: 120
outputs:
build_linux: ${{ steps.plan.outputs.build_linux }}
build_darwin: ${{ steps.plan.outputs.build_darwin }}
nix_validation: ${{ steps.plan.outputs.nix_validation }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Setup Nix
uses: ./.github/actions/setup-nix
- name: Check NixOS configurations
- name: Plan validation
id: plan
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BASE_SHA: ${{ github.event.before }}
shell: bash
run: |
set -euo pipefail
case "${{ github.event_name }}" in
pull_request)
plan="$(nix run .#check -- plan --base "$PR_BASE_SHA" --json)"
;;
push)
plan="$(nix run .#check -- plan --base "$PUSH_BASE_SHA" --json)"
;;
*)
plan="$(nix run .#check -- plan --all-files --all-hosts --json)"
;;
esac
printf '%s\n' "$plan"
nix_validation="$(jq -r '.requiresNixValidation' <<<"$plan")"
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
build_linux="$(jq -r '.nativeBuildSystems["x86_64-linux"] // false' <<<"$plan")"
build_darwin="$(jq -r '.nativeBuildSystems["aarch64-darwin"] // false' <<<"$plan")"
else
build_linux="$nix_validation"
build_darwin="$nix_validation"
fi
{
echo "nix_validation=$nix_validation"
echo "build_linux=$build_linux"
echo "build_darwin=$build_darwin"
} >> "$GITHUB_OUTPUT"
- name: Run fast checks
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BASE_SHA: ${{ github.event.before }}
shell: bash
run: |
set +e
set -uo pipefail
case "${{ github.event_name }}" in
pull_request)
nix run .#check -- fast --base "$PR_BASE_SHA"
;;
push)
nix run .#check -- fast --base "$PUSH_BASE_SHA"
;;
*)
nix run .#check -- fast --all-files
;;
esac
status=$?
if (( status != 0 )); then
git diff -- .
exit "$status"
fi
- name: Evaluate configurations
if: steps.plan.outputs.nix_validation == 'true' || github.event_name == 'workflow_dispatch'
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BASE_SHA: ${{ github.event.before }}
shell: bash
run: |
set -euo pipefail
case "${{ github.event_name }}" in
pull_request)
nix run .#check -- eval --base "$PR_BASE_SHA"
;;
push)
nix run .#check -- eval --base "$PUSH_BASE_SHA" --all-systems
;;
*)
nix run .#check -- eval --all-hosts --all-systems
;;
esac
build-linux:
name: Build Linux checks
needs: validate
if: needs.validate.outputs.build_linux == 'true'
runs-on: ubuntu-latest
timeout-minutes: 180
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Setup Nix
uses: ./.github/actions/setup-nix
- name: Build affected Linux checks
if: github.event_name == 'pull_request'
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
shell: bash
run: |
set -euo pipefail
nix run .#check -- build --base "$PR_BASE_SHA"
- name: Build all Linux checks
if: github.event_name != 'pull_request'
uses: ./.github/actions/check-nixos
build-darwin:
name: Build Darwin checks
needs: validate
if: needs.validate.outputs.build_darwin == 'true'
runs-on: macos-15
timeout-minutes: 180
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Setup Nix
uses: ./.github/actions/setup-nix
- name: Build affected Darwin checks
if: github.event_name == 'pull_request'
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
shell: bash
run: |
set -euo pipefail
nix run .#check -- build --base "$PR_BASE_SHA"
- name: Build all Darwin checks
if: github.event_name != 'pull_request'
shell: bash
run: |
set -euo pipefail
nix run .#nix-fast-build -- \
--flake .#checks.aarch64-darwin \
--skip-cached \
--no-nom \
--no-link
+66 -5
View File
@@ -12,9 +12,11 @@ permissions:
pull-requests: write
jobs:
update:
name: Update and validate flake inputs
name: Update and check Linux
runs-on: ubuntu-latest
timeout-minutes: 120
timeout-minutes: 180
outputs:
changed: ${{ steps.update.outputs.changed }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
@@ -22,18 +24,76 @@ jobs:
uses: ./.github/actions/setup-nix
- name: Update flake inputs
id: update
shell: bash
run: |
set -euo pipefail
nix flake update
if git diff --quiet -- flake.lock; then
echo 'changed=false' >> "$GITHUB_OUTPUT"
else
echo 'changed=true' >> "$GITHUB_OUTPUT"
fi
- name: Check updated NixOS configurations
- name: Evaluate every flake system
if: steps.update.outputs.changed == 'true'
shell: bash
run: |
set -euo pipefail
nix flake check \
--no-build \
--all-systems \
--keep-going \
--show-trace
- name: Build Linux checks
if: steps.update.outputs.changed == 'true'
uses: ./.github/actions/check-nixos
- name: Create update pull request
- name: Upload updated lock file
if: steps.update.outputs.changed == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: flake-lock
path: flake.lock
if-no-files-found: error
check-darwin:
name: Check Darwin
needs: update
if: needs.update.outputs.changed == 'true'
runs-on: macos-15
timeout-minutes: 180
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Download updated lock file
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: flake-lock
path: .
- name: Setup Nix
uses: ./.github/actions/setup-nix
- name: Build Darwin checks
shell: bash
run: |
set -euo pipefail
nix run .#nix-fast-build -- \
--flake .#checks.aarch64-darwin \
--skip-cached \
--no-nom \
--no-link
pull-request:
name: Create update pull request
needs:
- update
- check-darwin
if: needs.update.outputs.changed == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Download validated lock file
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: flake-lock
path: .
- name: Create update pull request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -45,4 +105,5 @@ jobs:
body: |
Automated update of `flake.lock`.
The updated inputs passed the Registry tests and a build of every NixOS configuration.
The updated inputs passed all-system evaluation and native builds of
the Linux and Darwin check sets.
+7 -1
View File
@@ -1,4 +1,6 @@
/*
**/__pycache__/
*.py[cod]
!.gitignore
!README.md
@@ -6,8 +8,10 @@
!AGENTS.md
!/docs/
!.github/
!/.codex/
!/.github/
!.gitea/
!/.vscode/
!.envrc
@@ -15,7 +19,9 @@
!/flake.nix
!/flake.lock
!/opencode.json
!/scripts/
!/shells/
!/flake/
!/overlays/
+28
View File
@@ -0,0 +1,28 @@
{
"nix.enableLanguageServer": true,
"nix.serverPath": "nixd",
"nix.serverSettings": {
"nixd": {
"formatting": {
"command": ["nixfmt"]
},
"nixpkgs": {
"expr": "import (builtins.getFlake (builtins.toString ./.)).inputs.nixpkgs { }"
},
"options": {
"nixos": {
"expr": "(builtins.getFlake (builtins.toString ./.)).nixosConfigurations.galleria.options"
},
"home-manager-nixos": {
"expr": "(builtins.getFlake (builtins.toString ./.)).nixosConfigurations.galleria.options.home-manager.users.type.getSubOptions []"
},
"darwin": {
"expr": "(builtins.getFlake (builtins.toString ./.)).darwinConfigurations.m2.options"
},
"home-manager-darwin": {
"expr": "(builtins.getFlake (builtins.toString ./.)).darwinConfigurations.m2.options.home-manager.users.type.getSubOptions []"
}
}
}
}
}
+18 -73
View File
@@ -351,7 +351,6 @@ modules/profiles/
├── base/
├── interface/
│ ├── cli/
│ ├── minimal/
│ ├── gui/
│ ├── macos/
│ ├── linux-desktop/
@@ -368,14 +367,8 @@ modules/profiles/
│ ├── desktop/
│ └── vm/
├── workload/
│ ├── camera/
│ ├── deploy-rs-target/
│ ├── development/
│ ├── game/
│ ├── machine-learning/
│ ├── network-lab/
│ ├── personal/
│ ├── photography/
│ ├── server/
│ └── remote-access/
└── security/
@@ -396,21 +389,16 @@ The profile layers have these responsibilities:
- `base` contains only invariants required by every supported host. It includes
`systems.nix` and the universal Atuin, tealdeer, trippy, and xh CLI tools;
optional secrets, interface, hardware, and workloads do not belong there.
- `interface` describes how the host is operated. `interface.minimal` is shared
by NixOS and macOS and provides the remote-administration CLI baseline,
including SSH, Nano, htop, Git, Zellij, and Zsh. `interface.cli` includes that
baseline and adds the full interactive command-line environment, including
the configured Neovim, Yazi, and `tio`. `interface.gui` owns
cross-platform graphical interface applications such as Vicinae.
`interface.macos` owns the macOS Finder, Dock, trackpad, and shared default
preferences and includes `interface.gui`.
- `interface` describes how the host is operated. `interface.cli` is shared by
NixOS and macOS and includes `tio`. `interface.gui` owns cross-platform
graphical interface applications such as Vicinae. `interface.macos` owns the
macOS Finder, Dock, trackpad, and shared default preferences and includes
`interface.gui`.
`interface.linux-desktop` owns the common labwc/niri desktop selection,
including Ghostty and Nautilus, and also includes `interface.gui`. Labwc and
niri remain independently selectable and do not imply CLI or personal
workloads.
- `platform` describes NixOS foundations and physical or virtual form factors.
`platform.nixos` selects the shared network foundation and `services.clatd`;
VM, laptop, and desktop platform profiles inherit both.
macOS does not need an empty symmetric platform profile.
- `workload` describes optional host uses. `workload.development` and
`workload.personal` are cross-platform profiles, not `*-linux` variants.
@@ -550,26 +538,9 @@ A host registry may use a specification like this:
profiles = [
"base"
"interface.minimal"
"interface.cli"
"platform.vm"
"workload.remote-access"
"workload.deploy-rs-target"
];
};
netsrv-01 = {
system = "x86_64-linux";
stateVersion = "26.05";
user = "moons";
path = ./netsrv-01;
profiles = [
"base"
"interface.minimal"
"platform.vm"
"workload.remote-access"
"workload.deploy-rs-target"
"workload.server"
];
};
@@ -581,7 +552,7 @@ A host registry may use a specification like this:
profiles = [
"base"
"interface.minimal"
"interface.cli"
"platform.vm"
"workload.server"
];
@@ -631,9 +602,7 @@ A host registry may use a specification like this:
"security.secrets"
"security.secure-boot"
"security.tpm-storage"
"workload.camera"
"workload.development"
"workload.network-lab"
"workload.personal"
];
};
@@ -655,10 +624,7 @@ A host registry may use a specification like this:
"security.tpm-storage"
"workload.development"
"workload.game"
"workload.machine-learning"
"workload.network-lab"
"workload.personal"
"workload.photography"
];
};
@@ -681,22 +647,14 @@ A host registry may use a specification like this:
```
The current role assignment is intentional: nix-example is the development VM;
ops is the minimal-interface remote-access VM with host-specific static
networking; netsrv-01 is the deploy-rs-managed container server VM;
internal-app-01 is the minimal-interface container server VM;
nix-builder is the minimal-interface remote Nix build VM with dedicated build
and store disks; and installer builds the minimal installation ISO without Home
Manager. x1g9 is a full NixOS desktop with niri,
ops is the remote-access VM with host-specific static networking;
internal-app-01 is the container server VM; and installer builds the minimal
installation ISO without Home Manager. x1g9 is a full NixOS desktop with niri,
labwc, ly, the shared Linux desktop applications, and the personal workload.
x1g13 is the secure NixOS development and personal ThinkPad, with the same
desktop sessions plus Tailscale client, SOPS, Secure Boot, and TPM-backed disk
unlock. galleria is the Intel/NVIDIA physical desktop shared with Windows; it
uses dedicated NixOS partitions, LUKS, Secure Boot, and TPM-backed disk unlock.
It selects `workload.photography` for AI-enabled darktable. The application
chooses CUDA support from the NVIDIA hardware unit's enable state and keeps
the default CUDA targets, including RTX 3060 Ti support, to reuse binary caches.
galleria and x1g13 select `workload.network-lab` for containerlab, Docker, and
the NanoKVM-USB desktop client with serial-port access.
m2 is the daily-use macOS development and personal machine with the macOS
interface defaults. Keep the desktop sessions independently selectable, and
keep the development and personal profiles usable across NixOS and Darwin.
@@ -729,15 +687,6 @@ configuration fragments. For example:
```text
hosts/
├── nix-builder/
│ ├── disko.nix
│ ├── hardware-configuration.nix
│ └── nixos.nix
├── netsrv-01/
│ ├── disko.nix
│ ├── hardware-configuration.nix
│ ├── networking.nix
│ └── nixos.nix
├── installer/
│ └── nixos.nix
├── internal-app-01/
@@ -767,15 +716,12 @@ hosts/
```
`hosts/x1g9/nixos.nix` explicitly loads `hardware-configuration.nix` with the
normal top-level Nix module `imports`. `hosts/nix-builder/nixos.nix` and
`hosts/x1g13/nixos.nix` load their generated hardware configuration and
host-local `disko.nix` the same way. The nix-builder Disko definition mounts its
existing VM filesystems by stable QEMU SCSI IDs and does not take destructive
ownership of them. Do not confuse these host imports with the prohibition on
top-level `imports` in unit configuration fragments. `hosts/galleria/disko.nix`
manages only the two dedicated NixOS partitions by PARTUUID and deliberately
excludes the Windows disk, Windows partitions, and the Windows EFI System
Partition.
normal top-level Nix module `imports`. `hosts/x1g13/nixos.nix` loads its
generated hardware configuration and host-local `disko.nix` the same way. Do
not confuse these host imports with the prohibition on top-level `imports` in
unit configuration fragments. `hosts/galleria/disko.nix` manages only the two
dedicated NixOS partitions by PARTUUID and deliberately excludes the Windows
disk, Windows partitions, and the Windows EFI System Partition.
Derive the system class from the host's `system`:
@@ -878,9 +824,8 @@ For profile changes, additionally:
`homebrew.casks` selection as well as module evaluation.
- Preserve the intended host roles: nix-example remains the development VM;
ops remains the statically networked remote-access VM; internal-app-01 remains
the container server VM; netsrv-01 remains the deploy-rs-managed container
server VM; installer remains the Home Manager-free installation ISO; x1g9
provides niri, labwc, ly, and the personal application set; x1g13
the container server VM; installer remains the Home Manager-free installation
ISO; x1g9 provides niri, labwc, ly, and the personal application set; x1g13
additionally provides the development, Tailscale client, secrets, Secure Boot,
and TPM storage roles; galleria remains the Intel/NVIDIA dual-boot desktop
with LUKS, Secure Boot, and TPM storage; m2 remains the daily-use development
+4 -1
View File
@@ -1,3 +1,6 @@
# moons14 dotfiles
My NixOS + Home Manager configurations build with flake.
My NixOS, nix-darwin, and Home Manager configurations built with flakes.
See [Coding-agent validation](docs/coding-agents.md) for the non-activating,
change-aware validation workflow used by Codex, OpenCode, and CI.
+86
View File
@@ -0,0 +1,86 @@
# Coding-agent workflow
This repository exposes deterministic validation and narrowly scoped research
tools for Codex, OpenCode, and editor agents. Live system activation is outside
this workflow.
## Validation commands
Use task-owned paths during the edit loop:
```console
nix run .#check -- plan --paths modules/applications/example/home.nix --json
nix run .#check -- fast --paths modules/applications/example/home.nix
nix run .#check -- eval --paths modules/applications/example/home.nix
nix run .#check -- build --paths modules/applications/example/home.nix
nix run .#check -- all --paths modules/applications/example/home.nix
```
For a committed pull-request range, replace `--paths ...` with
`--base <base-sha>`. `full` is reserved for CI, scheduled maintenance, or an
explicit repository-wide audit:
```console
nix run .#check -- full
```
The stages have distinct meanings:
- `plan` maps changed paths to Registry units, reverse `meta.includes`
dependencies, real hosts, and compatible build systems.
- `fast` parses changed Nix files, validates JSON, TOML, Python, and Agent Skill
frontmatter, checks whitespace, and runs configured hooks only for the
selected files.
- `eval` instantiates affected NixOS and nix-darwin configurations. Flake-wide,
validation-tool, shell, overlay, and test changes additionally evaluate every
flake system.
- `build` realizes affected configurations supported by the current platform
with no result link. Incompatible targets remain evaluation-only until a
matching runner handles them.
- `all` performs task-scoped file checks, all-system evaluation, and compatible
targeted builds.
- `full` runs all-file hooks, all-system evaluation, and every check for the
current platform through `nix-fast-build`.
The validation app constructs a filtered temporary `path:` flake from the
committed `HEAD` tree and overlays only task-owned changed paths. This isolates
unrelated worktree changes, makes new untracked Registry fragments visible to
Nix without staging them, and avoids copying ignored state such as `.direnv`.
None of these commands runs `nh os switch`, `nixos-rebuild switch`,
`darwin-rebuild switch`, `home-manager switch`, or another activation command.
The user performs activation separately.
## Agent Skills
Read `AGENTS.md` first. Use the repository skills as follows:
- `validate-nix-change` controls validation scope and evidence.
- `debug-nix-failure` classifies parse, evaluation, build, test, activation-log,
and runtime failures before proposing a correction.
- `test-nixos-service` adds a `pkgs.testers.runNixOSTest` check when a build
cannot prove service behavior.
- `update-flake-input` limits lock-file updates and validates them without
activating a host.
- `add-application-or-service` preserves Registry ownership and delegates
validation to `validate-nix-change`.
## MCP and language-server setup
The development workload installs `mcp-nixos`, `nixd`, `nix-fast-build`, and
`nix-tree`.
Project-local Codex and OpenCode configuration exposes:
- `mcp-nixos` for current NixOS, Home Manager, nix-darwin, package, and Nix
documentation queries;
- GitHub's remote MCP endpoint for Codex and OpenCode in read-only mode,
restricted to repository, pull-request, and Actions toolsets.
Set `GITHUB_PERSONAL_ACCESS_TOKEN` in the launching environment when GitHub MCP
access is needed. Do not commit the token or put it in a Nix expression because
that would expose it through source control or the Nix store.
The workspace VS Code settings use `nixd` and expose option sets for the
`galleria` NixOS configuration, its integrated Home Manager configuration, the
`m2` nix-darwin configuration, and its integrated Home Manager configuration.
-153
View File
@@ -1,153 +0,0 @@
# NixOSインストール手順(SOPSなし・ディスク暗号化なし)
この手順は、SOPSによるシークレット管理とLUKSによるディスク暗号化を
使用しないホスト向けの、独立したインストール手順である。
SOPSとディスク暗号化を使用する場合は、[暗号化ありの手順](install.md)を参照。
## 事前準備
1. [ISOビルド](iso-build.md)を参照してISOを作成
2. USBに書き込んで対象マシンでブート
## ネットワーク接続
### 有線LAN
DHCPで自動設定される。
### Wi-Fi(有線が使えない場合)
```bash
nmcli device wifi connect <SSID> --ask
```
## SSH接続
コンソールに表示されたIPアドレスに接続:
```bash
ssh root@<ip-address>
```
## インストール手順
### 1. dotfilesのクローン
```bash
git clone [email protected]:moons-14/dotfiles.git ~/dotfiles
cd ~/dotfiles
```
### 2. ホスト設定の作成
`hosts/<hostname>/nixos.nix`を作成し、`hosts/default.nix`にホストと使用する
プロファイルを登録する。ホスト固有の設定だけをホストディレクトリに置き、
再利用可能な設定は適切なunitまたはprofileに置く。
### 3. インストール先ディスクの確認
```bash
lsblk -o NAME,PATH,SIZE,MODEL,SERIAL,TYPE,FSTYPE,MOUNTPOINTS
ls -l /dev/disk/by-id/
```
以降の操作では指定したディスクの既存データが消去される。対象を必ず確認し、
可能であれば`/dev/sda`や`/dev/nvme0n1`ではなく、安定した
`/dev/disk/by-id/...`パスを使用する。
### 4. Disko設定の作成
`hosts/<hostname>/disko.nix`を作成する:
```nix
_:
{
disko.enableConfig = true;
disko.devices.disk.main = {
type = "disk";
device = "/dev/disk/by-id/<target-disk>";
content = {
type = "gpt";
partitions = {
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
}
```
`<target-disk>`を手順3で確認した実際のディスクIDに置き換える。指定した
ディスクの既存データは消去される。
### 5. パーティション作成とマウント
```bash
disko --mode destroy,format,mount hosts/<hostname>/disko.nix
```
Diskoの実行結果を確認する:
```bash
findmnt /mnt
findmnt /mnt/boot
```
### 6. ハードウェア設定の生成
```bash
nixos-generate-config --no-filesystems --root /mnt --show-hardware-config \
> ~/dotfiles/hosts/<hostname>/hardware-configuration.nix
```
### 7. ホストモジュールから設定を読み込む
`hosts/<hostname>/nixos.nix`で、生成したハードウェア設定とDisko設定を読み込む:
```nix
{
imports = [
./hardware-configuration.nix
./disko.nix
];
}
```
### 8. NixOSインストール
```bash
nixos-install --flake ~/dotfiles#<hostname>
```
SOPSを使用しないため、age鍵の登録、シークレットの再暗号化、SSHホストキーの
事前生成とコピーは不要である。OpenSSHを有効にしたホストでは、SSHホストキーは
通常の初回起動時に生成される。
### 9. 再起動
```bash
reboot
```
## インストール後の確認
- 正しいディスクから起動できるか
- `/`と`/boot`が意図したファイルシステムからマウントされているか
- ネットワークと、設定している場合はSSH接続が利用できるか
+44 -32
View File
@@ -1,10 +1,4 @@
# NixOSインストール手順(SOPS・ディスク暗号化あり)
この手順は、SOPSによるシークレット管理とLUKSによるディスク暗号化を
使用するホスト向けである。
どちらも使用しない場合は、
[SOPSなし・ディスク暗号化なしの手順](install-simple.md)を参照。
# NixOSインストール手順
## 事前準備
@@ -89,36 +83,54 @@ sops updatekeys secrets/hosts/<hostname>/*.yaml
新しいホスト用の`hosts/<hostname>/disko.nix`を作成。
LUKS暗号化とbtrfsの構成は`hosts/x1g13/disko.nix`を参照。
#### シンプル構成(暗号化なし)
```nix
_:
{
disko.enableConfig = true;
disko.devices.disk.main = {
type = "disk";
device = "/dev/sda";
content = {
type = "gpt";
partitions = {
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
}
```
#### LUKS暗号化 + btrfs
`hosts/x1g13/disko.nix`を参照。
### 7. ディスクのパーティション
```bash
cd ~/dotfiles
disko --mode destroy,format,mount hosts/<hostname>/disko.nix
nix run github:nix-community/disko -- --mode disko hosts/<hostname>/disko.nix
```
### 8. ハードウェア設定の生成
対象マシンのハードウェア設定を生成し、新しいホストのディレクトリへ直接保存:
```bash
nixos-generate-config --no-filesystems --root /mnt --show-hardware-config \
> ~/dotfiles/hosts/<hostname>/hardware-configuration.nix
```
`hosts/<hostname>/nixos.nix`から生成した設定とDisko設定を読み込む:
```nix
{
imports = [
./hardware-configuration.nix
./disko.nix
];
}
```
### 9. ホストキーのコピー
### 8. ホストキーのコピー
```bash
mkdir -p /mnt/etc/ssh
@@ -126,13 +138,13 @@ cp /tmp/ssh_host_ed25519_key* /mnt/etc/ssh/
chmod 600 /mnt/etc/ssh/ssh_host_ed25519_key
```
### 10. NixOSインストール
### 9. NixOSインストール
```bash
nixos-install --flake ~/dotfiles#<hostname>
```
### 11. 再起動
### 10. 再起動
```bash
reboot
-119
View File
@@ -1,119 +0,0 @@
# 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
```
+12 -72
View File
@@ -1,86 +1,26 @@
# カスタムインストーラー ISO
`hosts/installer` から、NixOS 26.05 ベースの `x86_64-linux` 用インストーラー
ISO を作成する。installer ホストは Home Manager を使用せず、`base` プロファイルと
ホスト固有のインストール支援設定だけを含む。
# カスタムISOビルド
## ビルド
flake 対応の Nix が利用できる環境で、リポジトリのルートから実行する。
`x86_64-linux` 以外のマシンで実行する場合は、対応する Linux リモートビルダーが
必要になる。
```bash
nix build .#nixosConfigurations.installer.config.system.build.isoImage
```
生成された ISO は次の場所にある。
```text
result/iso/nixos-minimal-*-x86_64-linux.iso
```
ファイル名に含まれる NixOS のバージョンとリビジョンは、`flake.lock` の更新に応じて
変わる。生成物を確認するには次を実行する。
## ISO書き込み
```bash
ls -lh result/iso/*.iso
sha256sum result/iso/*.iso
```
# USBデバイスの確認
lsblk
## USB メモリへの書き込み
書き込み先はパーティション(例: `/dev/sdX1`)ではなく、USB デバイス全体
(例: `/dev/sdX`)を指定する。この操作は指定したデバイスの内容を上書きするため、
サイズ、モデル、マウント先を確認する。
```bash
lsblk -p -o NAME,SIZE,TYPE,MODEL,MOUNTPOINTS
```
USB のマウント済みパーティションをアンマウントしてから、`/dev/sdX` と
`/dev/sdX1` を確認した実際のデバイス名に置き換えて書き込む。パーティションが
複数ある場合は、それぞれをアンマウントする。
```bash
sudo umount /dev/sdX1
sudo dd if=result/iso/nixos-minimal-*-x86_64-linux.iso \
of=/dev/sdX bs=4M conv=fsync status=progress
# 書き込み(/dev/sdXは実際のデバイスに置き換える)
sudo dd if=./result/nixos-minimal-*.iso of=/dev/sdX bs=4M status=progress
sync
```
書き込み完了後、USB を安全に取り外して対象マシンから起動する。
## ISOの特徴
## 起動後の接続
有線 LAN は DHCP で自動設定される。Wi-Fi を使用する場合は、インストーラーの
コンソールで NetworkManager を使って接続する。
```bash
nmcli device wifi list
nmcli device wifi connect <SSID> --ask
```
起動時にコンソールへ IPv4 アドレスと簡易ヘルプが表示される。表示されたアドレスへ
登録済みの SSH 鍵で接続する。
```bash
ssh root@<ip-address>
```
root のパスワードログインとキーボード対話認証は無効で、
`hosts/installer/nixos.nix` に登録された公開鍵だけが利用できる。
以降の作業は、構成に応じて次の手順を参照する:
- [SOPSなし・ディスク暗号化なし](install-simple.md)
- [SOPS・ディスク暗号化あり](install.md)
## ISO に含まれる主な設定とツール
- Nix flakes と `nix-command`
- NetworkManager、OpenSSH、起動時の IP アドレス表示
- `disko`、`parted`、`cryptsetup`、`btrfs-progs`、`efibootmgr`
- `sops`、`age`、`ssh-to-age`
- `age-plugin-yubikey`、`yubikey-manager`、`pcsc-tools` と `pcscd`
- `sbctl`、`tpm2-tools`
- `git`、`rsync`、`vim`、`wget`、`curl`、`jq`、`pciutils`、`util-linux`
- SSH鍵認証でrootログイン可能
- 有線LANはDHCPで自動設定
- WiFiは`nmcli`で手動設定可能
- disko/sops/ageなどのツールを内蔵
- ブート時にIPアドレスとヘルプを表示
Generated
+306 -627
View File
File diff suppressed because it is too large Load Diff
+6 -8
View File
@@ -38,6 +38,12 @@
url = "github:ghostty-org/ghostty";
};
# Speech to text
handy = {
url = "github:cjpais/Handy";
inputs.nixpkgs.follows = "nixpkgs";
};
# Shell / Launcher
vicinae.url = "github:vicinaehq/vicinae";
@@ -91,13 +97,6 @@
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";
@@ -114,7 +113,6 @@
nani-translate-linux.url = "git+https://github.com/zunoser/nani-translate-linux.git";
containerlab.url = "github:srl-labs/containerlab";
};
outputs =
+1
View File
@@ -3,5 +3,6 @@
./formatter.nix
./git-hooks.nix
./registry.nix
./validation.nix
];
}
+2
View File
@@ -34,7 +34,9 @@
];
};
actionlint.enable = true;
deadnix.enable = true;
ruff.enable = true;
statix.enable = true;
shellcheck.enable = true;
};
+37 -6
View File
@@ -25,11 +25,42 @@ let
else
{ };
configurations = dotfilesLib.hosts.mkConfigurations hostSpecs;
validationMetadata = {
schemaVersion = 1;
hosts = lib.mapAttrs (
name: spec:
let
isDarwin = lib.hasSuffix "-darwin" spec.system;
in
{
inherit (spec) system user;
kind = if isDarwin then "darwin" else "nixos";
homeManager = spec.homeManager or true;
selectedUnits = dotfilesLib.hosts.selectedUnits spec;
buildAttr =
if isDarwin then
"darwinConfigurations.${name}.system"
else
"nixosConfigurations.${name}.config.system.build.toplevel";
}
) hostSpecs;
units = lib.mapAttrs (_id: unit: {
inherit (unit) id relativePath;
directory = "modules/${lib.concatStringsSep "/" unit.relativePath}";
includes = unit.meta.includes;
fragments = builtins.attrNames (lib.filterAttrs (_: value: value != null) unit.fragments);
}) dotfilesLib.registry.units;
};
in
{
flake = {
inherit (configurations) darwinConfigurations nixosConfigurations;
lib = dotfilesLib;
lib = dotfilesLib // {
inherit validationMetadata;
};
};
perSystem =
@@ -37,10 +68,9 @@ in
let
nixosChecks =
lib.mapAttrs' (name: nixos: lib.nameValuePair "nixos-${name}" nixos.config.system.build.toplevel)
(
lib.filterAttrs (
_: nixos: nixos.pkgs.stdenv.hostPlatform.system == system
) configurations.nixosConfigurations
(lib.filterAttrs (name: _: hostSpecs.${name}.system == system) configurations.nixosConfigurations);
darwinChecks = lib.mapAttrs' (name: darwin: lib.nameValuePair "darwin-${name}" darwin.system) (
lib.filterAttrs (name: _: hostSpecs.${name}.system == system) configurations.darwinConfigurations
);
in
{
@@ -49,6 +79,7 @@ in
inherit inputs lib pkgs;
};
}
// nixosChecks;
// nixosChecks
// darwinChecks;
};
}
+51
View File
@@ -0,0 +1,51 @@
_: {
perSystem =
{ pkgs, config, ... }:
let
script = pkgs.writeText "dotfiles-check.py" (builtins.readFile ../scripts/dotfiles-check.py);
dotfilesCheck = pkgs.writeShellApplication {
name = "dotfiles-check";
runtimeInputs = [
pkgs.git
pkgs.nix
pkgs.python3
];
text = ''
exec python3 ${script} "$@"
'';
};
in
{
apps = {
check = {
type = "app";
program = "${dotfilesCheck}/bin/dotfiles-check";
};
nix-fast-build = {
type = "app";
program = "${pkgs.nix-fast-build}/bin/nix-fast-build";
};
};
packages = {
dotfiles-check = dotfilesCheck;
inherit (pkgs) nix-fast-build;
};
devShells.validation = config.pre-commit.devShell;
checks = {
validation-tool =
pkgs.runCommand "dotfiles-check-self-test"
{
nativeBuildInputs = [ dotfilesCheck ];
}
''
dotfiles-check self-test
touch "$out"
'';
dotnix-shell = config.devShells.dotnix;
};
};
}
+9 -17
View File
@@ -1,14 +1,15 @@
{
nix-builder = {
nix-example = {
system = "x86_64-linux";
stateVersion = "26.05";
user = "moons";
path = ./nix-builder;
path = ./nix-example;
profiles = [
"base"
"interface.minimal"
"interface.cli"
"platform.vm"
"workload.development"
"workload.remote-access"
];
};
@@ -21,25 +22,22 @@
profiles = [
"base"
"interface.minimal"
"interface.cli"
"platform.vm"
"workload.remote-access"
"workload.deploy-rs-target"
];
};
netsrv-01 = {
internal-app-01 = {
system = "x86_64-linux";
stateVersion = "26.05";
user = "moons";
path = ./netsrv-01;
path = ./internal-app-01;
profiles = [
"base"
"interface.minimal"
"interface.cli"
"platform.vm"
"workload.remote-access"
"workload.deploy-rs-target"
"workload.server"
];
};
@@ -91,10 +89,8 @@
"security.tpm-storage"
"workload.development"
"workload.game"
"workload.network-lab"
"workload.personal"
];
};
galleria = {
@@ -108,7 +104,7 @@
"interface.cli"
"interface.labwc"
"interface.niri"
"networking.tailscale-client"
"interface.wallpaperengine"
"platform.intel-nvidia-desktop"
"security.secrets"
"security.secure-boot"
@@ -116,11 +112,7 @@
"security.fingerprint"
"workload.development"
"workload.game"
"workload.machine-learning"
"workload.network-lab"
"workload.personal"
"workload.photography"
"workload.camera"
];
};
-5
View File
@@ -4,11 +4,6 @@
...
}:
{
# This desktop is permanently connected to AC power.
systemd.user.services.swayidle.Service.Environment = [
"SWAYIDLE_ASSUME_AC=1"
];
services.kanshi = {
enable = true;
-7
View File
@@ -8,13 +8,6 @@
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
];
+23 -18
View File
@@ -31,7 +31,6 @@
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; [
@@ -76,12 +75,6 @@
║ ║
║ Installation Workflow: ║
║ ║
║ Choose a guide after cloning: ║
║ Simple: docs/install-simple.md ║
║ SOPS + LUKS: docs/install.md ║
║ ║
║ The workflow below is for SOPS + LUKS: ║
║ ║
║ 1. Clone dotfiles: ║
║ git clone git@github.com:moons-14/dotfiles.git ~/dotfiles║
║ ║
@@ -109,24 +102,36 @@
║ # See hosts/x1g13/disko.nix for reference ║
║ ║
║ 7. Partition disk with disko: ║
║ disko --mode destroy,format,mount \ ║
║ hosts/<host>/disko.nix ║
║ nix run github:nix-community/disko -- \ ║
║ --mode disko hosts/<host>/disko.nix ║
║ ║
║ 8. Generate hardware configuration: ║
║ nixos-generate-config --no-filesystems --root /mnt \ ║
║ --show-hardware-config > \ ║
║ ~/dotfiles/hosts/<host>/hardware-configuration.nix ║
║ # Import hardware-configuration.nix and disko.nix ║
║ # from hosts/<host>/nixos.nix ║
║ ║
║ 9. Copy host key to installed system: ║
║ 8. Copy host key to installed system: ║
║ mkdir -p /mnt/etc/ssh ║
║ cp /tmp/ssh_host_ed25519_key* /mnt/etc/ssh/ ║
║ chmod 600 /mnt/etc/ssh/ssh_host_ed25519_key ║
║ ║
║ 10. Install NixOS: ║
║ 9. Install NixOS: ║
║ nixos-install --flake ~/dotfiles#<host> ║
║ ║
║ Disko Configuration Examples: ║
║ ║
║ Simple (no encryption): ║
║ disko.devices.disk.main = { ║
║ type = "disk"; ║
║ device = "/dev/sda"; ║
║ content = { ║
║ type = "gpt"; ║
║ partitions = { ║
║ ESP = { size = "512M"; type = "EF00"; ║
║ content = { type = "filesystem"; ║
║ format = "vfat"; mountpoint = "/boot"; }; }; ║
║ root = { size = "100%"; ║
║ content = { type = "filesystem"; ║
║ format = "ext4"; mountpoint = "/"; }; }; ║
║ }; ║
║ }; ║
║ }; ║
║ ║
║ LUKS + btrfs (see hosts/x1g13/disko.nix): ║
║ - Use partuuid for device path ║
║ - Set askPassword = true for LUKS ║
@@ -0,0 +1,36 @@
# 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/1b12ab98-2537-4207-a3f4-bb8ba7b53b00";
fsType = "ext4";
};
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/8365-C778";
fsType = "vfat";
options = [
"fmask=0077"
"dmask=0077"
];
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}
+3
View File
@@ -0,0 +1,3 @@
{
imports = [ ./hardware-configuration.nix ];
}
-30
View File
@@ -1,30 +0,0 @@
_: {
disko.enableConfig = true;
disko.devices.disk.main = {
type = "disk";
device = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi0";
content = {
type = "gpt";
partitions = {
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
}
@@ -1,27 +0,0 @@
# QEMU hardware baseline. Replace this with the output of
# `nixos-generate-config` after provisioning the VM if its hardware differs.
{
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 = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}
-40
View File
@@ -1,40 +0,0 @@
{
networking = {
interfaces = {
ens18 = {
useDHCP = false;
ipv4.addresses = [
{
address = "10.50.65.11";
prefixLength = 24;
}
];
};
ens19 = {
useDHCP = false;
ipv4.addresses = [
{
address = "10.50.66.10";
prefixLength = 24;
}
];
};
ens20 = {
useDHCP = false;
ipv4.addresses = [
{
address = "10.50.77.21";
prefixLength = 24;
}
];
};
};
defaultGateway = {
address = "10.50.66.1";
interface = "ens19";
};
};
}
-7
View File
@@ -1,7 +0,0 @@
{
imports = [
./hardware-configuration.nix
./disko.nix
./networking.nix
];
}
-66
View File
@@ -1,66 +0,0 @@
_:
let
osDisk = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi0";
in
{
disko.enableConfig = true;
# The VM disks already contain live filesystems. Model each existing
# filesystem without giving Disko ownership of their partitioning or data.
disko.devices.disk = {
boot = {
type = "disk";
device = "${osDisk}-part1";
destroy = false;
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
root = {
type = "disk";
device = "${osDisk}-part2";
destroy = false;
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
nix-build = {
type = "disk";
device = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi1";
destroy = false;
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/var/lib/nix-build";
mountOptions = [ "noatime" ];
};
};
nix-store = {
type = "disk";
device = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi2";
destroy = false;
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/nix/store";
mountOptions = [ "noatime" ];
};
};
};
fileSystems."/nix/store".neededForBoot = true;
nix.settings.build-dir = "/var/lib/nix-build";
services.fstrim.enable = true;
}
-83
View File
@@ -1,83 +0,0 @@
{
config,
pkgs,
primaryUser,
...
}:
let
homeDirectory = "/home/${primaryUser}";
fleetDirectory = "${homeDirectory}/srv/nix-fleet";
stateDirectory = "/var/lib/nix-fleet";
sourceDirectory = "${stateDirectory}/source";
git = "${pkgs.git}/bin/git";
nix = "${config.nix.package}/bin/nix";
rsync = "${pkgs.rsync}/bin/rsync";
cleanCheckoutConditions = [
"${git} -C ${fleetDirectory} diff --quiet"
"${git} -C ${fleetDirectory} diff --cached --quiet"
];
in
{
systemd.services.nix-fleet-converge = {
description = "Update inputs, build the fleet, and deploy changed hosts";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
environment = {
HOME = homeDirectory;
NIX_CONFIG = ''
accept-flake-config = true
max-jobs = 4
cores = 3
'';
};
serviceConfig = {
Type = "oneshot";
User = primaryUser;
Restart = "on-failure";
RestartSec = "5min";
StateDirectory = "nix-fleet";
StateDirectoryMode = "0750";
WorkingDirectory = stateDirectory;
UMask = "0077";
ExecCondition = cleanCheckoutConditions;
EnvironmentFile = "${fleetDirectory}/.env";
# Work in a disposable copy so updating the dotfiles lock never dirties
# the operator's nix-fleet checkout.
ExecStart = [
"${git} -C ${fleetDirectory} pull --ff-only"
"${rsync} --archive --delete --exclude=.git/ --exclude=.direnv/ --exclude=.env --exclude=result --exclude=result-* ${fleetDirectory}/ ${sourceDirectory}/"
"${nix} flake update --flake ${sourceDirectory} dotfiles"
"${nix} run ${sourceDirectory}#converge -- ${sourceDirectory} ${stateDirectory}"
];
};
};
systemd.timers.nix-fleet-converge = {
description = "Periodically converge the NixOS fleet";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "2min";
OnUnitInactiveSec = "5min";
RandomizedDelaySec = "30s";
Persistent = true;
Unit = "nix-fleet-converge.service";
};
};
# Only an actual successful deployment touches gc-request. This starts the
# builder's root nh-clean unit; remote host stores are never cleaned here.
systemd.paths.nix-fleet-gc = {
description = "Garbage-collect superseded fleet builds on nix-builder";
wantedBy = [ "multi-user.target" ];
pathConfig = {
PathChanged = "${stateDirectory}/gc-request";
Unit = "nh-clean.service";
};
};
}
@@ -1,26 +0,0 @@
# 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")
];
boot.initrd.availableKernelModules = [
"ata_piix"
"uhci_hcd"
"virtio_pci"
"virtio_scsi"
"sd_mod"
"sr_mod"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ ];
boot.extraModulePackages = [ ];
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}
-37
View File
@@ -1,37 +0,0 @@
{
lib,
...
}:
{
nix.settings = {
build-dir = "/var/lib/nix-build";
secret-key-files = [
"/var/lib/secrets/nix-cache-signing-key"
];
auto-optimise-store = lib.mkForce false;
keep-outputs = false;
keep-derivations = true;
};
services.harmonia.cache = {
enable = true;
signKeyPaths = [
"/var/lib/secrets/nix-cache-signing-key"
];
settings = {
bind = "[::]:5000";
priority = 30;
workers = 4;
};
};
networking.firewall.allowedTCPPorts = [
5000
];
}
-40
View File
@@ -1,40 +0,0 @@
{
networking = {
interfaces = {
ens18 = {
useDHCP = false;
ipv4.addresses = [
{
address = "10.50.65.10";
prefixLength = 24;
}
];
};
ens19 = {
useDHCP = false;
ipv4.addresses = [
{
address = "10.50.77.10";
prefixLength = 24;
}
];
};
ens20 = {
useDHCP = false;
ipv4.addresses = [
{
address = "10.50.68.10";
prefixLength = 24;
}
];
};
};
defaultGateway = {
address = "10.50.68.1";
interface = "ens20";
};
};
}
-9
View File
@@ -1,9 +0,0 @@
{
imports = [
./fleet-automation.nix
./disko.nix
./hardware-configuration.nix
./harmonia.nix
./networking.nix
];
}
@@ -0,0 +1,36 @@
# 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/8f0eaec6-5dc9-4821-aa8d-fb6809b5a5bf";
fsType = "ext4";
};
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/201C-961B";
fsType = "vfat";
options = [
"fmask=0077"
"dmask=0077"
];
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}
+3
View File
@@ -0,0 +1,3 @@
{
imports = [ ./hardware-configuration.nix ];
}
-30
View File
@@ -1,30 +0,0 @@
_: {
disko.enableConfig = true;
disko.devices.disk.main = {
type = "disk";
device = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi0";
content = {
type = "gpt";
partitions = {
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
}
+20 -12
View File
@@ -1,16 +1,8 @@
# 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.
# Do not modify this file! It was generated by `nixos-generate-config` and may
# be overwritten by future invocations.
{ lib, modulesPath, ... }:
{
lib,
modulesPath,
...
}:
{
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
];
imports = [ (modulesPath + "/profiles/qemu-guest.nix") ];
boot.initrd.availableKernelModules = [
"ata_piix"
@@ -24,5 +16,21 @@
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";
}
-40
View File
@@ -1,40 +0,0 @@
{
networking = {
interfaces = {
ens18 = {
useDHCP = false;
ipv4.addresses = [
{
address = "10.50.65.100";
prefixLength = 24;
}
];
};
ens19 = {
useDHCP = false;
ipv4.addresses = [
{
address = "10.50.80.10";
prefixLength = 24;
}
];
};
ens20 = {
useDHCP = false;
ipv4.addresses = [
{
address = "10.50.77.20";
prefixLength = 24;
}
];
};
};
defaultGateway = {
address = "10.50.80.1";
interface = "ens19";
};
};
}
+48 -4
View File
@@ -1,7 +1,51 @@
{
imports = [
./hardware-configuration.nix
./networking.nix
./disko.nix
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";
};
};
}
+2 -30
View File
@@ -1,37 +1,9 @@
{
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=${lib.concatStringsSep "," features}"
"--use-gl=angle"
"--use-angle=gl"
"--enable-features=MiddleClickAutoscroll"
];
};
+6 -1
View File
@@ -1,5 +1,10 @@
_: {
{ inputs, ... }:
{
description = "Codex Desktop for Linux";
includes = [ "applications.codex" ];
imports.nixos = [
inputs.codex-desktop-linux.nixosModules.default
];
}
+10 -2
View File
@@ -4,7 +4,15 @@
...
}:
{
environment.systemPackages = [
inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.chatgpt
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"
];
};
}
@@ -1,52 +0,0 @@
{
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
'';
};
};
}
-13
View File
@@ -1,13 +0,0 @@
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"
+4 -34
View File
@@ -1,36 +1,6 @@
{ inputs, pkgs, ... }:
{
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}
'';
};
home.packages = [
inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.codex
];
}
@@ -1,8 +0,0 @@
{ inputs, ... }:
{
description = "Container-based networking labs";
includes = [ "services.docker" ];
imports.nixos = [ inputs.containerlab.nixosModules.default ];
}
@@ -1,17 +0,0 @@
{
inputs,
pkgs,
primaryUser,
}:
{
programs.containerlab.enable = true;
users.users.${primaryUser}.extraGroups = [ "clab_admins" ];
programs.containerlab.package =
inputs.containerlab.packages.${pkgs.stdenv.hostPlatform.system}.default.overrideAttrs
(_old: {
vendorHash = "sha256-xp6YIoqJdUu1zEzw7s7+lh8iGJD4THokF5NPbxLH6zc=";
});
}
@@ -1,22 +0,0 @@
{
inputs,
osConfig,
pkgs,
...
}:
let
unstable = import inputs.nixpkgs-unstable {
inherit (pkgs.stdenv.hostPlatform) system;
# Keep default CUDA targets so dependencies match the CUDA binary cache.
config = pkgs.config // {
cudaSupport = osConfig.my.hardwares.nvidia.enable;
};
};
in
{
home.packages = [
(unstable.darktable.override {
withAi = true;
})
];
}
+1 -14
View File
@@ -1,15 +1,6 @@
{
inputs,
pkgs,
...
}:
let
unstable = inputs.nixpkgs-unstable.legacyPackages.${pkgs.stdenv.hostPlatform.system};
in
{
_: {
programs.vesktop = {
enable = true;
package = unstable.vesktop;
settings = {
discordBranch = "stable";
@@ -25,10 +16,6 @@ in
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"
-4
View File
@@ -1,4 +0,0 @@
{ pkgs, ... }:
{
home.packages = [ pkgs.ffmpeg ];
}
+1 -7
View File
@@ -1,12 +1,6 @@
{ inputs, system, ... }: {
programs.ghostty = {
# 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;
systemd.enable = true;
package = inputs.ghostty.packages.${system}.default;
settings.gtk-single-instance = false;
};
}
+2
View File
@@ -38,6 +38,8 @@ in
ignores = [
".direnv/"
".envrc"
"!.envrc.example"
];
signing = {
+8
View File
@@ -0,0 +1,8 @@
{
homebrew.casks = [ "handy" ];
launchd.agents.handy = {
command = "/usr/bin/open -gja Handy --args --start-hidden";
serviceConfig.RunAtLoad = true;
};
}
+74
View File
@@ -0,0 +1,74 @@
{
config,
inputs,
lib,
pkgs,
...
}:
let
handy = inputs.handy.packages.${pkgs.stdenv.hostPlatform.system}.handy;
settingsFile = "${config.xdg.configHome}/com.pais.handy/settings_store.json";
in
{
home.packages = [
handy
pkgs.wtype
];
# Handy has no disabled-shortcut setting. Empty bindings are rejected before
# registration, leaving niri and labwc as the sole owners of Ctrl+Space.
home.activation.disableHandyGlobalShortcuts = {
after = [ "writeBoundary" ];
before = [ ];
data = ''
settingsFile=${lib.escapeShellArg settingsFile}
mkdir -p "$(dirname "$settingsFile")"
if [ -f "$settingsFile" ]; then
${lib.getExe pkgs.jq} \
'.settings.bindings.transcribe.current_binding = ""
| .settings.bindings.transcribe_with_post_process.current_binding = ""' \
"$settingsFile" > "$settingsFile.tmp"
else
${lib.getExe pkgs.jq} -n '
{
settings: {
bindings: {
transcribe: {
id: "transcribe",
name: "Transcribe",
description: "Converts your speech into text.",
default_binding: "ctrl+space",
current_binding: ""
},
transcribe_with_post_process: {
id: "transcribe_with_post_process",
name: "Transcribe with Post-Processing",
description: "Converts your speech into text and applies AI post-processing.",
default_binding: "ctrl+shift+space",
current_binding: ""
}
}
}
}
' > "$settingsFile.tmp"
fi
mv "$settingsFile.tmp" "$settingsFile"
'';
};
systemd.user.services.handy = {
Unit = {
Description = "Handy speech-to-text";
After = [ "graphical-session.target" ];
PartOf = [ "graphical-session.target" ];
};
Service = {
ExecStart = "${lib.getExe handy} --start-hidden";
Restart = "on-failure";
RestartSec = 5;
};
Install.WantedBy = [ "graphical-session.target" ];
};
}
+3
View File
@@ -0,0 +1,3 @@
{
description = "Handy offline speech-to-text";
}
-4
View File
@@ -1,4 +0,0 @@
{ pkgs, ... }:
{
home.packages = [ pkgs.htop ];
}
@@ -1,4 +0,0 @@
{ pkgs, ... }:
{
home.packages = [ pkgs.python314Packages.huggingface-hub ];
}
+5 -1
View File
@@ -102,6 +102,8 @@ 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")
@@ -121,12 +123,14 @@ 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")
(execute "W-v" "vicinae vicinae://launch/clipboard/history?toggle=true")
(execute "W-j" "nani-translate-primary")
(execute "W-S-j" "nani-translate-ocr")
(execute "W-C-j" "${lib.getExe' pkgs.xdg-utils "xdg-open"} naniapp://translate")
(execute "W-space" "ghostty +toggle-quick-terminal")
(execute "W-p" "wdisplays")
(execute "W-z" "wl-find-cursor -c 0xCCFF453A -s 160 -d 1200")
(execute "C-space" "handy --toggle-transcription")
# Function keys (sync with niri modules/applications/niri/home.nix)
(execute "XF86AudioRaiseVolume" "noctalia msg volume-up")
+1
View File
@@ -4,5 +4,6 @@
includes = [
"systems.wayland"
"applications.screenshot"
"applications.wl-find-cursor"
];
}
+6 -1
View File
@@ -6,11 +6,16 @@
}:
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 = unstable.labwc;
package = labwc;
};
xdg.portal.config.labwc.default = [
@@ -0,0 +1,103 @@
From: Codex <[email protected]>
Subject: [PATCH] output: detach destroyed outputs from protocol handles
The DRM backend destroys and recreates outputs when a session is paused and
resumed. Remove the output from the ext-workspace group and the foreign
toplevel handles before wlroots finishes it. Both protocols attach bind
listeners to the output; leaving either listener behind makes
wlr_output_finish() abort.
---
src/foreign-toplevel/foreign.c | 14 ++++++++++++++
src/output.c | 6 ++++++
include/foreign-toplevel/foreign.h | 2 ++
3 files changed, 22 insertions(+)
diff --git a/src/output.c b/src/output.c
index 2eab8ec..6f6e7ef 100644
--- a/src/output.c
+++ b/src/output.c
@@ -27,8 +27,9 @@
#include "common/macros.h"
#include "common/mem.h"
#include "common/scene-helpers.h"
#include "common/string-helpers.h"
#include "config/rcxml.h"
+#include "foreign-toplevel/foreign.h"
#include "labwc.h"
#include "layers.h"
#include "node.h"
@@ -276,7 +277,15 @@ handle_output_destroy(struct wl_listener *listener, void *data)
struct output *output = wl_container_of(listener, output, destroy);
struct seat *seat = &server.seat;
regions_evacuate_output(output);
regions_destroy(seat, &output->regions);
+ wlr_ext_workspace_group_handle_v1_output_leave(
+ server.workspaces.ext_group, output->wlr_output);
+
+ struct view *view;
+ wl_list_for_each(view, &server.views, link) {
+ foreign_toplevel_remove_output(view->foreign_toplevel, output->wlr_output);
+ }
+
if (seat->overlay.active.output == output) {
overlay_finish(seat);
}
@@ -297,7 +305,6 @@ handle_output_destroy(struct wl_listener *listener, void *data)
output->workspace_osd = NULL;
}
- struct view *view;
wl_list_for_each(view, &server.views, link) {
if (view->output == output) {
view_on_output_destroy(view);
diff --git a/include/foreign-toplevel/foreign.h b/include/foreign-toplevel/foreign.h
index 69a340a..d8ec9b4 100644
--- a/include/foreign-toplevel/foreign.h
+++ b/include/foreign-toplevel/foreign.h
@@ -3,11 +3,14 @@
#define LABWC_FOREIGN_TOPLEVEL_H
struct view;
+struct wlr_output;
struct foreign_toplevel;
struct foreign_toplevel *foreign_toplevel_create(struct view *view);
void foreign_toplevel_set_parent(struct foreign_toplevel *toplevel,
struct foreign_toplevel *parent);
+void foreign_toplevel_remove_output(struct foreign_toplevel *toplevel,
+ struct wlr_output *output);
void foreign_toplevel_destroy(struct foreign_toplevel *toplevel);
#endif /* LABWC_FOREIGN_TOPLEVEL_H */
diff --git a/src/foreign-toplevel/foreign.c b/src/foreign-toplevel/foreign.c
index 7f1ec8d..0f628a3 100644
--- a/src/foreign-toplevel/foreign.c
+++ b/src/foreign-toplevel/foreign.c
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: GPL-2.0-only
#include "foreign-toplevel/foreign.h"
+#include <wlr/types/wlr_foreign_toplevel_management_v1.h>
#include <assert.h>
#include "common/mem.h"
@@ -34,6 +34,18 @@ foreign_toplevel_set_parent(struct foreign_toplevel *toplevel, struct foreign_to
parent ? &parent->wlr_toplevel : NULL);
}
+void
+foreign_toplevel_remove_output(struct foreign_toplevel *toplevel,
+ struct wlr_output *output)
+{
+ if (!toplevel || !toplevel->wlr_toplevel.handle) {
+ return;
+ }
+
+ wlr_foreign_toplevel_handle_v1_output_leave(
+ toplevel->wlr_toplevel.handle, output);
+}
+
void
foreign_toplevel_destroy(struct foreign_toplevel *toplevel)
{
--
2.51.0
@@ -0,0 +1,167 @@
# shellcheck shell=bash
set -o errexit -o nounset -o pipefail
playlist_file="$1"
default_id="$2"
configuration_id="$3"
shift 3
state_dir="${XDG_RUNTIME_DIR:?}/linux-wallpaperengine"
state_file="$state_dir/current"
usage() {
printf '%s\n' \
'Usage: wallpaperengine-wallpaper COMMAND [ID]' \
'' \
'Commands:' \
' select ID Select a declaratively configured ID for this session' \
' next Select the next configured ID' \
' previous Select the previous configured ID' \
' list List configured IDs and mark the selected one' \
' current Print the selected ID' \
' restart Restart the selected wallpaper' \
' stop Stop Wallpaper Engine for this session'
}
load_playlist() {
mapfile -t wallpaper_ids < <(awk '/^[0-9]+$/ && !seen[$0]++' "$playlist_file")
if [ "${#wallpaper_ids[@]}" -eq 0 ]; then
echo "wallpaperengine-wallpaper: no wallpaper IDs are configured in settings.nix" >&2
exit 1
fi
}
contains_id() {
local candidate="$1"
local id
for id in "${wallpaper_ids[@]}"; do
[ "$id" = "$candidate" ] && return 0
done
return 1
}
current_id() {
local saved_configuration=""
local saved_id=""
local -a saved_state=()
if [ -s "$state_file" ]; then
mapfile -t saved_state <"$state_file"
saved_configuration="${saved_state[0]:-}"
saved_id="${saved_state[1]:-}"
fi
if [ "$saved_configuration" = "$configuration_id" ] && contains_id "$saved_id"; then
printf '%s\n' "$saved_id"
else
printf '%s\n' "$default_id"
fi
}
select_id() {
local id="$1"
if ! contains_id "$id"; then
echo "wallpaperengine-wallpaper: ID is not declared in settings.nix: $id" >&2
exit 2
fi
mkdir -p "$state_dir"
printf '%s\n%s\n' "$configuration_id" "$id" >"$state_file"
systemctl --user restart linux-wallpaperengine.service
}
cycle() {
local step="$1"
local current
local index=0
local next_index
current=$(current_id)
for i in "${!wallpaper_ids[@]}"; do
if [ "${wallpaper_ids[$i]}" = "$current" ]; then
index="$i"
break
fi
done
next_index=$(((index + step + ${#wallpaper_ids[@]}) % ${#wallpaper_ids[@]}))
select_id "${wallpaper_ids[$next_index]}"
}
load_playlist
command="${1:-}"
if [ -z "$command" ]; then
usage
exit 2
fi
shift
case "$command" in
select)
[ "$#" -eq 1 ] || {
usage >&2
exit 2
}
select_id "$1"
;;
next)
[ "$#" -eq 0 ] || {
usage >&2
exit 2
}
cycle 1
;;
previous)
[ "$#" -eq 0 ] || {
usage >&2
exit 2
}
cycle -1
;;
list)
[ "$#" -eq 0 ] || {
usage >&2
exit 2
}
current=$(current_id)
for id in "${wallpaper_ids[@]}"; do
if [ "$id" = "$current" ]; then
printf '* %s\n' "$id"
else
printf ' %s\n' "$id"
fi
done
;;
current)
[ "$#" -eq 0 ] || {
usage >&2
exit 2
}
current_id
;;
restart)
[ "$#" -eq 0 ] || {
usage >&2
exit 2
}
systemctl --user restart linux-wallpaperengine.service
;;
stop)
[ "$#" -eq 0 ] || {
usage >&2
exit 2
}
systemctl --user stop linux-wallpaperengine.service
;;
help | -h | --help)
usage
;;
*)
usage >&2
exit 2
;;
esac
@@ -0,0 +1,57 @@
# shellcheck shell=bash
set -o errexit -o nounset -o pipefail
playlist_file="$1"
default_id="$2"
configuration_id="$3"
assets_dir="$4"
workshop_dir="$5"
scaling="$6"
shift 6
state_file="${XDG_RUNTIME_DIR:?}/linux-wallpaperengine/current"
wallpaper_id="$default_id"
saved_configuration=""
saved_id=""
saved_state=()
outputs=()
if [ -s "$state_file" ]; then
mapfile -t saved_state <"$state_file"
saved_configuration="${saved_state[0]:-}"
saved_id="${saved_state[1]:-}"
if [ "$saved_configuration" = "$configuration_id" ] &&
awk -v id="$saved_id" '$0 == id { found = 1 } END { exit !found }' "$playlist_file"; then
wallpaper_id="$saved_id"
fi
fi
wallpaper_path="$workshop_dir/$wallpaper_id"
if [ ! -d "$wallpaper_path" ]; then
echo "linux-wallpaperengine: missing declared wallpaper: $wallpaper_path" >&2
exit 1
fi
if [ ! -d "$assets_dir" ]; then
echo "linux-wallpaperengine: missing Wallpaper Engine assets: $assets_dir" >&2
exit 1
fi
mapfile -t outputs < <(wlr-randr --json | jq -r '.[] | select(.enabled == true) | .name')
if [ "${#outputs[@]}" -eq 0 ]; then
echo "linux-wallpaperengine: no enabled Wayland outputs were detected" >&2
exit 1
fi
engine_args=("$@" --assets-dir "$assets_dir")
for output in "${outputs[@]}"; do
engine_args+=(
--screen-root "$output"
--bg "$wallpaper_path"
--scaling "$scaling"
)
done
exec linux-wallpaperengine "${engine_args[@]}"
@@ -0,0 +1,229 @@
{
config,
lib,
pkgs,
...
}:
let
settings = import ../settings.nix;
inherit (settings)
assetsDirectory
fps
mute
scaling
selectedWallpaperId
switchIntervalSeconds
wallpaperIds
workshopDirectory
;
resolveHomePath =
path:
if lib.hasPrefix "~/" path then
"${config.home.homeDirectory}/${lib.removePrefix "~/" path}"
else if lib.hasPrefix "/" path then
path
else
"${config.home.homeDirectory}/${path}";
hasWallpapers = wallpaperIds != [ ];
defaultWallpaperId =
if selectedWallpaperId != null then
selectedWallpaperId
else if hasWallpapers then
lib.head wallpaperIds
else
"";
configurationId = builtins.hashString "sha256" (builtins.toJSON settings);
resolvedAssetsDirectory = resolveHomePath assetsDirectory;
resolvedWorkshopDirectory = resolveHomePath workshopDirectory;
playlist = pkgs.writeText "linux-wallpaperengine-playlist" (
lib.concatMapStringsSep "\n" (id: id) wallpaperIds + "\n"
);
engineArguments = lib.optional mute "--silent" ++ [
"--fps"
(toString fps)
];
controller = pkgs.writeShellApplication {
name = "wallpaperengine-wallpaper";
runtimeInputs = [
pkgs.coreutils
pkgs.gawk
pkgs.systemd
];
text = ''
exec ${lib.getExe pkgs.bash} ${./controller.sh} \
${lib.escapeShellArg playlist} \
${lib.escapeShellArg defaultWallpaperId} \
${lib.escapeShellArg configurationId} \
"$@"
'';
};
mkVicinaeScript =
{
name,
title,
description,
command,
mode ? "compact",
message ? null,
}:
{
name = "vicinae/scripts/wallpaper-engine/${name}";
value = {
executable = true;
text = ''
#!${lib.getExe pkgs.bash}
# @vicinae.schemaVersion 1
# @vicinae.title ${title}
# @vicinae.description ${description}
# @vicinae.mode ${mode}
# @vicinae.icon 🖼️
${lib.getExe controller} ${lib.escapeShellArg command}
${lib.optionalString (message != null) "printf '%s\\n' ${lib.escapeShellArg message}"}
'';
};
};
launcher = pkgs.writeShellApplication {
name = "linux-wallpaperengine-launcher";
runtimeInputs = [
pkgs.coreutils
pkgs.gawk
pkgs.jq
pkgs.linux-wallpaperengine
pkgs.wlr-randr
];
text = ''
exec ${lib.getExe pkgs.bash} ${./launcher.sh} \
${lib.escapeShellArg playlist} \
${lib.escapeShellArg defaultWallpaperId} \
${lib.escapeShellArg configurationId} \
${lib.escapeShellArg resolvedAssetsDirectory} \
${lib.escapeShellArg resolvedWorkshopDirectory} \
${lib.escapeShellArg scaling} \
${lib.escapeShellArgs engineArguments}
'';
};
in
assert lib.assertMsg (lib.all (
id: builtins.isString id && builtins.match "[0-9]+" id != null
) wallpaperIds) "linux-wallpaperengine: wallpaperIds must contain only numeric strings";
assert lib.assertMsg (
lib.unique wallpaperIds == wallpaperIds
) "linux-wallpaperengine: wallpaperIds must not contain duplicates";
assert lib.assertMsg (
selectedWallpaperId == null || builtins.elem selectedWallpaperId wallpaperIds
) "linux-wallpaperengine: selectedWallpaperId must be null or a member of wallpaperIds";
assert lib.assertMsg (
switchIntervalSeconds == null || (builtins.isInt switchIntervalSeconds && switchIntervalSeconds > 0)
) "linux-wallpaperengine: switchIntervalSeconds must be null or a positive integer";
assert lib.assertMsg (
builtins.isInt fps && fps > 0
) "linux-wallpaperengine: fps must be a positive integer";
assert lib.assertMsg (
builtins.isString assetsDirectory && assetsDirectory != ""
) "linux-wallpaperengine: assetsDirectory must be a non-empty string";
assert lib.assertMsg (
builtins.isString workshopDirectory && workshopDirectory != ""
) "linux-wallpaperengine: workshopDirectory must be a non-empty string";
assert lib.assertMsg (builtins.elem scaling [
"default"
"fill"
"fit"
"stretch"
]) "linux-wallpaperengine: scaling must be default, fill, fit, or stretch";
{
home.packages = [
controller
pkgs.linux-wallpaperengine
];
xdg.dataFile = builtins.listToAttrs [
(mkVicinaeScript {
name = "reload";
title = "Reload Wallpaper Engine";
description = "Restart the active Wallpaper Engine background";
command = "restart";
message = "Wallpaper Engine reloaded";
})
(mkVicinaeScript {
name = "next";
title = "Next Wallpaper Engine Wallpaper";
description = "Switch to the next configured Wallpaper Engine background";
command = "next";
message = "Switched to the next Wallpaper Engine wallpaper";
})
(mkVicinaeScript {
name = "previous";
title = "Previous Wallpaper Engine Wallpaper";
description = "Switch to the previous configured Wallpaper Engine background";
command = "previous";
message = "Switched to the previous Wallpaper Engine wallpaper";
})
(mkVicinaeScript {
name = "list";
title = "List Wallpaper Engine Wallpapers";
description = "Show the configured Wallpaper Engine backgrounds";
command = "list";
mode = "fullOutput";
})
(mkVicinaeScript {
name = "current";
title = "Current Wallpaper Engine Wallpaper";
description = "Show the active Wallpaper Engine background";
command = "current";
})
(mkVicinaeScript {
name = "stop";
title = "Stop Wallpaper Engine";
description = "Stop Wallpaper Engine for this session";
command = "stop";
message = "Wallpaper Engine stopped";
})
];
systemd.user.services = lib.optionalAttrs hasWallpapers {
linux-wallpaperengine = {
Unit = {
Description = "Linux Wallpaper Engine";
After = [ "graphical-session.target" ];
PartOf = [ "graphical-session.target" ];
};
Service = {
ExecStart = lib.getExe launcher;
Restart = "on-abnormal";
};
Install.WantedBy = [ "graphical-session.target" ];
};
linux-wallpaperengine-switch = {
Unit.Description = "Switch Linux Wallpaper Engine wallpaper";
Service = {
Type = "oneshot";
ExecStart = "${lib.getExe controller} next";
};
};
};
systemd.user.timers =
lib.optionalAttrs
(hasWallpapers && builtins.length wallpaperIds > 1 && switchIntervalSeconds != null)
{
linux-wallpaperengine-switch = {
Unit = {
Description = "Periodically switch Linux Wallpaper Engine wallpaper";
PartOf = [ "graphical-session.target" ];
};
Timer = {
OnActiveSec = "${toString switchIntervalSeconds}s";
OnUnitActiveSec = "${toString switchIntervalSeconds}s";
Unit = "linux-wallpaperengine-switch.service";
};
Install.WantedBy = [ "graphical-session.target" ];
};
};
}
@@ -0,0 +1,3 @@
{
description = "Wallpaper Engine backgrounds for Linux";
}
@@ -0,0 +1,23 @@
{
assetsDirectory = "~/.local/share/Steam/steamapps/common/wallpaper_engine/assets";
workshopDirectory = "~/.local/share/Steam/steamapps/workshop/content/431960";
wallpaperIds = [
"2833810156"
"2834355367"
"2836628501"
"2845095254"
"2859848175"
"2861661119"
"2982896307"
];
# null selects the first ID above.
selectedWallpaperId = null;
# Set a number such as 300 to rotate through multiple IDs.
switchIntervalSeconds = 300;
fps = 30;
mute = true;
scaling = "fill";
}
-4
View File
@@ -1,4 +0,0 @@
{ pkgs, ... }:
{
home.packages = [ pkgs.nano ];
}
@@ -1,5 +0,0 @@
{
description = "Sipeed NanoKVM-USB desktop client";
includes = [ "services.nanokvm-usb" ];
}
@@ -1,42 +0,0 @@
{ pkgs, ... }:
let
pname = "nanokvm-usb";
version = "1.1.4";
src = pkgs.fetchurl {
url = "https://github.com/sipeed/NanoKVM-USB/releases/download/v${version}/NanoKVM-USB-${version}-linux-x86_64.AppImage";
hash = "sha256-00MT4U2afv0qt3xFVhLr0SSmS2cLrKBlmfzqYEFuaVc=";
};
appimageContents = pkgs.appimageTools.extractType2 {
inherit pname src version;
};
nanokvm-usb = pkgs.appimageTools.wrapType2 {
inherit pname src version;
meta = {
description = "Sipeed NanoKVM-USB desktop client";
homepage = "https://github.com/sipeed/NanoKVM-USB";
license = pkgs.lib.licenses.gpl3Only;
platforms = [ "x86_64-linux" ];
mainProgram = "nanokvm-usb";
};
};
desktopItem = pkgs.makeDesktopItem {
name = pname;
desktopName = "NanoKVM-USB";
comment = "NanoKVM-USB Desktop";
exec = "nanokvm-usb --no-sandbox %U";
icon = "${appimageContents}/nanokvm-usb.png";
categories = [ "Utility" ];
startupWMClass = "NanoKVM-USB";
};
in
{
environment.systemPackages = [
nanokvm-usb
desktopItem
];
}
+9 -1
View File
@@ -162,6 +162,14 @@ 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"
'';
};
}
+39 -15
View File
@@ -7,8 +7,6 @@ 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";
@@ -26,18 +24,6 @@ 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";
@@ -55,6 +41,24 @@ 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"
@@ -106,7 +110,7 @@ in
"Mod+V" = {
action.spawn = [
"vicinae"
"vicinae://launch/clipboard/history"
"vicinae://launch/clipboard/history?toggle=true"
];
hotkey-overlay.title = "Clipboard History";
};
@@ -139,6 +143,26 @@ in
action.spawn = "wdisplays";
hotkey-overlay.title = "Display Settings: wdisplays";
};
"Mod+Z" = {
action.spawn = [
"wl-find-cursor"
"-c"
"0xCCFF453A"
"-s"
"160"
"-d"
"1200"
];
hotkey-overlay.title = "Find Cursor";
};
"Ctrl+Space" = {
action.spawn = [
"handy"
"--toggle-transcription"
];
hotkey-overlay.title = "Toggle Handy Transcription";
};
# Function keys (sync with labwc modules/applications/labwc/home.nix)
"XF86AudioRaiseVolume".action.spawn = [
"noctalia"
+1
View File
@@ -5,6 +5,7 @@
includes = [
"systems.wayland"
"applications.screenshot"
"applications.wl-find-cursor"
];
imports = {
+3 -3
View File
@@ -1,6 +1,6 @@
{ lib, pkgs }:
let
version = "0.56.0";
version = "0.46.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-hzCnAyiizm8ZDfE1ONEP6S2Pdnskclm+XdDBBhEYVqE=";
aarch64 = "sha256-vfcgDEFpORPymcRYmlqvsjhV4pU7lNC8Vd9FDPI9UjM=";
x86_64 = "sha256-yMrOpu1WIv2sGVgvY9qf2XCoX2AXMSqR2b8bQDRU6os=";
aarch64 = "sha256-pCp3NOlxFN6zeH67W04WGSPbUae+ktzR5vEunWqu00g=";
}
.${architecture};
in
+12 -3
View File
@@ -13,6 +13,15 @@ let
--replace-fail "@codexbarPath@" "${lib.getExe codexbar}"
'';
noctaliaPatched =
inputs.noctalia.packages.${pkgs.stdenv.hostPlatform.system}.default.overrideAttrs
(oldAttrs: {
patches = (oldAttrs.patches or [ ]) ++ [
./patches/window-switcher-labwc.patch
./patches/taskbar-overview-hook.patch
];
});
wallpapers = pkgs.fetchFromGitHub {
owner = "moons-14";
repo = "wallpapers";
@@ -42,7 +51,7 @@ in
programs.noctalia = {
enable = true;
package = inputs.noctalia.packages.${pkgs.stdenv.hostPlatform.system}.default;
package = noctaliaPatched;
systemd.enable = true;
@@ -157,7 +166,7 @@ in
network-connection = {
type = "custom_button";
glyph = "access-point";
actions.left = "exec nm-connection-editor";
actions.left = "nm-connection-editor";
};
tray = {
type = "tray";
@@ -178,7 +187,7 @@ in
"google-chrome"
"code"
"dev.zed.Zed"
"chatgpt"
"codex-desktop"
"vesktop"
];
show_all_outputs = true;
+4
View File
@@ -2,6 +2,10 @@
{
description = "Noctalia Wayland desktop shell";
includes = [
"applications.window-overview"
];
imports = {
nixos = [ inputs.noctalia.nixosModules.default ];
home = [ inputs.noctalia.homeModules.default ];
@@ -0,0 +1,43 @@
diff --git a/src/shell/bar/widgets/taskbar_widget.cpp b/src/shell/bar/widgets/taskbar_widget.cpp
--- a/src/shell/bar/widgets/taskbar_widget.cpp
+++ b/src/shell/bar/widgets/taskbar_widget.cpp
@@ -5,6 +5,7 @@
#include "compositors/workspace_backend.h"
#include "config/config_service.h"
#include "core/deferred_call.h"
+#include "core/process/process.h"
#include "i18n/i18n.h"
#include "render/core/color.h"
#include "render/core/renderer.h"
@@ -36,6 +37,18 @@
namespace {
+ [[nodiscard]] bool launchTaskbarOverview(const std::string& appId) {
+ constexpr const char* command = "noctalia-taskbar-overview";
+ if (appId.empty() || !process::commandExists(command)) {
+ return false;
+ }
+ return process::runAsync({
+ command,
+ "--app-id",
+ appId.c_str(),
+ });
+ }
+
// Integer centering; optional odd spare pixel on the end side (right/bottom).
[[nodiscard]] float centeredOffset(float extent, float content, float inset = 0.0F, bool oddSpareOnEnd = true) {
const float inner = std::max(0.0F, extent - inset * 2.0F);
@@ -373,6 +386,12 @@ void TaskbarWidget::activateOrLaunchPinned(const TaskModel& task) {
return;
}
+ const std::string overviewAppId =
+ !task.appId.empty() ? task.appId : (!task.desktopEntryId.empty() ? task.desktopEntryId : task.idLower);
+ if (launchTaskbarOverview(overviewAppId)) {
+ return;
+ }
+
const std::string cycleKey = !task.desktopEntryId.empty() ? task.desktopEntryId : task.idLower;
std::size_t& cursor = m_groupedAppCycleCursor[cycleKey];
if (cursor >= windows.size()) {
@@ -0,0 +1,26 @@
diff --git a/src/shell/switcher/window_switcher.cpp b/src/shell/switcher/window_switcher.cpp
--- a/src/shell/switcher/window_switcher.cpp
+++ b/src/shell/switcher/window_switcher.cpp
@@ -286,12 +286,19 @@ indexLiveToplevelsByWindowId(const CompositorPlatform& platform, std::unordered_
continue;
}
- const auto mappedId = platform.compositorWindowIdForToplevelInfo(info);
- if (!mappedId.has_value() || mappedId->empty()) {
- continue;
+ std::string key;
+ if (const auto mappedId = platform.compositorWindowIdForToplevelInfo(info);
+ mappedId.has_value() && !mappedId->empty()) {
+ key = canonicalWindowId(*mappedId);
}
- const std::string key = canonicalWindowId(*mappedId);
+
+ // Generic wlroots compositors such as labwc do not provide a
+ // compositor-specific window ID. The wlr toplevel handle is stable
+ // for the lifetime of the window and is sufficient for switcher identity.
+ if (key.empty() && wlrHandle != 0) {
+ key = "toplevel:" + std::to_string(wlrHandle);
+ }
if (key.empty()) {
continue;
}
-14
View File
@@ -2,19 +2,5 @@
{
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";
};
};
}
+4 -18
View File
@@ -13,6 +13,10 @@ lib.mkMerge [
User = "git";
AddKeysToAgent = "no";
};
"*.sfc.wide.ad.jp" = {
identityFile = "~/.ssh/id_ed25519_sk_rk";
identitiesOnly = true;
};
"*" = {
AddKeysToAgent = "no";
SetEnv.TERM = "xterm-256color";
@@ -20,27 +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
'';
};
home.activation.generateSshKey = {
after = [ "writeBoundary" ];
before = [ ];
data = ''
key="$HOME/.ssh/id_ed25519"
if [ ! -f "$key" ]; then
umask 077
mkdir -p "$HOME/.ssh"
${pkgs.openssh}/bin/ssh-keygen -t ed25519 -N "" -f "$key" \
-C "moons@$(${pkgs.hostname}/bin/hostname || echo host)"
echo "Generated SSH key at $key"
fi
'';
};
}
]
@@ -10,8 +10,6 @@ in
programs.vicinae = {
enable = true;
enableChromeIntegration = false;
settings = {
font.size = 11;
close_on_focus_loss = true;
@@ -8,6 +8,9 @@ let
in
{
programs.vicinae = {
package = pkgs.vicinae;
systemd = {
enable = true;
autoStart = true;
+1 -1
View File
@@ -118,7 +118,7 @@ let
];
userSettings = {
"nix.enableLanguageServer" = true;
"nix.serverPath" = "nil";
"nix.serverPath" = "nixd";
"nix.serverSettings" = {
nixd = {
formatting.command = [ "nixfmt" ];
@@ -0,0 +1,7 @@
{ pkgs, ... }:
let
windowOverview = pkgs.callPackage ../package.nix { };
in
{
home.packages = [ windowOverview ];
}
@@ -0,0 +1,7 @@
{
description = "Fullscreen Wayland window overview with captured previews";
includes = [
"systems.wayland"
];
}
@@ -0,0 +1,77 @@
{
lib,
fetchCrate,
libgbm,
libglvnd,
libxkbcommon,
jq,
makeWrapper,
pkg-config,
rustPlatform,
wayland,
wl-clipboard,
}:
rustPlatform.buildRustPackage rec {
pname = "window-overview";
version = "1.5.0";
src = fetchCrate {
pname = "wlr-chooser";
inherit version;
hash = "sha256-Jb59m1z+2G5istcbC0sg9jRVcC/bQh/OY0ny9OdTsdw=";
};
patches = [
./patches/window-overview.patch
./patches/niri-backend.patch
];
cargoHash = "sha256-KBLgtS8ULrmsOf6BN5SlkVQyXB12utvr/KonnKnCTCM=";
nativeBuildInputs = [
makeWrapper
pkg-config
];
buildInputs = [
libgbm
libglvnd
libxkbcommon
wayland
];
cargoBuildFlags = [
"--bin"
"wlr-switcher"
];
postInstall = ''
mv "$out/bin/wlr-switcher" "$out/bin/.window-overview-wrapped"
makeWrapper "$out/bin/.window-overview-wrapped" "$out/bin/window-overview" \
--add-flags "--layout grid" \
--prefix PATH : "${
lib.makeBinPath [
jq
wl-clipboard
]
}" \
--prefix LD_LIBRARY_PATH : "${
lib.makeLibraryPath [
libgbm
libglvnd
]
}:/run/opengl-driver/lib"
ln -s window-overview "$out/bin/noctalia-taskbar-overview"
'';
meta = {
description = "Fullscreen Wayland window overview with captured previews";
homepage = "https://github.com/sjourdois/wlr-utils";
license = with lib.licenses; [
asl20
mit
];
mainProgram = "window-overview";
platforms = lib.platforms.linux;
};
}
@@ -0,0 +1,431 @@
--- a/src/chooser_cli.rs
+++ b/src/chooser_cli.rs
@@ -81,6 +81,7 @@
initial_cycle: None,
snapshot: false,
cycle_socket: None,
+ niri_backend: false,
};
match run_overlay(opts, t0) {
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -65,7 +65,14 @@
// must connect, enumerate, and open sessions before any thumbnail appears.
let (tx, rx) = mpsc::channel();
let snapshot = opts.snapshot;
- std::thread::spawn(move || ui::capture_thread(tx, snapshot));
+ let niri_backend = opts.niri_backend;
+ std::thread::spawn(move || {
+ if niri_backend {
+ ui::niri_capture_thread(tx);
+ } else {
+ ui::capture_thread(tx, snapshot);
+ }
+ });
shell::tlog(t0, "capture-thread spawned");
let out: ui::Outcome = Arc::new(Mutex::new(None));
--- a/src/switcher_cli.rs
+++ b/src/switcher_cli.rs
@@ -88,6 +88,7 @@
let t0 = Instant::now();
let cli = Cli::parse();
i18n::init();
+ let niri_backend = std::env::var_os("NIRI_SOCKET").is_some();
if cli.doctor {
if let Err(e) = wlr_capture::doctor::report("wlr-switcher", env!("CARGO_PKG_VERSION")) {
@@ -129,32 +130,50 @@
initial_cycle: Some(!cli.reverse),
snapshot: true,
cycle_socket: Some(cycle_socket),
+ niri_backend,
};
// Pre-flight: wlr-switcher switches *windows*, which need the foreign-toplevel
// capture source (wlroots >= 0.20 / Sway >= 1.12). On older compositors connect()
// now succeeds for screen-only capture, but there are no windows to offer — so say
// so clearly and exit, instead of showing an empty dimmed overlay (issue #1).
- match wl::Client::connect() {
- Ok(client) if !client.can_capture_windows() => {
- eprintln!("{}", tr!("capture-no-window"));
- std::process::exit(2);
- }
- Ok(_) => {}
- Err(e) => {
- eprintln!("{}", tr!("error", error = format!("{e:#}")));
- std::process::exit(2);
+ if !niri_backend {
+ match wl::Client::connect() {
+ Ok(client) if !client.can_capture_windows() => {
+ eprintln!("{}", tr!("capture-no-window"));
+ std::process::exit(2);
+ }
+ Ok(_) => {}
+ Err(e) => {
+ eprintln!("{}", tr!("error", error = format!("{e:#}")));
+ std::process::exit(2);
+ }
}
}
match run_overlay(opts, t0) {
Ok(Some(sel)) => {
// Focus the picked window (outputs aren't focusable, so ignore them).
- if sel.is_window
- && let Err(e) = wl::activate_window(&sel.app_id, &sel.title, sel.dup_index)
- {
- eprintln!("{}", tr!("error", error = format!("{e:#}")));
- std::process::exit(2);
+ if sel.is_window {
+ let result: anyhow::Result<()> = if niri_backend {
+ let output = std::process::Command::new("niri")
+ .args(["msg", "action", "focus-window", "--id", &sel.identifier])
+ .output();
+ match output {
+ Ok(output) if output.status.success() => Ok(()),
+ Ok(output) => Err(anyhow::anyhow!(
+ "{}",
+ String::from_utf8_lossy(&output.stderr).trim()
+ )),
+ Err(e) => Err(e.into()),
+ }
+ } else {
+ wl::activate_window(&sel.app_id, &sel.title, sel.dup_index).map_err(Into::into)
+ };
+ if let Err(e) = result {
+ eprintln!("{}", tr!("error", error = format!("{e:#}")));
+ std::process::exit(2);
+ }
}
}
Ok(None) => std::process::exit(1), // cancelled
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -6,7 +6,9 @@
use crate::tr;
use std::collections::{HashMap, HashSet};
+use std::io::Write;
use std::path::PathBuf;
+use std::process::{Command, Stdio};
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
@@ -366,6 +368,288 @@
}
}
+#[derive(Clone)]
+struct NiriWindow {
+ id: String,
+ app_id: String,
+ title: String,
+}
+
+enum ClipboardSnapshot {
+ Empty,
+ Content { mime: String, data: Vec<u8> },
+ Unavailable,
+}
+
+fn save_clipboard() -> ClipboardSnapshot {
+ let Ok(types) = Command::new("wl-paste").arg("--list-types").output() else {
+ return ClipboardSnapshot::Unavailable;
+ };
+ if !types.status.success() {
+ return ClipboardSnapshot::Empty;
+ }
+ let types = String::from_utf8_lossy(&types.stdout);
+ let offered: Vec<&str> = types.lines().filter(|line| !line.is_empty()).collect();
+ let Some(mime) = offered
+ .iter()
+ .copied()
+ .find(|mime| *mime == "text/plain;charset=utf-8")
+ .or_else(|| offered.first().copied())
+ else {
+ return ClipboardSnapshot::Empty;
+ };
+ let Ok(data) = Command::new("wl-paste")
+ .args(["--type", mime])
+ .output()
+ else {
+ return ClipboardSnapshot::Unavailable;
+ };
+ if !data.status.success() {
+ return ClipboardSnapshot::Unavailable;
+ }
+ ClipboardSnapshot::Content {
+ mime: mime.to_owned(),
+ data: data.stdout,
+ }
+}
+
+fn restore_clipboard(snapshot: ClipboardSnapshot) {
+ match snapshot {
+ ClipboardSnapshot::Empty => {
+ let _ = Command::new("wl-copy").arg("--clear").status();
+ }
+ ClipboardSnapshot::Content { mime, data } => {
+ let Ok(mut copy) = Command::new("wl-copy")
+ .args(["--type", &mime])
+ .stdin(Stdio::piped())
+ .spawn()
+ else {
+ return;
+ };
+ if let Some(mut stdin) = copy.stdin.take() {
+ let _ = stdin.write_all(&data);
+ }
+ let _ = copy.wait();
+ }
+ ClipboardSnapshot::Unavailable => {}
+ }
+}
+
+/// Decode one field emitted by jq's `@tsv` formatter. It escapes the only
+/// characters that would otherwise make the line-oriented transport ambiguous.
+fn unescape_tsv(value: &str) -> String {
+ let mut decoded = String::with_capacity(value.len());
+ let mut chars = value.chars();
+ while let Some(ch) = chars.next() {
+ if ch != '\\' {
+ decoded.push(ch);
+ continue;
+ }
+ match chars.next() {
+ Some('t') => decoded.push('\t'),
+ Some('r') => decoded.push('\r'),
+ Some('n') => decoded.push('\n'),
+ Some('\\') => decoded.push('\\'),
+ Some(other) => {
+ decoded.push('\\');
+ decoded.push(other);
+ }
+ None => decoded.push('\\'),
+ }
+ }
+ decoded
+}
+
+/// Ask niri for its toplevel list. niri does not implement the
+/// ext-image-copy-capture window protocol, so its IPC is the source of both the
+/// stable window id and the metadata used by this backend.
+fn niri_windows() -> Result<Vec<NiriWindow>, String> {
+ let response = Command::new("niri")
+ .args(["msg", "-j", "windows"])
+ .output()
+ .map_err(|e| format!("could not run niri msg: {e}"))?;
+ if !response.status.success() {
+ return Err(String::from_utf8_lossy(&response.stderr).trim().to_owned());
+ }
+
+ let mut jq = Command::new("jq")
+ .args([
+ "-r",
+ ".[] | [(.id | tostring), (.app_id // \"\"), (.title // \"\")] | @tsv",
+ ])
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()
+ .map_err(|e| format!("could not run jq: {e}"))?;
+ jq.stdin
+ .take()
+ .ok_or_else(|| String::from("jq stdin was unavailable"))?
+ .write_all(&response.stdout)
+ .map_err(|e| format!("could not pass niri window list to jq: {e}"))?;
+ let output = jq
+ .wait_with_output()
+ .map_err(|e| format!("could not read jq output: {e}"))?;
+ if !output.status.success() {
+ return Err(String::from_utf8_lossy(&output.stderr).trim().to_owned());
+ }
+
+ let text = String::from_utf8(output.stdout)
+ .map_err(|e| format!("niri window list was not UTF-8: {e}"))?;
+ let mut windows = Vec::new();
+ for line in text.lines() {
+ let mut fields = line.splitn(3, '\t');
+ let Some(id) = fields.next() else { continue };
+ let Some(app_id) = fields.next() else { continue };
+ let Some(title) = fields.next() else { continue };
+ windows.push(NiriWindow {
+ id: id.to_owned(),
+ app_id: unescape_tsv(app_id),
+ title: unescape_tsv(title),
+ });
+ }
+ windows.sort_by(|a, b| {
+ a.app_id
+ .to_lowercase()
+ .cmp(&b.app_id.to_lowercase())
+ .then_with(|| a.title.to_lowercase().cmp(&b.title.to_lowercase()))
+ });
+ Ok(windows)
+}
+
+fn niri_window_source(w: &NiriWindow, dup_index: usize) -> Source {
+ let is_system = w.app_id.is_empty();
+ let (title, subtitle) = if is_system {
+ (w.title.clone(), String::new())
+ } else {
+ (w.app_id.clone(), w.title.clone())
+ };
+ Source {
+ key: w.id.clone(),
+ token: format!("Window: {}", w.id),
+ filter: format!("{} {}", w.app_id, w.title).to_lowercase(),
+ title,
+ subtitle,
+ is_window: true,
+ is_system,
+ app_id: w.app_id.clone(),
+ win_title: w.title.clone(),
+ dup_index,
+ }
+}
+
+/// Capture a static overview through niri's IPC. This is deliberately separate
+/// from the generic Wayland backend: niri can render any toplevel by id (even an
+/// occluded one), but does not advertise ext-image-copy-capture.
+pub fn niri_capture_thread(tx: Sender<Msg>) {
+ let windows = match niri_windows() {
+ Ok(windows) => windows,
+ Err(e) => {
+ eprintln!("niri overview backend: {e}");
+ return;
+ }
+ };
+
+ let mut dup: HashMap<(String, String), usize> = HashMap::new();
+ let mut current = Vec::with_capacity(windows.len());
+ for window in windows {
+ let e = dup
+ .entry((window.app_id.clone(), window.title.clone()))
+ .or_insert(0);
+ let source = niri_window_source(&window, *e);
+ *e += 1;
+ current.push((source, window));
+ }
+ if tx
+ .send(Msg::Sources(
+ current.iter().map(|(source, _)| source.clone()).collect(),
+ ))
+ .is_err()
+ {
+ return;
+ }
+
+ let capture_dir = wlr_capture::paths::runtime_dir().join(format!(
+ "window-overview-niri-{}",
+ std::process::id()
+ ));
+ if let Err(e) = std::fs::create_dir_all(&capture_dir) {
+ eprintln!("niri overview backend: could not create capture directory: {e}");
+ return;
+ }
+ // niri's screenshot action also sets the clipboard. Preserve the existing
+ // selection so opening the overview does not unexpectedly replace it.
+ let clipboard = save_clipboard();
+
+ for (source, window) in current {
+ if let Some(path) = icons::resolve(&window.app_id)
+ && let Some((w, h, rgba)) = icons::load(&path, 128)
+ && tx
+ .send(Msg::Icon {
+ key: source.key.clone(),
+ w: w as usize,
+ h: h as usize,
+ rgba,
+ })
+ .is_err()
+ {
+ break;
+ }
+
+ let path = capture_dir.join(format!("{}.png", window.id));
+ let command = Command::new("niri")
+ .args(["msg", "action", "screenshot-window", "--id"])
+ .arg(&window.id)
+ .args(["--write-to-disk", "true", "--show-pointer", "false", "--path"])
+ .arg(&path)
+ .output();
+ let Ok(output) = command else {
+ continue;
+ };
+ if !output.status.success() {
+ eprintln!(
+ "niri overview backend: could not capture window {}: {}",
+ window.id,
+ String::from_utf8_lossy(&output.stderr).trim()
+ );
+ continue;
+ }
+
+ // niri encodes PNGs off the compositor thread, after acknowledging the
+ // IPC action. Wait briefly for that completion without delaying the UI.
+ let mut bytes = None;
+ for _ in 0..200 {
+ match std::fs::read(&path) {
+ Ok(data) => {
+ bytes = Some(data);
+ break;
+ }
+ Err(_) => std::thread::sleep(Duration::from_millis(10)),
+ }
+ }
+ let Some(bytes) = bytes else { continue };
+ let Ok(image) = image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) else {
+ continue;
+ };
+ let image = image.into_rgba8();
+ let (w, h, rgba) = thumbnail_rgba(image.width(), image.height(), image.into_raw());
+ if tx
+ .send(Msg::Thumb {
+ key: source.key,
+ w,
+ h,
+ rgba,
+ })
+ .is_err()
+ {
+ break;
+ }
+ let _ = std::fs::remove_file(path);
+ }
+ let _ = std::fs::remove_dir(capture_dir);
+ restore_clipboard(clipboard);
+}
+
/// Cheap content fingerprint of a frame (subsampled FNV-1a), to tell whether a
/// capture actually changed between rounds — used by the headless bench.
fn quick_hash(rgba: &[u8]) -> u64 {
@@ -515,11 +799,14 @@
/// Downscale a capture to a thumbnail (max side `THUMB_MAX`), never upscaling.
fn thumbnail(img: wl::CapturedImage) -> (usize, usize, Vec<u8>) {
- let (w, h) = (img.width, img.height);
+ thumbnail_rgba(img.width, img.height, img.rgba)
+}
+
+fn thumbnail_rgba(w: u32, h: u32, rgba: Vec<u8>) -> (usize, usize, Vec<u8>) {
let scale = (THUMB_MAX as f32 / w as f32)
.min(THUMB_MAX as f32 / h as f32)
.min(1.0);
- let src = match image::RgbaImage::from_raw(w, h, img.rgba) {
+ let src = match image::RgbaImage::from_raw(w, h, rgba) {
Some(s) => s,
None => return (0, 0, Vec::new()),
};
@@ -557,6 +844,9 @@
pub snapshot: bool,
/// Receives next/previous commands from repeated compositor keybind launches.
pub cycle_socket: Option<std::os::unix::net::UnixDatagram>,
+ /// Use niri's IPC for window enumeration and snapshots instead of the
+ /// ext-image-copy-capture protocol that niri does not implement.
+ pub niri_backend: bool,
}
pub struct App {
@@ -0,0 +1,336 @@
--- a/src/chooser_cli.rs
+++ b/src/chooser_cli.rs
@@ -79,0 +80,4 @@
+ app_id_filter: None,
+ initial_cycle: None,
+ snapshot: false,
+ cycle_socket: None,
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -33,3 +33,3 @@
-/// Returns the held lock file (keep it alive), or `None` if another instance owns
-/// it — sway processes its own keybinding even over our exclusive keyboard grab,
-/// so re-pressing the bind would otherwise stack overlays.
+/// Returns the held lock and command socket for the first instance. Later
+/// invocations forward their requested cycle direction through the socket and
+/// return `None`, which also supports compositors that keep handling the binding.
@@ -36 +36,3 @@
-pub fn acquire_switch_lock() -> Option<std::fs::File> {
+pub fn acquire_switch_lock(
+ forward: bool,
+) -> Option<(std::fs::File, std::os::unix::net::UnixDatagram)> {
@@ -38,0 +41 @@
+ let socket_path = dir.join("wlr-switcher.sock");
@@ -45,2 +49,10 @@
- flock(&f, FlockOperation::NonBlockingLockExclusive).ok()?;
- Some(f)
+ if flock(&f, FlockOperation::NonBlockingLockExclusive).is_err() {
+ let socket = std::os::unix::net::UnixDatagram::unbound().ok()?;
+ let command = if forward { b"next" } else { b"prev" };
+ let _ = socket.send_to(command, socket_path);
+ return None;
+ }
+ let _ = std::fs::remove_file(&socket_path);
+ let socket = std::os::unix::net::UnixDatagram::bind(socket_path).ok()?;
+ socket.set_nonblocking(true).ok()?;
+ Some((f, socket))
@@ -56 +68,2 @@
- std::thread::spawn(move || ui::capture_thread(tx));
+ let snapshot = opts.snapshot;
+ std::thread::spawn(move || ui::capture_thread(tx, snapshot));
--- a/src/shell.rs
+++ b/src/shell.rs
@@ -617,0 +618,3 @@
+ Keysym::plus | Keysym::KP_Add => Key::Plus,
+ Keysym::equal => Key::Equals,
+ Keysym::minus | Keysym::KP_Subtract => Key::Minus,
--- a/src/switcher_cli.rs
+++ b/src/switcher_cli.rs
@@ -75,0 +76,6 @@
+ /// Show only windows whose Wayland app-id exactly matches this value.
+ #[arg(long)]
+ app_id: Option<String>,
+ /// Select the previous window when the overview first opens.
+ #[arg(long)]
+ reverse: bool,
@@ -96,2 +102,2 @@
- let _lock = match acquire_switch_lock() {
- Some(lock) => lock,
+ let (_lock, cycle_socket) = match acquire_switch_lock(!cli.reverse) {
+ Some(instance) => instance,
@@ -121,0 +128,4 @@
+ app_id_filter: cli.app_id,
+ initial_cycle: Some(!cli.reverse),
+ snapshot: true,
+ cycle_socket: Some(cycle_socket),
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -8,0 +9 @@
+use std::path::PathBuf;
@@ -41,0 +43,32 @@
+const DEFAULT_OVERVIEW_SCALE: f32 = 0.85;
+const MIN_OVERVIEW_SCALE: f32 = 0.55;
+const MAX_OVERVIEW_SCALE: f32 = 1.0;
+
+fn overview_scale_path() -> Option<PathBuf> {
+ if let Some(path) = std::env::var_os("XDG_STATE_HOME") {
+ return Some(PathBuf::from(path).join("window-overview/scale"));
+ }
+ std::env::var_os("HOME")
+ .map(PathBuf::from)
+ .map(|path| path.join(".local/state/window-overview/scale"))
+}
+
+fn load_overview_scale() -> f32 {
+ overview_scale_path()
+ .and_then(|path| std::fs::read_to_string(path).ok())
+ .and_then(|value| value.trim().parse::<f32>().ok())
+ .unwrap_or(DEFAULT_OVERVIEW_SCALE)
+ .clamp(MIN_OVERVIEW_SCALE, MAX_OVERVIEW_SCALE)
+}
+
+fn save_overview_scale(scale: f32) {
+ let Some(path) = overview_scale_path() else {
+ return;
+ };
+ let Some(parent) = path.parent() else {
+ return;
+ };
+ if std::fs::create_dir_all(parent).is_ok() {
+ let _ = std::fs::write(path, format!("{scale:.2}\n"));
+ }
+}
@@ -177 +210 @@
-pub fn capture_thread(tx: Sender<Msg>) {
+pub fn capture_thread(tx: Sender<Msg>, snapshot: bool) {
@@ -189,0 +223 @@
+ let mut captured: HashSet<String> = HashSet::new();
@@ -236,0 +271 @@
+ captured.retain(|key| present.contains(key.as_str()));
@@ -255 +290 @@
- if sessions.contains_key(&s.key) {
+ if sessions.contains_key(&s.key) || (snapshot && captured.contains(&s.key)) {
@@ -295 +330,3 @@
- let Some(key) = by_id.get(&id) else { continue };
+ let Some(key) = by_id.get(&id).cloned() else {
+ continue;
+ };
@@ -313,0 +351,6 @@
+ if snapshot {
+ captured.insert(key.clone());
+ sessions.remove(&key);
+ by_id.remove(&id);
+ client.close_session(&id);
+ }
@@ -508,0 +552,8 @@
+ /// Exact Wayland app-id filter, used by taskbar launches.
+ pub app_id_filter: Option<String>,
+ /// Initial cycle direction. `None` leaves the first item selected.
+ pub initial_cycle: Option<bool>,
+ /// Capture each window once instead of continuously refreshing previews.
+ pub snapshot: bool,
+ /// Receives next/previous commands from repeated compositor keybind launches.
+ pub cycle_socket: Option<std::os::unix::net::UnixDatagram>,
@@ -533,0 +585,5 @@
+ app_id_filter: Option<String>,
+ initial_forward: bool,
+ overview_scale: f32,
+ cycle_socket: Option<std::os::unix::net::UnixDatagram>,
+ queued_cycles: isize,
@@ -567 +623 @@
- pending_initial_select: false,
+ pending_initial_select: opts.initial_cycle.is_some(),
@@ -571,0 +628,5 @@
+ app_id_filter: opts.app_id_filter,
+ initial_forward: opts.initial_cycle.unwrap_or(true),
+ overview_scale: load_overview_scale(),
+ cycle_socket: opts.cycle_socket,
+ queued_cycles: 0,
@@ -604 +665,4 @@
- return; // nothing to cycle yet; keep the pending initial jump
+ self.queued_cycles = self
+ .queued_cycles
+ .saturating_add(if forward { 1 } else { -1 });
+ return;
@@ -639,0 +704,12 @@
+ loop {
+ let command = self.cycle_socket.as_ref().and_then(|socket| {
+ let mut buf = [0_u8; 8];
+ socket.recv(&mut buf).ok().map(|len| (buf, len))
+ });
+ match command {
+ Some((buf, len)) if &buf[..len] == b"next" => self.cycle(true),
+ Some((buf, len)) if &buf[..len] == b"prev" => self.cycle(false),
+ Some(_) => {}
+ None => break,
+ }
+ }
@@ -698,0 +775,5 @@
+ .filter(|s| {
+ self.app_id_filter
+ .as_ref()
+ .is_none_or(|app_id| s.app_id.eq_ignore_ascii_case(app_id))
+ })
@@ -713,2 +793,0 @@
- // Exposé covers the whole screen: dim almost to opaque so the real windows
- // behind are hidden (a client can't move them; this hides them instead).
@@ -716 +795 @@
- c[3] = c[3].max(0.96);
+ c[3] = 0.5;
@@ -734 +813,5 @@
- self.selected = if n > 1 { 1 } else { 0 };
+ self.selected = if n > 1 {
+ if self.initial_forward { 1 } else { n - 1 }
+ } else {
+ 0
+ };
@@ -737,0 +821,8 @@
+ if self.queued_cycles != 0 {
+ let n = self.visible().len();
+ if n > 0 {
+ self.selected = (self.selected as isize + self.queued_cycles)
+ .rem_euclid(n as isize) as usize;
+ self.queued_cycles = 0;
+ }
+ }
@@ -745 +836 @@
- let (esc, next, prev, enter) = ctx.input(|i| {
+ let (esc, next, prev, enter, zoom_in, zoom_out, wheel_zoom) = ctx.input(|i| {
@@ -746,0 +838,8 @@
+ let wheel_zoom = i
+ .events
+ .iter()
+ .filter_map(|event| match event {
+ egui::Event::MouseWheel { delta, .. } => Some(delta.y),
+ _ => None,
+ })
+ .sum::<f32>();
@@ -755,0 +855,3 @@
+ i.key_pressed(egui::Key::Plus) || i.key_pressed(egui::Key::Equals),
+ i.key_pressed(egui::Key::Minus),
+ wheel_zoom,
@@ -771,0 +874,12 @@
+ if self.view == View::Grid && (zoom_in || zoom_out || wheel_zoom != 0.0) {
+ let delta = if zoom_in {
+ 0.05
+ } else if zoom_out {
+ -0.05
+ } else {
+ wheel_zoom.signum() * 0.05
+ };
+ self.overview_scale =
+ (self.overview_scale + delta).clamp(MIN_OVERVIEW_SCALE, MAX_OVERVIEW_SCALE);
+ save_overview_scale(self.overview_scale);
+ }
@@ -1020 +1134,5 @@
- let area = ctx.content_rect().shrink(24.0);
+ let full_area = ctx.content_rect().shrink(24.0);
+ let area = egui::Rect::from_center_size(
+ full_area.center(),
+ full_area.size() * self.overview_scale,
+ );
@@ -1048,0 +1167 @@
+ let selected = *i == self.selected;
@@ -1062,9 +1181,3 @@
- rect.size() * (0.86 + 0.14 * ease),
- );
- self.paint_expose_tile(
- ui,
- s,
- scaled,
- *i == self.selected,
- resp.hovered(),
- ease,
+ rect.size()
+ * (0.86 + 0.14 * ease)
+ * if selected { 1.04 } else { 1.0 },
@@ -1071,0 +1185 @@
+ self.paint_expose_tile(ui, s, scaled, selected, resp.hovered(), ease);
@@ -1077,0 +1192,8 @@
+ ui.painter().text(
+ egui::pos2(full_area.right(), full_area.top()),
+ egui::Align2::RIGHT_TOP,
+ format!("− {:.0}% +", self.overview_scale * 100.0),
+ egui::FontId::proportional(14.0),
+ self.theme.text_dim,
+ );
+
@@ -1107 +1229,14 @@
- let white = egui::Color32::WHITE.gamma_multiply(a);
+ let accent = if s.is_window {
+ t.window_accent
+ } else {
+ t.screen_accent
+ };
+ let content_alpha = if selected || hovered { 1.0 } else { 0.55 };
+ let white = egui::Color32::WHITE.gamma_multiply(a * content_alpha);
+ if selected {
+ p.rect_filled(
+ rect.expand(8.0),
+ radius + 8.0,
+ fade(accent).gamma_multiply(0.35),
+ );
+ }
@@ -1127,0 +1263,7 @@
+ if !selected && !hovered {
+ p.rect_filled(
+ rect,
+ radius,
+ egui::Color32::from_black_alpha(72).gamma_multiply(a),
+ );
+ }
@@ -1136 +1278,5 @@
- egui::Color32::from_black_alpha(160).gamma_multiply(a),
+ if selected {
+ fade(accent).gamma_multiply(0.9)
+ } else {
+ egui::Color32::from_black_alpha(160).gamma_multiply(a)
+ },
@@ -1175,9 +1321,22 @@
- let accent = if s.is_window {
- t.window_accent
- } else {
- t.screen_accent
- };
- let (sw, col) = if selected {
- (3.0, accent)
- } else if hovered {
- (2.0, accent)
+ if selected {
+ p.rect_stroke(
+ rect.expand(4.0),
+ radius + 4.0,
+ egui::Stroke::new(3.0, fade(egui::Color32::WHITE)),
+ egui::StrokeKind::Inside,
+ );
+ p.rect_stroke(
+ rect,
+ radius,
+ egui::Stroke::new(6.0, fade(accent)),
+ egui::StrokeKind::Inside,
+ );
+ let marker = egui::pos2(rect.right() - 18.0, rect.top() + 18.0);
+ p.circle_filled(marker, 13.0, fade(accent));
+ p.text(
+ marker,
+ egui::Align2::CENTER_CENTER,
+ "✓",
+ egui::FontId::proportional(18.0),
+ fade(egui::Color32::WHITE),
+ );
@@ -1185,8 +1344,7 @@
- (1.0, t.thumb)
- };
- p.rect_stroke(
- rect,
- radius,
- egui::Stroke::new(sw, fade(col)),
- egui::StrokeKind::Inside,
- );
+ p.rect_stroke(
+ rect,
+ radius,
+ egui::Stroke::new(if hovered { 2.0 } else { 1.0 }, fade(accent)),
+ egui::StrokeKind::Inside,
+ );
+ }
@@ -0,0 +1,7 @@
{ inputs, pkgs, ... }:
let
unstable = inputs.nixpkgs-unstable.legacyPackages.${pkgs.stdenv.hostPlatform.system};
in
{
home.packages = [ unstable.wl-find-cursor ];
}
@@ -0,0 +1,3 @@
{
description = "Wayland cursor locator";
}
-8
View File
@@ -1,8 +0,0 @@
{
homebrew = {
enable = true;
masApps = {
Xcode = 497799835;
};
};
}
-4
View File
@@ -1,4 +0,0 @@
{ pkgs, ... }:
{
home.packages = [ pkgs.yt-dlp ];
}
-7
View File
@@ -4,12 +4,8 @@
extensions = [
"catppuccin"
"nix"
"dockerfile"
"terraform"
];
mutableUserSettings = false;
mutableUserKeymaps = false;
userKeymaps = import ./keymaps.nix;
userSettings = {
agent = {
@@ -40,9 +36,6 @@
light = "Catppuccin Latte";
dark = "Catppuccin Mocha";
};
edit_predictions = {
provider = "copilot";
};
};
};
}
-38
View File
@@ -1,38 +0,0 @@
[
{
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";
};
}
]
+4 -1
View File
@@ -5,7 +5,10 @@
modesetting.enable = true;
open = true;
powerManagement.enable = true;
powerManagement = {
enable = true;
kernelSuspendNotifier = true;
};
moduleParams.nvidia.NVreg_TemporaryFilePath = "/var/tmp";
+11 -30
View File
@@ -24,28 +24,23 @@ required on every supported host.
| Profile | Supported host class |
| ------------------------------------ | --------------------------------------------- |
| `base` | NixOS, macOS |
| `interface.minimal` | NixOS, macOS with Home Manager |
| `interface.cli` | NixOS, macOS with Home Manager |
| `interface.gui` | NixOS, macOS with Home Manager |
| `interface.macos` | macOS |
| `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 |
| `platform.laptop` | Physical NixOS laptop |
| `platform.thinkpad-x1` | Intel ThinkPad X1 running NixOS |
| `platform.vm` | UEFI QEMU NixOS guest with NFS client support |
| `workload.deploy-rs-target` | NixOS |
| `workload.development` | NixOS, macOS with Home Manager |
| `workload.game` | NixOS, macOS |
| `workload.machine-learning` | NixOS with Home Manager |
| `workload.network-lab` | NixOS |
| `workload.personal` | NixOS, macOS with Home Manager |
| `workload.photography` | NixOS with Home Manager |
| `workload.remote-access` | NixOS, macOS |
| `workload.camera` | NixOS |
| `workload.server` | NixOS, macOS with Home Manager |
| `networking.tailscale-client` | NixOS, macOS |
| `networking.tailscale-subnet-router` | NixOS |
@@ -64,36 +59,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.
`platform.nixos` selects the shared network foundation and the clatd service.
VM, laptop, and desktop platform profiles inherit both.
`interface.minimal` provides SSH client access and key generation, Nano, htop,
btop, fastfetch, unzip, wget, Direnv, Git, GnuPG, nh, Zellij, and Zsh for remote
administration. `interface.cli` includes that baseline and adds the configured
Neovim, Yazi, and the remaining interactive command-line tools.
`workload.machine-learning` provides the Hugging Face Hub CLI for hosts used
to download and publish machine learning models and datasets.
`workload.network-lab` provides containerlab using its upstream NixOS module,
the Docker service, and the NanoKVM-USB desktop client with serial-port access.
It is selected by galleria and x1g13.
`workload.photography` provides darktable with AI support from the locked
unstable package set. Its ONNX Runtime uses CUDA when the NVIDIA hardware unit
is enabled, and CPU inference otherwise. Keep the default CUDA capabilities
and forward compatibility to reuse the CUDA binary cache; these targets include
galleria's RTX 3060 Ti. OpenCL drivers remain owned by hardware units.
`workload.deploy-rs-target` enables OpenSSH and provisions the `nixdeploy`
service account with the narrowly scoped passwordless commands required for
deploy-rs activation and rollback confirmation.
`interface.wallpaperengine` is an opt-in Linux desktop appearance profile. It
enables Wallpaper Engine and disables Noctalia's wallpaper surface, so hosts
without this profile retain Noctalia's configured wallpaper.
`workload.personal` provides Pear Desktop on both NixOS and macOS. Home Manager
enables performance improvements, synced lyrics, tracker blocking, the album
color theme, and custom output-device selection while preserving user-owned
settings such as the selected device.
`workload.personal` also provides Handy for offline speech-to-text. It starts
hidden when the graphical session begins. On niri and labwc, `Ctrl+Space` is
owned by the compositor and invokes Handy's `--toggle-transcription` command;
Handy's own shortcut bindings are disabled on NixOS so it does not monitor
keyboard events under Wayland. Handy uses the Homebrew cask and starts at login
on macOS.
`workload.personal` provides ActivityWatch for local activity tracking. On
NixOS, its server and Wayland-compatible watcher run as Home Manager user
services. On macOS, the Homebrew cask starts at login.
+19 -4
View File
@@ -8,21 +8,36 @@
dust
eza
fd
fastfetch
fzf
htop
jq
lsof
nurl
ripgrep
tio
unrar
traceroute
tcpdump
iw
nmap
unzip
wget
]
++ lib.optionals stdenv.isLinux [
lm_sensors
psmisc
strace
];
home.activation.generateSshKey = {
after = [ "writeBoundary" ];
before = [ ];
data = ''
key="$HOME/.ssh/id_ed25519"
if [ ! -f "$key" ]; then
umask 077
mkdir -p "$HOME/.ssh"
${pkgs.openssh}/bin/ssh-keygen -t ed25519 -N "" -f "$key" \
-C "moons@$(${pkgs.hostname}/bin/hostname || echo host)"
echo "Generated SSH key at $key"
fi
'';
};
}
+8 -1
View File
@@ -2,10 +2,17 @@
description = "Cross-platform interactive command-line environment";
includes = [
"profiles.interface.minimal"
"applications.btop"
"applications.direnv"
"applications.git"
"applications.gnupg"
"applications.nh"
"applications.nix-index"
"applications.ssh"
"applications.vim"
"applications.yazi"
"applications.zellij"
"applications.zoxide"
"applications.zsh"
];
}

Some files were not shown because too many files have changed in this diff Show More