Compare commits
2 Commits
main
...
a2f2cc71f3
| Author | SHA1 | Date | |
|---|---|---|---|
| a2f2cc71f3 | |||
| 579169e17d |
287
.pi/extensions/herdr-agent-state.ts
Normal file
287
.pi/extensions/herdr-agent-state.ts
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
// installed by herdr
|
||||||
|
// managed by herdr; reinstalling or updating the integration overwrites this file.
|
||||||
|
// add custom hooks/plugins beside this file instead of editing it.
|
||||||
|
// HERDR_INTEGRATION_ID=pi
|
||||||
|
// HERDR_INTEGRATION_VERSION=6
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import net from "node:net";
|
||||||
|
|
||||||
|
const HERDR_ENV = process.env.HERDR_ENV;
|
||||||
|
const socketPath = process.env.HERDR_SOCKET_PATH;
|
||||||
|
const socketEndpoint =
|
||||||
|
process.platform === "win32" && socketPath ? `\\\\.\\pipe\\${socketPath}` : socketPath;
|
||||||
|
const paneId = process.env.HERDR_PANE_ID;
|
||||||
|
const source = "herdr:pi";
|
||||||
|
|
||||||
|
function enabled() {
|
||||||
|
return HERDR_ENV === "1" && !!socketPath && !!paneId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendRequestAttempt(request: unknown, timeoutMs: number): Promise<boolean> {
|
||||||
|
if (!enabled()) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let done = false;
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
const finish = (delivered: boolean) => {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
socket.destroy();
|
||||||
|
resolve(delivered);
|
||||||
|
};
|
||||||
|
|
||||||
|
const socket = net.createConnection(socketEndpoint!);
|
||||||
|
socket.on("error", () => finish(false));
|
||||||
|
socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
|
||||||
|
socket.on("data", () => finish(true));
|
||||||
|
socket.on("end", () => finish(false));
|
||||||
|
timeout = setTimeout(() => finish(false), timeoutMs);
|
||||||
|
timeout.unref?.();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendRequest(request: unknown): Promise<void> {
|
||||||
|
if (await sendRequestAttempt(request, 500)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await sendRequestAttempt(request, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentState = "working" | "blocked" | "idle";
|
||||||
|
|
||||||
|
type QueuedState = {
|
||||||
|
state: AgentState;
|
||||||
|
message?: string;
|
||||||
|
seq: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let reportSeq = Date.now() * 1000;
|
||||||
|
let currentAgentSessionId: string | undefined;
|
||||||
|
let currentAgentSessionPath: string | undefined;
|
||||||
|
|
||||||
|
function nextReportSeq(): number {
|
||||||
|
reportSeq += 1;
|
||||||
|
return reportSeq;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSessionRef(ctx: any): void {
|
||||||
|
try {
|
||||||
|
const file = ctx?.sessionManager?.getSessionFile?.();
|
||||||
|
currentAgentSessionPath =
|
||||||
|
typeof file === "string" && file.startsWith("/") ? file : undefined;
|
||||||
|
} catch {
|
||||||
|
currentAgentSessionPath = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const id = ctx?.sessionManager?.getSessionId?.();
|
||||||
|
currentAgentSessionId = typeof id === "string" && id.length > 0 ? id : undefined;
|
||||||
|
} catch {
|
||||||
|
currentAgentSessionId = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function withSessionRef(params: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
if (currentAgentSessionPath) {
|
||||||
|
return { ...params, agent_session_path: currentAgentSessionPath };
|
||||||
|
}
|
||||||
|
if (currentAgentSessionId) {
|
||||||
|
return { ...params, agent_session_id: currentAgentSessionId };
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentSessionRef(): Record<string, unknown> | undefined {
|
||||||
|
if (currentAgentSessionPath) {
|
||||||
|
return { agent_session_path: currentAgentSessionPath };
|
||||||
|
}
|
||||||
|
if (currentAgentSessionId) {
|
||||||
|
return { agent_session_id: currentAgentSessionId };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportSession(sessionStartSource?: string): Promise<void> {
|
||||||
|
const sessionRef = currentSessionRef();
|
||||||
|
if (!sessionRef) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendRequest({
|
||||||
|
id: `${source}:session:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
||||||
|
method: "pane.report_agent_session",
|
||||||
|
params: {
|
||||||
|
pane_id: paneId,
|
||||||
|
source,
|
||||||
|
agent: "pi",
|
||||||
|
seq: nextReportSeq(),
|
||||||
|
session_start_source: sessionStartSource,
|
||||||
|
...sessionRef,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendState(state: AgentState, message?: string, seq = nextReportSeq()): Promise<void> {
|
||||||
|
return sendRequest({
|
||||||
|
id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
||||||
|
method: "pane.report_agent",
|
||||||
|
params: withSessionRef({
|
||||||
|
pane_id: paneId,
|
||||||
|
source,
|
||||||
|
agent: "pi",
|
||||||
|
state,
|
||||||
|
message,
|
||||||
|
seq,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseAgent(): Promise<void> {
|
||||||
|
return sendRequest({
|
||||||
|
id: `${source}:release:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
||||||
|
method: "pane.release_agent",
|
||||||
|
params: {
|
||||||
|
pane_id: paneId,
|
||||||
|
source,
|
||||||
|
agent: "pi",
|
||||||
|
seq: nextReportSeq(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldReleaseOnSessionShutdown(event: any): boolean {
|
||||||
|
// Pi tears down and rebinds extension runtimes for internal lifecycle actions
|
||||||
|
// such as /reload, /new, /resume, and /fork. Those do not mean the pane's
|
||||||
|
// agent process has exited, and releasing hook authority there can suppress
|
||||||
|
// legitimate reports from the replacement runtime. Only a user/process quit
|
||||||
|
// should release Herdr's full-lifecycle authority.
|
||||||
|
const reason = event?.reason;
|
||||||
|
return reason === "quit";
|
||||||
|
}
|
||||||
|
|
||||||
|
let sendInFlight = false;
|
||||||
|
let queuedState: QueuedState | undefined;
|
||||||
|
|
||||||
|
function queueState(state: AgentState, message?: string): void {
|
||||||
|
queuedState = { state, message, seq: nextReportSeq() };
|
||||||
|
if (!sendInFlight) {
|
||||||
|
void drainStateQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drainStateQueue(): Promise<void> {
|
||||||
|
if (sendInFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendInFlight = true;
|
||||||
|
try {
|
||||||
|
while (queuedState) {
|
||||||
|
const next = queuedState;
|
||||||
|
queuedState = undefined;
|
||||||
|
await sendState(next.state, next.message, next.seq);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
sendInFlight = false;
|
||||||
|
if (queuedState) {
|
||||||
|
void drainStateQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (pi) {
|
||||||
|
if (!enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let agentActive = false;
|
||||||
|
let blockedCount = 0;
|
||||||
|
let blockedMessage: string | undefined;
|
||||||
|
let lastState: AgentState | undefined;
|
||||||
|
let lastMessage: string | undefined;
|
||||||
|
let rootSession = false;
|
||||||
|
|
||||||
|
function desiredState() {
|
||||||
|
if (blockedCount > 0) {
|
||||||
|
return { state: "blocked" as const, message: blockedMessage };
|
||||||
|
}
|
||||||
|
if (agentActive) {
|
||||||
|
return { state: "working" as const, message: undefined };
|
||||||
|
}
|
||||||
|
return { state: "idle" as const, message: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishState(force = false) {
|
||||||
|
const next = desiredState();
|
||||||
|
if (!force && next.state === lastState && next.message === lastMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastState = next.state;
|
||||||
|
lastMessage = next.message;
|
||||||
|
queueState(next.state, next.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
pi.events.on("herdr:blocked", (data) => {
|
||||||
|
if (!rootSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!data?.active) {
|
||||||
|
blockedCount = Math.max(0, blockedCount - 1);
|
||||||
|
if (blockedCount === 0) {
|
||||||
|
blockedMessage = undefined;
|
||||||
|
}
|
||||||
|
publishState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedCount += 1;
|
||||||
|
blockedMessage = data.label;
|
||||||
|
publishState();
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("session_start", async (event, ctx) => {
|
||||||
|
if (ctx?.hasUI !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rootSession = true;
|
||||||
|
updateSessionRef(ctx);
|
||||||
|
await reportSession(event?.reason);
|
||||||
|
// A reload can replace this extension mid-run without emitting another agent_start.
|
||||||
|
agentActive = ctx?.isIdle?.() === false;
|
||||||
|
publishState(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("agent_start", (_event, ctx) => {
|
||||||
|
if (!rootSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateSessionRef(ctx);
|
||||||
|
void reportSession();
|
||||||
|
agentActive = true;
|
||||||
|
publishState();
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("agent_settled", (_event, ctx) => {
|
||||||
|
if (!rootSession || ctx?.isIdle?.() !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
agentActive = false;
|
||||||
|
publishState();
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("session_shutdown", async (event) => {
|
||||||
|
if (!rootSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (shouldReleaseOnSessionShutdown(event)) {
|
||||||
|
await releaseAgent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
description: Review changes against repository style rules
|
|
||||||
argument-hint: "[target]"
|
|
||||||
---
|
|
||||||
|
|
||||||
Review target: `${ARGUMENTS:-the staged diff}`.
|
|
||||||
Audit the target for violations of the repository style rules.
|
|
||||||
|
|
||||||
Choose skills from the reviewed files:
|
|
||||||
|
|
||||||
- Use the `nix-style` skill for `*.nix` files.
|
|
||||||
- Use the `rust-style` skill for `*.rs` files.
|
|
||||||
- Use the `build-style` skill for `Cargo.toml` files.
|
|
||||||
- Use the `build-style` skill for Bazel `BUILD` files.
|
|
||||||
|
|
||||||
If the target includes multiple matching file types, use all matching skills.
|
|
||||||
If the target is a diff, choose skills from the changed files.
|
|
||||||
Do not use a style skill when no reviewed file matches it.
|
|
||||||
|
|
||||||
Report only actionable findings.
|
|
||||||
Order findings by severity.
|
|
||||||
Do not report preferences unless a loaded rule states them.
|
|
||||||
|
|
||||||
Use this format for each finding:
|
|
||||||
|
|
||||||
```text
|
|
||||||
`code-path:line`
|
|
||||||
> `smallest reviewed-code excerpt that shows the violation`
|
|
||||||
|
|
||||||
`skill::rule-file:line`: Finding title.
|
|
||||||
Summary: why the code breaks the rule
|
|
||||||
Fix: suggested fix
|
|
||||||
```
|
|
||||||
|
|
||||||
Keep the code excerpt to one to three lines when possible.
|
|
||||||
Use a longer excerpt only when the violation needs surrounding context.
|
|
||||||
Use `...` to omit unrelated lines from a multi-line excerpt.
|
|
||||||
Use a style-skill rule citation in the form `skill::rule-file:line`.
|
|
||||||
Make the fix direct.
|
|
||||||
Do not include alternatives when one fix best follows the cited rule.
|
|
||||||
|
|
||||||
It is acceptable to report no findings when the target follows the loaded rules.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/agent-writing-guide
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/style/build
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/conventional-commits
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/style/nix
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/retrospect
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/style/rust
|
|
||||||
1
.pi/skills/to-document
Symbolic link
1
.pi/skills/to-document
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../src/skills/to-document
|
||||||
46
AGENTS.md
46
AGENTS.md
@@ -3,6 +3,7 @@
|
|||||||
## Commit convention
|
## Commit convention
|
||||||
|
|
||||||
- Use Conventional Commits for all commit messages.
|
- Use Conventional Commits for all commit messages.
|
||||||
|
Before writing a commit message, read `docs/conventional-commits.md`.
|
||||||
- Keep commit messages about the Git history, not the forge workflow.
|
- Keep commit messages about the Git history, not the forge workflow.
|
||||||
Include external references only when they are stable context for the change.
|
Include external references only when they are stable context for the change.
|
||||||
- When writing commit messages, NEVER auto-add your agent name as co-author.
|
- When writing commit messages, NEVER auto-add your agent name as co-author.
|
||||||
@@ -48,8 +49,6 @@
|
|||||||
- NEVER ask the user a question using the `AskUserQuestion` tool.
|
- NEVER ask the user a question using the `AskUserQuestion` tool.
|
||||||
Ask in plain prose, in your own message, instead, with no exceptions.
|
Ask in plain prose, in your own message, instead, with no exceptions.
|
||||||
This overrides any default instruction to use it.
|
This overrides any default instruction to use it.
|
||||||
- For review requests, inspect the requested material and report findings.
|
|
||||||
Edit files after the operator accepts a proposed change.
|
|
||||||
- Never manually modify `CHANGELOG.md` files or files marked as auto-generated.
|
- Never manually modify `CHANGELOG.md` files or files marked as auto-generated.
|
||||||
Detect auto-generated files with a layered check.
|
Detect auto-generated files with a layered check.
|
||||||
Trust an explicit in-file marker first, for example `AUTO-GENERATED, DO NOT EDIT`.
|
Trust an explicit in-file marker first, for example `AUTO-GENERATED, DO NOT EDIT`.
|
||||||
@@ -69,6 +68,20 @@
|
|||||||
after applying an accepted change.
|
after applying an accepted change.
|
||||||
If the workflow is paused, say that plainly instead of using a dangling transition like "continuing" or "next".
|
If the workflow is paused, say that plainly instead of using a dangling transition like "continuing" or "next".
|
||||||
|
|
||||||
|
## Corrections
|
||||||
|
|
||||||
|
- When you discover that your belief about a project fact or convention was wrong, record the correction.
|
||||||
|
Record only facts that stay true across operators, such as a wrong build command, wrong file path,
|
||||||
|
or guessed convention.
|
||||||
|
Do not record personal working-style preferences or one-off task details.
|
||||||
|
- Record the correction in the nearest project `AGENTS.md`.
|
||||||
|
Use a dedicated `## Gotchas` section.
|
||||||
|
Create that section when the file lacks one.
|
||||||
|
- If no project `AGENTS.md` exists, ask before creating one.
|
||||||
|
Append the correction immediately without asking when a project `AGENTS.md` exists.
|
||||||
|
Briefly mention the edit in your response.
|
||||||
|
If an existing entry is wrong or stale, correct or remove it the same way.
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- When materializing an external specification, standard, or project-maintained convention, use the
|
- When materializing an external specification, standard, or project-maintained convention, use the
|
||||||
@@ -82,35 +95,10 @@
|
|||||||
committing a top-down rewrite.
|
committing a top-down rewrite.
|
||||||
- `rules_rust` `rust_test` targets cannot set both `crate` and `srcs`.
|
- `rules_rust` `rust_test` targets cannot set both `crate` and `srcs`.
|
||||||
Use one input style per test target.
|
Use one input style per test target.
|
||||||
- Rust tests in this monorepo live in separate `*_test.rs` files, not inline `#[cfg(test)]` modules.
|
|
||||||
- `rnix::ast::HasEntry::entries()` includes assignments and `inherit` statements.
|
|
||||||
Use `attrpath_values()` when code needs only assignment entries.
|
|
||||||
- Ariadne Rust tests are declared in Bazel `BUILD` files; do not add `[[test]]` entries to
|
|
||||||
`src/ariadne/Cargo.toml` unless explicitly requested.
|
|
||||||
- In this monorepo, "dotfiles" refers to the dotfiles/NixOS configuration under `src/dotfiles/` and `configs/nixos/`.
|
- In this monorepo, "dotfiles" refers to the dotfiles/NixOS configuration under `src/dotfiles/` and `configs/nixos/`.
|
||||||
- The `fix` app passes its current directory to Deadnix.
|
- The flake does not currently define `formatter.x86_64-linux`.
|
||||||
Run `nix run .#fix` from the repository root so path exclusions match.
|
Do not use `nix fmt` as a validation command until a formatter is added.
|
||||||
- Git pathspec searches started in a subdirectory default to that subtree.
|
|
||||||
Use `git -C <repository-root>` or top-anchored pathspecs for repository-wide history searches.
|
|
||||||
- A `**/*.nix` Git pathspec does not include Nix files directly in its starting directory.
|
|
||||||
Include both `*.nix` and `**/*.nix`, or use a recursive `*.nix` pathspec, when linting every Nix file.
|
|
||||||
- Ariadne extraction must preserve the game-root season prefix for archived source.
|
- Ariadne extraction must preserve the game-root season prefix for archived source.
|
||||||
Different game installs can contain compiled scripts with the same internal path.
|
Different game installs can contain compiled scripts with the same internal path.
|
||||||
- Clew owns Ren'Py extraction, preparation, and compiled-script AST export. Ariadne owns analysis over Clew-produced data.
|
- Clew owns Ren'Py extraction, preparation, and compiled-script AST export. Ariadne owns analysis over Clew-produced data.
|
||||||
- Bazel Python targets require `PYTHON_TOOLCHAIN_ROOT` to point at a Python installation root.
|
- Bazel Python targets require `PYTHON_TOOLCHAIN_ROOT` to point at a Python installation root.
|
||||||
- When checking whether a package exists in nixpkgs, check both this repository's pinned nixpkgs inputs
|
|
||||||
and the relevant current upstream channel. A package can exist on current `nixos-unstable` while being
|
|
||||||
absent from the pinned revision or from a different branch such as `nixpkgs-unstable`.
|
|
||||||
- OMP does not discover project skills from `.pi/skills`.
|
|
||||||
Put skills shared by Pi and OMP under `.agents/skills`, or configure OMP's `skills.customDirectories`.
|
|
||||||
- Pi is vendored under `third_party/pi` as a linear vendor import.
|
|
||||||
`pkgs.alexion.pi-coding-agent` packages that source and does not use Nixpkgs Pi.
|
|
||||||
Do not use `git subtree add` or `git subtree pull` for Pi PR branches.
|
|
||||||
Gitea rebases the synthetic upstream root commit without the `third_party/pi` prefix.
|
|
||||||
Preserve `git-subtree-dir` and `git-subtree-split` trailers on Pi vendor commits.
|
|
||||||
- The default Pi model for NixOS user configuration is set in `configs/nixos/modules/shell/pi/default.nix`, not in vendored Pi source.
|
|
||||||
- The `monorepo` and `status` libraries do not have `README.md` files.
|
|
||||||
- Check root and `src/skills/.pi/skills/` for enabled symlinks before removing a monorepo skill.
|
|
||||||
- Ariadne Rust source files live directly under `src/ariadne/`. The binary entry point is `ariadne_main.rs.
|
|
||||||
- Bazel build-style rules live under `src/skills/style/build/rules/`, not `src/skills/style/rules/`.
|
|
||||||
- Bazel passes arguments after `--` to the executed binary verbatim. A scaffold can define `//path` as a destination-path syntax.
|
|
||||||
|
|||||||
33
Cargo.lock
generated
33
Cargo.lock
generated
@@ -57,16 +57,15 @@ name = "ariadne"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"monorepo",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.7"
|
version = "4.6.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
|
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -74,9 +73,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.6.7"
|
version = "4.6.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
|
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -86,9 +85,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.6.7"
|
version = "4.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
|
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -98,9 +97,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "1.1.1"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
@@ -136,10 +135,6 @@ version = "2.8.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "monorepo"
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -207,10 +202,6 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "status"
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -219,9 +210,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "3.0.6"
|
version = "3.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -230,9 +221,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.26"
|
version = "1.0.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
|
|||||||
@@ -2,14 +2,5 @@
|
|||||||
members = [
|
members = [
|
||||||
"src/ariadne",
|
"src/ariadne",
|
||||||
"src/machoke",
|
"src/machoke",
|
||||||
"src/monorepo",
|
|
||||||
"src/status",
|
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
[workspace.dependencies]
|
|
||||||
clap = "4"
|
|
||||||
monorepo = { path = "src/monorepo" }
|
|
||||||
serde = "1.0"
|
|
||||||
serde_json = "1.0"
|
|
||||||
status = { path = "src/status" }
|
|
||||||
|
|||||||
@@ -14,5 +14,4 @@ rust = use_extension("//bazel:rust.bzl", "rust")
|
|||||||
use_repo(rust, "rust")
|
use_repo(rust, "rust")
|
||||||
|
|
||||||
register_toolchains("@python//:python")
|
register_toolchains("@python//:python")
|
||||||
|
|
||||||
register_toolchains("@rust//:rust")
|
register_toolchains("@rust//:rust")
|
||||||
|
|||||||
45
README.md
45
README.md
@@ -13,8 +13,7 @@ dev/ Monorepo root
|
|||||||
├── src/ Projects
|
├── src/ Projects
|
||||||
│ └── dotfiles/ Home and system configuration
|
│ └── dotfiles/ Home and system configuration
|
||||||
└── third_party/ External source and vendoring support
|
└── third_party/ External source and vendoring support
|
||||||
├── crates/ Generated Rust crate sources and Bazel targets
|
└── crates/ Generated Rust crate sources and Bazel targets
|
||||||
└── pi/ Vendored Pi source
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rust dependencies
|
## Rust dependencies
|
||||||
@@ -45,45 +44,3 @@ deps = ["//third_party/crates:crate_name"]
|
|||||||
Commit the generated files under `third_party/crates/`.
|
Commit the generated files under `third_party/crates/`.
|
||||||
Do not edit generated files manually.
|
Do not edit generated files manually.
|
||||||
|
|
||||||
## Vendored Pi
|
|
||||||
|
|
||||||
Pi source lives in `third_party/pi/`.
|
|
||||||
The directory is a linear vendor import from upstream Pi.
|
|
||||||
Local fork changes are carried directly in that vendored tree.
|
|
||||||
|
|
||||||
Use the upstream tag that matches the target CLI release.
|
|
||||||
The CLI version lives in `packages/coding-agent/package.json`.
|
|
||||||
|
|
||||||
Do not use `git subtree add` or `git subtree pull` for Pi PR branches.
|
|
||||||
Those commands create a synthetic upstream root commit.
|
|
||||||
Gitea rebase applies that root commit without the `third_party/pi` prefix.
|
|
||||||
That causes add/add conflicts on repository root files.
|
|
||||||
|
|
||||||
Update the vendor tree with a linear commit:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git fetch https://github.com/earendil-works/pi.git refs/tags/v<version>
|
|
||||||
split="$(git rev-parse FETCH_HEAD)"
|
|
||||||
rm -rf third_party/pi
|
|
||||||
mkdir -p third_party/pi
|
|
||||||
git archive --format=tar "$split" | tar -C third_party/pi -xf -
|
|
||||||
git add third_party/pi
|
|
||||||
git commit \
|
|
||||||
-m "chore(vendor): import pi <version>" \
|
|
||||||
-m "Imported as a linear vendor commit so forge rebases do not replay the upstream root tree." \
|
|
||||||
-m "git-subtree-dir: third_party/pi" \
|
|
||||||
-m "git-subtree-split: $split"
|
|
||||||
```
|
|
||||||
|
|
||||||
Keep the vendor import or update commit separate from local edits.
|
|
||||||
Do not remove the `git-subtree-dir` and `git-subtree-split` trailers.
|
|
||||||
They record the upstream source commit for future updates.
|
|
||||||
|
|
||||||
After the upstream update, resolve conflicts in `third_party/pi/`.
|
|
||||||
Keep local fork changes as ordinary commits after the vendor update.
|
|
||||||
Then verify the package:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
nix build .#nixosConfigurations.zeus.config.home-manager.users.alexion.programs.pi-coding-agent.package --no-link --print-out-paths
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
## Desktop
|
## Desktop
|
||||||
|
|
||||||
- When adding a graphical application, register its workspace icon in `registry.window-icon`.
|
- TODO: Verify this path after the NixOS configuration is migrated.
|
||||||
|
When adding a graphical application, add a `window-rewrite` icon mapping in `modules/desktop/session/waybar.nix`.
|
||||||
The workspace indicator uses this mapping to show an application icon instead of the generic default glyph.
|
The workspace indicator uses this mapping to show an application icon instead of the generic default glyph.
|
||||||
Match the mapping on the window class.
|
Match the mapping on the window class.
|
||||||
Store the icon as the rendered string.
|
|
||||||
Run `hyprctl clients -j | jq -r '.[].class' | sort -u` to list classes in the running session.
|
Run `hyprctl clients -j | jq -r '.[].class' | sort -u` to list classes in the running session.
|
||||||
- TODO: Verify this path after the NixOS configuration is migrated.
|
- TODO: Verify this path after the NixOS configuration is migrated.
|
||||||
Set Hyprland session environment variables in `modules/desktop/session/hyprland/default.nix` with `env = KEY,VALUE`.
|
Set Hyprland session environment variables in `modules/desktop/session/hyprland/default.nix` with `env = KEY,VALUE`.
|
||||||
@@ -40,31 +40,13 @@
|
|||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- `configs/nixos/registry/stable-uid.nix` receives library functions through `lib`.
|
|
||||||
Qualify its library calls with `lib.`.
|
|
||||||
- Guest modules are host-agnostic wiring, while host files own deployment-specific service configuration such as served volumes and host paths.
|
- Guest modules are host-agnostic wiring, while host files own deployment-specific service configuration such as served volumes and host paths.
|
||||||
For example, `guests/copyparty.nix` should expose options and translate them into guest mounts/service settings, while `hosts/pikachu/default.nix` chooses `/inbox`, `/media`, and their backing paths.
|
For example, `guests/copyparty.nix` should expose options and translate them into guest mounts/service settings, while `hosts/pikachu/default.nix` chooses `/inbox`, `/media`, and their backing paths.
|
||||||
Guest addresses and routes are host-level configuration because a guest module can be used on multiple hosts.
|
Guest addresses and routes are host-level configuration because a guest module can be used on multiple hosts.
|
||||||
- `sops.secrets.<name>.owner` must name a user known on the decrypting host.
|
- `sops.secrets.<name>.owner` must name a user known on the decrypting host.
|
||||||
Do not set the owner to a guest-only service user unless the host also declares that user.
|
Do not set the owner to a guest-only service user unless the host also declares that user.
|
||||||
Use a host-visible owner plus a shared group mode when the guest service can read the decrypted secret file through its group.
|
Use a host-visible owner plus a shared group mode when the guest service can read the decrypted secret file through its group.
|
||||||
- A read-only guest bind mount cannot contain nested writable bind mounts unless their mount-point
|
- Pikachu deploys from the monorepo checkout copied to `~/dev`.
|
||||||
directories already exist inside the read-only source.
|
Do not use the old `~/wrk/dotfiles` path for remote rebuild commands.
|
||||||
For aggregate Copyparty volumes such as `/srv` with nested `/srv/inbox`, keep the guest bind mount
|
|
||||||
writable and enforce read-only behavior through Copyparty access plus service `ReadWritePaths`.
|
|
||||||
- `deploy-to` builds from the source checkout, records the imported closure as the target system profile,
|
|
||||||
and activates it. Do not rely on a target-local monorepo checkout for remote deployment.
|
|
||||||
- On Pikachu, `nix copy --to ssh://pikachu` accepted a small `nix store add-file` probe,
|
|
||||||
but rejected locally built system paths that lacked trusted signatures.
|
|
||||||
Use a sudo `nix-store --import` path for unsigned deploy closures.
|
|
||||||
- Public DNS entries are not managed through the NixOS configuration.
|
|
||||||
Do not describe NixOS route choices as public-DNS management.
|
|
||||||
- Do not hand-author `hardware-configuration.nix` for a host that has not been generated on that machine.
|
- Do not hand-author `hardware-configuration.nix` for a host that has not been generated on that machine.
|
||||||
Generate it on the target system or live ISO, then stage it before evaluating the flake.
|
Generate it on the target system or live ISO, then stage it before evaluating the flake.
|
||||||
- Configure the Chaotic Nyx cache only on hosts that select `linuxPackages_cachyos` (currently Neogaia
|
|
||||||
and Zeus), not in the generic host module.
|
|
||||||
- Module-relative paths resolve from the module file. Update them when moving a module into a subdirectory.
|
|
||||||
- On Pikachu, `uptime` does not support `-s`. Use `who -b` to report the boot time.
|
|
||||||
- Evaluating a NixOS guest's inner `config.services.nginx` can force incomplete ACME values.
|
|
||||||
Evaluate the parent host's `config.system.build.toplevel` to validate an ACME-enabled guest.
|
|
||||||
- `pkgs.systemd` does not set `meta.mainProgram`. Use `lib.getExe' pkgs.systemd "systemctl"` instead of `lib.getExe pkgs.systemd`.
|
|
||||||
|
|||||||
43
configs/nixos/guests/actualbudget.nix
Normal file
43
configs/nixos/guests/actualbudget.nix
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.guests.actualbudget;
|
||||||
|
dataDir = "/data";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.guests.actualbudget = {
|
||||||
|
dataPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/actualbudget";
|
||||||
|
description = "Host path bind-mounted as Actual Budget's data directory.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.dataPath != null;
|
||||||
|
message = "guests.actualbudget.dataPath must point at the host directory that stores Actual Budget data.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
guests.actualbudget = {
|
||||||
|
interior = {
|
||||||
|
modules.services.actualbudget = {
|
||||||
|
enable = true;
|
||||||
|
inherit dataDir;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
mounts.${dataDir}.directory = cfg.dataPath;
|
||||||
|
limits = {
|
||||||
|
memory = lib.mkDefault "2G";
|
||||||
|
cpu = lib.mkDefault "200%";
|
||||||
|
tasksMax = lib.mkDefault 512;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,13 +5,51 @@
|
|||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.copyparty;
|
cfg = config.guests.copyparty;
|
||||||
|
|
||||||
|
volumeType = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
hostPath = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Host path bind-mounted into the Copyparty guest for this volume.";
|
||||||
|
};
|
||||||
|
|
||||||
|
path = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Guest path served by this Copyparty volume.";
|
||||||
|
};
|
||||||
|
|
||||||
|
access = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
||||||
|
default = { };
|
||||||
|
description = "Copyparty access map from permission string to account names.";
|
||||||
|
};
|
||||||
|
|
||||||
|
flags = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = "Copyparty volume flags.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.guests.copyparty = {
|
options.guests.copyparty = {
|
||||||
|
accountName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "alexion";
|
||||||
|
description = "Copyparty account name configured for password authentication.";
|
||||||
|
};
|
||||||
|
|
||||||
passwordSecret = lib.mkOption {
|
passwordSecret = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "services/copyparty/users/${config.user.name}/password";
|
default = "services/copyparty/users/alexion/password";
|
||||||
description = "SOPS secret name that contains the Copyparty account password.";
|
description = "SOPS secret name containing the Copyparty account password.";
|
||||||
|
};
|
||||||
|
|
||||||
|
stateDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/var/lib/copyparty";
|
||||||
|
description = "Guest path where Copyparty stores service state.";
|
||||||
};
|
};
|
||||||
|
|
||||||
statePath = lib.mkOption {
|
statePath = lib.mkOption {
|
||||||
@@ -28,39 +66,7 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
volumes = lib.mkOption {
|
volumes = lib.mkOption {
|
||||||
type = lib.types.attrsOf (
|
type = lib.types.attrsOf volumeType;
|
||||||
lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
hostPath = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Host path bind-mounted into the Copyparty guest for this volume.";
|
|
||||||
};
|
|
||||||
|
|
||||||
path = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Guest path served by this Copyparty volume.";
|
|
||||||
};
|
|
||||||
|
|
||||||
access = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
|
||||||
default = { };
|
|
||||||
description = "Copyparty access map from permission string to account names.";
|
|
||||||
};
|
|
||||||
|
|
||||||
readOnly = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
description = "Serve this Copyparty volume read-only.";
|
|
||||||
};
|
|
||||||
|
|
||||||
flags = lib.mkOption {
|
|
||||||
type = lib.types.attrs;
|
|
||||||
default = { };
|
|
||||||
description = "Copyparty volume flags.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
default = { };
|
default = { };
|
||||||
description = "Copyparty volumes keyed by their URL path.";
|
description = "Copyparty volumes keyed by their URL path.";
|
||||||
};
|
};
|
||||||
@@ -78,57 +84,41 @@ in
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
registry.homepage.${config.networking.hostName}.copyparty = {
|
|
||||||
tab = "Apps";
|
|
||||||
section = "Files";
|
|
||||||
name = "Copyparty";
|
|
||||||
description = "Files on ${config.networking.hostName}";
|
|
||||||
icon = "copyparty.svg";
|
|
||||||
href = lib.mkIf cfg.reverseProxy.enable "https://${cfg.reverseProxy.route.domain}/?h";
|
|
||||||
};
|
|
||||||
|
|
||||||
guests.copyparty = {
|
guests.copyparty = {
|
||||||
interior.modules.services.copyparty = {
|
interior.modules.services.copyparty = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
inherit (cfg) accountName stateDir;
|
||||||
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
||||||
reverseProxy = {
|
reverseProxy = {
|
||||||
enable = cfg.reverseProxy.enable;
|
enable = cfg.reverseProxy.enable;
|
||||||
inherit (cfg.reverseProxy) trustedSources;
|
inherit (cfg.reverseProxy) trustedSources;
|
||||||
};
|
};
|
||||||
volumes = lib.mapAttrs (
|
volumes = lib.mapAttrs (_urlPath: volume: {
|
||||||
_urlPath: volume:
|
inherit (volume) path access flags;
|
||||||
{
|
}) cfg.volumes;
|
||||||
inherit (volume)
|
|
||||||
path
|
|
||||||
readOnly
|
|
||||||
;
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs (volume.access != { }) { inherit (volume) access; }
|
|
||||||
// lib.optionalAttrs (volume.flags != { }) { inherit (volume) flags; }
|
|
||||||
) cfg.volumes;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mounts = {
|
mounts =
|
||||||
"/var/lib/copyparty" = {
|
{
|
||||||
directory = cfg.statePath;
|
${cfg.stateDir} = {
|
||||||
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
directory = cfg.statePath;
|
||||||
group = "storage";
|
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||||
mode = "0770";
|
group = "storage";
|
||||||
};
|
mode = "0770";
|
||||||
${"/run/secrets/${cfg.passwordSecret}"} = {
|
};
|
||||||
file = config.sops.secrets.${cfg.passwordSecret}.path;
|
${"/run/secrets/${cfg.passwordSecret}"} = {
|
||||||
readOnly = true;
|
file = config.sops.secrets.${cfg.passwordSecret}.path;
|
||||||
};
|
readOnly = true;
|
||||||
}
|
};
|
||||||
// lib.mapAttrs' (
|
|
||||||
_urlPath: volume:
|
|
||||||
lib.nameValuePair volume.path {
|
|
||||||
directory = volume.hostPath;
|
|
||||||
owner = "root";
|
|
||||||
group = "storage";
|
|
||||||
mode = "2770";
|
|
||||||
}
|
}
|
||||||
) cfg.volumes;
|
// lib.mapAttrs' (_urlPath: volume:
|
||||||
|
lib.nameValuePair volume.path {
|
||||||
|
directory = volume.hostPath;
|
||||||
|
owner = "root";
|
||||||
|
group = "storage";
|
||||||
|
mode = "2770";
|
||||||
|
}
|
||||||
|
) cfg.volumes;
|
||||||
limits = {
|
limits = {
|
||||||
memory = lib.mkDefault "2G";
|
memory = lib.mkDefault "2G";
|
||||||
cpu = lib.mkDefault "200%";
|
cpu = lib.mkDefault "200%";
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.guests.homepage;
|
|
||||||
environmentFile = "/run/secrets/homepage.env";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
guests.homepage = {
|
|
||||||
interior.modules.services.homepage = {
|
|
||||||
enable = true;
|
|
||||||
hostName = config.networking.hostName;
|
|
||||||
inherit environmentFile;
|
|
||||||
};
|
|
||||||
|
|
||||||
mounts.${environmentFile} = {
|
|
||||||
file = config.sops.templates."homepage.env".path;
|
|
||||||
readOnly = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
limits = {
|
|
||||||
memory = lib.mkDefault "1G";
|
|
||||||
cpu = lib.mkDefault "100%";
|
|
||||||
tasksMax = lib.mkDefault 256;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
sops.secrets = {
|
|
||||||
"services/metadata/latitude" = { };
|
|
||||||
"services/metadata/longitude" = { };
|
|
||||||
};
|
|
||||||
|
|
||||||
sops.templates."homepage.env" = {
|
|
||||||
owner = "root";
|
|
||||||
group = "storage";
|
|
||||||
mode = "0440";
|
|
||||||
content = ''
|
|
||||||
HOMEPAGE_VAR_LATITUDE=${config.sops.placeholder."services/metadata/latitude"}
|
|
||||||
HOMEPAGE_VAR_LONGITUDE=${config.sops.placeholder."services/metadata/longitude"}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
guestCfg = config.guests.navidrome;
|
|
||||||
serviceCfg = config.containers.navidrome.config.modules.services.navidrome;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.guests.navidrome = {
|
|
||||||
dataPath = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "/srv/navidrome";
|
|
||||||
description = "Host path mounted as Navidrome's data folder.";
|
|
||||||
};
|
|
||||||
|
|
||||||
libraryPath = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "/srv/music";
|
|
||||||
description = "Host path mounted as Navidrome's library folder.";
|
|
||||||
};
|
|
||||||
|
|
||||||
lastfmApiKeySecret = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "services/navidrome/lastfm/api-key";
|
|
||||||
description = "SOPS secret name containing the Navidrome Last.fm API key.";
|
|
||||||
};
|
|
||||||
|
|
||||||
lastfmSecretSecret = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "services/navidrome/lastfm/secret";
|
|
||||||
description = "SOPS secret name containing the Navidrome Last.fm secret.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf guestCfg.enable {
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = guestCfg.dataPath != null;
|
|
||||||
message = "guests.navidrome.dataPath must point at the host directory that stores Navidrome data.";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = guestCfg.libraryPath != null;
|
|
||||||
message = "guests.navidrome.libraryPath must point at the host directory that stores Navidrome's library folder.";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
registry.homepage.${config.networking.hostName}.navidrome = {
|
|
||||||
tab = "Apps";
|
|
||||||
section = "Media";
|
|
||||||
name = "Navidrome";
|
|
||||||
description = "Music";
|
|
||||||
icon = "navidrome.png";
|
|
||||||
};
|
|
||||||
|
|
||||||
guests.navidrome = {
|
|
||||||
interior.modules.services.navidrome.enable = true;
|
|
||||||
|
|
||||||
mounts = {
|
|
||||||
${serviceCfg.dataFolder} = {
|
|
||||||
directory = guestCfg.dataPath;
|
|
||||||
owner = config.registry."stable-uid".${config.networking.hostName}.navidrome;
|
|
||||||
group = "storage";
|
|
||||||
mode = "2770";
|
|
||||||
};
|
|
||||||
${serviceCfg.musicFolder} = {
|
|
||||||
directory = guestCfg.libraryPath;
|
|
||||||
owner = "root";
|
|
||||||
group = "storage";
|
|
||||||
mode = "2770";
|
|
||||||
};
|
|
||||||
${serviceCfg.environmentFile} = {
|
|
||||||
file = config.sops.templates."navidrome.env".path;
|
|
||||||
readOnly = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
limits = {
|
|
||||||
memory = lib.mkDefault "2G";
|
|
||||||
cpu = lib.mkDefault "200%";
|
|
||||||
tasksMax = lib.mkDefault 512;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
sops.secrets = {
|
|
||||||
${guestCfg.lastfmApiKeySecret}.sopsFile = ../secrets + "/${config.networking.hostName}.yaml";
|
|
||||||
${guestCfg.lastfmSecretSecret}.sopsFile = ../secrets + "/${config.networking.hostName}.yaml";
|
|
||||||
};
|
|
||||||
|
|
||||||
sops.templates."navidrome.env" = {
|
|
||||||
owner = "root";
|
|
||||||
group = "storage";
|
|
||||||
mode = "0440";
|
|
||||||
content = ''
|
|
||||||
ND_LASTFM_APIKEY=${config.sops.placeholder.${guestCfg.lastfmApiKeySecret}}
|
|
||||||
ND_LASTFM_SECRET=${config.sops.placeholder.${guestCfg.lastfmSecretSecret}}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -50,12 +50,6 @@ in
|
|||||||
description = "Public IP address advertised by the Palworld server.";
|
description = "Public IP address advertised by the Palworld server.";
|
||||||
};
|
};
|
||||||
|
|
||||||
autoStart = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
description = "Start the Palworld server after the guest reaches network readiness.";
|
|
||||||
};
|
|
||||||
|
|
||||||
passwordSecret = lib.mkOption {
|
passwordSecret = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "services/game-servers/password";
|
default = "services/game-servers/password";
|
||||||
@@ -88,47 +82,43 @@ in
|
|||||||
guests.palworld = {
|
guests.palworld = {
|
||||||
interior.modules.services.games.palworld = {
|
interior.modules.services.games.palworld = {
|
||||||
enable = true;
|
enable = true;
|
||||||
inherit (cfg)
|
inherit (cfg) serverName port publicIP;
|
||||||
serverName
|
|
||||||
port
|
|
||||||
publicIP
|
|
||||||
autoStart
|
|
||||||
;
|
|
||||||
inherit steamDir installDir stateDir;
|
inherit steamDir installDir stateDir;
|
||||||
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
||||||
adminPasswordFile = "/run/secrets/${cfg.adminPasswordSecret}";
|
adminPasswordFile = "/run/secrets/${cfg.adminPasswordSecret}";
|
||||||
};
|
};
|
||||||
|
|
||||||
mounts = {
|
mounts =
|
||||||
${steamDir} = {
|
{
|
||||||
directory = cfg.steamPath;
|
${steamDir} = {
|
||||||
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
|
directory = cfg.steamPath;
|
||||||
group = "storage";
|
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
|
||||||
mode = "0770";
|
group = "storage";
|
||||||
|
mode = "0770";
|
||||||
|
};
|
||||||
|
${installDir} = {
|
||||||
|
directory = cfg.installPath;
|
||||||
|
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
|
||||||
|
group = "storage";
|
||||||
|
mode = "0770";
|
||||||
|
};
|
||||||
|
${stateDir} = {
|
||||||
|
directory = cfg.statePath;
|
||||||
|
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
|
||||||
|
group = "storage";
|
||||||
|
mode = "0770";
|
||||||
|
};
|
||||||
|
${"/run/secrets/${cfg.passwordSecret}"} = {
|
||||||
|
file = config.sops.secrets.${cfg.passwordSecret}.path;
|
||||||
|
readOnly = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (cfg.adminPasswordSecret != cfg.passwordSecret) {
|
||||||
|
${"/run/secrets/${cfg.adminPasswordSecret}"} = {
|
||||||
|
file = config.sops.secrets.${cfg.adminPasswordSecret}.path;
|
||||||
|
readOnly = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
${installDir} = {
|
|
||||||
directory = cfg.installPath;
|
|
||||||
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
|
|
||||||
group = "storage";
|
|
||||||
mode = "0770";
|
|
||||||
};
|
|
||||||
${stateDir} = {
|
|
||||||
directory = cfg.statePath;
|
|
||||||
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
|
|
||||||
group = "storage";
|
|
||||||
mode = "0770";
|
|
||||||
};
|
|
||||||
${"/run/secrets/${cfg.passwordSecret}"} = {
|
|
||||||
file = config.sops.secrets.${cfg.passwordSecret}.path;
|
|
||||||
readOnly = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs (cfg.adminPasswordSecret != cfg.passwordSecret) {
|
|
||||||
${"/run/secrets/${cfg.adminPasswordSecret}"} = {
|
|
||||||
file = config.sops.secrets.${cfg.adminPasswordSecret}.path;
|
|
||||||
readOnly = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
limits = {
|
limits = {
|
||||||
memory = lib.mkDefault "8G";
|
memory = lib.mkDefault "8G";
|
||||||
@@ -137,19 +127,20 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
sops.secrets = {
|
sops.secrets =
|
||||||
${cfg.passwordSecret} = {
|
{
|
||||||
owner = "root";
|
${cfg.passwordSecret} = {
|
||||||
group = "storage";
|
owner = "root";
|
||||||
mode = "0440";
|
group = "storage";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (cfg.adminPasswordSecret != cfg.passwordSecret) {
|
||||||
|
${cfg.adminPasswordSecret} = {
|
||||||
|
owner = "root";
|
||||||
|
group = "storage";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
|
||||||
// lib.optionalAttrs (cfg.adminPasswordSecret != cfg.passwordSecret) {
|
|
||||||
${cfg.adminPasswordSecret} = {
|
|
||||||
owner = "root";
|
|
||||||
group = "storage";
|
|
||||||
mode = "0440";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ in
|
|||||||
enableACME = true;
|
enableACME = true;
|
||||||
locations."/".proxyPass = "http://10.23.20.32";
|
locations."/".proxyPass = "http://10.23.20.32";
|
||||||
};
|
};
|
||||||
|
"music.alexion.dev" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/".proxyPass = "http://10.23.20.32";
|
||||||
|
};
|
||||||
"photos.alexion.dev" = {
|
"photos.alexion.dev" = {
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
inputs,
|
inputs,
|
||||||
util,
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# neogaia — Dell XPS 13 9380 laptop.
|
# neogaia — Dell XPS 13 9380 laptop.
|
||||||
@@ -13,9 +13,10 @@
|
|||||||
./disk.nix
|
./disk.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
modules.host.cachyos.enable = true;
|
boot.kernelPackages = pkgs.linuxPackages_cachyos;
|
||||||
|
|
||||||
modules.network.enable = true;
|
# So wifi can be joined from the console.
|
||||||
|
networking.networkmanager.enable = true;
|
||||||
|
|
||||||
registry.address.neogaia.self = "10.23.50.146";
|
registry.address.neogaia.self = "10.23.50.146";
|
||||||
|
|
||||||
@@ -27,9 +28,15 @@
|
|||||||
peer = {
|
peer = {
|
||||||
publicKey = "sGEkSD+gEdIMXi4PBN3AhwHGc4tWMtcjOIey3s7yb3I=";
|
publicKey = "sGEkSD+gEdIMXi4PBN3AhwHGc4tWMtcjOIey3s7yb3I=";
|
||||||
endpoint = "100.0.90.192:51820";
|
endpoint = "100.0.90.192:51820";
|
||||||
|
allowedIPs = [ "10.23.0.0/16" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
registry.shell-shortcut.neogaia = {
|
||||||
|
vpn-on = "sudo systemctl start wireguard-wg-home.target";
|
||||||
|
vpn-off = ''sudo systemctl stop 'wireguard-wg-home*.service' wireguard-wg-home.target'';
|
||||||
|
};
|
||||||
|
|
||||||
modules.monorepo.enable = true;
|
modules.monorepo.enable = true;
|
||||||
modules.shell.herdr.enable = true;
|
modules.shell.herdr.enable = true;
|
||||||
modules.shell.gitea-axi.enable = true;
|
modules.shell.gitea-axi.enable = true;
|
||||||
@@ -40,23 +47,4 @@
|
|||||||
modules.desktop.apps.obsidian.enable = true;
|
modules.desktop.apps.obsidian.enable = true;
|
||||||
modules.desktop.apps.steam.enable = true;
|
modules.desktop.apps.steam.enable = true;
|
||||||
|
|
||||||
registry.waybar.neogaia.battery = {
|
|
||||||
order = 70;
|
|
||||||
settings = {
|
|
||||||
format = "{icon} {capacity}%";
|
|
||||||
format-charging = "${util.glyph "f0e7"} {capacity}%";
|
|
||||||
format-icons = [
|
|
||||||
(util.glyph "f244")
|
|
||||||
(util.glyph "f243")
|
|
||||||
(util.glyph "f242")
|
|
||||||
(util.glyph "f241")
|
|
||||||
(util.glyph "f240")
|
|
||||||
];
|
|
||||||
states = {
|
|
||||||
warning = 20;
|
|
||||||
critical = 10;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
_:
|
{ ... }:
|
||||||
# neogaia's disk layout for disko: one NVMe disk, GPT, with an EFI system
|
# neogaia's disk layout for disko: one NVMe disk, GPT, with an EFI system
|
||||||
# partition and a LUKS container holding btrfs subvolumes.
|
# partition and a LUKS container holding btrfs subvolumes.
|
||||||
# No swap partition, since swap is zram.
|
# No swap partition, since swap is zram.
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE alexion@neogaia
|
|
||||||
@@ -15,15 +15,52 @@
|
|||||||
|
|
||||||
hardware.cpu.intel.updateMicrocode = true;
|
hardware.cpu.intel.updateMicrocode = true;
|
||||||
|
|
||||||
modules.network.vlans = {
|
systemd.network = {
|
||||||
interface = "enp2s0";
|
enable = true;
|
||||||
ids = [ 20 ];
|
netdevs = {
|
||||||
|
"40-br-vlan20".netdevConfig = {
|
||||||
|
Name = "br-vlan20";
|
||||||
|
Kind = "bridge";
|
||||||
|
};
|
||||||
|
"40-vlan20" = {
|
||||||
|
netdevConfig = {
|
||||||
|
Name = "vlan20";
|
||||||
|
Kind = "vlan";
|
||||||
|
};
|
||||||
|
vlanConfig.Id = 20;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networks = {
|
||||||
|
"10-uplink" = {
|
||||||
|
matchConfig = {
|
||||||
|
Name = "enp2s0";
|
||||||
|
MACAddress = "78:55:36:07:af:49";
|
||||||
|
};
|
||||||
|
networkConfig.DHCP = "yes";
|
||||||
|
vlan = [ "vlan20" ];
|
||||||
|
linkConfig.RequiredForOnline = "routable";
|
||||||
|
};
|
||||||
|
"40-vlan20" = {
|
||||||
|
matchConfig.Name = "vlan20";
|
||||||
|
networkConfig.Bridge = "br-vlan20";
|
||||||
|
linkConfig.RequiredForOnline = "no";
|
||||||
|
};
|
||||||
|
"40-br-vlan20" = {
|
||||||
|
matchConfig.Name = "br-vlan20";
|
||||||
|
linkConfig.RequiredForOnline = "no";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
networking.useDHCP = false;
|
||||||
|
|
||||||
|
modules.network.vlans.ids = [ 20 ];
|
||||||
|
|
||||||
boot.zfs.forceImportRoot = false;
|
boot.zfs.forceImportRoot = false;
|
||||||
|
|
||||||
modules.host.zfs = {
|
modules.zfs = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
hostId = "2346edbd";
|
||||||
pools.pikachu = { };
|
pools.pikachu = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,8 +78,7 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "valheim.alexion.dev";
|
domain = "valheim.alexion.dev";
|
||||||
protocol = "udp";
|
udp.ports = [
|
||||||
ports = [
|
|
||||||
config.guests.valheim.port
|
config.guests.valheim.port
|
||||||
(config.guests.valheim.port + 1)
|
(config.guests.valheim.port + 1)
|
||||||
];
|
];
|
||||||
@@ -58,32 +94,28 @@
|
|||||||
installPath = "/pikachu/data/servers/palworld/tracion";
|
installPath = "/pikachu/data/servers/palworld/tracion";
|
||||||
statePath = "/pikachu/media/games/servers/palworld/tracion";
|
statePath = "/pikachu/media/games/servers/palworld/tracion";
|
||||||
serverName = "Tracion";
|
serverName = "Tracion";
|
||||||
autoStart = false;
|
|
||||||
adminPasswordSecret = "services/game-servers/admin/password";
|
adminPasswordSecret = "services/game-servers/admin/password";
|
||||||
publicIP = config.guests.palworld.address;
|
publicIP = config.guests.palworld.address;
|
||||||
reverseProxy = {
|
reverseProxy = {
|
||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "palworld.alexion.dev";
|
domain = "palworld.alexion.dev";
|
||||||
protocol = "udp";
|
udp.ports = [ config.guests.palworld.port ];
|
||||||
ports = [ config.guests.palworld.port ];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
guests.navidrome = {
|
guests.actualbudget = {
|
||||||
enable = true;
|
enable = true;
|
||||||
vlan = 20;
|
vlan = 20;
|
||||||
mac = "BC:24:11:C9:17:83";
|
mac = "BC:24:11:C1:CD:28";
|
||||||
dataPath = "/pikachu/data/navidrome";
|
dataPath = "/pikachu/data/actualbudget";
|
||||||
libraryPath = "/pikachu/media/music";
|
address = "10.23.20.42";
|
||||||
address = "10.23.20.43";
|
|
||||||
reverseProxy = {
|
reverseProxy = {
|
||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "music.alexion.dev";
|
domain = "budget.alexion.dev";
|
||||||
protocol = "https";
|
port = 5006;
|
||||||
port = 4533;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -95,20 +127,6 @@
|
|||||||
address = "10.23.20.109";
|
address = "10.23.20.109";
|
||||||
};
|
};
|
||||||
|
|
||||||
guests.homepage = {
|
|
||||||
enable = true;
|
|
||||||
vlan = 20;
|
|
||||||
address = "10.23.20.110";
|
|
||||||
reverseProxy = {
|
|
||||||
enable = true;
|
|
||||||
route = {
|
|
||||||
domain = "home";
|
|
||||||
protocol = "http";
|
|
||||||
port = 8082;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
guests.copyparty = {
|
guests.copyparty = {
|
||||||
enable = true;
|
enable = true;
|
||||||
vlan = 20;
|
vlan = 20;
|
||||||
@@ -119,31 +137,30 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "files.alexion.dev";
|
domain = "files.alexion.dev";
|
||||||
protocol = "https";
|
|
||||||
port = 3923;
|
port = 3923;
|
||||||
clientMaxBodySize = "1g";
|
|
||||||
};
|
};
|
||||||
trustedSources = [ "${registry.address.${config.networking.hostName}."reverse-proxy"}/32" ];
|
trustedSources = [ "${registry.address.${config.networking.hostName}."reverse-proxy"}/32" ];
|
||||||
};
|
};
|
||||||
volumes = {
|
volumes = {
|
||||||
"/" = {
|
|
||||||
hostPath = "/pikachu/data/copyparty/root";
|
|
||||||
path = "/srv";
|
|
||||||
readOnly = true;
|
|
||||||
};
|
|
||||||
"/inbox" = {
|
"/inbox" = {
|
||||||
hostPath = "/pikachu/__inbox";
|
hostPath = "/pikachu/__inbox";
|
||||||
path = "/pikachu/__inbox";
|
path = "/srv/inbox";
|
||||||
|
access.A = [ "alexion" ];
|
||||||
|
flags = [
|
||||||
|
"d2d"
|
||||||
|
"d2t"
|
||||||
|
"dthumb"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
"/media" = {
|
"/media" = {
|
||||||
hostPath = "/pikachu/media";
|
hostPath = "/pikachu/media";
|
||||||
path = "/pikachu/media";
|
path = "/srv/media";
|
||||||
};
|
access.A = [ "alexion" ];
|
||||||
"/opds" = {
|
flags = [
|
||||||
hostPath = "/pikachu/data/opds";
|
"d2d"
|
||||||
path = "/pikachu/data/opds";
|
"d2t"
|
||||||
readOnly = true;
|
"dthumb"
|
||||||
flags.opds = true;
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
_:
|
{ ... }:
|
||||||
# pikachu's install layout for disko: one NVMe boot disk with an EFI system partition and ext4 root.
|
# pikachu's install layout for disko: one NVMe boot disk with an EFI system partition and ext4 root.
|
||||||
# The existing 8 TB ZFS mirror is imported by name and is never declared here.
|
# The existing 8 TB ZFS mirror is imported by name and is never declared here.
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNqJIC6VRXyvrNf3n9su9KdPCikC3CjK/QrCK2reHdB alexion@pikachu
|
|
||||||
@@ -1,32 +1,25 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# zeus — Ryzen desktop with AMD graphics.
|
# zeus — Ryzen desktop with AMD graphics.
|
||||||
# Disk layout is in ./disk.nix.
|
# Disk layout is in ./disk.nix.
|
||||||
# `fileSystems` are derived from it, none declared here.
|
# `fileSystems` are derived from it, none declared here.
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [ ./disk.nix ] ++ lib.optional (builtins.pathExists ./hardware-configuration.nix) ./hardware-configuration.nix;
|
||||||
./disk.nix
|
|
||||||
]
|
|
||||||
++ lib.optional (builtins.pathExists ./hardware-configuration.nix) ./hardware-configuration.nix;
|
|
||||||
|
|
||||||
nixpkgs.hostPlatform = "x86_64-linux";
|
nixpkgs.hostPlatform = "x86_64-linux";
|
||||||
|
|
||||||
modules.host.cachyos.enable = true;
|
boot.kernelPackages = pkgs.linuxPackages_cachyos;
|
||||||
|
|
||||||
hardware.graphics = {
|
hardware.graphics = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enable32Bit = true;
|
enable32Bit = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
modules.network.enable = true;
|
networking.networkmanager.enable = true;
|
||||||
|
|
||||||
services.logind.settings.Login = {
|
|
||||||
HandlePowerKey = "ignore";
|
|
||||||
HandlePowerKeyLongPress = "ignore";
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.address.zeus.self = "10.23.50.221";
|
registry.address.zeus.self = "10.23.50.221";
|
||||||
|
|
||||||
@@ -39,29 +32,16 @@
|
|||||||
modules.shell.herdr.enable = true;
|
modules.shell.herdr.enable = true;
|
||||||
modules.shell.gitea-axi.enable = true;
|
modules.shell.gitea-axi.enable = true;
|
||||||
modules.shell.pi.enable = true;
|
modules.shell.pi.enable = true;
|
||||||
modules.shell.omp.enable = true;
|
|
||||||
|
|
||||||
modules.desktop.enable = true;
|
modules.desktop.enable = true;
|
||||||
modules.desktop.apps.obsidian.enable = true;
|
modules.desktop.apps.obsidian.enable = true;
|
||||||
modules.desktop.apps.r2modman.enable = true;
|
|
||||||
modules.desktop.apps.signal.enable = true;
|
modules.desktop.apps.signal.enable = true;
|
||||||
modules.desktop.apps.steam.enable = true;
|
modules.desktop.apps.steam.enable = true;
|
||||||
modules.desktop.apps.vencord.enable = true;
|
modules.desktop.apps.vencord.enable = true;
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.wayland.windowManager.hyprland.settings.monitor = [
|
home-manager.users.${config.user.name}.wayland.windowManager.hyprland.settings.monitor = [
|
||||||
{
|
"HDMI-A-1,1920x1080@120,0x0,1,transform,3"
|
||||||
output = "HDMI-A-1";
|
"DP-3,2560x1440@239.96,1080x0,1"
|
||||||
mode = "1920x1080@120";
|
|
||||||
position = "-1080x0";
|
|
||||||
scale = 1;
|
|
||||||
transform = 3;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
output = "DP-3";
|
|
||||||
mode = "2560x1440@239.96";
|
|
||||||
position = "0x0";
|
|
||||||
scale = 1;
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
_:
|
{ ... }:
|
||||||
# zeus's disk layout for disko: two NVMe disks with one EFI system partition
|
# zeus's disk layout for disko: two NVMe disks with one EFI system partition
|
||||||
# and two LUKS containers joined into one btrfs filesystem.
|
# and two LUKS containers joined into one btrfs filesystem.
|
||||||
# No swap partition, since swap is zram.
|
# No swap partition, since swap is zram.
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRBcHHuubptXCdvED/p2dR3MpjGN/AimqLasLY5984G alexion@zeus
|
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
pkgs,
|
||||||
inputs,
|
inputs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# The shared foundation for every host and guest.
|
# The shared foundation for every host and guest.
|
||||||
|
let
|
||||||
|
stateVersion = "26.05";
|
||||||
|
in
|
||||||
{
|
{
|
||||||
imports = [ inputs.home-manager.nixosModules.home-manager ];
|
imports = [ inputs.home-manager.nixosModules.home-manager ];
|
||||||
|
|
||||||
@@ -13,14 +17,14 @@
|
|||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "alexion";
|
default = "alexion";
|
||||||
description = ''
|
description = ''
|
||||||
Names the primary user account.
|
Primary interactive user for this system.
|
||||||
Host configuration uses this name for the Home Manager user.
|
This drives the system account and Home Manager user together.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
system.stateVersion = "26.05";
|
system.stateVersion = stateVersion;
|
||||||
|
|
||||||
nixpkgs.config.allowUnfree = true;
|
nixpkgs.config.allowUnfree = true;
|
||||||
nix.settings.experimental-features = [
|
nix.settings.experimental-features = [
|
||||||
@@ -38,7 +42,66 @@
|
|||||||
|
|
||||||
users.groups.storage.gid = 10000;
|
users.groups.storage.gid = 10000;
|
||||||
|
|
||||||
|
security.sudo.extraConfig = ''
|
||||||
|
Defaults env_keep += "EDITOR VISUAL"
|
||||||
|
'';
|
||||||
|
|
||||||
time.timeZone = "America/New_York";
|
time.timeZone = "America/New_York";
|
||||||
i18n.defaultLocale = "en_GB.UTF-8";
|
i18n.defaultLocale = "en_GB.UTF-8";
|
||||||
|
|
||||||
|
home-manager = {
|
||||||
|
useGlobalPkgs = true;
|
||||||
|
useUserPackages = true;
|
||||||
|
users.${config.user.name} = {
|
||||||
|
home.username = config.user.name;
|
||||||
|
home.homeDirectory = "/home/${config.user.name}";
|
||||||
|
home.stateVersion = stateVersion;
|
||||||
|
home.packages = with pkgs; [
|
||||||
|
eza
|
||||||
|
wget
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.shell-shortcut.${config.networking.hostName} = {
|
||||||
|
ls = {
|
||||||
|
command = "eza -alg --color=always --group-directories-first --icons=always";
|
||||||
|
abbreviation = false;
|
||||||
|
};
|
||||||
|
ll = {
|
||||||
|
command = "eza -lg --color=always --group-directories-first --icons=always";
|
||||||
|
abbreviation = false;
|
||||||
|
};
|
||||||
|
lt = {
|
||||||
|
command = "eza -aT -I '.git' --color=always --group-directories-first --icons=always";
|
||||||
|
abbreviation = false;
|
||||||
|
};
|
||||||
|
".." = "cd ..";
|
||||||
|
"..." = "cd ../..";
|
||||||
|
"...." = "cd ../../..";
|
||||||
|
|
||||||
|
cp = "cp -v";
|
||||||
|
tarnow = "tar -acf ";
|
||||||
|
untar = "tar -zxvf ";
|
||||||
|
wget = "wget -c ";
|
||||||
|
grep = "grep --color=auto";
|
||||||
|
fgrep = "fgrep --color=auto";
|
||||||
|
egrep = "egrep --color=auto";
|
||||||
|
jctl = "journalctl -p 3 -xb";
|
||||||
|
|
||||||
|
update = "sudo nixos-rebuild switch --flake .#${config.networking.hostName}";
|
||||||
|
cleanup = "sudo nix-collect-garbage -d";
|
||||||
|
sops-host = {
|
||||||
|
command = "sudo --preserve-env=EDITOR,VISUAL env SOPS_AGE_KEY_FILE=/var/lib/sops-nix/key.txt nix shell nixpkgs#sops nixpkgs#neovim -c sops";
|
||||||
|
abbreviation = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
modules.shell.fish.enable = lib.mkDefault true;
|
||||||
|
modules.shell.fish.defaultShell = lib.mkDefault true;
|
||||||
|
modules.shell.tmux.enable = lib.mkDefault true;
|
||||||
|
modules.shell.nvim.enable = lib.mkDefault true;
|
||||||
|
modules.shell.git.enable = lib.mkDefault true;
|
||||||
|
modules.shell.yazi.enable = lib.mkDefault true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
18
configs/nixos/modules/desktop/apps/alacritty.nix
Normal file
18
configs/nixos/modules/desktop/apps/alacritty.nix
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
# Alacritty as the desktop terminal.
|
||||||
|
let
|
||||||
|
cfg = config.modules.desktop.apps.alacritty;
|
||||||
|
user = config.user.name;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.desktop.apps.alacritty.enable = lib.mkEnableOption "Alacritty as the desktop terminal";
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
home-manager.users.${user}.home.packages = [ pkgs.alacritty ];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,12 +3,13 @@
|
|||||||
inputs,
|
inputs,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Configures Firefox as the desktop browser.
|
# Firefox as the desktop browser: stock mainline, hardened and de-monetized by policy.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.firefox;
|
cfg = config.modules.desktop.apps.firefox;
|
||||||
|
user = config.user.name;
|
||||||
|
firefoxAddons = inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.firefox = {
|
options.modules.desktop.apps.firefox = {
|
||||||
@@ -18,25 +19,19 @@ in
|
|||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "1.0";
|
default = "1.0";
|
||||||
description = ''
|
description = ''
|
||||||
Scales Firefox UI and page content.
|
Firefox UI and page scale, passed to `layout.css.devPixelsPerPx`.
|
||||||
|
Override it on hosts that need a larger browser scale, such as small
|
||||||
Sets `layout.css.devPixelsPerPx`.
|
high-DPI laptop panels.
|
||||||
Use a larger value for high-DPI displays.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.window-icon.${config.networking.hostName}.firefox = {
|
home-manager.users.${user} = hm: {
|
||||||
class = ".*[Ff]irefox.*";
|
|
||||||
icon = util.glyph "f269";
|
|
||||||
};
|
|
||||||
|
|
||||||
home-manager.users.${config.user.name} = hm: {
|
|
||||||
programs.firefox = {
|
programs.firefox = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
# Applies policies that users cannot change in Firefox.
|
# Enterprise policies: enforced and unchangeable from the browser UI.
|
||||||
policies = {
|
policies = {
|
||||||
DisableTelemetry = true;
|
DisableTelemetry = true;
|
||||||
DisableFirefoxStudies = true;
|
DisableFirefoxStudies = true;
|
||||||
@@ -45,7 +40,7 @@ in
|
|||||||
DontCheckDefaultBrowser = true;
|
DontCheckDefaultBrowser = true;
|
||||||
DisableFirefoxAccounts = true;
|
DisableFirefoxAccounts = true;
|
||||||
|
|
||||||
# Removes sponsored content from the new-tab page.
|
# Strip the monetized surfaces from the new-tab page.
|
||||||
FirefoxHome = {
|
FirefoxHome = {
|
||||||
SponsoredTopSites = false;
|
SponsoredTopSites = false;
|
||||||
SponsoredPocket = false;
|
SponsoredPocket = false;
|
||||||
@@ -54,19 +49,23 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
profiles.default = {
|
profiles.default = {
|
||||||
|
isDefault = true;
|
||||||
|
|
||||||
extensions = {
|
extensions = {
|
||||||
packages = with inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system}; [
|
packages = with firefoxAddons; [
|
||||||
ublock-origin
|
ublock-origin
|
||||||
proton-pass
|
proton-pass
|
||||||
sponsorblock
|
sponsorblock
|
||||||
];
|
];
|
||||||
|
|
||||||
# Home Manager overwrites Nord theme changes made in Firefox.
|
# The Nord chrome theme is a declared extension setting, so home-manager
|
||||||
|
# owns the extension-settings store, overwriting runtime changes to it.
|
||||||
force = true;
|
force = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Stylix uses a near-white selection background.
|
# Stylix's Nord mapping paints the selected address-bar result a
|
||||||
# Use the Nord selection color because its text stays readable.
|
# near-white grey, leaving its light text unreadable. Darken that one
|
||||||
|
# highlight to the Nord selection grey, from the same scheme.
|
||||||
extensions.settings."FirefoxColor@mozilla.com".settings.theme.colors.popup_highlight =
|
extensions.settings."FirefoxColor@mozilla.com".settings.theme.colors.popup_highlight =
|
||||||
let
|
let
|
||||||
c = hm.config.lib.stylix.colors;
|
c = hm.config.lib.stylix.colors;
|
||||||
@@ -78,26 +77,32 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
|
# Scale the UI and page by a fixed factor.
|
||||||
|
# Left at auto (-1), Firefox reads the panel's 1.5x and inflates its
|
||||||
|
# whole chrome while point-sized apps stay put.
|
||||||
|
# A shade under that brings it into line without dropping to true
|
||||||
|
# 1:1, which reads too small at this DPI.
|
||||||
"layout.css.devPixelsPerPx" = cfg.uiScale;
|
"layout.css.devPixelsPerPx" = cfg.uiScale;
|
||||||
|
|
||||||
# Enables Firefox Color after Firefox sideloads the add-on.
|
# Auto-enable the sideloaded Firefox Color add-on carrying the Nord
|
||||||
|
# chrome theme, which Firefox otherwise leaves disabled.
|
||||||
"extensions.autoDisableScopes" = 0;
|
"extensions.autoDisableScopes" = 0;
|
||||||
|
|
||||||
# Removes sponsored content outside Firefox policies.
|
# Sponsored surfaces the policies above do not reach.
|
||||||
"browser.urlbar.suggest.quicksuggest.sponsored" = false;
|
"browser.urlbar.suggest.quicksuggest.sponsored" = false;
|
||||||
"browser.newtabpage.activity-stream.showSponsored" = false;
|
"browser.newtabpage.activity-stream.showSponsored" = false;
|
||||||
"browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
|
"browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
search = {
|
search = {
|
||||||
# Home Manager replaces the engine list when this setting is enabled.
|
# Declaring search overwrites Firefox's own engine list wholesale,
|
||||||
# Engines added in Firefox do not survive a rebuild.
|
# so engines added later in the UI do not survive a rebuild.
|
||||||
force = true;
|
force = true;
|
||||||
default = "ddg";
|
default = "ddg";
|
||||||
|
|
||||||
# Hides commercial search engines.
|
# The general-purpose commercial engines, hidden to leave a lean
|
||||||
# Bangs still reach these engines.
|
# DuckDuckGo-and-Wikipedia list. They stay reachable through bangs.
|
||||||
# Entries with only `metaData` configure Firefox built-in engines.
|
# An engine carrying only metaData is treated as a builtin.
|
||||||
engines = {
|
engines = {
|
||||||
google.metaData.hidden = true;
|
google.metaData.hidden = true;
|
||||||
bing.metaData.hidden = true;
|
bing.metaData.hidden = true;
|
||||||
@@ -109,7 +114,8 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Applies the Nord theme through Stylix's Firefox Color add-on.
|
# Nord chrome for the one profile, applied through the Stylix-managed
|
||||||
|
# Firefox Color add-on that colorTheme enables.
|
||||||
stylix.targets.firefox = {
|
stylix.targets.firefox = {
|
||||||
enable = true;
|
enable = true;
|
||||||
profileNames = [ "default" ];
|
profileNames = [ "default" ];
|
||||||
@@ -127,8 +133,8 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Firefox creates `profiles.ini` on its first launch.
|
# Firefox writes profiles.ini itself on first launch, so home-manager is
|
||||||
# Force replacement during Home Manager activation.
|
# told to own the file rather than fail activation refusing to clobber it.
|
||||||
home.file."${hm.config.programs.firefox.configPath}/profiles.ini".force = true;
|
home.file."${hm.config.programs.firefox.configPath}/profiles.ini".force = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,30 +2,17 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Ghostty as the desktop terminal.
|
# Ghostty as the desktop terminal.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.desktop.apps.ghostty;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.ghostty.enable = lib.mkEnableOption "Ghostty as the desktop terminal";
|
options.modules.desktop.apps.ghostty.enable = lib.mkEnableOption "Ghostty as the desktop terminal";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.ghostty.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.keybind.${config.networking.hostName}.ghostty = {
|
|
||||||
keys = "SUPER + Return";
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = lib.getExe pkgs.ghostty;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.window-icon.${config.networking.hostName}.ghostty = {
|
|
||||||
class = "com.mitchellh.ghostty";
|
|
||||||
icon = util.glyph "f120";
|
|
||||||
};
|
|
||||||
|
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.ghostty ];
|
home-manager.users.${user}.home.packages = [ pkgs.ghostty ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,17 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Obsidian as the desktop note-taking vault.
|
# Obsidian as the desktop note-taking vault.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.desktop.apps.obsidian;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.obsidian.enable =
|
options.modules.desktop.apps.obsidian.enable = lib.mkEnableOption "Obsidian as the desktop note-taking vault";
|
||||||
lib.mkEnableOption "Obsidian as the desktop note-taking vault";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.obsidian.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.obsidian ];
|
home-manager.users.${user}.home.packages = [ pkgs.obsidian ];
|
||||||
|
|
||||||
registry.window-icon.${config.networking.hostName}.obsidian = {
|
|
||||||
class = "obsidian";
|
|
||||||
icon = util.glyph "f02d";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# r2modman, a Thunderstore mod manager.
|
|
||||||
let
|
|
||||||
user = config.user.name;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.modules.desktop.apps.r2modman.enable =
|
|
||||||
lib.mkEnableOption "r2modman, a Thunderstore mod manager";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.r2modman.enable {
|
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.r2modman ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -2,22 +2,17 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Signal desktop messenger.
|
# Signal desktop messenger.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.desktop.apps.signal;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.signal.enable = lib.mkEnableOption "Signal desktop messenger";
|
options.modules.desktop.apps.signal.enable = lib.mkEnableOption "Signal desktop messenger";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.signal.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.signal-desktop ];
|
home-manager.users.${user}.home.packages = [ pkgs.signal-desktop ];
|
||||||
|
|
||||||
registry.window-icon.${config.networking.hostName}.signal = {
|
|
||||||
class = "[Ss]ignal";
|
|
||||||
icon = util.glyph "f4ad";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Steam game launcher and runtime integration.
|
# Steam game launcher and runtime integration.
|
||||||
|
let
|
||||||
|
cfg = config.modules.desktop.apps.steam;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.steam.enable = lib.mkEnableOption "Steam game launcher";
|
options.modules.desktop.apps.steam.enable = lib.mkEnableOption "Steam game launcher";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.steam.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
programs.gamemode.enable = true;
|
|
||||||
|
|
||||||
programs.steam = {
|
programs.steam = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = pkgs.steam.override {
|
package = pkgs.steam.override {
|
||||||
@@ -20,14 +20,5 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
hardware.steam-hardware.enable = true;
|
hardware.steam-hardware.enable = true;
|
||||||
|
|
||||||
environment.systemPackages = [
|
|
||||||
pkgs.gamescope
|
|
||||||
];
|
|
||||||
|
|
||||||
registry.window-icon.${config.networking.hostName}.steam = {
|
|
||||||
class = "[Ss]team";
|
|
||||||
icon = util.glyph "f1b6";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,17 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Vesktop, a Discord client with Vencord built in.
|
# Vesktop, a Discord client with Vencord built in.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.desktop.apps.vencord;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.vencord.enable =
|
options.modules.desktop.apps.vencord.enable = lib.mkEnableOption "Vesktop, a Discord client with Vencord built in";
|
||||||
lib.mkEnableOption "Vesktop, a Discord client with Vencord built in";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.vencord.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.vesktop ];
|
home-manager.users.${user}.home.packages = [ pkgs.vesktop ];
|
||||||
|
|
||||||
registry.window-icon.${config.networking.hostName}.vesktop = {
|
|
||||||
class = "[Vv]esktop";
|
|
||||||
icon = util.glyph "f1ff";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
# The desktop aggregator: one flag brings up the whole graphical session.
|
# The desktop aggregator: one flag brings up the whole graphical session.
|
||||||
|
let
|
||||||
|
cfg = config.modules.desktop;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.enable = lib.mkEnableOption "the keyboard-driven Hyprland desktop";
|
options.modules.desktop.enable = lib.mkEnableOption "the keyboard-driven Hyprland desktop";
|
||||||
|
|
||||||
# Each piece is turned on at default priority, so a host can still override
|
# Each piece is turned on at default priority, so a host can still override
|
||||||
# any one of them while the single flag above enables the whole desktop.
|
# any one of them while the single flag above enables the whole desktop.
|
||||||
config = lib.mkIf config.modules.desktop.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
modules.desktop.apps.firefox.enable = lib.mkDefault true;
|
modules.desktop.apps.firefox.enable = lib.mkDefault true;
|
||||||
modules.desktop.apps.ghostty.enable = lib.mkDefault true;
|
modules.desktop.apps.ghostty.enable = lib.mkDefault true;
|
||||||
modules.desktop.session.hyprland.enable = lib.mkDefault true;
|
modules.desktop.session.hyprland.enable = lib.mkDefault true;
|
||||||
@@ -23,6 +26,5 @@
|
|||||||
modules.desktop.utilities.screenshot.enable = lib.mkDefault true;
|
modules.desktop.utilities.screenshot.enable = lib.mkDefault true;
|
||||||
modules.desktop.utilities.theming.enable = lib.mkDefault true;
|
modules.desktop.utilities.theming.enable = lib.mkDefault true;
|
||||||
modules.desktop.utilities.userdirs.enable = lib.mkDefault true;
|
modules.desktop.utilities.userdirs.enable = lib.mkDefault true;
|
||||||
registry.env.${config.networking.hostName}.NIXOS_OZONE_WL = "1";
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,41 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
util,
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# The Hyprland compositor.
|
# The Hyprland compositor.
|
||||||
|
let
|
||||||
|
cfg = config.modules.desktop.session.hyprland;
|
||||||
|
user = config.user.name;
|
||||||
|
cursor = config.stylix.cursor;
|
||||||
|
|
||||||
|
# Numbered-workspace switch and move for 1..9, the operator's i3 muscle memory.
|
||||||
|
workspaceBinds = lib.concatMap (n: [
|
||||||
|
"$mod, ${toString n}, workspace, ${toString n}"
|
||||||
|
"$mod SHIFT, ${toString n}, movetoworkspace, ${toString n}"
|
||||||
|
]) (lib.range 1 9);
|
||||||
|
|
||||||
|
# Flip the tiling strategy between the two built-in layouts, since neither a
|
||||||
|
# dispatcher nor a keyword toggles it on its own.
|
||||||
|
toggleLayout = pkgs.writeShellScript "hypr-toggle-layout" ''
|
||||||
|
if [ "$(hyprctl getoption -j general:layout | ${pkgs.jq}/bin/jq -r .str)" = dwindle ]; then
|
||||||
|
hyprctl keyword general:layout master
|
||||||
|
else
|
||||||
|
hyprctl keyword general:layout dwindle
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.hyprland.enable = lib.mkEnableOption "the Hyprland compositor";
|
options.modules.desktop.session.hyprland = {
|
||||||
|
enable = lib.mkEnableOption "the Hyprland compositor";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.hyprland.enable {
|
blur = lib.mkEnableOption ''
|
||||||
|
window blur. Off by default as the single biggest battery cost on a
|
||||||
|
laptop, left on for a host with the headroom to spend it'';
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
# This program integration owns the session, portals, and polkit, launched
|
# This program integration owns the session, portals, and polkit, launched
|
||||||
# through the universal Wayland session manager.
|
# through the universal Wayland session manager.
|
||||||
programs.hyprland = {
|
programs.hyprland = {
|
||||||
@@ -17,7 +43,7 @@
|
|||||||
withUWSM = true;
|
withUWSM = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.wayland.windowManager.hyprland = {
|
home-manager.users.${user}.wayland.windowManager.hyprland = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
# One package drives the whole session, so there is never a version split.
|
# One package drives the whole session, so there is never a version split.
|
||||||
@@ -29,216 +55,136 @@
|
|||||||
# uwsm owns the systemd graphical-session targets.
|
# uwsm owns the systemd graphical-session targets.
|
||||||
systemd.enable = false;
|
systemd.enable = false;
|
||||||
|
|
||||||
# Write the native Lua Hyprland config. Home Manager renders the Nix
|
# Write the native hyprlang hyprland.conf, whose variable and bind syntax
|
||||||
# settings below as hl.* calls.
|
# the settings below are expressed in.
|
||||||
configType = "lua";
|
configType = "hyprlang";
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
config = {
|
"$mod" = "SUPER";
|
||||||
input = {
|
"$terminal" = "ghostty";
|
||||||
kb_layout = config.services.xserver.xkb.layout;
|
|
||||||
repeat_delay = 250;
|
|
||||||
repeat_rate = 45;
|
|
||||||
accel_profile = "flat";
|
|
||||||
touchpad = {
|
|
||||||
natural_scroll = true;
|
|
||||||
tap_to_click = true;
|
|
||||||
disable_while_typing = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
general = {
|
# Session variables handed to the compositor directly.
|
||||||
gaps_in = 4;
|
# UWSM launches the session without the shell profile that would carry
|
||||||
gaps_out = 8;
|
# them, so a variable the compositor or its children must see is set
|
||||||
border_size = 2;
|
# here rather than through home-manager's sessionVariables.
|
||||||
layout = "dwindle";
|
env =
|
||||||
};
|
|
||||||
|
|
||||||
decoration = {
|
|
||||||
rounding = 6;
|
|
||||||
blur.enabled = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
xwayland.force_zero_scaling = true;
|
|
||||||
|
|
||||||
dwindle.preserve_split = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
env = lib.mapAttrsToList (name: value: {
|
|
||||||
_args = [
|
|
||||||
name
|
|
||||||
value
|
|
||||||
];
|
|
||||||
}) (lib.attrByPath [ config.networking.hostName ] { } config.registry.env);
|
|
||||||
|
|
||||||
bind = lib.mapAttrsToList (_name: keybind: {
|
|
||||||
_args = [
|
|
||||||
keybind.keys
|
|
||||||
(lib.generators.mkLuaInline (
|
|
||||||
{
|
|
||||||
exec = "hl.dsp.exec_cmd(${lib.generators.toLua { } keybind.action.command})";
|
|
||||||
focus = "hl.dsp.focus(${
|
|
||||||
lib.generators.toLua { } { inherit (keybind.action) direction workspace; }
|
|
||||||
})";
|
|
||||||
"move-window" = "hl.dsp.window.move(${
|
|
||||||
lib.generators.toLua { } { inherit (keybind.action) direction workspace; }
|
|
||||||
})";
|
|
||||||
"resize-window" = "hl.dsp.window.resize(${
|
|
||||||
lib.generators.toLua { } (
|
|
||||||
{
|
|
||||||
down = {
|
|
||||||
x = 0;
|
|
||||||
y = 10;
|
|
||||||
relative = true;
|
|
||||||
};
|
|
||||||
left = {
|
|
||||||
x = -10;
|
|
||||||
y = 0;
|
|
||||||
relative = true;
|
|
||||||
};
|
|
||||||
right = {
|
|
||||||
x = 10;
|
|
||||||
y = 0;
|
|
||||||
relative = true;
|
|
||||||
};
|
|
||||||
up = {
|
|
||||||
x = 0;
|
|
||||||
y = -10;
|
|
||||||
relative = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
.${keybind.action.direction}
|
|
||||||
)
|
|
||||||
})";
|
|
||||||
"toggle-floating" = ''hl.dsp.window.float({ action = "toggle" })'';
|
|
||||||
"toggle-fullscreen" = "hl.dsp.window.fullscreen()";
|
|
||||||
"close-window" = "hl.dsp.window.close()";
|
|
||||||
}
|
|
||||||
.${keybind.action.kind}
|
|
||||||
))
|
|
||||||
]
|
|
||||||
++ lib.optional keybind.repeating {
|
|
||||||
repeating = true;
|
|
||||||
}
|
|
||||||
++ lib.optional keybind.locked {
|
|
||||||
locked = true;
|
|
||||||
};
|
|
||||||
}) (lib.attrByPath [ config.networking.hostName ] { } config.registry.keybind);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
registry.keybind.${config.networking.hostName} = lib.mkMerge [
|
|
||||||
# Generate numeric workspace keybinds.
|
|
||||||
(lib.listToAttrs (
|
|
||||||
lib.concatMap (workspace: [
|
|
||||||
{
|
|
||||||
name = "hyprland-workspace-${toString workspace}";
|
|
||||||
value = {
|
|
||||||
keys = "SUPER + ${toString workspace}";
|
|
||||||
action = {
|
|
||||||
kind = "focus";
|
|
||||||
inherit workspace;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "hyprland-move-to-workspace-${toString workspace}";
|
|
||||||
value = {
|
|
||||||
keys = "SUPER + SHIFT + ${toString workspace}";
|
|
||||||
action = {
|
|
||||||
kind = "move-window";
|
|
||||||
inherit workspace;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
]) (lib.range 1 9)
|
|
||||||
))
|
|
||||||
|
|
||||||
# Generate directional keybinds.
|
|
||||||
(lib.listToAttrs (
|
|
||||||
lib.concatMap
|
|
||||||
(
|
|
||||||
{ direction, key }:
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name = "hyprland-focus-${direction}";
|
|
||||||
value = {
|
|
||||||
keys = "SUPER + ${key}";
|
|
||||||
action = {
|
|
||||||
kind = "focus";
|
|
||||||
inherit direction;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "hyprland-move-${direction}";
|
|
||||||
value = {
|
|
||||||
keys = "SUPER + SHIFT + ${key}";
|
|
||||||
action = {
|
|
||||||
kind = "move-window";
|
|
||||||
inherit direction;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "hyprland-resize-${direction}";
|
|
||||||
value = {
|
|
||||||
keys = "SUPER + ALT + ${key}";
|
|
||||||
repeating = true;
|
|
||||||
action = {
|
|
||||||
kind = "resize-window";
|
|
||||||
inherit direction;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
[
|
[
|
||||||
{
|
# Chromium and Electron apps read this to select native Wayland;
|
||||||
key = "H";
|
# nixpkgs wrappers (Obsidian's included) gate their Wayland flags on
|
||||||
direction = "left";
|
# it, so without it they run under XWayland and blur at this DPI.
|
||||||
}
|
"NIXOS_OZONE_WL,1"
|
||||||
{
|
|
||||||
key = "J";
|
|
||||||
direction = "down";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
key = "K";
|
|
||||||
direction = "up";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
key = "L";
|
|
||||||
direction = "right";
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
))
|
# Bibata ships XCursor only.
|
||||||
{
|
# The hyprcursor variables name the same theme, which Hyprland
|
||||||
hyprland-toggle-floating = {
|
# resolves through its XCursor fallback.
|
||||||
keys = "SUPER + Space";
|
++ lib.optionals (cursor != null) [
|
||||||
action.kind = "toggle-floating";
|
"XCURSOR_THEME,${cursor.name}"
|
||||||
|
"XCURSOR_SIZE,${toString cursor.size}"
|
||||||
|
"HYPRCURSOR_THEME,${cursor.name}"
|
||||||
|
"HYPRCURSOR_SIZE,${toString cursor.size}"
|
||||||
|
];
|
||||||
|
|
||||||
|
input = {
|
||||||
|
kb_layout = "us";
|
||||||
|
# Caps is a second Escape.
|
||||||
|
# Shift+Caps still toggles a real CapsLock.
|
||||||
|
kb_options = "caps:escape_shifted_capslock";
|
||||||
|
# Snappy key repeat.
|
||||||
|
repeat_delay = 250;
|
||||||
|
repeat_rate = 45;
|
||||||
|
accel_profile = "flat";
|
||||||
|
touchpad = {
|
||||||
|
natural_scroll = true;
|
||||||
|
tap-to-click = true;
|
||||||
|
disable_while_typing = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
hyprland-toggle-fullscreen = {
|
general = {
|
||||||
keys = "SUPER + F";
|
gaps_in = 4;
|
||||||
action.kind = "toggle-fullscreen";
|
gaps_out = 8;
|
||||||
|
border_size = 2;
|
||||||
|
layout = "dwindle";
|
||||||
};
|
};
|
||||||
|
|
||||||
hyprland-close-window = {
|
decoration = {
|
||||||
keys = "SUPER + SHIFT + Q";
|
rounding = 6;
|
||||||
action.kind = "close-window";
|
blur.enabled = cfg.blur;
|
||||||
};
|
};
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."hyprland/workspaces" = {
|
# XWayland clients render at the panel's native resolution instead of
|
||||||
position = "left";
|
# being raster-scaled by the compositor at the fractional monitor scale.
|
||||||
order = 10;
|
xwayland.force_zero_scaling = true;
|
||||||
settings = {
|
|
||||||
format = "{id} {windows}";
|
animations = {
|
||||||
format-window-separator = " ";
|
enabled = true;
|
||||||
window-rewrite-default = util.glyph "f2d0";
|
bezier = [ "ease, 0.25, 0.1, 0.25, 1.0" ];
|
||||||
window-rewrite = lib.mapAttrs' (
|
# Durations are in centiseconds.
|
||||||
_name: windowIcon: lib.nameValuePair "class<${windowIcon.class}>" windowIcon.icon
|
# Short values keep the motion subtle.
|
||||||
) (lib.attrByPath [ config.networking.hostName ] { } config.registry.window-icon);
|
animation = [
|
||||||
|
"windows, 1, 3, ease"
|
||||||
|
"fade, 1, 3, ease"
|
||||||
|
# Layer surfaces like the launcher fade in a touch quicker than windows.
|
||||||
|
"layersIn, 1, 2, ease"
|
||||||
|
"fadeLayersIn, 1, 2, ease"
|
||||||
|
"workspaces, 1, 3, ease"
|
||||||
|
"border, 1, 3, ease"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
dwindle = {
|
||||||
|
preserve_split = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
bind = [
|
||||||
|
"$mod, Return, exec, $terminal"
|
||||||
|
|
||||||
|
# Move focus.
|
||||||
|
"$mod, H, movefocus, l"
|
||||||
|
"$mod, J, movefocus, d"
|
||||||
|
"$mod, K, movefocus, u"
|
||||||
|
"$mod, L, movefocus, r"
|
||||||
|
|
||||||
|
# Move the window within the layout.
|
||||||
|
"$mod SHIFT, H, movewindow, l"
|
||||||
|
"$mod SHIFT, J, movewindow, d"
|
||||||
|
"$mod SHIFT, K, movewindow, u"
|
||||||
|
"$mod SHIFT, L, movewindow, r"
|
||||||
|
|
||||||
|
# Resize the active window.
|
||||||
|
"$mod ALT, H, resizeactive, -40 0"
|
||||||
|
"$mod ALT, J, resizeactive, 0 40"
|
||||||
|
"$mod ALT, K, resizeactive, 0 -40"
|
||||||
|
"$mod ALT, L, resizeactive, 40 0"
|
||||||
|
|
||||||
|
"$mod, Space, togglefloating,"
|
||||||
|
"$mod, F, fullscreen,"
|
||||||
|
# togglesplit is a dwindle layout message, reached through layoutmsg.
|
||||||
|
"$mod, T, layoutmsg, togglesplit"
|
||||||
|
"$mod SHIFT, T, exec, ${toggleLayout}"
|
||||||
|
"$mod SHIFT, Q, killactive,"
|
||||||
|
"$mod CTRL, Q, forcekillactive,"
|
||||||
|
]
|
||||||
|
++ workspaceBinds;
|
||||||
|
|
||||||
|
# Volume and brightness keys repeat while held, each raising a popup
|
||||||
|
# through the OSD client.
|
||||||
|
# Volume is capped at 100 percent.
|
||||||
|
# The client floors brightness so a full hold cannot black the screen out.
|
||||||
|
binde = [
|
||||||
|
", XF86AudioRaiseVolume, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume raise --max-volume 100"
|
||||||
|
", XF86AudioLowerVolume, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume lower"
|
||||||
|
", XF86MonBrightnessUp, exec, ${pkgs.swayosd}/bin/swayosd-client --brightness raise"
|
||||||
|
", XF86MonBrightnessDown, exec, ${pkgs.swayosd}/bin/swayosd-client --brightness lower"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Mute and media transport still fire while the session is locked.
|
||||||
|
bindl = [
|
||||||
|
", XF86AudioMute, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume mute-toggle"
|
||||||
|
", XF86AudioPlay, exec, ${pkgs.playerctl}/bin/playerctl play-pause"
|
||||||
|
", XF86AudioNext, exec, ${pkgs.playerctl}/bin/playerctl next"
|
||||||
|
", XF86AudioPrev, exec, ${pkgs.playerctl}/bin/playerctl previous"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,34 +5,62 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# hypridle: idle-triggered locking and display power management.
|
# hypridle: idle-triggered locking and display power management.
|
||||||
{
|
let
|
||||||
options.modules.desktop.session.hyprland.hypridle.enable =
|
cfg = config.modules.desktop.session.hyprland.hypridle;
|
||||||
lib.mkEnableOption "hypridle idle management";
|
user = config.user.name;
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.hyprland.hypridle.enable {
|
hyprctl = "${config.programs.hyprland.package}/bin/hyprctl";
|
||||||
|
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
||||||
|
|
||||||
|
# The guard drops the call when a locker is already up, so no idle trigger
|
||||||
|
# stacks a second hyprlock over the first.
|
||||||
|
lockCmd = "${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.desktop.session.hyprland.hypridle = {
|
||||||
|
enable = lib.mkEnableOption "hypridle idle management";
|
||||||
|
|
||||||
|
lockTimeout = lib.mkOption {
|
||||||
|
type = lib.types.ints.positive;
|
||||||
|
default = 300;
|
||||||
|
description = "Seconds of inactivity before the screen locks.";
|
||||||
|
};
|
||||||
|
|
||||||
|
screenOffTimeout = lib.mkOption {
|
||||||
|
type = lib.types.ints.positive;
|
||||||
|
default = 360;
|
||||||
|
description = "Seconds of inactivity before the displays are powered off.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
# Closing the lid suspends, and every suspend locks first through the
|
# Closing the lid suspends, and every suspend locks first through the
|
||||||
# before_sleep_cmd below, so the lid always lands at a locked screen.
|
# before_sleep_cmd below, so the lid always lands at a locked screen.
|
||||||
services.logind.settings.Login.HandleLidSwitch = "suspend";
|
services.logind.settings.Login.HandleLidSwitch = "suspend";
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.services.hypridle = {
|
home-manager.users.${user}.services.hypridle = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
general = {
|
general = {
|
||||||
# The guard drops the call when a locker is already up, so no idle
|
lock_cmd = lockCmd;
|
||||||
# trigger stacks a second hyprlock over the first.
|
|
||||||
lock_cmd = "${lib.getExe' pkgs.procps "pidof"} hyprlock || ${lib.getExe pkgs.hyprlock}";
|
|
||||||
before_sleep_cmd = "loginctl lock-session";
|
before_sleep_cmd = "loginctl lock-session";
|
||||||
# Waking pokes the compositor in case firmware leaves displays dark.
|
# Waking restores the displays the screen-off listener may have cut.
|
||||||
after_sleep_cmd = "${lib.getExe' config.programs.hyprland.package "hyprctl"} dispatch 'hl.dsp.dpms({ action = \"enable\" })'";
|
after_sleep_cmd = "${hyprctl} dispatch dpms on";
|
||||||
};
|
};
|
||||||
|
|
||||||
listener = [
|
listener = [
|
||||||
# Lock on idle.
|
# Lock on idle.
|
||||||
{
|
{
|
||||||
timeout = 300;
|
timeout = cfg.lockTimeout;
|
||||||
on-timeout = "loginctl lock-session";
|
on-timeout = "loginctl lock-session";
|
||||||
}
|
}
|
||||||
|
# Power the displays off a little later, restoring them on any activity.
|
||||||
|
{
|
||||||
|
timeout = cfg.screenOffTimeout;
|
||||||
|
on-timeout = "${hyprctl} dispatch dpms off";
|
||||||
|
on-resume = "${hyprctl} dispatch dpms on";
|
||||||
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,63 +2,53 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# The lock screen: a hyprlock session-lock surface the compositor owns.
|
# The lock screen: a hyprlock session-lock surface the compositor owns.
|
||||||
let
|
let
|
||||||
sleepIcon = util.glyph [
|
cfg = config.modules.desktop.session.hyprland.hyprlock;
|
||||||
"db81"
|
user = config.user.name;
|
||||||
"dcb2"
|
|
||||||
];
|
# The hyprlock this module installs, used by the lock keybind below.
|
||||||
sleepOffIcon = util.glyph [
|
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
||||||
"db81"
|
|
||||||
"dcb3"
|
sleepIcon = builtins.fromJSON ''"\udb81\udcb2"'';
|
||||||
];
|
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
||||||
|
|
||||||
idleLockToggle = pkgs.writeShellScript "lockscreen-idle-inhibit-toggle" ''
|
idleLockToggle = pkgs.writeShellScript "lockscreen-idle-inhibit-toggle" ''
|
||||||
unit=lockscreen-idle-inhibit.service
|
unit=lockscreen-idle-inhibit.service
|
||||||
|
|
||||||
refresh_waybar() {
|
refresh_waybar() {
|
||||||
${lib.getExe' pkgs.procps "pkill"} -RTMIN+9 waybar >/dev/null 2>&1 || true
|
${pkgs.procps}/bin/pkill -RTMIN+9 waybar >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet "$unit"; then
|
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet "$unit"; then
|
||||||
${lib.getExe' pkgs.systemd "systemctl"} --user stop "$unit"
|
${pkgs.systemd}/bin/systemctl --user stop "$unit"
|
||||||
refresh_waybar
|
refresh_waybar
|
||||||
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepIcon}"
|
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepIcon}"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ${lib.getExe' pkgs.systemd "systemd-run"} \
|
if ${pkgs.systemd}/bin/systemd-run \
|
||||||
--user \
|
--user \
|
||||||
--collect \
|
--collect \
|
||||||
--unit=lockscreen-idle-inhibit \
|
--unit=lockscreen-idle-inhibit \
|
||||||
--description="Disable idle-triggered screen locking" \
|
--description="Disable idle-triggered screen locking" \
|
||||||
${lib.getExe' pkgs.systemd "systemd-inhibit"} \
|
${pkgs.systemd}/bin/systemd-inhibit \
|
||||||
--what=idle \
|
--what=idle \
|
||||||
--who=lockscreen-hotkey \
|
--who=lockscreen-hotkey \
|
||||||
--why="lock screen disabled by hotkey" \
|
--why="lock screen disabled by hotkey" \
|
||||||
${lib.getExe' pkgs.coreutils "sleep"} infinity >/dev/null; then
|
${pkgs.coreutils}/bin/sleep infinity >/dev/null; then
|
||||||
refresh_waybar
|
refresh_waybar
|
||||||
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepOffIcon}"
|
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepOffIcon}"
|
||||||
fi
|
|
||||||
'';
|
|
||||||
|
|
||||||
idleLockStatus = pkgs.writeShellScript "waybar-idle-lock-status" ''
|
|
||||||
if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet lockscreen-idle-inhibit.service; then
|
|
||||||
printf '{"text":"${sleepOffIcon}","tooltip":"Idle locking inhibited","class":"inhibited"}\n'
|
|
||||||
else
|
|
||||||
printf '{"text":"","class":"idle"}\n'
|
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.hyprland.hyprlock.enable =
|
options.modules.desktop.session.hyprland.hyprlock.enable = lib.mkEnableOption "the hyprlock lock screen";
|
||||||
lib.mkEnableOption "the hyprlock lock screen";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.hyprland.hyprlock.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# Colors and the lock-screen background come from Stylix's hyprlock target,
|
# Colors and the lock-screen background come from Stylix's hyprlock target,
|
||||||
# so only geometry and behaviour are set here.
|
# so only geometry and behaviour are set here.
|
||||||
programs.hyprlock = {
|
programs.hyprlock = {
|
||||||
@@ -91,36 +81,14 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
# Lock on Super+X.
|
||||||
hyprlock-lock = {
|
# The guard drops the keypress when a locker is already up, so a second
|
||||||
keys = "SUPER + X";
|
# hyprlock never stacks over the first.
|
||||||
action = {
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
kind = "exec";
|
"$mod, X, exec, ${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}"
|
||||||
# The guard drops the call when a locker is already up, so no manual
|
"$mod CTRL, X, exec, ${idleLockToggle}"
|
||||||
# trigger stacks a second hyprlock over the first.
|
];
|
||||||
command = "${lib.getExe' pkgs.procps "pidof"} hyprlock || ${lib.getExe pkgs.hyprlock}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
hyprlock-toggle-idle-inhibit = {
|
|
||||||
keys = "SUPER + CTRL + X";
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${idleLockToggle}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."custom/idle-lock" = {
|
|
||||||
order = 20;
|
|
||||||
settings = {
|
|
||||||
return-type = "json";
|
|
||||||
exec = "${idleLockStatus}";
|
|
||||||
interval = "once";
|
|
||||||
signal = 9;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,18 @@
|
|||||||
}:
|
}:
|
||||||
# Text login: greetd running the tuigreet greeter, which starts the session
|
# Text login: greetd running the tuigreet greeter, which starts the session
|
||||||
# through the universal Wayland session manager.
|
# through the universal Wayland session manager.
|
||||||
|
let
|
||||||
|
cfg = config.modules.desktop.session.login;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.login.enable =
|
options.modules.desktop.session.login.enable = lib.mkEnableOption "greetd with the tuigreet text greeter";
|
||||||
lib.mkEnableOption "greetd with the tuigreet text greeter";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.login.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
services.greetd = {
|
services.greetd = {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings.default_session.command = lib.escapeShellArgs [
|
settings.default_session.command =
|
||||||
(lib.getExe pkgs.tuigreet)
|
"${lib.getExe pkgs.tuigreet} --time --remember "
|
||||||
"--time"
|
+ "--cmd 'uwsm start -e -D Hyprland hyprland.desktop'";
|
||||||
"--remember"
|
|
||||||
"--cmd"
|
|
||||||
"${lib.getExe config.programs.uwsm.package} start -e -D Hyprland hyprland.desktop"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,29 +2,20 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall.
|
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall.
|
||||||
let
|
let
|
||||||
dndStatus = pkgs.writeShellScript "waybar-dnd-status" ''
|
cfg = config.modules.desktop.session.mako;
|
||||||
if ${lib.getExe' pkgs.mako "makoctl"} mode 2>/dev/null | ${lib.getExe pkgs.gnugrep} -qx dnd; then
|
user = config.user.name;
|
||||||
printf '{"text":"${util.glyph "f1f6"}","tooltip":"Do not disturb: on","class":"dnd"}\n'
|
|
||||||
else
|
|
||||||
printf '{"text":"${util.glyph "f0f3"}","tooltip":"Notifications on","class":"active"}\n'
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
|
|
||||||
dndToggle = pkgs.writeShellScript "waybar-dnd-toggle" ''
|
makoctl = "${pkgs.mako}/bin/makoctl";
|
||||||
${lib.getExe' pkgs.mako "makoctl"} mode -t dnd >/dev/null 2>&1
|
|
||||||
${lib.getExe' pkgs.procps "pkill"} -RTMIN+8 waybar
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.mako.enable = lib.mkEnableOption "the mako notification daemon";
|
options.modules.desktop.session.mako.enable = lib.mkEnableOption "the mako notification daemon";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.mako.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# Colors and the popup font come from Stylix's mako target, so only
|
# Colors and the popup font come from Stylix's mako target, so only
|
||||||
# behaviour is set here.
|
# behaviour is set here.
|
||||||
services.mako = {
|
services.mako = {
|
||||||
@@ -41,25 +32,13 @@ in
|
|||||||
"mode=dnd".invisible = true;
|
"mode=dnd".invisible = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName}.mako-history = {
|
# Recall the last notification from history, keyboard-only like the rest
|
||||||
keys = "SUPER + N";
|
# of the session.
|
||||||
action = {
|
# $mod is defined by the compositor config these binds share.
|
||||||
kind = "exec";
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
command = "${lib.getExe' pkgs.mako "makoctl"} restore";
|
"$mod, N, exec, ${makoctl} restore"
|
||||||
};
|
];
|
||||||
};
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."custom/dnd" = {
|
|
||||||
order = 80;
|
|
||||||
settings = {
|
|
||||||
return-type = "json";
|
|
||||||
exec = "${dndStatus}";
|
|
||||||
on-click = "${dndToggle}";
|
|
||||||
interval = "once";
|
|
||||||
signal = 8;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
# XDG desktop portal routing for in-app screen sharing and file dialogs.
|
# XDG desktop portal routing for in-app screen sharing and file dialogs.
|
||||||
|
let
|
||||||
|
cfg = config.modules.desktop.session.portals;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.portals.enable =
|
options.modules.desktop.session.portals.enable =
|
||||||
lib.mkEnableOption "XDG desktop portals for in-app screen sharing and file dialogs";
|
lib.mkEnableOption "XDG desktop portals for in-app screen sharing and file dialogs";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.portals.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
# The backend packages arrive with the compositor, so only the routing is set
|
# The backend packages arrive with the compositor, so only the routing is set
|
||||||
# here.
|
# here.
|
||||||
# Screen sharing, screenshots, and global shortcuts need the compositor's
|
# Screen sharing, screenshots, and global shortcuts need the compositor's
|
||||||
|
|||||||
@@ -6,61 +6,47 @@
|
|||||||
}:
|
}:
|
||||||
# rofi as the desktop launcher and dmenu-style menu frontend.
|
# rofi as the desktop launcher and dmenu-style menu frontend.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.desktop.session.rofi;
|
||||||
|
user = config.user.name;
|
||||||
|
|
||||||
# A session menu built on the same themed rofi, so the power actions look
|
# A session menu built on the same themed rofi, so the power actions look
|
||||||
# like every other menu the launcher drives.
|
# like every other menu the launcher drives.
|
||||||
powerMenu = pkgs.writeShellScript "rofi-power-menu" ''
|
powerMenu = pkgs.writeShellScript "rofi-power-menu" ''
|
||||||
chosen=$(printf '%s\n' Lock Suspend 'Log out' Reboot 'Shut down' \
|
chosen=$(printf '%s\n' Lock Suspend 'Log out' Reboot 'Shut down' \
|
||||||
| ${lib.getExe pkgs.rofi} -dmenu -i -p Power)
|
| ${pkgs.rofi}/bin/rofi -dmenu -i -p Power)
|
||||||
case "$chosen" in
|
case "$chosen" in
|
||||||
Lock) ${lib.getExe' pkgs.systemd "loginctl"} lock-session ;;
|
Lock) ${pkgs.systemd}/bin/loginctl lock-session ;;
|
||||||
Suspend) ${lib.getExe' pkgs.systemd "systemctl"} suspend ;;
|
Suspend) ${pkgs.systemd}/bin/systemctl suspend ;;
|
||||||
'Log out') ${lib.getExe' pkgs.systemd "loginctl"} terminate-session "$XDG_SESSION_ID" ;;
|
'Log out') ${pkgs.systemd}/bin/loginctl terminate-session "$XDG_SESSION_ID" ;;
|
||||||
Reboot) ${lib.getExe' pkgs.systemd "systemctl"} reboot ;;
|
Reboot) ${pkgs.systemd}/bin/systemctl reboot ;;
|
||||||
'Shut down') ${lib.getExe' pkgs.systemd "systemctl"} poweroff ;;
|
'Shut down') ${pkgs.systemd}/bin/systemctl poweroff ;;
|
||||||
esac
|
esac
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.rofi.enable = lib.mkEnableOption "the rofi launcher";
|
options.modules.desktop.session.rofi.enable = lib.mkEnableOption "the rofi launcher";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.rofi.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name} = hm: {
|
home-manager.users.${user} = {
|
||||||
# Stylix supplies the Nord theme.
|
# No theme is set here: Stylix's rofi target supplies the Nord one.
|
||||||
programs.rofi = {
|
programs.rofi = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
# Application launch is the only mode, kept minimal so the prompt opens
|
# Application launch is the only mode, kept minimal so the prompt opens
|
||||||
# as fast as rofi can read the desktop entries.
|
# as fast as rofi can read the desktop entries.
|
||||||
# Icons are off: resolving one per entry is the bulk of drun's startup.
|
# Icons are off: resolving one per entry is the bulk of drun's startup.
|
||||||
settings = {
|
extraConfig = {
|
||||||
font = lib.mkForce "JetBrains Mono 12";
|
|
||||||
modi = "drun";
|
modi = "drun";
|
||||||
show-icons = false;
|
show-icons = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
theme.window = {
|
|
||||||
border = hm.config.lib.formats.rasi.mkLiteral "2px";
|
|
||||||
border-color = hm.config.lib.formats.rasi.mkLiteral "#FFFFFF";
|
|
||||||
border-radius = hm.config.lib.formats.rasi.mkLiteral "8px";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
|
||||||
rofi-launcher = {
|
|
||||||
keys = "SUPER + R";
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe pkgs.rofi} -show drun";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
rofi-power-menu = {
|
# The launcher and the session menu, kept next to the tool they drive.
|
||||||
keys = "SUPER + SHIFT + X";
|
# $mod is defined by the compositor config these binds share.
|
||||||
action = {
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
kind = "exec";
|
"$mod, R, exec, ${pkgs.rofi}/bin/rofi -show drun"
|
||||||
command = "${powerMenu}";
|
"$mod SHIFT, X, exec, ${powerMenu}"
|
||||||
};
|
];
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,57 +6,165 @@
|
|||||||
}:
|
}:
|
||||||
# Waybar: a top status bar reading system state at a glance.
|
# Waybar: a top status bar reading system state at a glance.
|
||||||
let
|
let
|
||||||
moduleNames =
|
cfg = config.modules.desktop.session.waybar;
|
||||||
position:
|
vpnClient = config.modules.network.vpn-client;
|
||||||
map (entry: entry.name) (
|
user = config.user.name;
|
||||||
lib.sort (left: right: left.module.order < right.module.order) (
|
|
||||||
lib.filter (entry: entry.module.position == position) moduleEntries
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
moduleEntries = lib.mapAttrsToList (name: module: {
|
# A Nerd Font Private-Use-Area codepoint as a literal character, decoded
|
||||||
inherit name module;
|
# through JSON so the glyph survives as bytes rather than an editor paste.
|
||||||
}) modules;
|
g = code: builtins.fromJSON ''"\u${code}"'';
|
||||||
|
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
||||||
|
|
||||||
modules = lib.attrByPath [ config.networking.hostName ] { } config.registry.waybar;
|
# Renders and toggles mako's do-not-disturb mode.
|
||||||
|
# With no mako daemon answering, the status reads "notifications on", keeping
|
||||||
|
# the widget inert rather than broken.
|
||||||
|
dndStatus = pkgs.writeShellScript "waybar-dnd-status" ''
|
||||||
|
if ${pkgs.mako}/bin/makoctl mode 2>/dev/null | ${pkgs.gnugrep}/bin/grep -qx dnd; then
|
||||||
|
printf '{"text":"${g "f1f6"}","tooltip":"Do not disturb: on","class":"dnd"}\n'
|
||||||
|
else
|
||||||
|
printf '{"text":"${g "f0f3"}","tooltip":"Notifications on","class":"active"}\n'
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
dndToggle = pkgs.writeShellScript "waybar-dnd-toggle" ''
|
||||||
|
${pkgs.mako}/bin/makoctl mode -t dnd >/dev/null 2>&1
|
||||||
|
${pkgs.procps}/bin/pkill -RTMIN+8 waybar
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Lit while a wf-recorder capture is running.
|
||||||
|
# Empty text collapses the widget when idle, so it shows nothing until then.
|
||||||
|
recStatus = pkgs.writeShellScript "waybar-recording-status" ''
|
||||||
|
if ${pkgs.procps}/bin/pgrep -x wf-recorder >/dev/null; then
|
||||||
|
printf '{"text":"${g "f03d"}","tooltip":"Recording","class":"recording"}\n'
|
||||||
|
else
|
||||||
|
printf '{"text":"","class":"idle"}\n'
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
idleLockStatus = pkgs.writeShellScript "waybar-idle-lock-status" ''
|
||||||
|
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet lockscreen-idle-inhibit.service; then
|
||||||
|
printf '{"text":"${sleepOffIcon}","tooltip":"Idle locking inhibited","class":"inhibited"}\n'
|
||||||
|
else
|
||||||
|
printf '{"text":"","class":"idle"}\n'
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
vpnStatus = pkgs.writeShellScript "waybar-vpn-status" ''
|
||||||
|
interface=${lib.escapeShellArg vpnClient.interface}
|
||||||
|
|
||||||
|
if ! ${pkgs.iproute2}/bin/ip link show dev "$interface" >/dev/null 2>&1; then
|
||||||
|
printf '{"text":"","class":"idle"}\n'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
ipaddr=$(${pkgs.iproute2}/bin/ip -4 -o addr show dev "$interface" | ${pkgs.gawk}/bin/awk '{ print $4; exit }')
|
||||||
|
|
||||||
|
${pkgs.jq}/bin/jq -cn \
|
||||||
|
--arg text "" \
|
||||||
|
--arg tooltip "VPN $interface: ''${ipaddr:-no IPv4}" \
|
||||||
|
--arg class "active" \
|
||||||
|
'{ text: $text, tooltip: $tooltip, class: $class }'
|
||||||
|
'';
|
||||||
|
|
||||||
|
networkStatus = pkgs.writeShellScript "waybar-network-status" ''
|
||||||
|
active=$(
|
||||||
|
${pkgs.networkmanager}/bin/nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status \
|
||||||
|
| ${pkgs.gawk}/bin/awk -F: '$3 == "connected" && $2 != "loopback" { print; exit }'
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -z "$active" ]; then
|
||||||
|
${pkgs.jq}/bin/jq -cn \
|
||||||
|
--arg text "${g "f127"} off" \
|
||||||
|
--arg tooltip "Network disconnected" \
|
||||||
|
--arg class "disconnected" \
|
||||||
|
'{ text: $text, tooltip: $tooltip, class: $class }'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=: read -r device type _state connection <<< "$active"
|
||||||
|
|
||||||
|
case "$type" in
|
||||||
|
wifi) icon="${g "f1eb"}" ;;
|
||||||
|
ethernet) icon="LAN" ;;
|
||||||
|
*) icon="NET" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
ipaddr=$(${pkgs.iproute2}/bin/ip -4 -o addr show dev "$device" | ${pkgs.gawk}/bin/awk '{ print $4; exit }')
|
||||||
|
|
||||||
|
${pkgs.jq}/bin/jq -cn \
|
||||||
|
--arg text "$icon $connection" \
|
||||||
|
--arg tooltip "$device: ''${ipaddr:-no IPv4}" \
|
||||||
|
--arg class "$type" \
|
||||||
|
'{ text: $text, tooltip: $tooltip, class: $class }'
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.waybar.enable = lib.mkEnableOption "the Waybar status bar";
|
options.modules.desktop.session.waybar.enable = lib.mkEnableOption "the Waybar status bar";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.waybar.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
# A symbols font backs the module glyphs, so pango falls back to it for the
|
# A symbols font backs the module glyphs, so pango falls back to it for the
|
||||||
# codepoints Stylix's monospace font does not carry.
|
# codepoints Stylix's monospace font does not carry.
|
||||||
fonts.packages = [ pkgs.nerd-fonts.symbols-only ];
|
fonts.packages = [ pkgs.nerd-fonts.symbols-only ];
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.programs.waybar = {
|
home-manager.users.${user}.programs.waybar = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = pkgs.alexion.waybar;
|
|
||||||
|
|
||||||
# A user service bound to graphical-session.target, which uwsm activates,
|
# A user service bound to graphical-session.target, which uwsm activates,
|
||||||
# so the bar comes up with the session and no compositor exec-once.
|
# so the bar comes up with the session and no compositor exec-once.
|
||||||
systemd.enable = true;
|
systemd.enable = true;
|
||||||
|
|
||||||
settings.mainBar = lib.mkMerge [
|
style = ''
|
||||||
{
|
.modules-right {
|
||||||
layer = "top";
|
margin-right: 8px;
|
||||||
position = "top";
|
|
||||||
height = 30;
|
|
||||||
spacing = 8;
|
|
||||||
margin-right = 8;
|
|
||||||
|
|
||||||
modules-left = moduleNames "left";
|
|
||||||
modules-center = moduleNames "center";
|
|
||||||
modules-right = moduleNames "right";
|
|
||||||
}
|
}
|
||||||
(lib.mapAttrs (_name: module: module.settings) modules)
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName} = {
|
#custom-dnd {
|
||||||
clock = {
|
margin-right: 8px;
|
||||||
position = "center";
|
}
|
||||||
order = 10;
|
'';
|
||||||
settings = {
|
|
||||||
|
settings.mainBar = {
|
||||||
|
layer = "top";
|
||||||
|
position = "top";
|
||||||
|
height = 30;
|
||||||
|
spacing = 8;
|
||||||
|
|
||||||
|
modules-left = [ "hyprland/workspaces" ];
|
||||||
|
modules-center = [ "clock" ];
|
||||||
|
modules-right = [
|
||||||
|
"custom/recording"
|
||||||
|
"custom/idle-lock"
|
||||||
|
"mpris"
|
||||||
|
"wireplumber"
|
||||||
|
]
|
||||||
|
++ lib.optional vpnClient.enable "custom/vpn"
|
||||||
|
++ [
|
||||||
|
"custom/network"
|
||||||
|
"battery"
|
||||||
|
"custom/dnd"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Per-application icons for the windows on each workspace, mapped from
|
||||||
|
# window class with a neutral glyph for anything unlisted.
|
||||||
|
"hyprland/workspaces" = {
|
||||||
|
on-click = "activate";
|
||||||
|
format = "{id} {windows}";
|
||||||
|
format-window-separator = " ";
|
||||||
|
window-rewrite-default = g "f2d0";
|
||||||
|
window-rewrite = {
|
||||||
|
"class<Alacritty>" = g "f120";
|
||||||
|
"class<com.mitchellh.ghostty>" = g "f120";
|
||||||
|
"class<.*[Ff]irefox.*>" = g "f269";
|
||||||
|
"class<chromium.*>" = g "f268";
|
||||||
|
"class<[Cc]ode>" = g "f121";
|
||||||
|
"class<obsidian>" = g "f02d";
|
||||||
|
"class<[Ss]ignal>" = g "f4ad";
|
||||||
|
"class<[Ss]team>" = g "f1b6";
|
||||||
|
"class<[Vv]esktop>" = g "f1ff";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
clock = {
|
||||||
format = "{:%a %d %b %H:%M}";
|
format = "{:%a %d %b %H:%M}";
|
||||||
tooltip-format = "<tt>{calendar}</tt>";
|
tooltip-format = "<tt>{calendar}</tt>";
|
||||||
calendar = {
|
calendar = {
|
||||||
@@ -65,6 +173,77 @@ in
|
|||||||
format.today = "<b><u>{}</u></b>";
|
format.today = "<b><u>{}</u></b>";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mpris = {
|
||||||
|
format = "${g "f001"} {title}";
|
||||||
|
format-paused = "${g "f04c"} {title}";
|
||||||
|
max-length = 40;
|
||||||
|
on-click = "play-pause";
|
||||||
|
on-scroll-up = "next";
|
||||||
|
on-scroll-down = "previous";
|
||||||
|
};
|
||||||
|
|
||||||
|
wireplumber = {
|
||||||
|
format = "{icon} {volume}%";
|
||||||
|
format-muted = "${g "f026"} muted";
|
||||||
|
format-icons = [
|
||||||
|
(g "f026")
|
||||||
|
(g "f027")
|
||||||
|
(g "f028")
|
||||||
|
];
|
||||||
|
on-click = "${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
|
||||||
|
};
|
||||||
|
|
||||||
|
"custom/vpn" = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${vpnStatus}";
|
||||||
|
interval = 5;
|
||||||
|
};
|
||||||
|
|
||||||
|
"custom/network" = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${networkStatus}";
|
||||||
|
interval = 5;
|
||||||
|
};
|
||||||
|
|
||||||
|
battery = {
|
||||||
|
format = "{icon} {capacity}%";
|
||||||
|
format-charging = "${g "f0e7"} {capacity}%";
|
||||||
|
format-icons = [
|
||||||
|
(g "f244")
|
||||||
|
(g "f243")
|
||||||
|
(g "f242")
|
||||||
|
(g "f241")
|
||||||
|
(g "f240")
|
||||||
|
];
|
||||||
|
states = {
|
||||||
|
warning = 20;
|
||||||
|
critical = 10;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
"custom/dnd" = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${dndStatus}";
|
||||||
|
on-click = "${dndToggle}";
|
||||||
|
interval = "once";
|
||||||
|
signal = 8;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Recording state has no event to hook, so the widget samples the
|
||||||
|
# wf-recorder process once a second.
|
||||||
|
"custom/recording" = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${recStatus}";
|
||||||
|
interval = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
"custom/idle-lock" = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${idleLockStatus}";
|
||||||
|
interval = "once";
|
||||||
|
signal = 9;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,34 +1,12 @@
|
|||||||
{
|
{ config, lib, ... }:
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
util,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# PipeWire as the desktop audio server.
|
# PipeWire as the desktop audio server.
|
||||||
let
|
let
|
||||||
volumeKey = keys: command: {
|
cfg = config.modules.desktop.utilities.audio;
|
||||||
inherit keys;
|
|
||||||
repeating = true;
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe' pkgs.swayosd "swayosd-client"} --output-volume ${command}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mediaKey = keys: command: {
|
|
||||||
inherit keys;
|
|
||||||
locked = true;
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe pkgs.playerctl} ${command}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.audio.enable = lib.mkEnableOption "the PipeWire audio server";
|
options.modules.desktop.utilities.audio.enable = lib.mkEnableOption "the PipeWire audio server";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.audio.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
# Realtime scheduling for the audio threads, so playback survives load.
|
# Realtime scheduling for the audio threads, so playback survives load.
|
||||||
security.rtkit.enable = true;
|
security.rtkit.enable = true;
|
||||||
|
|
||||||
@@ -37,50 +15,5 @@ in
|
|||||||
alsa.enable = true;
|
alsa.enable = true;
|
||||||
pulse.enable = true;
|
pulse.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
|
||||||
volume-up = volumeKey "XF86AudioRaiseVolume" "raise --max-volume 100";
|
|
||||||
volume-down = volumeKey "XF86AudioLowerVolume" "lower";
|
|
||||||
mute = {
|
|
||||||
keys = "XF86AudioMute";
|
|
||||||
locked = true;
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe' pkgs.swayosd "swayosd-client"} --output-volume mute-toggle";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
media-play = mediaKey "XF86AudioPlay" "play-pause";
|
|
||||||
media-next = mediaKey "XF86AudioNext" "next";
|
|
||||||
media-previous = mediaKey "XF86AudioPrev" "previous";
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName} = {
|
|
||||||
mpris = {
|
|
||||||
order = 30;
|
|
||||||
settings = {
|
|
||||||
format = "${util.glyph "f001"} {title}";
|
|
||||||
format-paused = "${util.glyph "f04c"} {title}";
|
|
||||||
max-length = 40;
|
|
||||||
on-click = "play-pause";
|
|
||||||
on-scroll-up = "next";
|
|
||||||
on-scroll-down = "previous";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
wireplumber = {
|
|
||||||
order = 40;
|
|
||||||
settings = {
|
|
||||||
format = "{icon} {volume}%";
|
|
||||||
format-muted = "${util.glyph "f026"} muted";
|
|
||||||
format-icons = [
|
|
||||||
(util.glyph "f026")
|
|
||||||
(util.glyph "f027")
|
|
||||||
(util.glyph "f028")
|
|
||||||
];
|
|
||||||
on-click = "${lib.getExe' pkgs.wireplumber "wpctl"} set-mute @DEFAULT_AUDIO_SINK@ toggle";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,29 @@
|
|||||||
}:
|
}:
|
||||||
# Clipboard history: cliphist records every copy, picked back through rofi.
|
# Clipboard history: cliphist records every copy, picked back through rofi.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.desktop.utilities.clipboard;
|
||||||
|
user = config.user.name;
|
||||||
|
|
||||||
|
cliphist = "${pkgs.cliphist}/bin/cliphist";
|
||||||
|
rofi = "${pkgs.rofi}/bin/rofi";
|
||||||
|
wl-copy = "${pkgs.wl-clipboard}/bin/wl-copy";
|
||||||
|
|
||||||
# The picker reuses the themed rofi, so history looks like every other menu
|
# The picker reuses the themed rofi, so history looks like every other menu
|
||||||
# the launcher drives.
|
# the launcher drives.
|
||||||
# decode is needed because list emits id-prefixed lines rather than the copied
|
# decode is needed because list emits id-prefixed lines rather than the copied
|
||||||
# bytes, so the chosen id has to be resolved back before it can be re-copied.
|
# bytes, so the chosen id has to be resolved back before it can be re-copied.
|
||||||
picker = pkgs.writeShellScript "clipboard-picker" ''
|
picker = pkgs.writeShellScript "clipboard-picker" ''
|
||||||
${lib.getExe pkgs.cliphist} list \
|
${cliphist} list \
|
||||||
| ${lib.getExe pkgs.rofi} -dmenu -i -p Clipboard \
|
| ${rofi} -dmenu -i -p Clipboard \
|
||||||
| ${lib.getExe pkgs.cliphist} decode \
|
| ${cliphist} decode \
|
||||||
| ${lib.getExe' pkgs.wl-clipboard "wl-copy"}
|
| ${wl-copy}
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.clipboard.enable =
|
options.modules.desktop.utilities.clipboard.enable = lib.mkEnableOption "cliphist clipboard history";
|
||||||
lib.mkEnableOption "cliphist clipboard history";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.clipboard.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# wl-copy and wl-paste on PATH, so the shell can pipe into and out of the
|
# wl-copy and wl-paste on PATH, so the shell can pipe into and out of the
|
||||||
# clipboard.
|
# clipboard.
|
||||||
# The watchers and picker above reach wl-clipboard by store path, so this
|
# The watchers and picker above reach wl-clipboard by store path, so this
|
||||||
@@ -32,14 +38,11 @@ in
|
|||||||
# Two watchers record text and images to history, bound to the graphical
|
# Two watchers record text and images to history, bound to the graphical
|
||||||
# session so uwsm starts and stops them with it.
|
# session so uwsm starts and stops them with it.
|
||||||
services.cliphist.enable = true;
|
services.cliphist.enable = true;
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName}.clipboard-history = {
|
# $mod is defined by the compositor config these binds share.
|
||||||
keys = "SUPER + SHIFT + V";
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
action = {
|
"$mod SHIFT, V, exec, ${picker}"
|
||||||
kind = "exec";
|
];
|
||||||
command = "${picker}";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,52 +5,35 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# SwayOSD: a transient on-screen popup for volume and brightness changes.
|
# SwayOSD: a transient on-screen popup for volume and brightness changes.
|
||||||
|
let
|
||||||
|
cfg = config.modules.desktop.utilities.osd;
|
||||||
|
user = config.user.name;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.osd.enable =
|
options.modules.desktop.utilities.osd.enable = lib.mkEnableOption "the SwayOSD volume and brightness popup";
|
||||||
lib.mkEnableOption "the SwayOSD volume and brightness popup";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.osd.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
environment.systemPackages = [ pkgs.swayosd ];
|
environment.systemPackages = [ pkgs.swayosd ];
|
||||||
|
|
||||||
# The udev rule chgrps each backlight's brightness node to `video` and adds
|
# The udev rule chgrps each backlight's brightness node to `video` and adds
|
||||||
# group write, so the server dims the panel without root.
|
# group write, so the server dims the panel without root.
|
||||||
# Membership below grants the running session that access.
|
# Membership below grants the running session that access.
|
||||||
services.udev.packages = [ pkgs.swayosd ];
|
services.udev.packages = [ pkgs.swayosd ];
|
||||||
users.users.${config.user.name}.extraGroups = [ "video" ];
|
users.users.${user}.extraGroups = [ "video" ];
|
||||||
|
|
||||||
# The server draws the popups, so it runs for the whole graphical session.
|
# The server draws the popups, so it runs for the whole graphical session.
|
||||||
# It binds to the target uwsm activates.
|
# It binds to the target uwsm activates.
|
||||||
home-manager.users.${config.user.name}.systemd.user.services.swayosd = {
|
home-manager.users.${user}.systemd.user.services.swayosd = {
|
||||||
Unit = {
|
Unit = {
|
||||||
Description = "SwayOSD on-screen display server";
|
Description = "SwayOSD on-screen display server";
|
||||||
PartOf = [ "graphical-session.target" ];
|
PartOf = [ "graphical-session.target" ];
|
||||||
After = [ "graphical-session.target" ];
|
After = [ "graphical-session.target" ];
|
||||||
};
|
};
|
||||||
Service = {
|
Service = {
|
||||||
ExecStart = lib.getExe' pkgs.swayosd "swayosd-server";
|
ExecStart = "${pkgs.swayosd}/bin/swayosd-server";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
Install.WantedBy = [ "graphical-session.target" ];
|
Install.WantedBy = [ "graphical-session.target" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
|
||||||
brightness-up = {
|
|
||||||
keys = "XF86MonBrightnessUp";
|
|
||||||
repeating = true;
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe' pkgs.swayosd "swayosd-client"} --brightness raise";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
brightness-down = {
|
|
||||||
keys = "XF86MonBrightnessDown";
|
|
||||||
repeating = true;
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe' pkgs.swayosd "swayosd-client"} --brightness lower";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,67 +2,53 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Keybound screen recording: pick a region, then toggle a video-only capture.
|
# Keybound screen recording: pick a region, then toggle a video-only capture.
|
||||||
let
|
let
|
||||||
# A single key starts and stops recording.
|
cfg = config.modules.desktop.utilities.recording;
|
||||||
# Waybar refreshes on state changes instead of polling.
|
user = config.user.name;
|
||||||
recordingToggle = pkgs.writeShellScript "screen-record-toggle" ''
|
|
||||||
refresh_waybar() {
|
|
||||||
${lib.getExe' pkgs.procps "pkill"} -RTMIN+10 waybar >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
|
slurp = "${pkgs.slurp}/bin/slurp";
|
||||||
${lib.getExe' pkgs.procps "pkill"} -INT -x wf-recorder
|
wf-recorder = "${pkgs.wf-recorder}/bin/wf-recorder";
|
||||||
|
notify-send = "${pkgs.libnotify}/bin/notify-send";
|
||||||
|
pgrep = "${pkgs.procps}/bin/pgrep";
|
||||||
|
pkill = "${pkgs.procps}/bin/pkill";
|
||||||
|
date = "${pkgs.coreutils}/bin/date";
|
||||||
|
mkdir = "${pkgs.coreutils}/bin/mkdir";
|
||||||
|
xdgUserDir = "${pkgs.xdg-user-dirs}/bin/xdg-user-dir";
|
||||||
|
|
||||||
|
# A single key both starts and stops.
|
||||||
|
# A running wf-recorder is stopped with SIGINT so it finalises the file.
|
||||||
|
# Otherwise slurp picks a region and wf-recorder runs in the foreground until
|
||||||
|
# that stop arrives, so this instance lives for the whole recording and then
|
||||||
|
# reports it saved.
|
||||||
|
# No -a means no audio is captured.
|
||||||
|
toggle = pkgs.writeShellScript "screen-record-toggle" ''
|
||||||
|
if ${pgrep} -x wf-recorder >/dev/null; then
|
||||||
|
${pkill} -INT -x wf-recorder
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
region=$(${lib.getExe pkgs.slurp}) || exit 0
|
region=$(${slurp}) || exit 0
|
||||||
dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} VIDEOS)/Recordings"
|
dir="$(${xdgUserDir} VIDEOS)/Recordings"
|
||||||
${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
|
${mkdir} -p "$dir"
|
||||||
file="$dir/recording-$(${lib.getExe' pkgs.coreutils "date"} +%Y%m%d-%H%M%S).mp4"
|
file="$dir/recording-$(${date} +%Y%m%d-%H%M%S).mp4"
|
||||||
|
|
||||||
${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording started" "Region capture, no audio."
|
${notify-send} -a "Screen recording" "Recording started" "Region capture, no audio."
|
||||||
${lib.getExe pkgs.wf-recorder} -g "$region" -f "$file" &
|
${wf-recorder} -g "$region" -f "$file"
|
||||||
recorder=$!
|
${notify-send} -a "Screen recording" "Recording saved" "$file"
|
||||||
refresh_waybar
|
|
||||||
wait "$recorder" || true
|
|
||||||
refresh_waybar
|
|
||||||
${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording saved" "$file"
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Empty text collapses the widget when recording is idle.
|
|
||||||
recordingStatus = pkgs.writeShellScript "waybar-recording-status" ''
|
|
||||||
if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
|
|
||||||
printf '{"text":"${util.glyph "f03d"}","tooltip":"Recording","class":"recording"}\n'
|
|
||||||
else
|
|
||||||
printf '{"text":"","class":"idle"}\n'
|
|
||||||
fi
|
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.recording.enable =
|
options.modules.desktop.utilities.recording.enable = lib.mkEnableOption "wf-recorder screen recording";
|
||||||
lib.mkEnableOption "wf-recorder screen recording";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.recording.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.keybind.${config.networking.hostName}.recording-toggle = {
|
home-manager.users.${user} = {
|
||||||
keys = "SUPER + SHIFT + R";
|
# $mod is defined by the compositor config these binds share.
|
||||||
action = {
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
kind = "exec";
|
"$mod SHIFT, R, exec, ${toggle}"
|
||||||
command = "${recordingToggle}";
|
];
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."custom/recording" = {
|
|
||||||
order = 10;
|
|
||||||
settings = {
|
|
||||||
return-type = "json";
|
|
||||||
exec = "${recordingStatus}";
|
|
||||||
interval = "once";
|
|
||||||
signal = 10;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,54 +4,54 @@
|
|||||||
pkgs,
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Keyboard-driven screenshots copy to the clipboard before Satty opens for
|
# Keyboard-driven screenshots that open in satty for annotation, then land in
|
||||||
# annotation. Satty copies annotated screenshots when it saves them.
|
# both the clipboard and a dated file.
|
||||||
let
|
let
|
||||||
screenshotKey = keys: target: {
|
cfg = config.modules.desktop.utilities.screenshot;
|
||||||
inherit keys;
|
user = config.user.name;
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${capture target}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Copy the original screenshot before Satty opens.
|
grimblast = "${pkgs.grimblast}/bin/grimblast";
|
||||||
# Satty copies and saves annotated screenshots when the user presses Enter.
|
satty = "${pkgs.satty}/bin/satty";
|
||||||
|
wl-copy = "${pkgs.wl-clipboard}/bin/wl-copy";
|
||||||
|
xdgUserDir = "${pkgs.xdg-user-dirs}/bin/xdg-user-dir";
|
||||||
|
|
||||||
|
# satty's copy action also saves, so one keystroke lands the shot in both the
|
||||||
|
# clipboard and a file.
|
||||||
capture =
|
capture =
|
||||||
target:
|
target:
|
||||||
pkgs.writeShellScript "screenshot-${target}" ''
|
pkgs.writeShellScript "screenshot-${target}" ''
|
||||||
dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} PICTURES)/Screenshots"
|
dir="$(${xdgUserDir} PICTURES)/Screenshots"
|
||||||
${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
|
mkdir -p "$dir"
|
||||||
${lib.getExe pkgs.grimblast} save ${target} - \
|
${grimblast} save ${target} - \
|
||||||
| ${lib.getExe' pkgs.coreutils "tee"} >(${lib.getExe' pkgs.wl-clipboard "wl-copy"}) \
|
| ${satty} --filename - \
|
||||||
| ${lib.getExe pkgs.satty} --filename - \
|
|
||||||
--output-filename "$dir/screenshot-%Y%m%d-%H%M%S.png" \
|
--output-filename "$dir/screenshot-%Y%m%d-%H%M%S.png" \
|
||||||
--copy-command ${lib.getExe' pkgs.wl-clipboard "wl-copy"} \
|
--copy-command ${wl-copy} \
|
||||||
--save-after-copy \
|
--save-after-copy \
|
||||||
--early-exit \
|
--early-exit \
|
||||||
--actions-on-enter save-to-clipboard
|
--actions-on-enter save-to-clipboard
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.screenshot.enable =
|
options.modules.desktop.utilities.screenshot.enable = lib.mkEnableOption "screenshots via grimblast and satty";
|
||||||
lib.mkEnableOption "screenshots via grimblast and satty";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.screenshot.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# satty is a one-shot annotation surface, so float it rather than letting
|
# satty is a one-shot annotation surface, so float it rather than letting
|
||||||
# it claim a tile in the layout.
|
# it claim a tile in the layout.
|
||||||
wayland.windowManager.hyprland.settings.window_rule = [
|
# windowrule v3 syntax: space-separated field/value tokens, comma
|
||||||
{
|
# separated, with matchers under a match: prefix and effects bare.
|
||||||
match.class = "^(com\\.gabm\\.satty)$";
|
wayland.windowManager.hyprland.settings.windowrule = [
|
||||||
float = true;
|
"float 1, match:class ^(com\\.gabm\\.satty)$"
|
||||||
}
|
|
||||||
];
|
];
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
# Print with plain/Shift/Ctrl for region/window/full.
|
||||||
screenshot-area = screenshotKey "Print" "area";
|
# Super+L is already the hjkl focus and movement bind, so screenshots take
|
||||||
screenshot-active = screenshotKey "SHIFT + Print" "active";
|
# the Print key instead.
|
||||||
screenshot-screen = screenshotKey "CTRL + Print" "screen";
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
|
", Print, exec, ${capture "area"}"
|
||||||
|
"SHIFT, Print, exec, ${capture "active"}"
|
||||||
|
"CTRL, Print, exec, ${capture "screen"}"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
}:
|
}:
|
||||||
# Nord theming for the graphical layer, driven from one base16 scheme by Stylix.
|
# Nord theming for the graphical layer, driven from one base16 scheme by Stylix.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.desktop.utilities.theming;
|
||||||
|
user = config.user.name;
|
||||||
|
|
||||||
# A single static Nord wallpaper, drawn as a vertical gradient across the
|
# A single static Nord wallpaper, drawn as a vertical gradient across the
|
||||||
# Polar Night shades so it needs no committed binary and no network fetch.
|
# Polar Night shades so it needs no committed binary and no network fetch.
|
||||||
wallpaper = pkgs.runCommand "nord-wallpaper.png" { } ''
|
wallpaper = pkgs.runCommand "nord-wallpaper.png" { } ''
|
||||||
${lib.getExe pkgs.imagemagick} \
|
${pkgs.imagemagick}/bin/magick \
|
||||||
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
|
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
|
||||||
"$out"
|
"$out"
|
||||||
'';
|
'';
|
||||||
@@ -18,7 +21,7 @@ in
|
|||||||
options.modules.desktop.utilities.theming.enable =
|
options.modules.desktop.utilities.theming.enable =
|
||||||
lib.mkEnableOption "Nord theming of the graphical layer via Stylix";
|
lib.mkEnableOption "Nord theming of the graphical layer via Stylix";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.theming.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
stylix = {
|
stylix = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
@@ -57,7 +60,7 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# Stylix drives the cursor through home-manager's pointer-cursor config,
|
# Stylix drives the cursor through home-manager's pointer-cursor config,
|
||||||
# whose generation must be switched on explicitly.
|
# whose generation must be switched on explicitly.
|
||||||
home.pointerCursor.enable = true;
|
home.pointerCursor.enable = true;
|
||||||
@@ -66,10 +69,5 @@ in
|
|||||||
# base16 mapping Stylix would apply, so its target stays off.
|
# base16 mapping Stylix would apply, so its target stays off.
|
||||||
stylix.targets.nixvim.enable = false;
|
stylix.targets.nixvim.enable = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.env.${config.networking.hostName} = {
|
|
||||||
HYPRCURSOR_THEME = config.stylix.cursor.name;
|
|
||||||
HYPRCURSOR_SIZE = toString config.stylix.cursor.size;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,15 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
# Declares XDG user directories, so tools that query them use predictable
|
# Declares the XDG user directories, so a tool that queries them lands in a
|
||||||
# folders a host can relocate from one place.
|
# well-known folder a host can relocate from one place.
|
||||||
let
|
let
|
||||||
homeDirectory = "/home/${config.user.name}";
|
cfg = config.modules.desktop.utilities.userdirs;
|
||||||
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.userdirs.enable = lib.mkEnableOption "XDG user directories";
|
options.modules.desktop.utilities.userdirs.enable = lib.mkEnableOption "XDG user directories";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.userdirs.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name}.xdg.userDirs = {
|
# Write ~/.config/user-dirs.dirs from the option defaults.
|
||||||
enable = true;
|
home-manager.users.${user}.xdg.userDirs.enable = true;
|
||||||
createDirectories = true;
|
|
||||||
setSessionVariables = true;
|
|
||||||
|
|
||||||
desktop = "${homeDirectory}/.ignoreme";
|
|
||||||
documents = "${homeDirectory}/docs";
|
|
||||||
download = "${homeDirectory}/dwn";
|
|
||||||
music = "${homeDirectory}/mus";
|
|
||||||
pictures = "${homeDirectory}/pic";
|
|
||||||
publicShare = "${homeDirectory}/.ignoreme";
|
|
||||||
templates = "${homeDirectory}/.ignoreme";
|
|
||||||
videos = "${homeDirectory}/vid";
|
|
||||||
projects = "${homeDirectory}/.ignoreme";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.modules.guest;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.guest.enable = lib.mkEnableOption "nested guest configuration";
|
options.modules.guest.enable = lib.mkEnableOption "nested-guest base configuration";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.guest.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
modules.network.ssh.enable = lib.mkDefault false;
|
modules.network.ssh.enable = lib.mkDefault true;
|
||||||
modules.network.ssh.hostKeys.restore = lib.mkDefault false;
|
modules.network.ssh.hostKeys.restore = lib.mkDefault false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
52
configs/nixos/modules/host.nix
Normal file
52
configs/nixos/modules/host.nix
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.modules.host;
|
||||||
|
user = config.user;
|
||||||
|
hostSecrets = ../secrets + "/${config.networking.hostName}.yaml";
|
||||||
|
passwordSecret = "${user.name}-password";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.host.enable = lib.mkEnableOption "physical-host base configuration";
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
nix.settings.extra-substituters = [ "https://nyx-cache.chaotic.cx/" ];
|
||||||
|
nix.settings.extra-trusted-public-keys = [
|
||||||
|
"nyx-cache.chaotic.cx:dJxTrgMC3V3cFfyIiBQDQorG6k1LsqurH/srpMSq7qk="
|
||||||
|
];
|
||||||
|
|
||||||
|
nix.gc.automatic = true;
|
||||||
|
nix.gc.dates = "Mon 03:15";
|
||||||
|
nix.gc.options = "--delete-older-than 30d";
|
||||||
|
|
||||||
|
nix.optimise.automatic = true;
|
||||||
|
nix.optimise.dates = [ "Thu 03:45" ];
|
||||||
|
|
||||||
|
boot.loader.systemd-boot.enable = true;
|
||||||
|
boot.loader.systemd-boot.configurationLimit = 15;
|
||||||
|
boot.loader.efi.canTouchEfiVariables = true;
|
||||||
|
|
||||||
|
hardware.enableRedistributableFirmware = true;
|
||||||
|
zramSwap.enable = true;
|
||||||
|
|
||||||
|
modules.network.ssh.enable = true;
|
||||||
|
modules.network.ssh.hostKeys.sopsFile = hostSecrets;
|
||||||
|
modules.network.ssh.userKey.sopsFile = hostSecrets;
|
||||||
|
|
||||||
|
services.xserver.xkb.layout = "us";
|
||||||
|
services.xserver.xkb.options = "caps:escape_shifted_capslock";
|
||||||
|
console.useXkbConfig = true;
|
||||||
|
|
||||||
|
sops.defaultSopsFile = ../secrets/shared.yaml;
|
||||||
|
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
|
||||||
|
sops.age.sshKeyPaths = [ ];
|
||||||
|
sops.gnupg.sshKeyPaths = [ ];
|
||||||
|
|
||||||
|
users.mutableUsers = false;
|
||||||
|
sops.secrets.${passwordSecret}.neededForUsers = true;
|
||||||
|
users.users.${user.name}.hashedPasswordFile = config.sops.secrets.${passwordSecret}.path;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
options.modules.host.cachyos.enable = lib.mkEnableOption "the CachyOS kernel";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.host.cachyos.enable {
|
|
||||||
boot.kernelPackages = pkgs.linuxPackages_cachyos;
|
|
||||||
|
|
||||||
nix.settings.extra-substituters = [ "https://nyx-cache.chaotic.cx/" ];
|
|
||||||
nix.settings.extra-trusted-public-keys = [
|
|
||||||
"nyx-cache.chaotic.cx:dJxTrgMC3V3cFfyIiBQDQorG6k1LsqurH/srpMSq7qk="
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
hostSecrets = ../../secrets + "/${config.networking.hostName}.yaml";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.modules.host.enable = lib.mkEnableOption "physical host configuration";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.host.enable {
|
|
||||||
nix.gc.automatic = true;
|
|
||||||
nix.gc.dates = "Mon 03:15";
|
|
||||||
nix.gc.options = "--delete-older-than 30d";
|
|
||||||
|
|
||||||
nix.optimise.automatic = true;
|
|
||||||
nix.optimise.dates = [ "Thu 03:45" ];
|
|
||||||
|
|
||||||
boot.loader.systemd-boot.enable = true;
|
|
||||||
boot.loader.systemd-boot.configurationLimit = 15;
|
|
||||||
boot.loader.efi.canTouchEfiVariables = true;
|
|
||||||
|
|
||||||
hardware.enableRedistributableFirmware = true;
|
|
||||||
zramSwap.enable = true;
|
|
||||||
|
|
||||||
modules.network.ssh.enable = true;
|
|
||||||
modules.network.ssh.hostKeys.sopsFile = hostSecrets;
|
|
||||||
modules.network.ssh.userKey.sopsFile = hostSecrets;
|
|
||||||
|
|
||||||
services.xserver.xkb.layout = "us";
|
|
||||||
console.useXkbConfig = true;
|
|
||||||
|
|
||||||
sops.defaultSopsFile = ../../secrets/shared.yaml;
|
|
||||||
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
|
|
||||||
sops.age.sshKeyPaths = [ ];
|
|
||||||
sops.gnupg.sshKeyPaths = [ ];
|
|
||||||
|
|
||||||
users.mutableUsers = false;
|
|
||||||
sops.secrets."${config.user.name}-password".neededForUsers = true;
|
|
||||||
users.users.${config.user.name}.hashedPasswordFile =
|
|
||||||
config.sops.secrets."${config.user.name}-password".path;
|
|
||||||
|
|
||||||
security.sudo.extraConfig = ''
|
|
||||||
Defaults env_keep += "EDITOR VISUAL"
|
|
||||||
'';
|
|
||||||
|
|
||||||
home-manager = {
|
|
||||||
useGlobalPkgs = true;
|
|
||||||
useUserPackages = true;
|
|
||||||
users.${config.user.name} = {
|
|
||||||
home.username = config.user.name;
|
|
||||||
home.homeDirectory = "/home/${config.user.name}";
|
|
||||||
home.stateVersion = config.system.stateVersion;
|
|
||||||
home.sessionVariables = lib.attrByPath [ config.networking.hostName ] { } config.registry.env;
|
|
||||||
home.packages = with pkgs; [
|
|
||||||
eza
|
|
||||||
unzip
|
|
||||||
wget
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.shell-shortcut.${config.networking.hostName} = {
|
|
||||||
ls = {
|
|
||||||
command = "eza -alg --color=always --group-directories-first --icons=always";
|
|
||||||
abbreviation = false;
|
|
||||||
};
|
|
||||||
ll = {
|
|
||||||
command = "eza -lg --color=always --group-directories-first --icons=always";
|
|
||||||
abbreviation = false;
|
|
||||||
};
|
|
||||||
lt = {
|
|
||||||
command = "eza -aT -I '.git' --color=always --group-directories-first --icons=always";
|
|
||||||
abbreviation = false;
|
|
||||||
};
|
|
||||||
".." = "cd ..";
|
|
||||||
"..." = "cd ../..";
|
|
||||||
"...." = "cd ../../..";
|
|
||||||
|
|
||||||
cp = "cp -v";
|
|
||||||
tarnow = "tar -acf ";
|
|
||||||
untar = "tar -zxvf ";
|
|
||||||
wget = "wget -c ";
|
|
||||||
grep = "grep --color=auto";
|
|
||||||
fgrep = "fgrep --color=auto";
|
|
||||||
egrep = "egrep --color=auto";
|
|
||||||
jctl = "journalctl -p 3 -xb";
|
|
||||||
|
|
||||||
update = "sudo nixos-rebuild switch --flake .#${config.networking.hostName}";
|
|
||||||
cleanup = "sudo nix-collect-garbage -d";
|
|
||||||
sops-host = {
|
|
||||||
command = "sudo --preserve-env=EDITOR,VISUAL env SOPS_AGE_KEY_FILE=/var/lib/sops-nix/key.txt nix shell nixpkgs#sops nixpkgs#neovim -c sops";
|
|
||||||
abbreviation = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
modules.shell.fish.enable = lib.mkDefault true;
|
|
||||||
modules.shell.fish.defaultShell = lib.mkDefault true;
|
|
||||||
modules.shell.tmux.enable = lib.mkDefault true;
|
|
||||||
modules.shell.nvim.enable = lib.mkDefault true;
|
|
||||||
modules.shell.git.enable = lib.mkDefault true;
|
|
||||||
modules.shell.yazi.enable = lib.mkDefault true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# Imports existing ZFS pools that hold durable service state.
|
|
||||||
let
|
|
||||||
cfg = config.modules.host.zfs;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.modules.host.zfs = {
|
|
||||||
enable = lib.mkEnableOption "ZFS pool imports for durable service state";
|
|
||||||
|
|
||||||
pools = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf (lib.types.attrsOf lib.types.path);
|
|
||||||
default = { };
|
|
||||||
example = lib.literalExpression ''
|
|
||||||
{
|
|
||||||
tank = {
|
|
||||||
media = "/srv/media";
|
|
||||||
downloads = "/srv/downloads";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
description = ''
|
|
||||||
Lists pools that NixOS imports at boot.
|
|
||||||
Maps each dataset name to its mount point.
|
|
||||||
NixOS does not create or destroy these pools during rebuilds.
|
|
||||||
An empty map imports the pool without declaring file systems.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
boot.supportedFilesystems = [ "zfs" ];
|
|
||||||
|
|
||||||
# This module derives a stable host ID from the configured hostname.
|
|
||||||
networking.hostId = builtins.substring 0 8 (
|
|
||||||
builtins.hashString "sha256" config.networking.hostName
|
|
||||||
);
|
|
||||||
|
|
||||||
boot.zfs.extraPools = lib.attrNames cfg.pools;
|
|
||||||
|
|
||||||
fileSystems = lib.mkMerge (
|
|
||||||
lib.mapAttrsToList (
|
|
||||||
pool: mounts:
|
|
||||||
lib.mapAttrs' (
|
|
||||||
dataset: mountpoint:
|
|
||||||
lib.nameValuePair mountpoint {
|
|
||||||
device = "${pool}/${dataset}";
|
|
||||||
fsType = "zfs";
|
|
||||||
}
|
|
||||||
) mounts
|
|
||||||
) cfg.pools
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -4,10 +4,13 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Tools for developing this monorepo.
|
# Tools for developing this monorepo.
|
||||||
|
let
|
||||||
|
cfg = config.modules.monorepo;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.monorepo.enable = lib.mkEnableOption "tools for developing this monorepo";
|
options.modules.monorepo.enable = lib.mkEnableOption "tools for developing this monorepo";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.monorepo.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
modules.shell.bazel.enable = true;
|
modules.shell.bazel.enable = true;
|
||||||
modules.shell.python.enable = true;
|
modules.shell.python.enable = true;
|
||||||
modules.shell.rust.enable = true;
|
modules.shell.rust.enable = true;
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
util,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# NetworkManager-managed networking.
|
|
||||||
let
|
|
||||||
networkStatus = pkgs.writeShellScript "waybar-network-status" ''
|
|
||||||
active=$(
|
|
||||||
${lib.getExe' pkgs.networkmanager "nmcli"} -t -f DEVICE,TYPE,STATE,CONNECTION device status \
|
|
||||||
| ${lib.getExe' pkgs.gawk "awk"} -F: '$3 == "connected" && $2 != "loopback" { print; exit }'
|
|
||||||
)
|
|
||||||
|
|
||||||
if [ -z "$active" ]; then
|
|
||||||
${lib.getExe pkgs.jq} -cn \
|
|
||||||
--arg text "${util.glyph "f127"} off" \
|
|
||||||
--arg tooltip "Network disconnected" \
|
|
||||||
--arg class "disconnected" \
|
|
||||||
'{ text: $text, tooltip: $tooltip, class: $class }'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
IFS=: read -r device type _state connection <<< "$active"
|
|
||||||
|
|
||||||
case "$type" in
|
|
||||||
wifi) icon="${util.glyph "f1eb"}" ;;
|
|
||||||
ethernet) icon="LAN" ;;
|
|
||||||
*) icon="NET" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
ipaddr=$(${lib.getExe' pkgs.iproute2 "ip"} -4 -o addr show dev "$device" | ${lib.getExe' pkgs.gawk "awk"} '{ print $4; exit }')
|
|
||||||
|
|
||||||
${lib.getExe pkgs.jq} -cn \
|
|
||||||
--arg text "$icon $connection" \
|
|
||||||
--arg tooltip "$device: ''${ipaddr:-no IPv4}" \
|
|
||||||
--arg class "$type" \
|
|
||||||
'{ text: $text, tooltip: $tooltip, class: $class }'
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.modules.network.enable = lib.mkEnableOption "NetworkManager-managed networking";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.network.enable {
|
|
||||||
networking.networkmanager.enable = true;
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."custom/network" = {
|
|
||||||
order = 60;
|
|
||||||
settings = {
|
|
||||||
return-type = "json";
|
|
||||||
exec = "${networkStatus}";
|
|
||||||
interval = 5;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -4,202 +4,343 @@
|
|||||||
registry,
|
registry,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Configures inbound and outbound SSH access.
|
# SSH on this machine, in both directions.
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.ssh;
|
cfg = config.modules.network.ssh;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
|
||||||
workstationNames = [
|
inherit (lib)
|
||||||
"neogaia"
|
concatLists
|
||||||
"zeus"
|
concatStringsSep
|
||||||
];
|
elem
|
||||||
serverNames = [ "pikachu" ];
|
filter
|
||||||
|
genAttrs
|
||||||
|
hasAttr
|
||||||
|
imap0
|
||||||
|
listToAttrs
|
||||||
|
mapAttrs
|
||||||
|
mapAttrsToList
|
||||||
|
mkIf
|
||||||
|
mkMerge
|
||||||
|
mkOption
|
||||||
|
nameValuePair
|
||||||
|
optional
|
||||||
|
optionalAttrs
|
||||||
|
types
|
||||||
|
unique
|
||||||
|
;
|
||||||
|
|
||||||
isWorkstation = lib.elem config.networking.hostName workstationNames;
|
hostKeySecret = type: "ssh-host-${type}-key";
|
||||||
isServer = lib.elem config.networking.hostName serverNames;
|
userKeySecret = "ssh-user-ed25519-key";
|
||||||
|
|
||||||
clientKeyFor = host: builtins.readFile (../../hosts + "/${host}/ssh_user_ed25519_key.pub");
|
targetType = types.submodule (
|
||||||
hostKeysFor =
|
{ name, ... }:
|
||||||
host:
|
{
|
||||||
map builtins.readFile [
|
options = {
|
||||||
(../../hosts + "/${host}/ssh_host_ed25519_key.pub")
|
hostName = mkOption {
|
||||||
(../../hosts + "/${host}/ssh_host_rsa_key.pub")
|
type = types.str;
|
||||||
];
|
default = name;
|
||||||
|
description = ''
|
||||||
|
The network address OpenSSH connects to for this target.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
managedTargets = lib.genAttrs (workstationNames ++ serverNames) (name: {
|
user = mkOption {
|
||||||
hostName = registry.address.${name}.self;
|
type = types.str;
|
||||||
clientKey = clientKeyFor name;
|
default = config.user.name;
|
||||||
hostKeys = hostKeysFor name;
|
description = ''
|
||||||
});
|
The remote login name OpenSSH uses for this target.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
extraTargets = {
|
port = mkOption {
|
||||||
# TODO: Pin Gitea's host key after migrating it from Proxmox.
|
type = types.port;
|
||||||
gitea = {
|
default = 22;
|
||||||
hostName = "git.alexion.dev";
|
description = ''
|
||||||
port = 2022;
|
The TCP port OpenSSH uses for this target.
|
||||||
user = "gitea";
|
'';
|
||||||
aliases = [
|
};
|
||||||
"gitea"
|
|
||||||
"git.alexion.dev"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mkTarget = name: target: {
|
aliases = mkOption {
|
||||||
hostName = target.hostName or name;
|
type = types.listOf types.str;
|
||||||
user = target.user or user;
|
default = [ name ];
|
||||||
port = target.port or 22;
|
description = ''
|
||||||
aliases = target.aliases or [ name ];
|
Host patterns written into the generated OpenSSH client block.
|
||||||
clientKey = target.clientKey or null;
|
'';
|
||||||
hostKeys = target.hostKeys or [ ];
|
};
|
||||||
};
|
|
||||||
targets = lib.mapAttrs mkTarget (managedTargets // extraTargets);
|
clientKey = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
The public key this target offers when it connects outward.
|
||||||
|
Other machines admit this key according to the host groups below.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hostKeys = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
The public keys this target presents when it accepts inbound SSH.
|
||||||
|
These keys generate system-wide known-host entries.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
keyWithoutComment = key: concatStringsSep " " (lib.take 2 (lib.splitString " " key));
|
||||||
|
|
||||||
|
targetNamesIn = names: filter (name: hasAttr name cfg.targets) names;
|
||||||
|
|
||||||
|
workstationNames = targetNamesIn cfg.hosts.workstations;
|
||||||
|
serverNames = targetNamesIn cfg.hosts.servers;
|
||||||
|
|
||||||
|
clientKeysFor = names: filter (key: key != null) (map (name: cfg.targets.${name}.clientKey) names);
|
||||||
|
|
||||||
|
currentHost = config.networking.hostName;
|
||||||
|
isServer = elem currentHost cfg.hosts.servers;
|
||||||
|
isWorkstation = elem currentHost cfg.hosts.workstations;
|
||||||
|
|
||||||
clientKeysFor = names: lib.filter (key: key != null) (map (name: targets.${name}.clientKey) names);
|
|
||||||
defaultAuthorizedKeys = lib.flatten (
|
defaultAuthorizedKeys = lib.flatten (
|
||||||
clientKeysFor workstationNames ++ lib.optional isServer (clientKeysFor serverNames)
|
clientKeysFor workstationNames
|
||||||
|
++ optional isServer (clientKeysFor serverNames)
|
||||||
);
|
);
|
||||||
|
|
||||||
outboundTargetNames =
|
outboundTargetNames =
|
||||||
let
|
let
|
||||||
groupTargets =
|
groupTargets =
|
||||||
if isServer then
|
if isServer then
|
||||||
builtins.attrNames extraTargets ++ serverNames
|
[ "gitea" ] ++ cfg.hosts.servers
|
||||||
else if isWorkstation then
|
else if isWorkstation then
|
||||||
builtins.attrNames extraTargets ++ serverNames ++ workstationNames
|
[ "gitea" ] ++ cfg.hosts.servers ++ cfg.hosts.workstations
|
||||||
else
|
else
|
||||||
builtins.attrNames extraTargets;
|
[ "gitea" ];
|
||||||
in
|
in
|
||||||
lib.filter (name: name != config.networking.hostName) groupTargets;
|
filter (name: name != currentHost) (targetNamesIn groupTargets);
|
||||||
|
|
||||||
knownHosts = lib.listToAttrs (
|
sshSettingsFor = name:
|
||||||
lib.concatLists (
|
let
|
||||||
lib.mapAttrsToList (
|
target = cfg.targets.${name};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
header = "Host ${concatStringsSep " " target.aliases}";
|
||||||
|
HostName = target.hostName;
|
||||||
|
User = target.user;
|
||||||
|
}
|
||||||
|
// optionalAttrs (target.port != 22) { Port = target.port; };
|
||||||
|
|
||||||
|
knownHostNamesFor = target:
|
||||||
|
let
|
||||||
|
names = unique (target.aliases ++ [ target.hostName ]);
|
||||||
|
withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}";
|
||||||
|
in
|
||||||
|
map withPort names;
|
||||||
|
|
||||||
|
knownHosts = listToAttrs (
|
||||||
|
concatLists (
|
||||||
|
mapAttrsToList (
|
||||||
targetName: target:
|
targetName: target:
|
||||||
lib.imap0 (
|
imap0 (i: key:
|
||||||
i: key:
|
nameValuePair "${targetName}-${toString i}" {
|
||||||
lib.nameValuePair "${targetName}-${toString i}" {
|
|
||||||
hostNames = knownHostNamesFor target;
|
hostNames = knownHostNamesFor target;
|
||||||
publicKey = keyWithoutComment key;
|
publicKey = keyWithoutComment key;
|
||||||
}
|
}
|
||||||
) target.hostKeys
|
) target.hostKeys
|
||||||
) targets
|
) cfg.targets
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
knownHostNamesFor =
|
|
||||||
target:
|
|
||||||
let
|
|
||||||
withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}";
|
|
||||||
names = lib.unique (target.aliases ++ [ target.hostName ]);
|
|
||||||
in
|
|
||||||
map withPort names;
|
|
||||||
|
|
||||||
keyWithoutComment = key: lib.concatStringsSep " " (lib.take 2 (lib.splitString " " key));
|
|
||||||
|
|
||||||
sshSettingsFor =
|
|
||||||
name:
|
|
||||||
let
|
|
||||||
target = targets.${name};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
header = "Host ${lib.concatStringsSep " " target.aliases}";
|
|
||||||
HostName = target.hostName;
|
|
||||||
User = target.user;
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs (target.port != 22) { Port = target.port; };
|
|
||||||
|
|
||||||
hostKeySecret = type: "ssh-host-${type}-key";
|
|
||||||
hostKeyTypes = [
|
|
||||||
"ed25519"
|
|
||||||
"rsa"
|
|
||||||
];
|
|
||||||
userKeySecret = "ssh-user-ed25519-key";
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.network.ssh = {
|
options.modules.network.ssh = {
|
||||||
enable = lib.mkEnableOption "the OpenSSH daemon";
|
enable = lib.mkEnableOption "the OpenSSH daemon, with host keys restored from secrets";
|
||||||
|
|
||||||
hostKeys.restore = lib.mkOption {
|
hosts = {
|
||||||
type = lib.types.bool;
|
servers = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ "pikachu" ];
|
||||||
|
description = ''
|
||||||
|
Hosts that serve durable services.
|
||||||
|
They admit workstation keys and server keys, and they receive aliases for other servers and the forge.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
workstations = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [
|
||||||
|
"neogaia"
|
||||||
|
"zeus"
|
||||||
|
];
|
||||||
|
description = ''
|
||||||
|
Hosts the operator works from.
|
||||||
|
Their keys are admitted by every host, and they receive aliases for the whole fleet and the forge.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
targets = mkOption {
|
||||||
|
type = types.attrsOf targetType;
|
||||||
|
default = {
|
||||||
|
gitea = {
|
||||||
|
hostName = "git.alexion.dev";
|
||||||
|
port = 2022;
|
||||||
|
user = "gitea";
|
||||||
|
aliases = [
|
||||||
|
"gitea"
|
||||||
|
"git.alexion.dev"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
neogaia = {
|
||||||
|
hostName = registry.address.neogaia.self;
|
||||||
|
clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE alexion@neogaia";
|
||||||
|
hostKeys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJS+wp7K123+4BT6G4f954R6WyrbWveY7VlpoBUf6I5p neogaia"
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCo2wWUKxyAS4J5TqbWf8glDhJvS5XmdRqFhMeJwG3pOB+4AccZ1T8LU7ZN+RjtRi3j2qXBJvIHuzhtQNtmT59TxocvfobYiqOgJpvVO5K6yD8ZoUJs6ziDkIduI9w9mdRIESoi+dBbVu8n24r61cKDVh+jWX+yjzkOcWcOzqDyQhhkjqblZ1WMAdujEMuEPvif1i2LCxStUaZqRGcx09m/ME2fYcaJrpuxxxvX2+CPJNicoo6Rx9i7ZjAoNuvH+jui4KT62DzlQtQtCl2CFUOM0gCPSa+MbNQ9elfHPvGzEcwOIMo2cuy9KURUkQu+sAgaG8S1PEniDDTecskHtuRdmPZawnQGpIhzo919Q6wUgjT8scK4mmSXRWmGmkMt0GNA2tfj5tDks6r5Q8XsYqtWs4rsOEvfmxVSdM771w+fqDBAil99Jsh0ksPK9+Bwgg8cMDzLLFDn8JA5y2G1HocMMom+u5DYKwPXEKnCILkasB8y24+O3PhSu1EuWw277w6EUEXvU03rCf0Ak/ULjxp9a00EGlloEwSmFI7Aub9XHDr87IdbGInEn+PMqyBYADiN+3h6nE2JO+nMa6i/CHdebmT+T7YJvuTKHD9sjFmQsYaghlq03DZrhHcm4hgUvE1dqGojHrhk/WgA3EWTWtK/+BP0Vy2jXaaz+qAx+EGnhQ== neogaia"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
zeus = {
|
||||||
|
hostName = registry.address.zeus.self;
|
||||||
|
clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRBcHHuubptXCdvED/p2dR3MpjGN/AimqLasLY5984G alexion@zeus";
|
||||||
|
hostKeys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ4GBPt966LR4LpmXgiMMkepIqNIlXTUBh4zdNeek/lN root@zeus"
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC3xOxGAJCG9PE0FVufgJX9VRO64IUUlRGHSOfYfAI4Mit46xKsr7z/OvqPPE9z8rOIcVPbLkc/mkMuHbDgolleunAFCIeuIIYpkL4o4/TZECh9BtN2WdPhjcD7S0rV+rR7p24Qy5bTCSwZmsQq8QxHTVPjjBngtPGRSpKVrZAuHCcdCJ13hBWAvnaav5KysKC89nMj/7FIh02v8kipGfq5LGCZ20Qp2lmv2DS24NhBPXzcw8pLN47I5GYVkZh/0RWOByteo3eXtKCF+JCwfUPCTP+zHGD+1tbO8//B2fkt3Pe9lUrn5eM2ZnX1emXSWfQng3cMqkftzWqvBwv0sSDU+9YV6H2Ury+GrrfukpBtQVe9PQcBWqbRPhxuagncR+af/tx0ZohZS0t74m3WgLtZ4r2mpTZS4oBmRsfvWzMFGSgwtva+3Y+NsLjPPM1Cz5Dn05JJlidzcABfsUg2H23vkmO2otaQI01dUni+szlRn4w6sVyxNcvY2fQGjmt9k7jmVI5D2A3yl+OLJguAAlw3ia335X1tNUG8FoI/VyZNhOckIihmbTtihNIg37Ro4yiuPRhnUWFvg2hcBCa9hzNP7XDUPnoCRQyJ4adQHQA/x4Kh+HJ6pZqtGcdqU80fzmWtrLkWG6v5/NRKs6soYDup7oQIKbJo9TtXvqtzmVmovQ== root@zeus"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
pikachu = {
|
||||||
|
hostName = registry.address.pikachu.self;
|
||||||
|
clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNqJIC6VRXyvrNf3n9su9KdPCikC3CjK/QrCK2reHdB alexion@pikachu";
|
||||||
|
hostKeys = [
|
||||||
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKljRf4pJO+pqEqjpPz08gOYq3g1PpxvE66xVw7uMEnA root@pikachu"
|
||||||
|
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCy/riwm7dflA3mT+3a0/2CIoS2LbAsK/vn35kOoNeuzn0yhiF+imexP6tkB3S2t+H5ybRzkbbuNZcynFfeCqthFc8kvbdCnt8Diqoeg96fZ6ecvh5QE5yH9op8534EySetZ/exakFLnF+6EiWMuWUW3DFwsc2kcgDJObqSE8gTx/d7JK953MiTFmSJBFyg1RtQ3ZnMT+iCrvY2dyCLQai7VeF8koVKF2c0leAq2Hc75rb/L9md8MoJa64iPiz7hwTCin3xoFyaY/5hNVvyqFd5PivgR69gLdJkuVsUYO2mJzhur8cYmJD+pGjJ0U45hyE9TMrCFjeJHHuvSt3+2kph62wv95jLNk0WmMlwgyunISxENCSVVtNYdBMXhUh8VhEAW17QpVUg9EnPvxOdTKEjrvfOZYASWUa51JKbgBgexVgFbxdjDZR88DZa31AVBts/cx/59gXTUahFXMYLdZgssx+5uibZQWnvCyfUV9WLbfmK1lgL6hzReg1VkQ87iGr6skjtQYemJxRaFNA1+Q5f3kmG3KncuK/594a3qXYP4gC6A2blf8om1YZ4aXXh6f+GFKLjoEw1vvM2rJ+rjzfymwDX+pxVQ9L13OEtVZc9Ez76pOkbm1hqdbL0gY45+0cpxodhWV0wMQJBDXL1MHP8qcs+/vw0GxVK5l1SnWBGlw== root@pikachu"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
SSH targets known to the fleet.
|
||||||
|
The inventory holds connection details plus public keys used for authorization and host verification.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraAuthorizedKeys = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = ''
|
||||||
|
Additional client public keys admitted by this host.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
authorizedKeys = mkOption {
|
||||||
|
type = types.nullOr (types.listOf types.str);
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Complete override for client public keys admitted by this host.
|
||||||
|
Leave null to derive access from `modules.network.ssh.hosts` and `modules.network.ssh.targets`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraSettings = mkOption {
|
||||||
|
type = types.attrsOf types.anything;
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Additional OpenSSH client settings merged into the generated Home Manager configuration.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hostKeys.restore = mkOption {
|
||||||
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = ''
|
description = ''
|
||||||
Restores host keys from secrets.
|
Restore the host keys from secrets rather than letting the daemon generate its own.
|
||||||
Preserves the fingerprint across reimages.
|
A machine with its own identity keeps its fingerprint across a reimage by restoring committed keys.
|
||||||
Guests disable this option and generate their own host keys.
|
A guest carries no host identity, so it turns this off and presents a self-generated key instead.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
hostKeys.sopsFile = lib.mkOption {
|
hostKeys.sopsFile = mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = types.nullOr types.path;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Sets encrypted file that contains host private keys.
|
Encrypted file holding this host's SSH host private keys, one entry per key type, named `ssh-host-<type>-key`.
|
||||||
Names each secret `ssh-host-<type>-key`.
|
Required when `restore` is on.
|
||||||
Requires a value when `restore` is enabled.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
userKey.sopsFile = lib.mkOption {
|
hostKeys.types = mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = types.listOf types.str;
|
||||||
|
default = [
|
||||||
|
"ed25519"
|
||||||
|
"rsa"
|
||||||
|
];
|
||||||
|
description = ''
|
||||||
|
Key types to restore, naming both the entries read from the encrypted file and the algorithms the daemon offers.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
userKey.sopsFile = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Sets encrypted file that contains the SSH client private key.
|
Encrypted file holding this machine's SSH client private key, under the entry `ssh-user-ed25519-key`.
|
||||||
Names the secret `ssh-user-ed25519-key`.
|
Left unset on a machine that authenticates to no remote server, such as a guest.
|
||||||
Guests leave this unset because they do not authenticate to remote servers.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (
|
config = mkIf cfg.enable (
|
||||||
lib.mkMerge [
|
mkMerge [
|
||||||
{
|
{
|
||||||
services.openssh.enable = true;
|
services.openssh.enable = true;
|
||||||
|
|
||||||
users.users.${user}.openssh.authorizedKeys.keys = defaultAuthorizedKeys;
|
users.users.${user}.openssh.authorizedKeys.keys =
|
||||||
|
if cfg.authorizedKeys != null then
|
||||||
|
cfg.authorizedKeys
|
||||||
|
else
|
||||||
|
defaultAuthorizedKeys ++ cfg.extraAuthorizedKeys;
|
||||||
|
|
||||||
programs.ssh.knownHosts = knownHosts;
|
programs.ssh.knownHosts = knownHosts;
|
||||||
|
|
||||||
home-manager.users.${user}.programs.ssh = {
|
home-manager.users.${user}.programs.ssh = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enableDefaultConfig = false;
|
enableDefaultConfig = false;
|
||||||
settings = lib.mapAttrs (name: _: sshSettingsFor name) (
|
settings =
|
||||||
lib.genAttrs outboundTargetNames (name: name)
|
mapAttrs (name: _: sshSettingsFor name) (genAttrs outboundTargetNames (name: name))
|
||||||
);
|
// cfg.extraSettings;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
(lib.mkIf cfg.hostKeys.restore {
|
(mkIf cfg.hostKeys.restore {
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = cfg.hostKeys.sopsFile != null;
|
assertion = cfg.hostKeys.sopsFile != null;
|
||||||
message = "Set `modules.network.ssh.hostKeys.sopsFile` when host-key restoration is enabled.";
|
message = "modules.network.ssh.hostKeys.restore requires modules.network.ssh.hostKeys.sopsFile to name the encrypted host keys.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
sops.secrets = lib.genAttrs (map hostKeySecret hostKeyTypes) (_: {
|
sops.secrets = genAttrs (map hostKeySecret cfg.hostKeys.types) (_: {
|
||||||
inherit (cfg.hostKeys) sopsFile;
|
inherit (cfg.hostKeys) sopsFile;
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
restartUnits = [ "sshd.service" ];
|
restartUnits = [ "sshd.service" ];
|
||||||
});
|
});
|
||||||
|
|
||||||
services.openssh.hostKeys = [ ];
|
services.openssh.hostKeys = [ ];
|
||||||
services.openssh.extraConfig = lib.concatStringsSep "" (
|
services.openssh.extraConfig = concatStringsSep "" (
|
||||||
map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") hostKeyTypes
|
map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") cfg.hostKeys.types
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
(lib.mkIf (cfg.userKey.sopsFile != null) {
|
(mkIf (cfg.userKey.sopsFile != null) {
|
||||||
sops.secrets.${userKeySecret} = {
|
sops.secrets.${userKeySecret} = {
|
||||||
inherit (cfg.userKey) sopsFile;
|
inherit (cfg.userKey) sopsFile;
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = user;
|
owner = user;
|
||||||
};
|
};
|
||||||
|
|
||||||
home-manager.users.${user}.programs.ssh.settings."*" = {
|
home-manager.users.${user}.programs.ssh.settings."*".IdentityFile =
|
||||||
IdentityFile = config.sops.secrets.${userKeySecret}.path;
|
config.sops.secrets.${userKeySecret}.path;
|
||||||
IdentitiesOnly = true;
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,17 +4,24 @@
|
|||||||
util,
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Configures bridges for tagged VLANs on a physical interface that uses DHCP.
|
# The host networking foundation: per-VLAN bridges over a tagged trunk.
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.vlans;
|
cfg = config.modules.network.vlans;
|
||||||
|
|
||||||
|
inherit (util) bridgeName;
|
||||||
|
|
||||||
|
vlanName = id: "${cfg.trunk}.${toString id}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.network.vlans = {
|
options.modules.network.vlans = {
|
||||||
interface = lib.mkOption {
|
enable = lib.mkEnableOption "host VLAN bridges over a tagged trunk";
|
||||||
|
|
||||||
|
trunk = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "enp1s0";
|
example = "enp1s0";
|
||||||
description = ''
|
description = ''
|
||||||
Names the physical interface that carries untagged DHCP traffic and tagged VLANs.
|
Physical interface that carries 802.1Q-tagged traffic for every VLAN.
|
||||||
|
Each declared VLAN attaches to its own bridge.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,37 +34,66 @@ in
|
|||||||
30
|
30
|
||||||
];
|
];
|
||||||
description = ''
|
description = ''
|
||||||
Creates bridges for the specified tagged VLAN IDs.
|
Tagged VLAN ids to materialize.
|
||||||
|
Each id N creates one bridge named `br-vlanN`.
|
||||||
VLAN ID N creates bridge `br-vlanN`.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
management = {
|
||||||
|
vlan = lib.mkOption {
|
||||||
|
type = lib.types.nullOr (lib.types.ints.between 1 4094);
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
VLAN whose bridge carries the host's own management address.
|
||||||
|
The value must be one of `ids`.
|
||||||
|
Left null, the host takes no address on any bridge.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
address = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "10.0.10.2/24";
|
||||||
|
description = ''
|
||||||
|
Host static management address in CIDR form.
|
||||||
|
Left null, the host takes its management address by DHCP.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf (cfg.ids != [ ]) {
|
config = lib.mkIf cfg.enable {
|
||||||
networking = {
|
assertions = [
|
||||||
useDHCP = false;
|
{
|
||||||
networkmanager.unmanaged = [
|
assertion = cfg.management.vlan == null || lib.elem cfg.management.vlan cfg.ids;
|
||||||
"interface-name:${cfg.interface}"
|
message = "modules.network.vlans.management.vlan (${toString cfg.management.vlan}) must name one of modules.network.vlans.ids.";
|
||||||
]
|
}
|
||||||
++ map (id: "interface-name:${util.bridgeName id}") cfg.ids
|
{
|
||||||
++ map (id: "interface-name:vlan${toString id}") cfg.ids;
|
assertion = cfg.management.address == null || cfg.management.vlan != null;
|
||||||
};
|
message = "modules.network.vlans.management.address needs modules.network.vlans.management.vlan to say which bridge carries it.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
# networkd owns these interfaces so NetworkManager does not contend for them.
|
||||||
|
networking.networkmanager.unmanaged =
|
||||||
|
[ "interface-name:${cfg.trunk}" ]
|
||||||
|
++ map (id: "interface-name:${bridgeName id}") cfg.ids
|
||||||
|
++ map (id: "interface-name:${vlanName id}") cfg.ids;
|
||||||
|
|
||||||
systemd.network = {
|
systemd.network = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
netdevs = lib.mkMerge (
|
netdevs = lib.mkMerge (
|
||||||
map (id: {
|
map (id: {
|
||||||
"40-${util.bridgeName id}" = {
|
"40-${bridgeName id}" = {
|
||||||
netdevConfig = {
|
netdevConfig = {
|
||||||
Name = util.bridgeName id;
|
Name = bridgeName id;
|
||||||
Kind = "bridge";
|
Kind = "bridge";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"40-vlan${toString id}" = {
|
"40-${vlanName id}" = {
|
||||||
netdevConfig = {
|
netdevConfig = {
|
||||||
Name = "vlan${toString id}";
|
Name = vlanName id;
|
||||||
Kind = "vlan";
|
Kind = "vlan";
|
||||||
};
|
};
|
||||||
vlanConfig.Id = id;
|
vlanConfig.Id = id;
|
||||||
@@ -67,25 +103,37 @@ in
|
|||||||
|
|
||||||
networks = lib.mkMerge (
|
networks = lib.mkMerge (
|
||||||
[
|
[
|
||||||
|
# The trunk carries only tagged frames and takes no address.
|
||||||
{
|
{
|
||||||
"10-${cfg.interface}" = {
|
"30-${cfg.trunk}" = {
|
||||||
matchConfig.Name = cfg.interface;
|
matchConfig.Name = cfg.trunk;
|
||||||
networkConfig.DHCP = "yes";
|
networkConfig.LinkLocalAddressing = "no";
|
||||||
vlan = map (id: "vlan${toString id}") cfg.ids;
|
linkConfig.RequiredForOnline = "no";
|
||||||
linkConfig.RequiredForOnline = "routable";
|
vlan = map vlanName cfg.ids;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
++ map (id: {
|
++ map (id: {
|
||||||
"40-vlan${toString id}" = {
|
# Enslaved to its bridge, carrying no address itself.
|
||||||
matchConfig.Name = "vlan${toString id}";
|
"40-${vlanName id}" = {
|
||||||
networkConfig.Bridge = util.bridgeName id;
|
matchConfig.Name = vlanName id;
|
||||||
linkConfig.RequiredForOnline = "no";
|
networkConfig.Bridge = bridgeName id;
|
||||||
};
|
|
||||||
"40-${util.bridgeName id}" = {
|
|
||||||
matchConfig.Name = util.bridgeName id;
|
|
||||||
linkConfig.RequiredForOnline = "no";
|
linkConfig.RequiredForOnline = "no";
|
||||||
};
|
};
|
||||||
|
# The management bridge carries the host address.
|
||||||
|
# Other bridges provide L2 segments for guests.
|
||||||
|
"40-${bridgeName id}" = lib.mkMerge [
|
||||||
|
{
|
||||||
|
matchConfig.Name = bridgeName id;
|
||||||
|
linkConfig.RequiredForOnline = "no";
|
||||||
|
}
|
||||||
|
(lib.mkIf (cfg.management.vlan == id) (
|
||||||
|
if cfg.management.address == null then
|
||||||
|
{ networkConfig.DHCP = "yes"; }
|
||||||
|
else
|
||||||
|
{ address = [ cfg.management.address ]; }
|
||||||
|
))
|
||||||
|
];
|
||||||
}) cfg.ids
|
}) cfg.ids
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,34 +1,10 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.vpn-client;
|
cfg = config.modules.network.vpn-client;
|
||||||
|
|
||||||
vpnIcon = util.glyph [
|
|
||||||
"db83"
|
|
||||||
"dc9d"
|
|
||||||
];
|
|
||||||
|
|
||||||
vpnStatus = pkgs.writeShellScript "waybar-vpn-status" ''
|
|
||||||
interface=${lib.escapeShellArg cfg.interface}
|
|
||||||
|
|
||||||
if ! ${lib.getExe' pkgs.iproute2 "ip"} link show dev "$interface" >/dev/null 2>&1; then
|
|
||||||
printf '{"text":"","class":"idle"}\n'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
ipaddr=$(${lib.getExe' pkgs.iproute2 "ip"} -4 -o addr show dev "$interface" | ${lib.getExe' pkgs.gawk "awk"} '{ print $4; exit }')
|
|
||||||
|
|
||||||
${lib.getExe pkgs.jq} -cn \
|
|
||||||
--arg text "${vpnIcon}" \
|
|
||||||
--arg tooltip "VPN $interface: ''${ipaddr:-no IPv4}" \
|
|
||||||
--arg class "active" \
|
|
||||||
'{ text: $text, tooltip: $tooltip, class: $class }'
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.network.vpn-client = {
|
options.modules.network.vpn-client = {
|
||||||
@@ -37,31 +13,61 @@ in
|
|||||||
interface = lib.mkOption {
|
interface = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "wg-home";
|
default = "wg-home";
|
||||||
description = "Names the WireGuard interface.";
|
description = "WireGuard interface name.";
|
||||||
};
|
};
|
||||||
|
|
||||||
address = lib.mkOption {
|
address = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "10.23.100.3/32";
|
example = "10.23.100.3/32";
|
||||||
description = "Sets the client tunnel address in CIDR notation.";
|
description = "Client tunnel address in CIDR notation.";
|
||||||
|
};
|
||||||
|
|
||||||
|
privateKeySecret = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "wireguard-home-private-key";
|
||||||
|
description = "SOPS secret containing the WireGuard client private key.";
|
||||||
|
};
|
||||||
|
|
||||||
|
autoStart = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Whether to start the VPN tunnel during boot.";
|
||||||
|
};
|
||||||
|
|
||||||
|
mtu = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.int;
|
||||||
|
default = 1280;
|
||||||
|
description = "Tunnel MTU, or null to use the WireGuard default.";
|
||||||
};
|
};
|
||||||
|
|
||||||
sopsFile = lib.mkOption {
|
sopsFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = lib.types.nullOr lib.types.path;
|
||||||
default = null;
|
default = null;
|
||||||
description = "Names the encrypted file that contains the WireGuard private key.";
|
description = "Encrypted secrets file that contains privateKeySecret.";
|
||||||
};
|
};
|
||||||
|
|
||||||
peer = {
|
peer = {
|
||||||
publicKey = lib.mkOption {
|
publicKey = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Sets the WireGuard public key for the remote peer.";
|
description = "WireGuard public key for the remote peer.";
|
||||||
};
|
};
|
||||||
|
|
||||||
endpoint = lib.mkOption {
|
endpoint = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "203.0.113.10:51820";
|
example = "203.0.113.10:51820";
|
||||||
description = "Sets the WireGuard endpoint for the remote peer.";
|
description = "WireGuard endpoint for the remote peer.";
|
||||||
|
};
|
||||||
|
|
||||||
|
allowedIPs = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ "10.23.0.0/16" ];
|
||||||
|
description = "Routes to send through the VPN tunnel.";
|
||||||
|
};
|
||||||
|
|
||||||
|
persistentKeepalive = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.int;
|
||||||
|
default = 25;
|
||||||
|
description = "Persistent keepalive interval in seconds, or null to disable it.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -70,39 +76,28 @@ in
|
|||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = cfg.sopsFile != null;
|
assertion = cfg.sopsFile != null;
|
||||||
message = "Set modules.network.vpn-client.sopsFile when the VPN client is enabled.";
|
message = "modules.network.vpn-client.sopsFile must name the encrypted secrets file containing the WireGuard private key.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
sops.secrets.wireguard-home-private-key.sopsFile = cfg.sopsFile;
|
sops.secrets.${cfg.privateKeySecret}.sopsFile = cfg.sopsFile;
|
||||||
|
|
||||||
registry.shell-shortcut.${config.networking.hostName} = {
|
systemd.targets."wireguard-${cfg.interface}" = lib.mkIf (!cfg.autoStart) {
|
||||||
vpn-on = "sudo systemctl start wireguard-${cfg.interface}.target";
|
wantedBy = lib.mkForce [ ];
|
||||||
vpn-off = "sudo systemctl stop 'wireguard-${cfg.interface}*.service' wireguard-${cfg.interface}.target";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."custom/vpn" = {
|
|
||||||
order = 50;
|
|
||||||
settings = {
|
|
||||||
return-type = "json";
|
|
||||||
exec = "${vpnStatus}";
|
|
||||||
interval = 5;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.targets."wireguard-${cfg.interface}".wantedBy = lib.mkForce [ ];
|
|
||||||
|
|
||||||
networking.wireguard.interfaces.${cfg.interface} = {
|
networking.wireguard.interfaces.${cfg.interface} = {
|
||||||
ips = [ cfg.address ];
|
ips = [ cfg.address ];
|
||||||
mtu = 1280;
|
mtu = cfg.mtu;
|
||||||
privateKeyFile = config.sops.secrets.wireguard-home-private-key.path;
|
privateKeyFile = config.sops.secrets.${cfg.privateKeySecret}.path;
|
||||||
|
|
||||||
peers = [
|
peers = [
|
||||||
{
|
({
|
||||||
inherit (cfg.peer) publicKey endpoint;
|
inherit (cfg.peer) publicKey endpoint allowedIPs;
|
||||||
allowedIPs = [ "10.23.0.0/16" ];
|
|
||||||
persistentKeepalive = 25;
|
|
||||||
}
|
}
|
||||||
|
// lib.optionalAttrs (cfg.peer.persistentKeepalive != null) {
|
||||||
|
inherit (cfg.peer) persistentKeepalive;
|
||||||
|
})
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
62
configs/nixos/modules/services/actualbudget.nix
Normal file
62
configs/nixos/modules/services/actualbudget.nix
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.modules.services.actualbudget;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.services.actualbudget = {
|
||||||
|
enable = lib.mkEnableOption "Actual Budget";
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 5006;
|
||||||
|
description = "Port Actual Budget listens on.";
|
||||||
|
};
|
||||||
|
|
||||||
|
dataDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/data";
|
||||||
|
description = "Directory where Actual Budget stores server and user files.";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "actual";
|
||||||
|
description = "User account that runs Actual Budget.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "storage";
|
||||||
|
description = "Group account that owns Actual Budget's writable state.";
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkMerge [
|
||||||
|
{
|
||||||
|
registry."stable-uid".${config.networking.hostName}.actualbudget = 20001;
|
||||||
|
}
|
||||||
|
|
||||||
|
(lib.mkIf cfg.enable {
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
uid = config.registry."stable-uid".${config.networking.hostName}.actualbudget;
|
||||||
|
group = cfg.group;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.actual = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
user = cfg.user;
|
||||||
|
group = cfg.group;
|
||||||
|
settings = {
|
||||||
|
inherit (cfg) port dataDir;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -2,135 +2,138 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.copyparty;
|
cfg = config.modules.services.copyparty;
|
||||||
|
|
||||||
accountName = config.user.name;
|
volumeType = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
path = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Filesystem path served by this volume.";
|
||||||
|
};
|
||||||
|
|
||||||
serviceGroup = "storage";
|
access = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
||||||
|
default = { };
|
||||||
|
example = {
|
||||||
|
A = [ "alexion" ];
|
||||||
|
};
|
||||||
|
description = "Copyparty access map from permission string to account names.";
|
||||||
|
};
|
||||||
|
|
||||||
serviceStateDir = "/var/lib/copyparty";
|
flags = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
serviceUser = "copyparty";
|
default = [ ];
|
||||||
|
example = [ "dthumb" ];
|
||||||
writableVolumes = lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes);
|
description = "Copyparty volume flags.";
|
||||||
|
};
|
||||||
port = 3923;
|
};
|
||||||
defaultAccess = {
|
|
||||||
A = [ accountName ];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultWritableFlags = {
|
renderAccess = permission: accounts: " ${permission}: ${lib.concatStringsSep ", " accounts}";
|
||||||
d2d = true;
|
|
||||||
d2t = true;
|
|
||||||
dthumb = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
credentialHash = builtins.substring 0 16 (builtins.hashString "sha256" accountName);
|
hasFlag = name: flags: lib.any (flag: lib.hasPrefix "${name}:" flag || lib.hasPrefix "${name}=" flag) flags;
|
||||||
|
|
||||||
credentialName = "password-${credentialHash}";
|
defaultPermissionFlags = flags:
|
||||||
|
lib.optionals (!(hasFlag "chmod_d" flags)) [ "chmod_d: 2770" ]
|
||||||
|
++ lib.optionals (!(hasFlag "chmod_f" flags)) [ "chmod_f: 660" ];
|
||||||
|
|
||||||
passwordPlaceholder = "@COPYPARTY_PASSWORD_${credentialHash}@";
|
renderFlags = flags: lib.concatMapStringsSep "\n" (flag: " ${flag}") (flags ++ defaultPermissionFlags flags);
|
||||||
|
|
||||||
globalSettings = {
|
renderVolume = urlPath: volume: ''
|
||||||
i = "0.0.0.0";
|
[${urlPath}]
|
||||||
usernames = true;
|
${volume.path}
|
||||||
"http-only" = true;
|
accs:
|
||||||
"no-crt" = true;
|
${lib.concatStringsSep "\n" (lib.mapAttrsToList renderAccess volume.access)}
|
||||||
"no-thumb" = true;
|
${lib.optionalString (volume.flags != [ ]) ''
|
||||||
"no-snap" = true;
|
flags:
|
||||||
"no-rescan" = true;
|
${renderFlags volume.flags}
|
||||||
"ses-db" = "${serviceStateDir}/sessions.db";
|
''}
|
||||||
"chpw-db" = "${serviceStateDir}/chpw.json";
|
'';
|
||||||
"shr-db" = "${serviceStateDir}/shares.db";
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs cfg.reverseProxy.enable {
|
|
||||||
rproxy = 1;
|
|
||||||
"xff-hdr" = "x-forwarded-for";
|
|
||||||
};
|
|
||||||
|
|
||||||
globalExtraConfig = lib.optionalString cfg.reverseProxy.enable (
|
renderTrustedSource = source: " xff-src: ${source}";
|
||||||
lib.concatMapStringsSep "\n" (source: "xff-src: ${source}") cfg.reverseProxy.trustedSources
|
|
||||||
);
|
|
||||||
|
|
||||||
volumeFlags =
|
renderedReverseProxy = lib.optionalString cfg.reverseProxy.enable ''
|
||||||
flags:
|
rproxy: 1
|
||||||
lib.optionalAttrs (flags != { }) (
|
xff-hdr: x-forwarded-for
|
||||||
{
|
${lib.concatStringsSep "\n" (map renderTrustedSource cfg.reverseProxy.trustedSources)}
|
||||||
chmod_d = "2770";
|
'';
|
||||||
chmod_f = "660";
|
|
||||||
}
|
|
||||||
// flags
|
|
||||||
);
|
|
||||||
|
|
||||||
renderedVolumes = lib.mapAttrs (_urlPath: volume: {
|
renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes);
|
||||||
inherit (volume) path;
|
|
||||||
access = if volume.readOnly then { r = [ accountName ]; } else defaultAccess // volume.access;
|
|
||||||
flags = volumeFlags (lib.optionalAttrs (!volume.readOnly) defaultWritableFlags // volume.flags);
|
|
||||||
}) cfg.volumes;
|
|
||||||
|
|
||||||
configText = util.formats.copyparty.render {
|
configScript = pkgs.writeShellScript "copyparty-config" ''
|
||||||
inherit globalExtraConfig;
|
set -eu
|
||||||
accounts.${accountName} = passwordPlaceholder;
|
|
||||||
global = globalSettings;
|
|
||||||
volumes = renderedVolumes;
|
|
||||||
};
|
|
||||||
|
|
||||||
configFile = pkgs.writeText "copyparty.conf" configText;
|
umask 077
|
||||||
|
password="$(${pkgs.coreutils}/bin/tr -d '\n' < "$CREDENTIALS_DIRECTORY/password")"
|
||||||
|
|
||||||
replacePassword = ''
|
cat > /run/copyparty/copyparty.conf <<EOF
|
||||||
${lib.getExe pkgs.replace-secret} ${lib.escapeShellArg passwordPlaceholder} "$CREDENTIALS_DIRECTORY/${credentialName}" /run/copyparty/copyparty.conf
|
[global]
|
||||||
|
p: ${toString cfg.port}
|
||||||
|
usernames
|
||||||
|
http-only
|
||||||
|
no-crt
|
||||||
|
no-thumb
|
||||||
|
no-snap
|
||||||
|
no-rescan
|
||||||
|
ses-db: ${cfg.stateDir}/sessions.db
|
||||||
|
chpw-db: ${cfg.stateDir}/chpw.json
|
||||||
|
shr-db: ${cfg.stateDir}/shares.db
|
||||||
|
${renderedReverseProxy}
|
||||||
|
[accounts]
|
||||||
|
${cfg.accountName}: $password
|
||||||
|
|
||||||
|
${renderedVolumes}
|
||||||
|
EOF
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.copyparty = {
|
options.modules.services.copyparty = {
|
||||||
enable = lib.mkEnableOption "Copyparty";
|
enable = lib.mkEnableOption "Copyparty";
|
||||||
|
|
||||||
passwordFile = lib.mkOption {
|
package = lib.mkPackageOption pkgs "copyparty-min" { };
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 3923;
|
||||||
|
description = "Port Copyparty listens on.";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Sets the runtime file that contains the Copyparty account password.";
|
default = "copyparty";
|
||||||
|
description = "User account that runs Copyparty.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "storage";
|
||||||
|
description = "Group account that owns Copyparty's writable state and served files.";
|
||||||
|
};
|
||||||
|
|
||||||
|
accountName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "alexion";
|
||||||
|
description = "Copyparty account name configured for password authentication.";
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "Runtime file containing the Copyparty account password.";
|
||||||
|
};
|
||||||
|
|
||||||
|
stateDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/var/lib/copyparty";
|
||||||
|
description = "Directory where Copyparty stores service state.";
|
||||||
};
|
};
|
||||||
|
|
||||||
volumes = lib.mkOption {
|
volumes = lib.mkOption {
|
||||||
type = lib.types.attrsOf (
|
type = lib.types.attrsOf volumeType;
|
||||||
lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
path = lib.mkOption {
|
|
||||||
type = lib.types.externalPath;
|
|
||||||
description = "Sets the filesystem path served by this volume.";
|
|
||||||
};
|
|
||||||
|
|
||||||
access = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
|
||||||
default = { };
|
|
||||||
example = {
|
|
||||||
A = [ "alexion" ];
|
|
||||||
};
|
|
||||||
description = "Maps Copyparty permission strings to account names.";
|
|
||||||
};
|
|
||||||
|
|
||||||
readOnly = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
description = "Serves this volume read-only.";
|
|
||||||
};
|
|
||||||
|
|
||||||
flags = lib.mkOption {
|
|
||||||
type = lib.types.attrs;
|
|
||||||
default = { };
|
|
||||||
example = {
|
|
||||||
dthumb = true;
|
|
||||||
};
|
|
||||||
description = "Sets Copyparty volume flags.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
default = { };
|
default = { };
|
||||||
description = "Defines Copyparty volumes by URL path.";
|
description = "Copyparty volumes keyed by their URL path.";
|
||||||
};
|
};
|
||||||
|
|
||||||
reverseProxy = {
|
reverseProxy = {
|
||||||
@@ -139,7 +142,7 @@ in
|
|||||||
trustedSources = lib.mkOption {
|
trustedSources = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = "Lists proxy sources that can set Copyparty's forwarded client IP header.";
|
description = "IP ranges that may supply Copyparty's forwarded client IP header.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -161,49 +164,43 @@ in
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
users.users.${serviceUser} = {
|
users.users.${cfg.user} = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
uid = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
uid = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||||
group = serviceGroup;
|
group = cfg.group;
|
||||||
home = serviceStateDir;
|
home = cfg.stateDir;
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.copyparty = {
|
systemd.services.copyparty = {
|
||||||
description = "Copyparty file server";
|
description = "Copyparty file server";
|
||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
preStart = ''
|
|
||||||
${lib.getExe' pkgs.coreutils "install"} -m 600 ${configFile} /run/copyparty/copyparty.conf
|
|
||||||
${replacePassword}
|
|
||||||
'';
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ReloadSignal = "SIGUSR1";
|
ReloadSignal = "SIGUSR1";
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
User = serviceUser;
|
User = cfg.user;
|
||||||
Group = serviceGroup;
|
Group = cfg.group;
|
||||||
WorkingDirectory = serviceStateDir;
|
WorkingDirectory = cfg.stateDir;
|
||||||
Environment = [
|
Environment = [
|
||||||
"HOME=${serviceStateDir}"
|
"HOME=${cfg.stateDir}"
|
||||||
"XDG_CONFIG_HOME=${serviceStateDir}"
|
"XDG_CONFIG_HOME=${cfg.stateDir}"
|
||||||
];
|
];
|
||||||
StateDirectory = "copyparty";
|
StateDirectory = "copyparty";
|
||||||
RuntimeDirectory = "copyparty";
|
RuntimeDirectory = "copyparty";
|
||||||
RuntimeDirectoryMode = "0700";
|
RuntimeDirectoryMode = "0700";
|
||||||
LoadCredential = [ "${credentialName}:${cfg.passwordFile}" ];
|
LoadCredential = [ "password:${cfg.passwordFile}" ];
|
||||||
ExecStart = "${lib.getExe pkgs.copyparty-min} -c /run/copyparty/copyparty.conf";
|
ExecStartPre = configScript;
|
||||||
|
ExecStart = "${lib.getExe cfg.package} -c /run/copyparty/copyparty.conf";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
NoNewPrivileges = true;
|
NoNewPrivileges = true;
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
ProtectSystem = "strict";
|
ProtectSystem = "strict";
|
||||||
ProtectHome = true;
|
ProtectHome = true;
|
||||||
ReadWritePaths = [
|
ReadWritePaths = [ cfg.stateDir ] ++ map (volume: volume.path) (lib.attrValues cfg.volumes);
|
||||||
serviceStateDir
|
|
||||||
]
|
|
||||||
++ map (volume: volume.path) writableVolumes;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ port ];
|
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,12 +56,6 @@
|
|||||||
description = "Advertise the server in Palworld's public lobby.";
|
description = "Advertise the server in Palworld's public lobby.";
|
||||||
};
|
};
|
||||||
|
|
||||||
autoStart = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
description = "Start the Palworld server after the guest reaches network readiness.";
|
|
||||||
};
|
|
||||||
|
|
||||||
rcon = {
|
rcon = {
|
||||||
enable = lib.mkEnableOption "Palworld RCON" // {
|
enable = lib.mkEnableOption "Palworld RCON" // {
|
||||||
default = true;
|
default = true;
|
||||||
@@ -92,33 +86,33 @@
|
|||||||
startScript = pkgs.writeShellScript "palworld-start" ''
|
startScript = pkgs.writeShellScript "palworld-start" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
${lib.getExe' pkgs.coreutils "install"} -d \
|
${pkgs.coreutils}/bin/install -d \
|
||||||
-m 0770 \
|
-m 0770 \
|
||||||
${lib.escapeShellArg service.stateDir} \
|
${lib.escapeShellArg service.stateDir} \
|
||||||
${lib.escapeShellArg (builtins.dirOf savedDir)}
|
${lib.escapeShellArg (builtins.dirOf savedDir)}
|
||||||
|
|
||||||
if [ -L ${lib.escapeShellArg savedDir} ]; then
|
if [ -L ${lib.escapeShellArg savedDir} ]; then
|
||||||
current_target="$(${lib.getExe' pkgs.coreutils "readlink"} ${lib.escapeShellArg savedDir})"
|
current_target="$(${pkgs.coreutils}/bin/readlink ${lib.escapeShellArg savedDir})"
|
||||||
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
|
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
|
||||||
${lib.getExe' pkgs.coreutils "rm"} ${lib.escapeShellArg savedDir}
|
${pkgs.coreutils}/bin/rm ${lib.escapeShellArg savedDir}
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then
|
if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then
|
||||||
if ${lib.getExe' pkgs.findutils "find"} ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${lib.getExe' pkgs.gnugrep "grep"} -q .; then
|
if ${pkgs.findutils}/bin/find ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${pkgs.gnugrep}/bin/grep -q .; then
|
||||||
${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
|
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
|
||||||
else
|
else
|
||||||
${lib.getExe' pkgs.coreutils "cp"} -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
|
${pkgs.coreutils}/bin/cp -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
|
||||||
${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
|
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -e ${lib.escapeShellArg savedDir} ]; then
|
if [ ! -e ${lib.escapeShellArg savedDir} ]; then
|
||||||
${lib.getExe' pkgs.coreutils "ln"} -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
|
${pkgs.coreutils}/bin/ln -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||||
admin_password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
|
admin_password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
|
||||||
|
|
||||||
exec ./PalServer.sh \
|
exec ./PalServer.sh \
|
||||||
"$@" \
|
"$@" \
|
||||||
@@ -132,25 +126,22 @@
|
|||||||
inherit (service) steamDir installDir stateDir;
|
inherit (service) steamDir installDir stateDir;
|
||||||
serviceName = "palworld-${lib.toLower service.serverName}";
|
serviceName = "palworld-${lib.toLower service.serverName}";
|
||||||
appId = 2394010;
|
appId = 2394010;
|
||||||
inherit (service) autoStart;
|
|
||||||
executable = "${startScript}";
|
executable = "${startScript}";
|
||||||
arguments = [
|
arguments =
|
||||||
"-useperfthreads"
|
[
|
||||||
"-NoAsyncLoadingThread"
|
"-useperfthreads"
|
||||||
"-UseMultithreadForDS"
|
"-NoAsyncLoadingThread"
|
||||||
"-port=${toString service.port}"
|
"-UseMultithreadForDS"
|
||||||
"-publicport=${toString service.port}"
|
"-port=${toString service.port}"
|
||||||
"-servername=${service.serverName}"
|
"-publicport=${toString service.port}"
|
||||||
"-players=${toString service.maxPlayers}"
|
"-servername=${service.serverName}"
|
||||||
]
|
"-players=${toString service.maxPlayers}"
|
||||||
++ lib.optional service.publicLobby "-publiclobby"
|
]
|
||||||
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
|
++ lib.optional service.publicLobby "-publiclobby"
|
||||||
++ lib.optionals service.rcon.enable [
|
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
|
||||||
"-rcon"
|
++ lib.optional service.rcon.enable "-rcon";
|
||||||
"-RCONPort=${toString service.rcon.port}"
|
|
||||||
];
|
|
||||||
udpPorts = [ service.port ];
|
udpPorts = [ service.port ];
|
||||||
tcpPorts = lib.optional service.rcon.enable service.rcon.port;
|
tcpPorts = [ service.port ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
startScript = pkgs.writeShellScript "valheim-start" ''
|
startScript = pkgs.writeShellScript "valheim-start" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||||
exec ./valheim_server.x86_64 "$@" -password "$password"
|
exec ./valheim_server.x86_64 "$@" -password "$password"
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
@@ -75,9 +75,6 @@
|
|||||||
service.stateDir
|
service.stateDir
|
||||||
"-public"
|
"-public"
|
||||||
"0"
|
"0"
|
||||||
"-modifier"
|
|
||||||
"raids"
|
|
||||||
"none"
|
|
||||||
];
|
];
|
||||||
udpPorts = [
|
udpPorts = [
|
||||||
service.port
|
service.port
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
registry,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.modules.services.homepage;
|
|
||||||
|
|
||||||
derivedAllowedHosts = lib.unique (
|
|
||||||
lib.optional (selfRoute != null) selfRoute.domain
|
|
||||||
++ lib.optionals (selfAddress != null) [
|
|
||||||
selfAddress
|
|
||||||
"${selfAddress}:${toString cfg.listenPort}"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
selfRoute = selfEntry.route or null;
|
|
||||||
|
|
||||||
selfEntry = registry.join cfg.hostName cfg.entryName;
|
|
||||||
selfAddress = selfEntry.address or null;
|
|
||||||
homepageServices = map mkTab (lib.filter (tab: lib.hasAttr tab entriesByTab) orderedTabs);
|
|
||||||
|
|
||||||
mkTab =
|
|
||||||
tab:
|
|
||||||
let
|
|
||||||
sections = entriesByTab.${tab};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
${tab} = map (section: { ${section} = sections.${section}; }) (orderedSections tab sections);
|
|
||||||
};
|
|
||||||
|
|
||||||
entriesByTab = lib.foldl' insertEntry { } sortedEntries;
|
|
||||||
|
|
||||||
insertEntry =
|
|
||||||
acc: item:
|
|
||||||
acc
|
|
||||||
// {
|
|
||||||
${item.homepage.tab} = (acc.${item.homepage.tab} or { }) // {
|
|
||||||
${item.homepage.section} = (acc.${item.homepage.tab}.${item.homepage.section} or [ ]) ++ [
|
|
||||||
{ ${item.homepage.name} = mkServiceAttrs item; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
sortedEntries = lib.sort (left: right: left.homepage.name < right.homepage.name) homepageEntries;
|
|
||||||
|
|
||||||
homepageEntries = lib.concatLists (
|
|
||||||
lib.mapAttrsToList (
|
|
||||||
host: entries:
|
|
||||||
lib.mapAttrsToList (entry: homepage: {
|
|
||||||
inherit host entry homepage;
|
|
||||||
composite = registry.join host entry;
|
|
||||||
}) entries
|
|
||||||
) registry.homepage
|
|
||||||
);
|
|
||||||
|
|
||||||
orderedTabs = tabOrder ++ lib.filter (tab: !lib.elem tab tabOrder) (lib.attrNames entriesByTab);
|
|
||||||
|
|
||||||
tabOrder = [
|
|
||||||
"Apps"
|
|
||||||
"Download"
|
|
||||||
"System"
|
|
||||||
];
|
|
||||||
|
|
||||||
mkRegistryPath =
|
|
||||||
registryName: host: entry:
|
|
||||||
"registry.${registryName}.${host}.${entry}";
|
|
||||||
|
|
||||||
mkHref =
|
|
||||||
item:
|
|
||||||
if item.homepage.href != null then
|
|
||||||
item.homepage.href
|
|
||||||
else if (item.composite.route or null) != null then
|
|
||||||
if item.composite.route.protocol == "https" then
|
|
||||||
"https://${item.composite.route.domain}"
|
|
||||||
else if item.composite.route.protocol == "http" then
|
|
||||||
"http://${item.composite.route.domain}"
|
|
||||||
else
|
|
||||||
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching HTTP route entry"
|
|
||||||
else
|
|
||||||
throw "${mkRegistryPath "homepage" item.host item.entry} needs href or a matching route entry";
|
|
||||||
|
|
||||||
mkServiceAttrs =
|
|
||||||
item:
|
|
||||||
lib.filterAttrs (_: value: value != null) {
|
|
||||||
href = mkHref item;
|
|
||||||
inherit (item.homepage) description icon;
|
|
||||||
};
|
|
||||||
|
|
||||||
sectionOrderByTab = {
|
|
||||||
Apps = [
|
|
||||||
"Media"
|
|
||||||
"Files"
|
|
||||||
"Other"
|
|
||||||
];
|
|
||||||
Download = [
|
|
||||||
"Library Managers"
|
|
||||||
"Other Utilities"
|
|
||||||
"Download Clients"
|
|
||||||
];
|
|
||||||
System = [
|
|
||||||
"Monitoring"
|
|
||||||
"Network"
|
|
||||||
"Machines"
|
|
||||||
"Docker"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
orderedSections =
|
|
||||||
tab: sections:
|
|
||||||
let
|
|
||||||
sectionOrder = sectionOrderByTab.${tab} or [ ];
|
|
||||||
in
|
|
||||||
lib.filter (section: lib.hasAttr section sections) sectionOrder
|
|
||||||
++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections);
|
|
||||||
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.modules.services.homepage = {
|
|
||||||
enable = lib.mkEnableOption "Homepage Dashboard";
|
|
||||||
|
|
||||||
listenPort = lib.mkOption {
|
|
||||||
type = lib.types.port;
|
|
||||||
default = 8082;
|
|
||||||
description = "Port for Homepage to bind to.";
|
|
||||||
};
|
|
||||||
|
|
||||||
hostName = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Host registry key that owns the Homepage guest entry.";
|
|
||||||
};
|
|
||||||
|
|
||||||
entryName = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "homepage";
|
|
||||||
description = "Registry entry name for this Homepage service.";
|
|
||||||
};
|
|
||||||
|
|
||||||
allowedHosts = lib.mkOption {
|
|
||||||
type = lib.types.nullOr (lib.types.listOf lib.types.str);
|
|
||||||
default = null;
|
|
||||||
description = "Host headers accepted by Homepage. Defaults to matching address and route registry entries.";
|
|
||||||
};
|
|
||||||
|
|
||||||
environmentFile = lib.mkOption {
|
|
||||||
type = lib.types.path;
|
|
||||||
default = "/run/secrets/homepage.env";
|
|
||||||
description = "Environment file passed to Homepage.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
services.homepage-dashboard = {
|
|
||||||
enable = true;
|
|
||||||
openFirewall = true;
|
|
||||||
inherit (cfg) listenPort;
|
|
||||||
environmentFiles = [ cfg.environmentFile ];
|
|
||||||
allowedHosts = lib.concatStringsSep "," (
|
|
||||||
if cfg.allowedHosts == null then derivedAllowedHosts else cfg.allowedHosts
|
|
||||||
);
|
|
||||||
services = homepageServices;
|
|
||||||
widgets = [
|
|
||||||
{
|
|
||||||
greeting = {
|
|
||||||
text_size = "xl";
|
|
||||||
text = "tracion";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
openmeteo = {
|
|
||||||
units = "metric";
|
|
||||||
latitude = "{{HOMEPAGE_VAR_LATITUDE}}";
|
|
||||||
longitude = "{{HOMEPAGE_VAR_LONGITUDE}}";
|
|
||||||
cache = 5;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
datetime = {
|
|
||||||
text_size = "xl";
|
|
||||||
locale = "en";
|
|
||||||
format = {
|
|
||||||
dateStyle = "full";
|
|
||||||
timeStyle = "medium";
|
|
||||||
hour12 = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
settings = {
|
|
||||||
title = "Tracion";
|
|
||||||
theme = "dark";
|
|
||||||
color = "slate";
|
|
||||||
showStats = false;
|
|
||||||
hideVersion = true;
|
|
||||||
bookmarksStyle = "icons";
|
|
||||||
useEqualHeights = true;
|
|
||||||
layout = {
|
|
||||||
Apps = {
|
|
||||||
style = "row";
|
|
||||||
columns = 1;
|
|
||||||
tab = "Apps";
|
|
||||||
Media = {
|
|
||||||
style = "row";
|
|
||||||
columns = 2;
|
|
||||||
};
|
|
||||||
Files = {
|
|
||||||
style = "row";
|
|
||||||
columns = 2;
|
|
||||||
};
|
|
||||||
Other = {
|
|
||||||
style = "row";
|
|
||||||
columns = 3;
|
|
||||||
iconsOnly = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Download = {
|
|
||||||
style = "row";
|
|
||||||
columns = 1;
|
|
||||||
tab = "Download";
|
|
||||||
};
|
|
||||||
System = {
|
|
||||||
tab = "System";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.modules.services.navidrome;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.modules.services.navidrome = {
|
|
||||||
enable = lib.mkEnableOption "Navidrome";
|
|
||||||
|
|
||||||
dataFolder = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "/var/lib/navidrome";
|
|
||||||
readOnly = true;
|
|
||||||
description = "Path for Navidrome's `DataFolder` setting.";
|
|
||||||
};
|
|
||||||
|
|
||||||
musicFolder = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "/music";
|
|
||||||
readOnly = true;
|
|
||||||
description = "Path for Navidrome's `MusicFolder` setting.";
|
|
||||||
};
|
|
||||||
|
|
||||||
environmentFile = lib.mkOption {
|
|
||||||
type = lib.types.path;
|
|
||||||
default = "/run/secrets/navidrome.env";
|
|
||||||
readOnly = true;
|
|
||||||
description = "Path to Navidrome's environment file.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkMerge [
|
|
||||||
{
|
|
||||||
registry."stable-uid".${config.networking.hostName}.navidrome = 20004;
|
|
||||||
}
|
|
||||||
|
|
||||||
(lib.mkIf cfg.enable {
|
|
||||||
users.users.navidrome = {
|
|
||||||
isSystemUser = true;
|
|
||||||
uid = config.registry."stable-uid".${config.networking.hostName}.navidrome;
|
|
||||||
group = "storage";
|
|
||||||
home = cfg.dataFolder;
|
|
||||||
};
|
|
||||||
|
|
||||||
services.navidrome = {
|
|
||||||
enable = true;
|
|
||||||
user = "navidrome";
|
|
||||||
group = "storage";
|
|
||||||
openFirewall = true;
|
|
||||||
inherit (cfg) environmentFile;
|
|
||||||
settings = {
|
|
||||||
Address = "0.0.0.0";
|
|
||||||
Port = 4533;
|
|
||||||
DataFolder = cfg.dataFolder;
|
|
||||||
MusicFolder = cfg.musicFolder;
|
|
||||||
EnableSharing = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -5,89 +5,94 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.services.reverse-proxy;
|
||||||
|
|
||||||
|
mkRegistryPath = registryName: host: entry: "registry.${registryName}.${host}.${entry}";
|
||||||
|
|
||||||
|
mkBackend = address: route: "http://${address}:${toString route.port}";
|
||||||
|
|
||||||
|
mkVirtualHost = host: entry:
|
||||||
|
let
|
||||||
|
compositeEntry = registry.join host entry;
|
||||||
|
route = compositeEntry.route;
|
||||||
|
address = compositeEntry.address or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit host entry;
|
||||||
|
name = route.domain;
|
||||||
|
value = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/".proxyPass = mkBackend address route;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
generatedVirtualHostList = lib.concatLists (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
host: routes:
|
||||||
|
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry) (
|
||||||
|
lib.filterAttrs (_entry: route: route.port != null) routes
|
||||||
|
)
|
||||||
|
) registry.route
|
||||||
|
);
|
||||||
|
|
||||||
|
virtualHostsByDomain = lib.foldl' (
|
||||||
|
acc: virtualHost:
|
||||||
|
acc
|
||||||
|
// {
|
||||||
|
${virtualHost.name} = (acc.${virtualHost.name} or [ ]) ++ [ virtualHost ];
|
||||||
|
}
|
||||||
|
) { } generatedVirtualHostList;
|
||||||
|
|
||||||
|
duplicateDomainMessages = lib.concatLists (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
domain: virtualHosts:
|
||||||
|
if lib.length virtualHosts > 1 then
|
||||||
|
[
|
||||||
|
"duplicate route domain ${domain} in ${lib.concatMapStringsSep " and " (virtualHost: mkRegistryPath "route" virtualHost.host virtualHost.entry) virtualHosts}"
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[ ]
|
||||||
|
) virtualHostsByDomain
|
||||||
|
);
|
||||||
|
|
||||||
|
generatedVirtualHosts =
|
||||||
|
if duplicateDomainMessages != [ ] then
|
||||||
|
throw (builtins.head duplicateDomainMessages)
|
||||||
|
else
|
||||||
|
lib.listToAttrs generatedVirtualHostList;
|
||||||
|
|
||||||
|
mkUdpStream = host: entry: port:
|
||||||
|
let
|
||||||
|
compositeEntry = registry.join host entry;
|
||||||
|
address = compositeEntry.address or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit host entry port address;
|
||||||
|
};
|
||||||
|
|
||||||
generatedUdpStreams = lib.concatLists (
|
generatedUdpStreams = lib.concatLists (
|
||||||
lib.mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
host: routes:
|
host: routes:
|
||||||
lib.concatLists (
|
lib.concatLists (
|
||||||
lib.mapAttrsToList (
|
lib.mapAttrsToList (entry: route: map (mkUdpStream host entry) route.udp.ports) routes
|
||||||
entry: route: lib.optionals (route.protocol == "udp") (map (mkUdpStream host entry) route.ports)
|
|
||||||
) routes
|
|
||||||
)
|
)
|
||||||
) registry.route
|
) registry.route
|
||||||
);
|
);
|
||||||
|
|
||||||
mkUdpStream =
|
|
||||||
host: entry: port:
|
|
||||||
let
|
|
||||||
address =
|
|
||||||
compositeEntry.address
|
|
||||||
or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
|
||||||
compositeEntry = registry.join host entry;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit
|
|
||||||
host
|
|
||||||
entry
|
|
||||||
port
|
|
||||||
address
|
|
||||||
;
|
|
||||||
};
|
|
||||||
|
|
||||||
generatedStreamConfig = lib.concatMapStringsSep "\n" (stream: ''
|
generatedStreamConfig = lib.concatMapStringsSep "\n" (stream: ''
|
||||||
server {
|
server {
|
||||||
listen ${toString stream.port} udp;
|
listen ${toString stream.port} udp;
|
||||||
proxy_pass ${stream.address}:${toString stream.port};
|
proxy_pass ${stream.address}:${toString stream.port};
|
||||||
}
|
}
|
||||||
'') generatedUdpStreams;
|
'') generatedUdpStreams;
|
||||||
generatedVirtualHosts = lib.listToAttrs generatedVirtualHostList;
|
|
||||||
|
|
||||||
# Route registry validation already requires unique domains.
|
|
||||||
generatedVirtualHostList = lib.concatLists (
|
|
||||||
lib.mapAttrsToList (
|
|
||||||
host: routes:
|
|
||||||
lib.mapAttrsToList (entry: route: mkVirtualHost host entry route.protocol) (
|
|
||||||
lib.filterAttrs (_entry: route: route.protocol != "udp") routes
|
|
||||||
)
|
|
||||||
) registry.route
|
|
||||||
);
|
|
||||||
|
|
||||||
mkVirtualHost =
|
|
||||||
host: entry: mode:
|
|
||||||
let
|
|
||||||
isHttp = mode == "http";
|
|
||||||
address =
|
|
||||||
compositeEntry.address
|
|
||||||
or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
|
||||||
compositeEntry = registry.join host entry;
|
|
||||||
inherit (compositeEntry) route;
|
|
||||||
inherit (route) port;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
name = route.domain;
|
|
||||||
value = {
|
|
||||||
forceSSL = !isHttp;
|
|
||||||
enableACME = !isHttp;
|
|
||||||
extraConfig = lib.optionalString (route.clientMaxBodySize != null) ''
|
|
||||||
client_max_body_size ${route.clientMaxBodySize};
|
|
||||||
'';
|
|
||||||
locations."/".proxyPass = mkBackend address port;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mkBackend = address: port: "http://${address}:${toString port}";
|
|
||||||
|
|
||||||
mkRegistryPath =
|
|
||||||
registryName: host: entry:
|
|
||||||
"registry.${registryName}.${host}.${entry}";
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.reverse-proxy = {
|
options.modules.services.reverse-proxy = {
|
||||||
enable = lib.mkEnableOption "the public edge reverse proxy";
|
enable = lib.mkEnableOption "the public edge reverse proxy";
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf config.modules.services.reverse-proxy.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
networking.firewall.allowedTCPPorts = [
|
networking.firewall.allowedTCPPorts = [
|
||||||
80
|
80
|
||||||
443
|
443
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
serviceName = lib.mkOption {
|
serviceName = lib.mkOption {
|
||||||
type = lib.types.strMatching "[a-z0-9][a-z0-9-]*";
|
type = lib.types.str;
|
||||||
example = "valheim";
|
example = "valheim";
|
||||||
description = "Lowercase systemd service basename for the game server.";
|
description = "Name of the systemd service that runs the game server.";
|
||||||
};
|
};
|
||||||
|
|
||||||
appId = lib.mkOption {
|
appId = lib.mkOption {
|
||||||
@@ -65,12 +65,6 @@
|
|||||||
default = [ ];
|
default = [ ];
|
||||||
description = "TCP ports opened for the game service.";
|
description = "TCP ports opened for the game service.";
|
||||||
};
|
};
|
||||||
|
|
||||||
autoStart = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
description = "Start the game service after the guest reaches network readiness.";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkMerge [
|
config = lib.mkMerge [
|
||||||
@@ -80,14 +74,14 @@
|
|||||||
|
|
||||||
(lib.mkIf config.modules.services.steamcmd.enable (
|
(lib.mkIf config.modules.services.steamcmd.enable (
|
||||||
let
|
let
|
||||||
group = "storage";
|
|
||||||
|
|
||||||
service = config.modules.services.steamcmd;
|
service = config.modules.services.steamcmd;
|
||||||
user = "steamcmd";
|
user = "steamcmd";
|
||||||
|
group = "storage";
|
||||||
|
|
||||||
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
|
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
${lib.getExe' pkgs.coreutils "install"} -d \
|
${pkgs.coreutils}/bin/install -d \
|
||||||
-o ${user} \
|
-o ${user} \
|
||||||
-g ${group} \
|
-g ${group} \
|
||||||
-m 0770 \
|
-m 0770 \
|
||||||
@@ -144,18 +138,6 @@
|
|||||||
WorkingDirectory = "-${service.steamDir}";
|
WorkingDirectory = "-${service.steamDir}";
|
||||||
ExecStartPre = "+${prepareScript}";
|
ExecStartPre = "+${prepareScript}";
|
||||||
ExecStart = updateScript;
|
ExecStart = updateScript;
|
||||||
TimeoutStartSec = "15min";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
"${service.serviceName}-autostart" = {
|
|
||||||
description = "Queue ${service.serviceName} game server startup";
|
|
||||||
after = [ "network-online.target" ];
|
|
||||||
wants = [ "network-online.target" ];
|
|
||||||
wantedBy = lib.mkIf service.autoStart [ "multi-user.target" ];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
ExecStart = "${lib.getExe' pkgs.systemd "systemctl"} --no-block start ${service.serviceName}.service";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,19 +147,19 @@
|
|||||||
"network-online.target"
|
"network-online.target"
|
||||||
"steamcmd-${service.serviceName}-update.service"
|
"steamcmd-${service.serviceName}-update.service"
|
||||||
];
|
];
|
||||||
wants = [
|
wants = [ "network-online.target" ];
|
||||||
"network-online.target"
|
wantedBy = [ "multi-user.target" ];
|
||||||
"steamcmd-${service.serviceName}-update.service"
|
|
||||||
];
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
User = user;
|
User = user;
|
||||||
Group = group;
|
Group = group;
|
||||||
WorkingDirectory = "-${service.installDir}";
|
WorkingDirectory = "-${service.installDir}";
|
||||||
ExecStartPre = "+${prepareScript}";
|
ExecStartPre = [
|
||||||
|
"+${prepareScript}"
|
||||||
|
updateScript
|
||||||
|
];
|
||||||
ExecStart = startScript;
|
ExecStart = startScript;
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
TimeoutStartSec = "15min";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,11 +5,14 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Bazel for the primary user's shell environment.
|
# Bazel for the primary user's shell environment.
|
||||||
|
let
|
||||||
|
cfg = config.modules.shell.bazel;
|
||||||
|
user = config.user.name;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.bazel.enable =
|
options.modules.shell.bazel.enable = lib.mkEnableOption "Bazel for the primary user's shell environment";
|
||||||
lib.mkEnableOption "Bazel for the primary user's shell environment";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.bazel.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${config.user.name}.home.packages = [ pkgs.bazel ];
|
home-manager.users.${user}.home.packages = [ pkgs.bazel ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
26
configs/nixos/modules/shell/direnv/default.nix
Normal file
26
configs/nixos/modules/shell/direnv/default.nix
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
# direnv for the primary user, configured through home-manager.
|
||||||
|
let
|
||||||
|
cfg = config.modules.shell.direnv;
|
||||||
|
user = config.user.name;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.shell.direnv.enable =
|
||||||
|
lib.mkEnableOption "direnv for the primary user, with nix-direnv shell caching";
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
home-manager.users.${user}.programs.direnv = {
|
||||||
|
enable = true;
|
||||||
|
silent = true;
|
||||||
|
|
||||||
|
# nix-direnv caches the evaluated shell so re-entering a directory is instant
|
||||||
|
# instead of re-running `nix develop`, and adds the `use flake` stdlib helper.
|
||||||
|
nix-direnv.enable = true;
|
||||||
|
stdlib = builtins.readFile ./repo-flake.sh;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
33
configs/nixos/modules/shell/direnv/repo-flake.sh
Normal file
33
configs/nixos/modules/shell/direnv/repo-flake.sh
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
if declare -f use_flake >/dev/null; then
|
||||||
|
__dotfiles_use_flake_definition="$(declare -f use_flake)"
|
||||||
|
eval "${__dotfiles_use_flake_definition/use_flake/__dotfiles_nix_direnv_use_flake}"
|
||||||
|
unset __dotfiles_use_flake_definition
|
||||||
|
|
||||||
|
use_flake() {
|
||||||
|
local flake_expr="${1:-.}"
|
||||||
|
|
||||||
|
if [[ "$flake_expr" == //* ]]; then
|
||||||
|
local git_root
|
||||||
|
if ! git_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then
|
||||||
|
log_error 'use flake // requires a Git repository'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local flake_suffix="${flake_expr#//}"
|
||||||
|
if [[ -n "$flake_suffix" && "$flake_suffix" != \#* && "$flake_suffix" != /* ]]; then
|
||||||
|
flake_expr="path:${git_root}/${flake_suffix}"
|
||||||
|
else
|
||||||
|
flake_expr="path:${git_root}${flake_suffix}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
shift
|
||||||
|
__dotfiles_nix_direnv_use_flake "$flake_expr" "$@"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
__dotfiles_nix_direnv_use_flake "$@"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
log_error 'nix-direnv must define use_flake before the // wrapper loads'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
@@ -7,9 +7,13 @@
|
|||||||
# fish for the primary user, configured through home-manager.
|
# fish for the primary user, configured through home-manager.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.fish;
|
cfg = config.modules.shell.fish;
|
||||||
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
|
|
||||||
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
|
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
|
||||||
|
envVars = lib.attrByPath [ config.networking.hostName ] { } config.registry.env-var;
|
||||||
|
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
|
||||||
|
exportEnvVar = name: value: ''
|
||||||
|
set -gx ${name} ${lib.escapeShellArg value}
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.fish = {
|
options.modules.shell.fish = {
|
||||||
@@ -95,14 +99,17 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
deploy-to = {
|
deploy-to = {
|
||||||
description = "Build the current checkout locally, import it on a host, and activate it";
|
description = "Deploy the current dotfiles branch to a host through target-local nixos-rebuild";
|
||||||
body = builtins.readFile ./functions/deploy-to.fish;
|
body = builtins.readFile ./functions/deploy-to.fish;
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# Rendered by home-manager into ~/.config/fish/config.fish.
|
# Rendered by home-manager into ~/.config/fish/config.fish.
|
||||||
interactiveShellInit = builtins.readFile ./config.fish;
|
interactiveShellInit = lib.concatStringsSep "\n" (
|
||||||
|
(lib.mapAttrsToList exportEnvVar envVars)
|
||||||
|
++ [ (builtins.readFile ./config.fish) ]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
if test (count $argv) -ne 1
|
if test (count $argv) -lt 1; or test (count $argv) -gt 2
|
||||||
echo "usage: deploy-to <host>" >&2
|
echo "usage: deploy-to <host> [branch]" >&2
|
||||||
return 2
|
return 2
|
||||||
end
|
end
|
||||||
|
|
||||||
set host $argv[1]
|
set host $argv[1]
|
||||||
|
set branch $argv[2]
|
||||||
|
set branch_source explicit
|
||||||
|
|
||||||
|
if test -z "$branch"
|
||||||
|
set branch_source current
|
||||||
|
set branch (git branch --show-current)
|
||||||
|
end
|
||||||
|
|
||||||
git rev-parse --show-toplevel >/dev/null 2>&1
|
git rev-parse --show-toplevel >/dev/null 2>&1
|
||||||
or begin
|
or begin
|
||||||
@@ -11,85 +18,69 @@ or begin
|
|||||||
return 2
|
return 2
|
||||||
end
|
end
|
||||||
|
|
||||||
set repository_root (git rev-parse --show-toplevel)
|
git fetch origin
|
||||||
or return $status
|
or return $status
|
||||||
|
|
||||||
set untracked_nix_files (git -C $repository_root ls-files --others --exclude-standard -- '*.nix' flake.nix flake.lock)
|
if test -z "$branch"; or not git rev-parse --verify --quiet refs/remotes/origin/$branch >/dev/null
|
||||||
if test (count $untracked_nix_files) -gt 0
|
if test "$branch_source" = explicit
|
||||||
echo "deploy-to: warning: untracked Nix files are not part of Git flake evaluation" >&2
|
echo "deploy-to: origin/$branch does not exist" >&2
|
||||||
printf ' %s\n' $untracked_nix_files >&2
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
set branch_source upstream
|
||||||
|
set upstream_ref (git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null)
|
||||||
|
set branch (string replace -r '^origin/' '' -- $upstream_ref)
|
||||||
end
|
end
|
||||||
|
|
||||||
set installable "$repository_root#nixosConfigurations.$host.config.system.build.toplevel"
|
if test -z "$branch"; or not git rev-parse --verify --quiet refs/remotes/origin/$branch >/dev/null
|
||||||
|
set branch_source default
|
||||||
|
set origin_head (git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null)
|
||||||
|
set branch (string replace -r '^origin/' '' -- $origin_head)
|
||||||
|
end
|
||||||
|
|
||||||
echo "deploy-to: building $installable" >&2
|
if test -z "$branch"
|
||||||
set system_path (nix build --no-link --print-out-paths $installable)
|
echo "deploy-to: could not determine a deploy branch" >&2
|
||||||
|
return 2
|
||||||
|
end
|
||||||
|
|
||||||
|
set remote_ref refs/remotes/origin/$branch
|
||||||
|
git rev-parse --verify --quiet $remote_ref >/dev/null
|
||||||
or begin
|
or begin
|
||||||
echo "deploy-to: build failed for $host" >&2
|
echo "deploy-to: origin/$branch does not exist" >&2
|
||||||
return $status
|
|
||||||
end
|
|
||||||
|
|
||||||
if test (count $system_path) -ne 1
|
|
||||||
echo "deploy-to: expected one build output, got "(count $system_path) >&2
|
|
||||||
return 1
|
return 1
|
||||||
end
|
end
|
||||||
|
|
||||||
set closure_paths (nix-store -qR $system_path)
|
set has_local_changes false
|
||||||
or begin
|
if not git diff --quiet $remote_ref --
|
||||||
echo "deploy-to: closure query failed for $host" >&2
|
set has_local_changes true
|
||||||
return $status
|
|
||||||
end
|
end
|
||||||
|
|
||||||
echo "deploy-to: checking which closure paths $host is missing" >&2
|
set escaped_branch (string escape -- $branch)
|
||||||
set missing_paths (printf '%s\n' $closure_paths | ssh $host 'xargs -r nix-store --check-validity --print-invalid')
|
set escaped_host (string escape -- $host)
|
||||||
or begin
|
set remote_command "cd ~/dev && git fetch origin && git switch -- $escaped_branch && git reset --hard origin/$escaped_branch && git clean -fd"
|
||||||
echo "deploy-to: remote closure check failed for $host" >&2
|
|
||||||
return $status
|
|
||||||
end
|
|
||||||
|
|
||||||
if test (count $missing_paths) -gt 0
|
set patch_file ""
|
||||||
set nar_file (mktemp --suffix=.nar)
|
set remote_patch_file ""
|
||||||
or return $status
|
if test "$has_local_changes" = true
|
||||||
set remote_nar "/tmp/deploy-to-$host-"(date +%s)"-$fish_pid.nar"
|
set patch_file (mktemp)
|
||||||
|
set remote_patch_file "/tmp/deploy-to-$host.patch"
|
||||||
echo "deploy-to: exporting "(count $missing_paths)" missing closure paths for $system_path" >&2
|
git diff --binary $remote_ref -- >$patch_file
|
||||||
nix-store --export $missing_paths >$nar_file
|
|
||||||
or begin
|
or begin
|
||||||
set export_status $status
|
rm -f $patch_file
|
||||||
rm -f $nar_file
|
return $status
|
||||||
echo "deploy-to: closure export failed for $host" >&2
|
|
||||||
return $export_status
|
|
||||||
end
|
end
|
||||||
|
|
||||||
echo "deploy-to: copying closure archive to $host:$remote_nar" >&2
|
scp $patch_file $host:$remote_patch_file
|
||||||
scp $nar_file $host:$remote_nar
|
|
||||||
set scp_status $status
|
set scp_status $status
|
||||||
rm -f $nar_file
|
rm -f $patch_file
|
||||||
if test $scp_status -ne 0
|
if test $scp_status -ne 0
|
||||||
echo "deploy-to: closure archive copy failed for $host" >&2
|
|
||||||
return $scp_status
|
return $scp_status
|
||||||
end
|
end
|
||||||
|
|
||||||
echo "deploy-to: importing closure on $host" >&2
|
set escaped_remote_patch_file (string escape -- $remote_patch_file)
|
||||||
ssh -t $host "sudo sh -c 'nix-store --import < $remote_nar' && rm -f $remote_nar"
|
set remote_command "$remote_command && git apply --index $escaped_remote_patch_file && rm -f $escaped_remote_patch_file"
|
||||||
or begin
|
|
||||||
echo "deploy-to: closure import failed for $host" >&2
|
|
||||||
return $status
|
|
||||||
end
|
|
||||||
else
|
|
||||||
echo "deploy-to: $host already has the full closure for $system_path" >&2
|
|
||||||
end
|
end
|
||||||
|
|
||||||
echo "deploy-to: recording $system_path as the system profile on $host" >&2
|
set remote_command "$remote_command && sudo nixos-rebuild switch --flake .#$escaped_host --print-build-logs"
|
||||||
ssh -t $host sudo nix-env --profile /nix/var/nix/profiles/system --set $system_path
|
|
||||||
or begin
|
|
||||||
echo "deploy-to: system profile update failed for $host" >&2
|
|
||||||
return $status
|
|
||||||
end
|
|
||||||
|
|
||||||
echo "deploy-to: activating $system_path on $host" >&2
|
ssh -t $host $remote_command
|
||||||
ssh -t $host sudo "$system_path/bin/switch-to-configuration" switch
|
|
||||||
or begin
|
|
||||||
echo "deploy-to: activation failed for $host" >&2
|
|
||||||
return $status
|
|
||||||
end
|
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
}:
|
}:
|
||||||
# git for the primary user, configured through home-manager.
|
# git for the primary user, configured through home-manager.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.git;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.git.enable =
|
options.modules.shell.git.enable =
|
||||||
lib.mkEnableOption "git for the primary user, carrying the operator's commit identity";
|
lib.mkEnableOption "git for the primary user, carrying the operator's commit identity";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.git.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user}.programs.git = {
|
home-manager.users.${user}.programs.git = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
}:
|
}:
|
||||||
# gitea-axi for the primary user, installed through its own home-manager module.
|
# gitea-axi for the primary user, installed through its own home-manager module.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.gitea-axi;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.gitea-axi.enable =
|
options.modules.shell.gitea-axi.enable =
|
||||||
lib.mkEnableOption "gitea-axi, an agent-ergonomic CLI for Gitea issues and pull requests";
|
lib.mkEnableOption "gitea-axi, an agent-ergonomic CLI for Gitea issues and pull requests";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.gitea-axi.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.sharedModules = [ inputs.gitea-axi.homeModules.default ];
|
home-manager.sharedModules = [ inputs.gitea-axi.homeModules.default ];
|
||||||
home-manager.users.${user}.programs.gitea-axi.enable = true;
|
home-manager.users.${user}.programs.gitea-axi.enable = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,58 +6,42 @@
|
|||||||
}:
|
}:
|
||||||
# Herdr, a terminal multiplexer for coding agents.
|
# Herdr, a terminal multiplexer for coding agents.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.herdr;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.herdr.enable =
|
options.modules.shell.herdr.enable = lib.mkEnableOption "Herdr, a terminal multiplexer for coding agents";
|
||||||
lib.mkEnableOption "Herdr, a terminal multiplexer for coding agents";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.herdr.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.shell-shortcut.${config.networking.hostName}.hs = "herdr --session";
|
registry.shell-shortcut.${config.networking.hostName}.hs = "herdr --session";
|
||||||
|
|
||||||
home-manager.users.${user} = lib.mkMerge [
|
home-manager.users.${user} = {
|
||||||
{
|
home.packages = [ pkgs.herdr ];
|
||||||
home.packages = [ pkgs.herdr ];
|
|
||||||
|
|
||||||
xdg.configFile."herdr/config.toml".text = ''
|
xdg.configFile."herdr/config.toml".text = ''
|
||||||
onboarding = false
|
[keys]
|
||||||
|
prefix = "ctrl+space"
|
||||||
|
detach = "prefix+d"
|
||||||
|
reload_config = "prefix+r"
|
||||||
|
new_workspace = "prefix+c"
|
||||||
|
new_tab = "prefix+shift+c"
|
||||||
|
rename_workspace = "prefix+comma"
|
||||||
|
rename_tab = "prefix+<"
|
||||||
|
split_vertical = "prefix+backslash"
|
||||||
|
split_horizontal = "prefix+minus"
|
||||||
|
switch_workspace = "prefix+1..9"
|
||||||
|
switch_tab = "prefix+shift+1..9"
|
||||||
|
focus_pane_left = "prefix+h"
|
||||||
|
focus_pane_down = "prefix+j"
|
||||||
|
focus_pane_up = "prefix+k"
|
||||||
|
focus_pane_right = "prefix+l"
|
||||||
|
|
||||||
[keys]
|
[ui]
|
||||||
prefix = "ctrl+space"
|
prompt_new_tab_name = false
|
||||||
detach = "prefix+d"
|
|
||||||
reload_config = "prefix+r"
|
|
||||||
new_workspace = "prefix+c"
|
|
||||||
new_tab = "prefix+shift+c"
|
|
||||||
rename_workspace = "prefix+comma"
|
|
||||||
rename_tab = "prefix+<"
|
|
||||||
split_vertical = "prefix+backslash"
|
|
||||||
split_horizontal = "prefix+minus"
|
|
||||||
switch_workspace = "prefix+1..9"
|
|
||||||
switch_tab = "prefix+shift+1..9"
|
|
||||||
focus_pane_left = "prefix+h"
|
|
||||||
focus_pane_down = "prefix+j"
|
|
||||||
focus_pane_up = "prefix+k"
|
|
||||||
focus_pane_right = "prefix+l"
|
|
||||||
|
|
||||||
[ui]
|
[experimental]
|
||||||
prompt_new_tab_name = false
|
kitty_graphics = true
|
||||||
|
'';
|
||||||
[experimental]
|
};
|
||||||
kitty_graphics = true
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
(lib.mkIf config.modules.shell.pi.enable {
|
|
||||||
home.file.".pi/agent/extensions/herdr-agent-state.ts" = {
|
|
||||||
source = "${pkgs.herdr.src}/src/integration/assets/pi/herdr-agent-state.ts";
|
|
||||||
force = true;
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf config.modules.shell.omp.enable {
|
|
||||||
home.file.".omp/agent/extensions/herdr-omp-agent-state.ts" = {
|
|
||||||
source = "${pkgs.herdr.src}/src/integration/assets/omp/herdr-agent-state.ts";
|
|
||||||
force = true;
|
|
||||||
};
|
|
||||||
})
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
}:
|
}:
|
||||||
# Neovim for the primary user, configured declaratively through nixvim.
|
# Neovim for the primary user, configured declaratively through nixvim.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.nvim;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.nvim.enable =
|
options.modules.shell.nvim.enable = lib.mkEnableOption "Neovim, configured declaratively via nixvim";
|
||||||
lib.mkEnableOption "Neovim, configured declaratively via nixvim";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.nvim.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.shell-shortcut.${config.networking.hostName} = {
|
registry.shell-shortcut.${config.networking.hostName} = {
|
||||||
vi = "nvim";
|
vi = "nvim";
|
||||||
vim = "nvim";
|
vim = "nvim";
|
||||||
@@ -31,7 +31,6 @@ in
|
|||||||
ripgrep # backs the picker's live grep
|
ripgrep # backs the picker's live grep
|
||||||
fd # backs the picker's file finder
|
fd # backs the picker's file finder
|
||||||
wl-clipboard # provides a non-blocking clipboard provider on Wayland
|
wl-clipboard # provides a non-blocking clipboard provider on Wayland
|
||||||
nixd # provides Nix language-server support
|
|
||||||
];
|
];
|
||||||
|
|
||||||
globals.mapleader = " ";
|
globals.mapleader = " ";
|
||||||
@@ -189,8 +188,6 @@ in
|
|||||||
installRustc = false;
|
installRustc = false;
|
||||||
installRustfmt = false;
|
installRustfmt = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
servers.nixd.enable = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
gitsigns = {
|
gitsigns = {
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# Oh My Pi, installed alongside upstream Pi as the `omp` command.
|
|
||||||
let
|
|
||||||
user = config.user.name;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.modules.shell.omp = {
|
|
||||||
enable = lib.mkEnableOption "Oh My Pi, a batteries-included fork of Pi";
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.omp.enable {
|
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.omp ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -8,22 +8,31 @@
|
|||||||
# home-manager, which ships the package and manages global settings.
|
# home-manager, which ships the package and manages global settings.
|
||||||
# The login credential is left unmanaged, so it survives rebuilds.
|
# The login credential is left unmanaged, so it survives rebuilds.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.pi;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
piDir = "${config.users.users.${user}.home}/.pi/agent";
|
piDir = "${config.users.users.${user}.home}/.pi/agent";
|
||||||
|
patchedPi = pkgs.pi-coding-agent.overrideAttrs (old: {
|
||||||
|
patches = (old.patches or [ ]) ++ [
|
||||||
|
./patches/pi-project-resource-ancestors.patch
|
||||||
|
./patches/pi-tool-lookup-validation.patch
|
||||||
|
];
|
||||||
|
});
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.pi.enable =
|
options.modules.shell.pi = {
|
||||||
lib.mkEnableOption "Pi, a terminal coding agent, configured through home-manager";
|
enable = lib.mkEnableOption ''
|
||||||
|
Pi, a terminal coding agent, configured through home-manager'';
|
||||||
|
};
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.pi.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${user} = {
|
||||||
programs.pi-coding-agent = {
|
programs.pi-coding-agent = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = pkgs.alexion.pi-coding-agent;
|
package = patchedPi;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
defaultProvider = "openai-codex";
|
defaultProvider = "openai-codex";
|
||||||
defaultModel = "gpt-5.6-terra";
|
defaultModel = "gpt-5.5";
|
||||||
defaultThinkingLevel = "medium";
|
defaultThinkingLevel = "medium";
|
||||||
theme = "dark";
|
theme = "dark";
|
||||||
enableInstallTelemetry = false;
|
enableInstallTelemetry = false;
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts
|
||||||
|
--- a/packages/coding-agent/src/core/package-manager.ts
|
||||||
|
+++ b/packages/coding-agent/src/core/package-manager.ts
|
||||||
|
@@ -459,14 +459,14 @@ function findGitRepoRoot(startDir: string): string | null {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-function collectAncestorAgentsSkillDirs(startDir: string): string[] {
|
||||||
|
- const skillDirs: string[] = [];
|
||||||
|
+function collectAncestorConfigDirs(startDir: string, configDirName: string): string[] {
|
||||||
|
+ const configDirs: string[] = [];
|
||||||
|
const resolvedStartDir = resolve(startDir);
|
||||||
|
const gitRepoRoot = findGitRepoRoot(resolvedStartDir);
|
||||||
|
|
||||||
|
let dir = resolvedStartDir;
|
||||||
|
while (true) {
|
||||||
|
- skillDirs.push(join(dir, ".agents", "skills"));
|
||||||
|
+ configDirs.push(join(dir, configDirName));
|
||||||
|
if (gitRepoRoot && dir === gitRepoRoot) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
@@ -477,7 +477,7 @@ function collectAncestorAgentsSkillDirs(startDir: string): string[] {
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
- return skillDirs;
|
||||||
|
+ return configDirs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAutoPromptEntries(dir: string): string[] {
|
||||||
|
@@ -2388,16 +2388,13 @@ export class DefaultPackageManager implements PackageManager {
|
||||||
|
prompts: join(globalBaseDir, "prompts"),
|
||||||
|
themes: join(globalBaseDir, "themes"),
|
||||||
|
};
|
||||||
|
- const projectDirs = {
|
||||||
|
- extensions: join(projectBaseDir, "extensions"),
|
||||||
|
- skills: join(projectBaseDir, "skills"),
|
||||||
|
- prompts: join(projectBaseDir, "prompts"),
|
||||||
|
- themes: join(projectBaseDir, "themes"),
|
||||||
|
- };
|
||||||
|
const userAgentsSkillsDir = join(getHomeDir(), ".agents", "skills");
|
||||||
|
const projectTrusted = this.settingsManager.isProjectTrusted();
|
||||||
|
- const projectAgentsSkillDirs = projectTrusted
|
||||||
|
- ? collectAncestorAgentsSkillDirs(this.cwd).filter((dir) => resolve(dir) !== resolve(userAgentsSkillsDir))
|
||||||
|
+ const projectConfigDirs = projectTrusted
|
||||||
|
+ ? collectAncestorConfigDirs(this.cwd, CONFIG_DIR_NAME).filter((dir) => resolve(dir) !== resolve(globalBaseDir))
|
||||||
|
+ : [];
|
||||||
|
+ const projectAgentsDirs = projectTrusted
|
||||||
|
+ ? collectAncestorConfigDirs(this.cwd, ".agents").filter((dir) => resolve(dir) !== resolve(dirname(userAgentsSkillsDir)))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const addResources = (
|
||||||
|
@@ -2414,56 +2411,54 @@ export class DefaultPackageManager implements PackageManager {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
- if (projectTrusted) {
|
||||||
|
- // Project extensions from .pi/
|
||||||
|
+ // Project resources from .pi/ in cwd and ancestors.
|
||||||
|
+ for (const configDir of projectConfigDirs) {
|
||||||
|
+ const configMetadata: PathMetadata = {
|
||||||
|
+ ...projectMetadata,
|
||||||
|
+ baseDir: configDir,
|
||||||
|
+ };
|
||||||
|
addResources(
|
||||||
|
"extensions",
|
||||||
|
- collectAutoExtensionEntries(projectDirs.extensions),
|
||||||
|
- projectMetadata,
|
||||||
|
+ collectAutoExtensionEntries(join(configDir, "extensions")),
|
||||||
|
+ configMetadata,
|
||||||
|
projectOverrides.extensions,
|
||||||
|
- projectBaseDir,
|
||||||
|
+ configDir,
|
||||||
|
);
|
||||||
|
-
|
||||||
|
- // Project skills from .pi/
|
||||||
|
addResources(
|
||||||
|
"skills",
|
||||||
|
- collectAutoSkillEntries(projectDirs.skills, "pi"),
|
||||||
|
- projectMetadata,
|
||||||
|
+ collectAutoSkillEntries(join(configDir, "skills"), "pi"),
|
||||||
|
+ configMetadata,
|
||||||
|
projectOverrides.skills,
|
||||||
|
- projectBaseDir,
|
||||||
|
+ configDir,
|
||||||
|
+ );
|
||||||
|
+ addResources(
|
||||||
|
+ "prompts",
|
||||||
|
+ collectAutoPromptEntries(join(configDir, "prompts")),
|
||||||
|
+ configMetadata,
|
||||||
|
+ projectOverrides.prompts,
|
||||||
|
+ configDir,
|
||||||
|
+ );
|
||||||
|
+ addResources(
|
||||||
|
+ "themes",
|
||||||
|
+ collectAutoThemeEntries(join(configDir, "themes")),
|
||||||
|
+ configMetadata,
|
||||||
|
+ projectOverrides.themes,
|
||||||
|
+ configDir,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
- // Project skills from .agents/ (each with its own baseDir)
|
||||||
|
- for (const agentsSkillsDir of projectAgentsSkillDirs) {
|
||||||
|
- const agentsBaseDir = dirname(agentsSkillsDir); // the .agents directory
|
||||||
|
+ // Project skills from .agents/ in cwd and ancestors.
|
||||||
|
+ for (const agentsDir of projectAgentsDirs) {
|
||||||
|
const agentsMetadata: PathMetadata = {
|
||||||
|
...projectMetadata,
|
||||||
|
- baseDir: agentsBaseDir,
|
||||||
|
+ baseDir: agentsDir,
|
||||||
|
};
|
||||||
|
addResources(
|
||||||
|
"skills",
|
||||||
|
- collectAutoSkillEntries(agentsSkillsDir, "agents"),
|
||||||
|
+ collectAutoSkillEntries(join(agentsDir, "skills"), "agents"),
|
||||||
|
agentsMetadata,
|
||||||
|
projectOverrides.skills,
|
||||||
|
- agentsBaseDir,
|
||||||
|
- );
|
||||||
|
- }
|
||||||
|
-
|
||||||
|
- if (projectTrusted) {
|
||||||
|
- addResources(
|
||||||
|
- "prompts",
|
||||||
|
- collectAutoPromptEntries(projectDirs.prompts),
|
||||||
|
- projectMetadata,
|
||||||
|
- projectOverrides.prompts,
|
||||||
|
- projectBaseDir,
|
||||||
|
- );
|
||||||
|
- addResources(
|
||||||
|
- "themes",
|
||||||
|
- collectAutoThemeEntries(projectDirs.themes),
|
||||||
|
- projectMetadata,
|
||||||
|
- projectOverrides.themes,
|
||||||
|
- projectBaseDir,
|
||||||
|
+ agentsDir,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts
|
||||||
|
--- a/packages/coding-agent/src/utils/tools-manager.ts 2026-08-01 18:41:36.970496010 -0400
|
||||||
|
+++ b/packages/coding-agent/src/utils/tools-manager.ts 2026-08-01 18:41:37.028186009 -0400
|
||||||
|
@@ -74,8 +74,8 @@
|
||||||
|
function commandExists(cmd: string): boolean {
|
||||||
|
try {
|
||||||
|
const result = spawnSync(cmd, ["--version"], { stdio: "pipe" });
|
||||||
|
- // Check for ENOENT error (command not found)
|
||||||
|
- return result.error === undefined || result.error === null;
|
||||||
|
+ // Downloaded generic Linux binaries can exist but fail on NixOS before they print a version.
|
||||||
|
+ return (result.error === undefined || result.error === null) && result.status === 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@@ -88,7 +87,7 @@
|
||||||
|
|
||||||
|
// Check our tools directory first
|
||||||
|
const localPath = join(TOOLS_DIR, config.binaryName + (platform() === "win32" ? ".exe" : ""));
|
||||||
|
- if (existsSync(localPath)) {
|
||||||
|
+ if (existsSync(localPath) && commandExists(localPath)) {
|
||||||
|
return localPath;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -6,14 +6,14 @@
|
|||||||
}:
|
}:
|
||||||
# Python tools for the primary user's shell environment.
|
# Python tools for the primary user's shell environment.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.python;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.python.enable =
|
options.modules.shell.python.enable = lib.mkEnableOption "Python tools for the primary user's shell environment";
|
||||||
lib.mkEnableOption "Python tools for the primary user's shell environment";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.python.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.env.${config.networking.hostName}.PYTHON_TOOLCHAIN_ROOT = "${pkgs.python3}";
|
registry.env-var.${config.networking.hostName}.PYTHON_TOOLCHAIN_ROOT = "${pkgs.python3}";
|
||||||
|
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.python3 ];
|
home-manager.users.${user}.home.packages = [ pkgs.python3 ];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
}:
|
}:
|
||||||
# Rust tools for the primary user's shell environment.
|
# Rust tools for the primary user's shell environment.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.rust;
|
||||||
|
user = config.user.name;
|
||||||
toolchain = pkgs.symlinkJoin {
|
toolchain = pkgs.symlinkJoin {
|
||||||
name = "rust-toolchain";
|
name = "rust-toolchain";
|
||||||
paths = with pkgs; [
|
paths = with pkgs; [
|
||||||
@@ -16,14 +18,12 @@ let
|
|||||||
rustfmt
|
rustfmt
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
user = config.user.name;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.rust.enable =
|
options.modules.shell.rust.enable = lib.mkEnableOption "Rust tools for the primary user's shell environment";
|
||||||
lib.mkEnableOption "Rust tools for the primary user's shell environment";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.rust.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.env.${config.networking.hostName}.RUST_TOOLCHAIN_ROOT = "${toolchain}";
|
registry.env-var.${config.networking.hostName}.RUST_TOOLCHAIN_ROOT = "${toolchain}";
|
||||||
|
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${user} = {
|
||||||
home.packages = [
|
home.packages = [
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
# tmux for the primary user, configured through home-manager.
|
# tmux for the primary user, configured through home-manager.
|
||||||
# Settings without a home-manager option are read from ./extra.conf.
|
# Settings without a home-manager option are read from ./extra.conf.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.tmux;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.tmux.enable = lib.mkEnableOption "tmux, configured via home-manager";
|
options.modules.shell.tmux.enable = lib.mkEnableOption "tmux, configured via home-manager";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.tmux.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.shell-shortcut.${config.networking.hostName}.tmx = "tmux new-session -A -s";
|
registry.shell-shortcut.${config.networking.hostName}.tmx = "tmux new-session -A -s";
|
||||||
|
|
||||||
home-manager.users.${user}.programs.tmux = {
|
home-manager.users.${user}.programs.tmux = {
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
}:
|
}:
|
||||||
# Yazi, a terminal file manager.
|
# Yazi, a terminal file manager.
|
||||||
let
|
let
|
||||||
|
cfg = config.modules.shell.yazi;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.yazi.enable = lib.mkEnableOption "Yazi, a terminal file manager";
|
options.modules.shell.yazi.enable = lib.mkEnableOption "Yazi, a terminal file manager";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.yazi.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user}.programs.yazi = {
|
home-manager.users.${user}.programs.yazi = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enableFishIntegration = true;
|
enableFishIntegration = true;
|
||||||
|
|||||||
72
configs/nixos/modules/zfs.nix
Normal file
72
configs/nixos/modules/zfs.nix
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
# The host-level ZFS pool import: durable service state a host mounts, never rebuilds.
|
||||||
|
let
|
||||||
|
cfg = config.modules.zfs;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.zfs = {
|
||||||
|
enable = lib.mkEnableOption "importing durable ZFS pools that hold service state";
|
||||||
|
|
||||||
|
hostId = lib.mkOption {
|
||||||
|
type = lib.types.strMatching "[0-9a-f]{8}";
|
||||||
|
example = "deadbeef";
|
||||||
|
description = ''
|
||||||
|
This host's 8-hex-digit ZFS host id, written to `networking.hostId`. ZFS
|
||||||
|
stamps an imported pool with the importing host's id, so a pool still
|
||||||
|
held by another machine is refused rather than silently dual-mounted. It
|
||||||
|
must be fixed for the machine and distinct across machines that can reach
|
||||||
|
the same pool.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
pools = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (lib.types.attrsOf lib.types.path);
|
||||||
|
default = { };
|
||||||
|
example = lib.literalExpression ''
|
||||||
|
{
|
||||||
|
tank = {
|
||||||
|
media = "/srv/media";
|
||||||
|
downloads = "/srv/downloads";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
description = ''
|
||||||
|
The ZFS pools to import at boot, keyed by pool name, each pool mapping a
|
||||||
|
dataset path relative to it to that dataset's mountpoint. A pool is
|
||||||
|
durable state imported as it stands, never created or destroyed by a
|
||||||
|
rebuild, so a service's data survives any rebuild or reimage. A pool
|
||||||
|
with an empty map is still imported, leaving each dataset to its own ZFS
|
||||||
|
`mountpoint` property.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
# The ZFS stack in the kernel and boot, needed even where the root filesystem is another kind.
|
||||||
|
boot.supportedFilesystems = [ "zfs" ];
|
||||||
|
|
||||||
|
# ZFS refuses to import a pool without a host id to stamp its ownership onto.
|
||||||
|
networking.hostId = cfg.hostId;
|
||||||
|
|
||||||
|
# The declared pools are imported at boot, distinct from any pool backing the root filesystem.
|
||||||
|
boot.zfs.extraPools = lib.attrNames cfg.pools;
|
||||||
|
|
||||||
|
# Each declared dataset is mounted at its host path as a native ZFS filesystem.
|
||||||
|
fileSystems = lib.mkMerge (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
pool: mounts:
|
||||||
|
lib.mapAttrs' (
|
||||||
|
dataset: mountpoint:
|
||||||
|
lib.nameValuePair mountpoint {
|
||||||
|
device = "${pool}/${dataset}";
|
||||||
|
fsType = "zfs";
|
||||||
|
}
|
||||||
|
) mounts
|
||||||
|
) cfg.pools
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{ lib, util, ... }:
|
{ lib, ... }:
|
||||||
{
|
{
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
|
|
||||||
checks = [
|
checks = [
|
||||||
{
|
{
|
||||||
assertion = util.registry.predicates.uniqueValue (entry: entry.value);
|
assertion = lib.registry.predicates.uniqueValue (entry: entry.value);
|
||||||
message = "Address registry values must be unique.";
|
message = "Address registry values must be unique.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
{ lib, ... }:
|
|
||||||
{
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
tab = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "Apps";
|
|
||||||
description = "Groups the entry in a Homepage tab.";
|
|
||||||
};
|
|
||||||
|
|
||||||
section = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "Media";
|
|
||||||
description = "Groups the entry in a Homepage section.";
|
|
||||||
};
|
|
||||||
|
|
||||||
name = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "Navidrome";
|
|
||||||
description = "Sets the display name in Homepage.";
|
|
||||||
};
|
|
||||||
|
|
||||||
description = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "Music";
|
|
||||||
description = "Sets the description in Homepage.";
|
|
||||||
};
|
|
||||||
|
|
||||||
icon = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "navidrome.png";
|
|
||||||
description = "Sets the icon in Homepage.";
|
|
||||||
};
|
|
||||||
|
|
||||||
href = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "https://music.alexion.dev";
|
|
||||||
description = "Overrides the generated Homepage link.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
{ lib, util, ... }:
|
|
||||||
{
|
|
||||||
checks = [
|
|
||||||
{
|
|
||||||
assertion = util.registry.predicates.uniquePerHost (entry: entry.value.keys);
|
|
||||||
message = "Desktop keybinds must use unique key chords per host.";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion =
|
|
||||||
entries:
|
|
||||||
lib.all (
|
|
||||||
entry:
|
|
||||||
(entry.value.action.kind == "exec" && entry.value.action.command != null)
|
|
||||||
|| (
|
|
||||||
builtins.elem entry.value.action.kind [
|
|
||||||
"focus"
|
|
||||||
"move-window"
|
|
||||||
]
|
|
||||||
&& lib.xor (entry.value.action.direction != null) (entry.value.action.workspace != null)
|
|
||||||
)
|
|
||||||
|| (
|
|
||||||
entry.value.action.kind == "resize-window"
|
|
||||||
&& entry.value.action.direction != null
|
|
||||||
&& entry.value.action.workspace == null
|
|
||||||
)
|
|
||||||
|| (
|
|
||||||
builtins.elem entry.value.action.kind [
|
|
||||||
"toggle-floating"
|
|
||||||
"toggle-fullscreen"
|
|
||||||
"close-window"
|
|
||||||
]
|
|
||||||
&& entry.value.action.command == null
|
|
||||||
&& entry.value.action.direction == null
|
|
||||||
&& entry.value.action.workspace == null
|
|
||||||
)
|
|
||||||
) entries;
|
|
||||||
message = "Desktop keybind actions must set the fields their kind requires.";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
keys = lib.mkOption {
|
|
||||||
description = "Sets the key chord for this desktop keybind.";
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "SUPER + Return";
|
|
||||||
};
|
|
||||||
|
|
||||||
repeating = lib.mkOption {
|
|
||||||
description = "Repeats the keybind action while the key chord is held.";
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
example = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
locked = lib.mkOption {
|
|
||||||
description = "Allows the keybind action while the session is locked.";
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
example = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
action = lib.mkOption {
|
|
||||||
description = "Declares the action this keybind runs.";
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
kind = lib.mkOption {
|
|
||||||
description = "Selects the keybind action kind.";
|
|
||||||
type = lib.types.enum [
|
|
||||||
"exec"
|
|
||||||
"focus"
|
|
||||||
"move-window"
|
|
||||||
"resize-window"
|
|
||||||
"toggle-floating"
|
|
||||||
"toggle-fullscreen"
|
|
||||||
"close-window"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
command = lib.mkOption {
|
|
||||||
description = "Command executed by an exec keybind.";
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "ghostty";
|
|
||||||
};
|
|
||||||
|
|
||||||
direction = lib.mkOption {
|
|
||||||
description = "Direction targeted by a keybind action.";
|
|
||||||
type = lib.types.nullOr (
|
|
||||||
lib.types.enum [
|
|
||||||
"down"
|
|
||||||
"left"
|
|
||||||
"right"
|
|
||||||
"up"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
default = null;
|
|
||||||
example = "left";
|
|
||||||
};
|
|
||||||
|
|
||||||
workspace = lib.mkOption {
|
|
||||||
description = "Workspace targeted by a keybind action.";
|
|
||||||
type = lib.types.nullOr lib.types.int;
|
|
||||||
default = null;
|
|
||||||
example = 1;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,17 @@
|
|||||||
{ lib, util, ... }:
|
{ lib, ... }:
|
||||||
{
|
{
|
||||||
checks = [
|
checks = [
|
||||||
{
|
{
|
||||||
assertion = util.registry.predicates.uniqueValue (entry: entry.value.domain);
|
assertion = lib.registry.predicates.uniqueValue (entry: entry.value.domain);
|
||||||
message = "Route domains must be unique.";
|
message = "Route domains must be unique.";
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
assertion =
|
assertion = entries:
|
||||||
entries:
|
lib.registry.predicates.uniqueValue (entry: entry.value) (
|
||||||
util.registry.predicates.uniqueValue (entry: entry.value) (
|
lib.concatMap (entry: map (port: entry // { value = port; }) entry.value.udp.ports) entries
|
||||||
lib.concatMap (entry: map (port: entry // { value = port; }) entry.value.ports) entries
|
|
||||||
);
|
);
|
||||||
message = "Route UDP ports must be unique.";
|
message = "Route UDP ports must be unique.";
|
||||||
}
|
}
|
||||||
{
|
|
||||||
assertion =
|
|
||||||
entries:
|
|
||||||
lib.all (entry: entry.value.port != null && entry.value.ports == [ ]) (
|
|
||||||
lib.filter (entry: entry.value.protocol != "udp") entries
|
|
||||||
);
|
|
||||||
message = "HTTP and HTTPS routes must set port and not ports.";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion =
|
|
||||||
entries:
|
|
||||||
lib.all (entry: entry.value.port == null && entry.value.ports != [ ]) (
|
|
||||||
lib.filter (entry: entry.value.protocol == "udp") entries
|
|
||||||
);
|
|
||||||
message = "UDP routes must set ports and not port.";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion =
|
|
||||||
entries:
|
|
||||||
lib.all (entry: entry.value.protocol != "udp" || entry.value.clientMaxBodySize == null) entries;
|
|
||||||
message = "UDP routes must not set clientMaxBodySize.";
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
@@ -42,40 +19,24 @@
|
|||||||
domain = lib.mkOption {
|
domain = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "files.alexion.dev";
|
example = "files.alexion.dev";
|
||||||
description = "Records the public domain for this route.";
|
description = "Public domain served by this route entry.";
|
||||||
};
|
|
||||||
|
|
||||||
protocol = lib.mkOption {
|
|
||||||
type = lib.types.enum [
|
|
||||||
"http"
|
|
||||||
"https"
|
|
||||||
"udp"
|
|
||||||
];
|
|
||||||
description = "Selects the public protocol for this route.";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
port = lib.mkOption {
|
port = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.port;
|
type = lib.types.nullOr lib.types.port;
|
||||||
default = null;
|
default = null;
|
||||||
example = 3923;
|
example = 3923;
|
||||||
description = "Sets the HTTP backend port for an HTTP or HTTPS route.";
|
description = "Backend TCP port served by the matching address registry entry.";
|
||||||
};
|
};
|
||||||
|
|
||||||
ports = lib.mkOption {
|
udp.ports = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.port;
|
type = lib.types.listOf lib.types.port;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
example = [
|
example = [
|
||||||
2456
|
2456
|
||||||
2457
|
2457
|
||||||
];
|
];
|
||||||
description = "Sets the UDP ports for a UDP route.";
|
description = "UDP ports served by the matching address registry entry.";
|
||||||
};
|
|
||||||
|
|
||||||
clientMaxBodySize = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "1g";
|
|
||||||
description = "Maximum HTTP request body the reverse proxy accepts.";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
{
|
let
|
||||||
type = lib.types.coercedTo lib.types.str (command: { inherit command; }) (
|
entryType = lib.types.submodule {
|
||||||
lib.types.submodule {
|
options = {
|
||||||
options = {
|
command = lib.mkOption {
|
||||||
command = lib.mkOption {
|
type = lib.types.str;
|
||||||
type = lib.types.str;
|
example = "nvim";
|
||||||
example = "nvim";
|
description = "Command expanded by this shell shortcut.";
|
||||||
description = "Sets the command expanded by this shortcut.";
|
|
||||||
};
|
|
||||||
|
|
||||||
abbreviation = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = true;
|
|
||||||
description = "Uses a Fish abbreviation instead of an alias.";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
);
|
abbreviation = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Whether fish realizes this shell shortcut as an abbreviation instead of an alias.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
type = lib.types.coercedTo lib.types.str (command: { inherit command; }) entryType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
let
|
let
|
||||||
duplicateUids =
|
inherit (lib)
|
||||||
entries:
|
filterAttrs
|
||||||
lib.filterAttrs (_uid: entryNames: builtins.length entryNames > 1) (entryNamesByUid entries);
|
groupBy
|
||||||
|
mapAttrs
|
||||||
|
unique
|
||||||
|
;
|
||||||
|
|
||||||
entryNamesByUid =
|
serviceNamesByUid = entries:
|
||||||
entries:
|
mapAttrs (_uid: uidEntries: unique (map (entry: entry.entry) uidEntries)) (
|
||||||
lib.mapAttrs (_uid: uidEntries: lib.unique (map (entry: entry.entry) uidEntries)) (
|
groupBy (entry: toString entry.value) entries
|
||||||
lib.groupBy (entry: toString entry.value) entries
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
duplicateUids = entries:
|
||||||
|
filterAttrs (_uid: serviceNames: builtins.length serviceNames > 1) (serviceNamesByUid entries);
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Next stable UID: 20005
|
# Next stable UID: 20003
|
||||||
|
replicate = true;
|
||||||
|
|
||||||
type = lib.types.ints.positive;
|
type = lib.types.ints.positive;
|
||||||
|
|
||||||
checks = [
|
checks = [
|
||||||
{
|
{
|
||||||
assertion = entries: duplicateUids entries == { };
|
assertion = entries: duplicateUids entries == { };
|
||||||
message = "Multiple entries use the same stable UID.";
|
message = "Stable UID allocations must be unique.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
{ lib, ... }:
|
|
||||||
{
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
position = lib.mkOption {
|
|
||||||
type = lib.types.enum [
|
|
||||||
"left"
|
|
||||||
"center"
|
|
||||||
"right"
|
|
||||||
];
|
|
||||||
default = "right";
|
|
||||||
description = "Selects the Waybar section that shows the module.";
|
|
||||||
};
|
|
||||||
|
|
||||||
order = lib.mkOption {
|
|
||||||
type = lib.types.int;
|
|
||||||
default = 100;
|
|
||||||
example = 50;
|
|
||||||
description = "Sorts the module within its Waybar section.";
|
|
||||||
};
|
|
||||||
|
|
||||||
settings = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf lib.types.anything;
|
|
||||||
default = { };
|
|
||||||
description = "Sets the Waybar module configuration.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user