Compare commits
186 Commits
88762118b9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| dcc4efcc75 | |||
| a23a317335 | |||
| 6e545c0a15 | |||
| 6081e3e325 | |||
| 719370c8f7 | |||
| 2b4e66a75c | |||
| c44769ff70 | |||
| 1397576538 | |||
| 5fbe1850e0 | |||
| 57bb21af1a | |||
| ffc40e547b | |||
| 55f61f71d8 | |||
| 08563c6769 | |||
| 8462b3a5a8 | |||
| d0872e7d8c | |||
| 9be9ad7675 | |||
| 5ee328cdf8 | |||
| 6532a822cd | |||
| 20a3990979 | |||
| b7ab518f58 | |||
| 3f1e34ef95 | |||
| d0abfc1625 | |||
| 031090e4ea | |||
| 2edad6c5df | |||
| 1d5acc09c6 | |||
| ee29e53f7c | |||
| 3c9d0ec886 | |||
| f0c5544b10 | |||
| 4dd0a5a4f5 | |||
| a4a795f7ee | |||
| ecdf877308 | |||
| d1face10ec | |||
| 337d9a2014 | |||
| f059068873 | |||
| 7662c04cda | |||
| 33128fc3c7 | |||
| 68415ad47e | |||
| 6b7201e6fa | |||
| 4360f4863b | |||
| 370cab8f83 | |||
| e74a08b853 | |||
| 250b0b71f9 | |||
| 10299cec19 | |||
| 91654b3068 | |||
| be9c3c07a3 | |||
| a8bd4bf4cf | |||
| a2fc568704 | |||
| 0bf5ebae87 | |||
| fddec14128 | |||
| 85fadc5bfc | |||
| 3992030949 | |||
| c8a596aa39 | |||
| 4908c4751f | |||
| 79c56dfb1c | |||
| 7140b6d258 | |||
| f173887228 | |||
| 3a487050fd | |||
| 65c84f5b39 | |||
| b3c4903c82 | |||
| ec1c0fba56 | |||
| 1f2f1a79ad | |||
| 46a47fa061 | |||
| 8d357e960b | |||
| f48bac5ec1 | |||
| d53246caf0 | |||
| 92f7f0542d | |||
| 4f7fbef35b | |||
| c7c3257b9d | |||
| 7d37b92e17 | |||
| 9ece182099 | |||
| 9aea13b8c2 | |||
| 369c7512c6 | |||
| 8c91a210b2 | |||
| d602581a99 | |||
| c3a6271582 | |||
| a7db0b745c | |||
| 4a8e8920ea | |||
| 3e9295134e | |||
| c91094234f | |||
| 11b490d1e8 | |||
| 43b5d07a27 | |||
| 1b4dc77627 | |||
| 1cbdcee8ac | |||
| 3fbb0aca32 | |||
| 87776c8994 | |||
| 17597526c5 | |||
| 14c9a418b6 | |||
| 589e37f52e | |||
| 78492739f3 | |||
| 2265dfc558 | |||
| becbb50491 | |||
| 54335ead82 | |||
| 16c69e01e1 | |||
| 5af563d70f | |||
| 0e5731145e | |||
| b6469567e1 | |||
| 8f0837a4b5 | |||
| 0937f15260 | |||
| 61efef1a7d | |||
| 8eabdee2a0 | |||
| 43b3ff4b09 | |||
| 3b2d16743e | |||
| 11d11c7c02 | |||
| 869d6a2cbd | |||
| e5b76515f1 | |||
| 324d40c85d | |||
| dd82367dcb | |||
| ac5cc8d659 | |||
| 5e5c3e2534 | |||
| 09a2093f90 | |||
| 8000367613 | |||
| 38dff04ece | |||
| 0c1a1c1ffa | |||
| 27d9194ac3 | |||
| c4ea12b3ce | |||
| 16ce11fc52 | |||
| a73636a84f | |||
| 80db2b61e6 | |||
| 7cc066b55d | |||
| 74e7d2d211 | |||
| 98d734e977 | |||
| dda05559a5 | |||
| 594877c4ba | |||
| f9aff6839e | |||
| 4b7d3834ab | |||
| 213cd84642 | |||
| 6e19169e11 | |||
| be8744795c | |||
| ad00301ac9 | |||
| 87dc93513b | |||
| 58f2350d5f | |||
| 4b9beae501 | |||
| addcf7ca8e | |||
| 6887b104d6 | |||
| a6fe84190e | |||
| 1fa3962fed | |||
| 113018a62c | |||
| 11f1eba26f | |||
| d974a46211 | |||
| 6b29f2c2b9 | |||
| 49cf043035 | |||
| 37539682d4 | |||
| 7a3a3d6b90 | |||
| 87916a8e17 | |||
| 498d0dd467 | |||
| b4b77faaa9 | |||
| cf9a671fb8 | |||
| 247712582a | |||
| ddc7952399 | |||
| 885124c240 | |||
| 7b09cea3af | |||
| 82ad65a3a4 | |||
| cbdf482422 | |||
| 82b727e437 | |||
| 652b92d4f4 | |||
| 8154feba5a | |||
| e15ce7f16b | |||
| d24f58a4c9 | |||
| 5d40b43dd9 | |||
| 9a038d9821 | |||
| fca37666d3 | |||
| aa1a330ef3 | |||
| 7be45ac44c | |||
| 4b3ca27637 | |||
| a73b59aa0a | |||
| 6159d87d10 | |||
| e65eb8389f | |||
| 212ed506a2 | |||
| a8a18d27e9 | |||
| aac222bee1 | |||
| b6613b3a04 | |||
| 9e7759c9f5 | |||
| 4def357d09 | |||
| 5ebc7018c3 | |||
| 25ecd986b5 | |||
| b3bda4a739 | |||
| 8fe2e6f844 | |||
| 7625b031ec | |||
| 6a364f5ce9 | |||
| 1a5edc1a5f | |||
| cd220b8030 | |||
| ec96a948b6 | |||
| a3ed3b0b9d | |||
| 9e7469796d | |||
| 8bd9ff166e | |||
| 5758a781d8 |
@@ -1,287 +0,0 @@
|
|||||||
// 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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
42
.pi/prompts/review.md
Normal file
42
.pi/prompts/review.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
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
.pi/skills/build-style
Symbolic link
1
.pi/skills/build-style
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../src/skills/style/build
|
||||||
1
.pi/skills/conventional-commits
Symbolic link
1
.pi/skills/conventional-commits
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../src/skills/conventional-commits
|
||||||
1
.pi/skills/craft-skill
Symbolic link
1
.pi/skills/craft-skill
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../src/skills/craft-skill
|
||||||
1
.pi/skills/nix-style
Symbolic link
1
.pi/skills/nix-style
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../src/skills/style/nix
|
||||||
1
.pi/skills/rust-style
Symbolic link
1
.pi/skills/rust-style
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../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
|
||||||
24
AGENTS.md
24
AGENTS.md
@@ -3,7 +3,6 @@
|
|||||||
## 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.
|
||||||
@@ -95,10 +94,33 @@
|
|||||||
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 flake does not currently define `formatter.x86_64-linux`.
|
- The flake does not currently define `formatter.x86_64-linux`.
|
||||||
Do not use `nix fmt` as a validation command until a formatter is added.
|
Do not use `nix fmt` as a validation command until a formatter is added.
|
||||||
|
- The `fix` app passes its current directory to Deadnix.
|
||||||
|
Run `nix run .#fix` from the repository root so path exclusions match.
|
||||||
|
- 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.
|
||||||
|
|||||||
33
Cargo.lock
generated
33
Cargo.lock
generated
@@ -57,15 +57,16 @@ 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.6"
|
version = "4.6.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -73,9 +74,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.6.6"
|
version = "4.6.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -85,9 +86,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.6.4"
|
version = "4.6.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -97,9 +98,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
@@ -135,6 +136,10 @@ 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"
|
||||||
@@ -202,6 +207,10 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "status"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -210,9 +219,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "3.0.5"
|
version = "3.0.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -221,9 +230,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
|
|||||||
@@ -2,5 +2,14 @@
|
|||||||
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,4 +14,5 @@ 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,7 +13,8 @@ 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
|
||||||
@@ -44,3 +45,45 @@ 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
|
||||||
|
|
||||||
- TODO: Verify this path after the NixOS configuration is migrated.
|
- When adding a graphical application, register its workspace icon in `registry.window-icon`.
|
||||||
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,13 +40,31 @@
|
|||||||
|
|
||||||
## 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.
|
||||||
- Pikachu deploys from the monorepo checkout copied to `~/dev`.
|
- A read-only guest bind mount cannot contain nested writable bind mounts unless their mount-point
|
||||||
Do not use the old `~/wrk/dotfiles` path for remote rebuild commands.
|
directories already exist inside the read-only source.
|
||||||
|
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`.
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
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,51 +5,13 @@
|
|||||||
}:
|
}:
|
||||||
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/alexion/password";
|
default = "services/copyparty/users/${config.user.name}/password";
|
||||||
description = "SOPS secret name containing the Copyparty account password.";
|
description = "SOPS secret name that contains 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 {
|
||||||
@@ -66,7 +28,39 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
volumes = lib.mkOption {
|
volumes = lib.mkOption {
|
||||||
type = lib.types.attrsOf volumeType;
|
type = lib.types.attrsOf (
|
||||||
|
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.";
|
||||||
};
|
};
|
||||||
@@ -84,41 +78,57 @@ 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 (_urlPath: volume: {
|
volumes = lib.mapAttrs (
|
||||||
inherit (volume) path access flags;
|
_urlPath: volume:
|
||||||
}) 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" = {
|
||||||
${cfg.stateDir} = {
|
directory = cfg.statePath;
|
||||||
directory = cfg.statePath;
|
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||||
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
group = "storage";
|
||||||
group = "storage";
|
mode = "0770";
|
||||||
mode = "0770";
|
};
|
||||||
};
|
${"/run/secrets/${cfg.passwordSecret}"} = {
|
||||||
${"/run/secrets/${cfg.passwordSecret}"} = {
|
file = config.sops.secrets.${cfg.passwordSecret}.path;
|
||||||
file = config.sops.secrets.${cfg.passwordSecret}.path;
|
readOnly = true;
|
||||||
readOnly = true;
|
};
|
||||||
};
|
}
|
||||||
|
// lib.mapAttrs' (
|
||||||
|
_urlPath: volume:
|
||||||
|
lib.nameValuePair volume.path {
|
||||||
|
directory = volume.hostPath;
|
||||||
|
owner = "root";
|
||||||
|
group = "storage";
|
||||||
|
mode = "2770";
|
||||||
}
|
}
|
||||||
// lib.mapAttrs' (_urlPath: volume:
|
) cfg.volumes;
|
||||||
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%";
|
||||||
|
|||||||
46
configs/nixos/guests/homepage.nix
Normal file
46
configs/nixos/guests/homepage.nix
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
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"}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
103
configs/nixos/guests/navidrome.nix
Normal file
103
configs/nixos/guests/navidrome.nix
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
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}}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
155
configs/nixos/guests/palworld.nix
Normal file
155
configs/nixos/guests/palworld.nix
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.guests.palworld;
|
||||||
|
steamDir = "/data/steamcmd";
|
||||||
|
installDir = "/data/server";
|
||||||
|
stateDir = "/data/state";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.guests.palworld = {
|
||||||
|
steamPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/steamcmd";
|
||||||
|
description = "Host path bind-mounted as SteamCMD's metadata directory.";
|
||||||
|
};
|
||||||
|
|
||||||
|
installPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/palworld/server";
|
||||||
|
description = "Host path bind-mounted as Palworld's server install directory.";
|
||||||
|
};
|
||||||
|
|
||||||
|
statePath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/palworld/state";
|
||||||
|
description = "Host path bind-mounted as Palworld's durable state directory.";
|
||||||
|
};
|
||||||
|
|
||||||
|
serverName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "palworld";
|
||||||
|
description = "Name shown for the Palworld server.";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 8211;
|
||||||
|
description = "UDP port for the Palworld server.";
|
||||||
|
};
|
||||||
|
|
||||||
|
publicIP = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
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 {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "services/game-servers/password";
|
||||||
|
description = "SOPS secret name containing the Palworld server password.";
|
||||||
|
};
|
||||||
|
|
||||||
|
adminPasswordSecret = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "services/game-servers/password";
|
||||||
|
description = "SOPS secret name containing the Palworld admin password.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.steamPath != null;
|
||||||
|
message = "guests.palworld.steamPath must point at the host directory that stores SteamCMD metadata.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.installPath != null;
|
||||||
|
message = "guests.palworld.installPath must point at the host directory that stores Palworld server files.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.statePath != null;
|
||||||
|
message = "guests.palworld.statePath must point at the host directory that stores Palworld durable state.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
guests.palworld = {
|
||||||
|
interior.modules.services.games.palworld = {
|
||||||
|
enable = true;
|
||||||
|
inherit (cfg)
|
||||||
|
serverName
|
||||||
|
port
|
||||||
|
publicIP
|
||||||
|
autoStart
|
||||||
|
;
|
||||||
|
inherit steamDir installDir stateDir;
|
||||||
|
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
||||||
|
adminPasswordFile = "/run/secrets/${cfg.adminPasswordSecret}";
|
||||||
|
};
|
||||||
|
|
||||||
|
mounts = {
|
||||||
|
${steamDir} = {
|
||||||
|
directory = cfg.steamPath;
|
||||||
|
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
limits = {
|
||||||
|
memory = lib.mkDefault "8G";
|
||||||
|
cpu = lib.mkDefault "400%";
|
||||||
|
tasksMax = lib.mkDefault 2048;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sops.secrets = {
|
||||||
|
${cfg.passwordSecret} = {
|
||||||
|
owner = "root";
|
||||||
|
group = "storage";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.optionalAttrs (cfg.adminPasswordSecret != cfg.passwordSecret) {
|
||||||
|
${cfg.adminPasswordSecret} = {
|
||||||
|
owner = "root";
|
||||||
|
group = "storage";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -23,11 +23,6 @@ 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;
|
||||||
|
|||||||
116
configs/nixos/guests/valheim.nix
Normal file
116
configs/nixos/guests/valheim.nix
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.guests.valheim;
|
||||||
|
steamDir = "/data/steamcmd";
|
||||||
|
installDir = "/data/server";
|
||||||
|
stateDir = "/data/state";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.guests.valheim = {
|
||||||
|
steamPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/steamcmd";
|
||||||
|
description = "Host path bind-mounted as SteamCMD's metadata directory.";
|
||||||
|
};
|
||||||
|
|
||||||
|
installPath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/valheim/server";
|
||||||
|
description = "Host path bind-mounted as Valheim's server install directory.";
|
||||||
|
};
|
||||||
|
|
||||||
|
statePath = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "/srv/valheim/state";
|
||||||
|
description = "Host path bind-mounted as Valheim's world state directory.";
|
||||||
|
};
|
||||||
|
|
||||||
|
serverName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "valheim";
|
||||||
|
description = "Name shown for the Valheim server.";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 2456;
|
||||||
|
description = "Base UDP port for the Valheim server.";
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordSecret = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "services/game-servers/password";
|
||||||
|
description = "SOPS secret name containing the Valheim server password.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.steamPath != null;
|
||||||
|
message = "guests.valheim.steamPath must point at the host directory that stores SteamCMD metadata.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.installPath != null;
|
||||||
|
message = "guests.valheim.installPath must point at the host directory that stores Valheim server files.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.statePath != null;
|
||||||
|
message = "guests.valheim.statePath must point at the host directory that stores Valheim world state.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
guests.valheim = {
|
||||||
|
interior.modules.services.games.valheim = {
|
||||||
|
enable = true;
|
||||||
|
inherit (cfg) serverName port;
|
||||||
|
inherit steamDir installDir stateDir;
|
||||||
|
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
||||||
|
};
|
||||||
|
|
||||||
|
mounts = {
|
||||||
|
${steamDir} = {
|
||||||
|
directory = cfg.steamPath;
|
||||||
|
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
limits = {
|
||||||
|
memory = lib.mkDefault "4G";
|
||||||
|
cpu = lib.mkDefault "400%";
|
||||||
|
tasksMax = lib.mkDefault 1024;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sops.secrets.${cfg.passwordSecret} = {
|
||||||
|
owner = "root";
|
||||||
|
group = "storage";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
inputs,
|
inputs,
|
||||||
pkgs,
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# neogaia — Dell XPS 13 9380 laptop.
|
# neogaia — Dell XPS 13 9380 laptop.
|
||||||
@@ -13,10 +13,9 @@
|
|||||||
./disk.nix
|
./disk.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
boot.kernelPackages = pkgs.linuxPackages_cachyos;
|
modules.host.cachyos.enable = true;
|
||||||
|
|
||||||
# So wifi can be joined from the console.
|
modules.network.enable = true;
|
||||||
networking.networkmanager.enable = true;
|
|
||||||
|
|
||||||
registry.address.neogaia.self = "10.23.50.146";
|
registry.address.neogaia.self = "10.23.50.146";
|
||||||
|
|
||||||
@@ -28,15 +27,9 @@
|
|||||||
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;
|
||||||
@@ -47,4 +40,23 @@
|
|||||||
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
configs/nixos/hosts/neogaia/ssh_user_ed25519_key.pub
Normal file
1
configs/nixos/hosts/neogaia/ssh_user_ed25519_key.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE alexion@neogaia
|
||||||
@@ -15,77 +15,75 @@
|
|||||||
|
|
||||||
hardware.cpu.intel.updateMicrocode = true;
|
hardware.cpu.intel.updateMicrocode = true;
|
||||||
|
|
||||||
systemd.network = {
|
modules.network.vlans = {
|
||||||
enable = true;
|
interface = "enp2s0";
|
||||||
netdevs = {
|
ids = [ 20 ];
|
||||||
"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.zfs = {
|
modules.host.zfs = {
|
||||||
enable = true;
|
enable = true;
|
||||||
hostId = "2346edbd";
|
|
||||||
pools.pikachu = { };
|
pools.pikachu = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.address.pikachu.self = "10.23.10.102";
|
registry.address.pikachu.self = "10.23.10.102";
|
||||||
|
|
||||||
modules.services.games.valheim = {
|
guests.valheim = {
|
||||||
enable = true;
|
|
||||||
steamDir = "/pikachu/data/steamcmd";
|
|
||||||
installDir = "/pikachu/data/servers/valheim/main";
|
|
||||||
stateDir = "/pikachu/media/games/servers/valheim/main";
|
|
||||||
serverName = "main";
|
|
||||||
password = "foobarbaz";
|
|
||||||
};
|
|
||||||
|
|
||||||
guests.actualbudget = {
|
|
||||||
enable = true;
|
enable = true;
|
||||||
vlan = 20;
|
vlan = 20;
|
||||||
mac = "BC:24:11:C1:CD:28";
|
address = "10.23.20.197";
|
||||||
dataPath = "/pikachu/data/actualbudget";
|
steamPath = "/pikachu/data/steamcmd";
|
||||||
address = "10.23.20.42";
|
installPath = "/pikachu/data/servers/valheim/main";
|
||||||
|
statePath = "/pikachu/media/games/servers/valheim/main";
|
||||||
|
serverName = "main";
|
||||||
reverseProxy = {
|
reverseProxy = {
|
||||||
enable = true;
|
enable = true;
|
||||||
route = {
|
route = {
|
||||||
domain = "budget.alexion.dev";
|
domain = "valheim.alexion.dev";
|
||||||
port = 5006;
|
protocol = "udp";
|
||||||
|
ports = [
|
||||||
|
config.guests.valheim.port
|
||||||
|
(config.guests.valheim.port + 1)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
guests.palworld = {
|
||||||
|
enable = true;
|
||||||
|
vlan = 20;
|
||||||
|
address = "10.23.20.250";
|
||||||
|
steamPath = "/pikachu/data/steamcmd";
|
||||||
|
installPath = "/pikachu/data/servers/palworld/tracion";
|
||||||
|
statePath = "/pikachu/media/games/servers/palworld/tracion";
|
||||||
|
serverName = "Tracion";
|
||||||
|
autoStart = false;
|
||||||
|
adminPasswordSecret = "services/game-servers/admin/password";
|
||||||
|
publicIP = config.guests.palworld.address;
|
||||||
|
reverseProxy = {
|
||||||
|
enable = true;
|
||||||
|
route = {
|
||||||
|
domain = "palworld.alexion.dev";
|
||||||
|
protocol = "udp";
|
||||||
|
ports = [ config.guests.palworld.port ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
guests.navidrome = {
|
||||||
|
enable = true;
|
||||||
|
vlan = 20;
|
||||||
|
mac = "BC:24:11:C9:17:83";
|
||||||
|
dataPath = "/pikachu/data/navidrome";
|
||||||
|
libraryPath = "/pikachu/media/music";
|
||||||
|
address = "10.23.20.43";
|
||||||
|
reverseProxy = {
|
||||||
|
enable = true;
|
||||||
|
route = {
|
||||||
|
domain = "music.alexion.dev";
|
||||||
|
protocol = "https";
|
||||||
|
port = 4533;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -97,6 +95,20 @@
|
|||||||
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;
|
||||||
@@ -107,30 +119,31 @@
|
|||||||
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 = "/srv/inbox";
|
path = "/pikachu/__inbox";
|
||||||
access.A = [ "alexion" ];
|
|
||||||
flags = [
|
|
||||||
"d2d"
|
|
||||||
"d2t"
|
|
||||||
"dthumb"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
"/media" = {
|
"/media" = {
|
||||||
hostPath = "/pikachu/media";
|
hostPath = "/pikachu/media";
|
||||||
path = "/srv/media";
|
path = "/pikachu/media";
|
||||||
access.A = [ "alexion" ];
|
};
|
||||||
flags = [
|
"/opds" = {
|
||||||
"d2d"
|
hostPath = "/pikachu/data/opds";
|
||||||
"d2t"
|
path = "/pikachu/data/opds";
|
||||||
"dthumb"
|
readOnly = true;
|
||||||
];
|
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
configs/nixos/hosts/pikachu/ssh_user_ed25519_key.pub
Normal file
1
configs/nixos/hosts/pikachu/ssh_user_ed25519_key.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNqJIC6VRXyvrNf3n9su9KdPCikC3CjK/QrCK2reHdB alexion@pikachu
|
||||||
@@ -1,25 +1,32 @@
|
|||||||
{
|
{
|
||||||
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 = [ ./disk.nix ] ++ lib.optional (builtins.pathExists ./hardware-configuration.nix) ./hardware-configuration.nix;
|
imports = [
|
||||||
|
./disk.nix
|
||||||
|
]
|
||||||
|
++ lib.optional (builtins.pathExists ./hardware-configuration.nix) ./hardware-configuration.nix;
|
||||||
|
|
||||||
nixpkgs.hostPlatform = "x86_64-linux";
|
nixpkgs.hostPlatform = "x86_64-linux";
|
||||||
|
|
||||||
boot.kernelPackages = pkgs.linuxPackages_cachyos;
|
modules.host.cachyos.enable = true;
|
||||||
|
|
||||||
hardware.graphics = {
|
hardware.graphics = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enable32Bit = true;
|
enable32Bit = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.networkmanager.enable = true;
|
modules.network.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";
|
||||||
|
|
||||||
@@ -32,16 +39,29 @@
|
|||||||
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"
|
{
|
||||||
"DP-3,2560x1440@239.96,1080x0,1"
|
output = "HDMI-A-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
configs/nixos/hosts/zeus/ssh_user_ed25519_key.pub
Normal file
1
configs/nixos/hosts/zeus/ssh_user_ed25519_key.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRBcHHuubptXCdvED/p2dR3MpjGN/AimqLasLY5984G alexion@zeus
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
{
|
{
|
||||||
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 ];
|
||||||
|
|
||||||
@@ -17,14 +13,14 @@ in
|
|||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "alexion";
|
default = "alexion";
|
||||||
description = ''
|
description = ''
|
||||||
Primary interactive user for this system.
|
Names the primary user account.
|
||||||
This drives the system account and Home Manager user together.
|
Host configuration uses this name for the Home Manager user.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
system.stateVersion = stateVersion;
|
system.stateVersion = "26.05";
|
||||||
|
|
||||||
nixpkgs.config.allowUnfree = true;
|
nixpkgs.config.allowUnfree = true;
|
||||||
nix.settings.experimental-features = [
|
nix.settings.experimental-features = [
|
||||||
@@ -44,56 +40,5 @@ in
|
|||||||
|
|
||||||
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";
|
|
||||||
};
|
|
||||||
|
|
||||||
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,18 +0,0 @@
|
|||||||
{
|
|
||||||
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,13 +3,12 @@
|
|||||||
inputs,
|
inputs,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Firefox as the desktop browser: stock mainline, hardened and de-monetized by policy.
|
# Configures Firefox as the desktop browser.
|
||||||
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 = {
|
||||||
@@ -19,19 +18,25 @@ in
|
|||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "1.0";
|
default = "1.0";
|
||||||
description = ''
|
description = ''
|
||||||
Firefox UI and page scale, passed to `layout.css.devPixelsPerPx`.
|
Scales Firefox UI and page content.
|
||||||
Override it on hosts that need a larger browser scale, such as small
|
|
||||||
high-DPI laptop panels.
|
Sets `layout.css.devPixelsPerPx`.
|
||||||
|
Use a larger value for high-DPI displays.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
home-manager.users.${user} = hm: {
|
registry.window-icon.${config.networking.hostName}.firefox = {
|
||||||
|
class = ".*[Ff]irefox.*";
|
||||||
|
icon = util.glyph "f269";
|
||||||
|
};
|
||||||
|
|
||||||
|
home-manager.users.${config.user.name} = hm: {
|
||||||
programs.firefox = {
|
programs.firefox = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
# Enterprise policies: enforced and unchangeable from the browser UI.
|
# Applies policies that users cannot change in Firefox.
|
||||||
policies = {
|
policies = {
|
||||||
DisableTelemetry = true;
|
DisableTelemetry = true;
|
||||||
DisableFirefoxStudies = true;
|
DisableFirefoxStudies = true;
|
||||||
@@ -40,7 +45,7 @@ in
|
|||||||
DontCheckDefaultBrowser = true;
|
DontCheckDefaultBrowser = true;
|
||||||
DisableFirefoxAccounts = true;
|
DisableFirefoxAccounts = true;
|
||||||
|
|
||||||
# Strip the monetized surfaces from the new-tab page.
|
# Removes sponsored content from the new-tab page.
|
||||||
FirefoxHome = {
|
FirefoxHome = {
|
||||||
SponsoredTopSites = false;
|
SponsoredTopSites = false;
|
||||||
SponsoredPocket = false;
|
SponsoredPocket = false;
|
||||||
@@ -49,23 +54,19 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
profiles.default = {
|
profiles.default = {
|
||||||
isDefault = true;
|
|
||||||
|
|
||||||
extensions = {
|
extensions = {
|
||||||
packages = with firefoxAddons; [
|
packages = with inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system}; [
|
||||||
ublock-origin
|
ublock-origin
|
||||||
proton-pass
|
proton-pass
|
||||||
sponsorblock
|
sponsorblock
|
||||||
];
|
];
|
||||||
|
|
||||||
# The Nord chrome theme is a declared extension setting, so home-manager
|
# Home Manager overwrites Nord theme changes made in Firefox.
|
||||||
# owns the extension-settings store, overwriting runtime changes to it.
|
|
||||||
force = true;
|
force = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Stylix's Nord mapping paints the selected address-bar result a
|
# Stylix uses a near-white selection background.
|
||||||
# near-white grey, leaving its light text unreadable. Darken that one
|
# Use the Nord selection color because its text stays readable.
|
||||||
# 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;
|
||||||
@@ -77,32 +78,26 @@ 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;
|
||||||
|
|
||||||
# Auto-enable the sideloaded Firefox Color add-on carrying the Nord
|
# Enables Firefox Color after Firefox sideloads the add-on.
|
||||||
# chrome theme, which Firefox otherwise leaves disabled.
|
|
||||||
"extensions.autoDisableScopes" = 0;
|
"extensions.autoDisableScopes" = 0;
|
||||||
|
|
||||||
# Sponsored surfaces the policies above do not reach.
|
# Removes sponsored content outside Firefox policies.
|
||||||
"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 = {
|
||||||
# Declaring search overwrites Firefox's own engine list wholesale,
|
# Home Manager replaces the engine list when this setting is enabled.
|
||||||
# so engines added later in the UI do not survive a rebuild.
|
# Engines added in Firefox do not survive a rebuild.
|
||||||
force = true;
|
force = true;
|
||||||
default = "ddg";
|
default = "ddg";
|
||||||
|
|
||||||
# The general-purpose commercial engines, hidden to leave a lean
|
# Hides commercial search engines.
|
||||||
# DuckDuckGo-and-Wikipedia list. They stay reachable through bangs.
|
# Bangs still reach these engines.
|
||||||
# An engine carrying only metaData is treated as a builtin.
|
# Entries with only `metaData` configure Firefox built-in engines.
|
||||||
engines = {
|
engines = {
|
||||||
google.metaData.hidden = true;
|
google.metaData.hidden = true;
|
||||||
bing.metaData.hidden = true;
|
bing.metaData.hidden = true;
|
||||||
@@ -114,8 +109,7 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Nord chrome for the one profile, applied through the Stylix-managed
|
# Applies the Nord theme through Stylix's Firefox Color add-on.
|
||||||
# Firefox Color add-on that colorTheme enables.
|
|
||||||
stylix.targets.firefox = {
|
stylix.targets.firefox = {
|
||||||
enable = true;
|
enable = true;
|
||||||
profileNames = [ "default" ];
|
profileNames = [ "default" ];
|
||||||
@@ -133,8 +127,8 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Firefox writes profiles.ini itself on first launch, so home-manager is
|
# Firefox creates `profiles.ini` on its first launch.
|
||||||
# told to own the file rather than fail activation refusing to clobber it.
|
# Force replacement during Home Manager activation.
|
||||||
home.file."${hm.config.programs.firefox.configPath}/profiles.ini".force = true;
|
home.file."${hm.config.programs.firefox.configPath}/profiles.ini".force = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,17 +2,30 @@
|
|||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.apps.ghostty.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,17 +2,23 @@
|
|||||||
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 = lib.mkEnableOption "Obsidian as the desktop note-taking vault";
|
options.modules.desktop.apps.obsidian.enable =
|
||||||
|
lib.mkEnableOption "Obsidian as the desktop note-taking vault";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.desktop.apps.obsidian.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";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
18
configs/nixos/modules/desktop/apps/r2modman.nix
Normal file
18
configs/nixos/modules/desktop/apps/r2modman.nix
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
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,17 +2,22 @@
|
|||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.apps.signal.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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.apps.steam.enable {
|
||||||
|
programs.gamemode.enable = true;
|
||||||
|
|
||||||
programs.steam = {
|
programs.steam = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = pkgs.steam.override {
|
package = pkgs.steam.override {
|
||||||
@@ -20,5 +20,14 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
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,17 +2,23 @@
|
|||||||
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 = lib.mkEnableOption "Vesktop, a Discord client with Vencord built in";
|
options.modules.desktop.apps.vencord.enable =
|
||||||
|
lib.mkEnableOption "Vesktop, a Discord client with Vencord built in";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.desktop.apps.vencord.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,14 +1,11 @@
|
|||||||
{ 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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.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;
|
||||||
@@ -26,5 +23,6 @@ in
|
|||||||
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,41 +1,15 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# 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 = {
|
options.modules.desktop.session.hyprland.enable = lib.mkEnableOption "the Hyprland compositor";
|
||||||
enable = lib.mkEnableOption "the Hyprland compositor";
|
|
||||||
|
|
||||||
blur = lib.mkEnableOption ''
|
config = lib.mkIf config.modules.desktop.session.hyprland.enable {
|
||||||
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 = {
|
||||||
@@ -43,7 +17,7 @@ in
|
|||||||
withUWSM = true;
|
withUWSM = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
home-manager.users.${user}.wayland.windowManager.hyprland = {
|
home-manager.users.${config.user.name}.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.
|
||||||
@@ -55,136 +29,216 @@ in
|
|||||||
# uwsm owns the systemd graphical-session targets.
|
# uwsm owns the systemd graphical-session targets.
|
||||||
systemd.enable = false;
|
systemd.enable = false;
|
||||||
|
|
||||||
# Write the native hyprlang hyprland.conf, whose variable and bind syntax
|
# Write the native Lua Hyprland config. Home Manager renders the Nix
|
||||||
# the settings below are expressed in.
|
# settings below as hl.* calls.
|
||||||
configType = "hyprlang";
|
configType = "lua";
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
"$mod" = "SUPER";
|
config = {
|
||||||
"$terminal" = "ghostty";
|
input = {
|
||||||
|
kb_layout = config.services.xserver.xkb.layout;
|
||||||
# Session variables handed to the compositor directly.
|
repeat_delay = 250;
|
||||||
# UWSM launches the session without the shell profile that would carry
|
repeat_rate = 45;
|
||||||
# them, so a variable the compositor or its children must see is set
|
accel_profile = "flat";
|
||||||
# here rather than through home-manager's sessionVariables.
|
touchpad = {
|
||||||
env =
|
natural_scroll = true;
|
||||||
[
|
tap_to_click = true;
|
||||||
# Chromium and Electron apps read this to select native Wayland;
|
disable_while_typing = true;
|
||||||
# nixpkgs wrappers (Obsidian's included) gate their Wayland flags on
|
};
|
||||||
# it, so without it they run under XWayland and blur at this DPI.
|
|
||||||
"NIXOS_OZONE_WL,1"
|
|
||||||
]
|
|
||||||
# Bibata ships XCursor only.
|
|
||||||
# The hyprcursor variables name the same theme, which Hyprland
|
|
||||||
# resolves through its XCursor fallback.
|
|
||||||
++ lib.optionals (cursor != null) [
|
|
||||||
"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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
general = {
|
||||||
|
gaps_in = 4;
|
||||||
|
gaps_out = 8;
|
||||||
|
border_size = 2;
|
||||||
|
layout = "dwindle";
|
||||||
|
};
|
||||||
|
|
||||||
|
decoration = {
|
||||||
|
rounding = 6;
|
||||||
|
blur.enabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
xwayland.force_zero_scaling = true;
|
||||||
|
|
||||||
|
dwindle.preserve_split = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
general = {
|
env = lib.mapAttrsToList (name: value: {
|
||||||
gaps_in = 4;
|
_args = [
|
||||||
gaps_out = 8;
|
name
|
||||||
border_size = 2;
|
value
|
||||||
layout = "dwindle";
|
|
||||||
};
|
|
||||||
|
|
||||||
decoration = {
|
|
||||||
rounding = 6;
|
|
||||||
blur.enabled = cfg.blur;
|
|
||||||
};
|
|
||||||
|
|
||||||
# XWayland clients render at the panel's native resolution instead of
|
|
||||||
# being raster-scaled by the compositor at the fractional monitor scale.
|
|
||||||
xwayland.force_zero_scaling = true;
|
|
||||||
|
|
||||||
animations = {
|
|
||||||
enabled = true;
|
|
||||||
bezier = [ "ease, 0.25, 0.1, 0.25, 1.0" ];
|
|
||||||
# Durations are in centiseconds.
|
|
||||||
# Short values keep the motion subtle.
|
|
||||||
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"
|
|
||||||
];
|
];
|
||||||
|
}) (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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key = "H";
|
||||||
|
direction = "left";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
key = "J";
|
||||||
|
direction = "down";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
key = "K";
|
||||||
|
direction = "up";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
key = "L";
|
||||||
|
direction = "right";
|
||||||
|
}
|
||||||
|
]
|
||||||
|
))
|
||||||
|
{
|
||||||
|
hyprland-toggle-floating = {
|
||||||
|
keys = "SUPER + Space";
|
||||||
|
action.kind = "toggle-floating";
|
||||||
};
|
};
|
||||||
|
|
||||||
dwindle = {
|
hyprland-toggle-fullscreen = {
|
||||||
preserve_split = true;
|
keys = "SUPER + F";
|
||||||
|
action.kind = "toggle-fullscreen";
|
||||||
};
|
};
|
||||||
|
|
||||||
bind = [
|
hyprland-close-window = {
|
||||||
"$mod, Return, exec, $terminal"
|
keys = "SUPER + SHIFT + Q";
|
||||||
|
action.kind = "close-window";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
# Move focus.
|
registry.waybar.${config.networking.hostName}."hyprland/workspaces" = {
|
||||||
"$mod, H, movefocus, l"
|
position = "left";
|
||||||
"$mod, J, movefocus, d"
|
order = 10;
|
||||||
"$mod, K, movefocus, u"
|
settings = {
|
||||||
"$mod, L, movefocus, r"
|
format = "{id} {windows}";
|
||||||
|
format-window-separator = " ";
|
||||||
# Move the window within the layout.
|
window-rewrite-default = util.glyph "f2d0";
|
||||||
"$mod SHIFT, H, movewindow, l"
|
window-rewrite = lib.mapAttrs' (
|
||||||
"$mod SHIFT, J, movewindow, d"
|
_name: windowIcon: lib.nameValuePair "class<${windowIcon.class}>" windowIcon.icon
|
||||||
"$mod SHIFT, K, movewindow, u"
|
) (lib.attrByPath [ config.networking.hostName ] { } config.registry.window-icon);
|
||||||
"$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,62 +5,34 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# hypridle: idle-triggered locking and display power management.
|
# hypridle: idle-triggered locking and display power management.
|
||||||
let
|
|
||||||
cfg = config.modules.desktop.session.hyprland.hypridle;
|
|
||||||
user = config.user.name;
|
|
||||||
|
|
||||||
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 = {
|
options.modules.desktop.session.hyprland.hypridle.enable =
|
||||||
enable = lib.mkEnableOption "hypridle idle management";
|
lib.mkEnableOption "hypridle idle management";
|
||||||
|
|
||||||
lockTimeout = lib.mkOption {
|
config = lib.mkIf config.modules.desktop.session.hyprland.hypridle.enable {
|
||||||
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.${user}.services.hypridle = {
|
home-manager.users.${config.user.name}.services.hypridle = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
general = {
|
general = {
|
||||||
lock_cmd = lockCmd;
|
# The guard drops the call when a locker is already up, so no idle
|
||||||
|
# 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 restores the displays the screen-off listener may have cut.
|
# Waking pokes the compositor in case firmware leaves displays dark.
|
||||||
after_sleep_cmd = "${hyprctl} dispatch dpms on";
|
after_sleep_cmd = "${lib.getExe' config.programs.hyprland.package "hyprctl"} dispatch 'hl.dsp.dpms({ action = \"enable\" })'";
|
||||||
};
|
};
|
||||||
|
|
||||||
listener = [
|
listener = [
|
||||||
# Lock on idle.
|
# Lock on idle.
|
||||||
{
|
{
|
||||||
timeout = cfg.lockTimeout;
|
timeout = 300;
|
||||||
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,53 +2,63 @@
|
|||||||
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
|
||||||
cfg = config.modules.desktop.session.hyprland.hyprlock;
|
sleepIcon = util.glyph [
|
||||||
user = config.user.name;
|
"db81"
|
||||||
|
"dcb2"
|
||||||
# The hyprlock this module installs, used by the lock keybind below.
|
];
|
||||||
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
sleepOffIcon = util.glyph [
|
||||||
|
"db81"
|
||||||
sleepIcon = builtins.fromJSON ''"\udb81\udcb2"'';
|
"dcb3"
|
||||||
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() {
|
||||||
${pkgs.procps}/bin/pkill -RTMIN+9 waybar >/dev/null 2>&1 || true
|
${lib.getExe' pkgs.procps "pkill"} -RTMIN+9 waybar >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet "$unit"; then
|
if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet "$unit"; then
|
||||||
${pkgs.systemd}/bin/systemctl --user stop "$unit"
|
${lib.getExe' pkgs.systemd "systemctl"} --user stop "$unit"
|
||||||
refresh_waybar
|
refresh_waybar
|
||||||
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepIcon}"
|
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepIcon}"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ${pkgs.systemd}/bin/systemd-run \
|
if ${lib.getExe' pkgs.systemd "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" \
|
||||||
${pkgs.systemd}/bin/systemd-inhibit \
|
${lib.getExe' pkgs.systemd "systemd-inhibit"} \
|
||||||
--what=idle \
|
--what=idle \
|
||||||
--who=lockscreen-hotkey \
|
--who=lockscreen-hotkey \
|
||||||
--why="lock screen disabled by hotkey" \
|
--why="lock screen disabled by hotkey" \
|
||||||
${pkgs.coreutils}/bin/sleep infinity >/dev/null; then
|
${lib.getExe' pkgs.coreutils "sleep"} infinity >/dev/null; then
|
||||||
refresh_waybar
|
refresh_waybar
|
||||||
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepOffIcon}"
|
${lib.getExe' pkgs.swayosd "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 = lib.mkEnableOption "the hyprlock lock screen";
|
options.modules.desktop.session.hyprland.hyprlock.enable =
|
||||||
|
lib.mkEnableOption "the hyprlock lock screen";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.desktop.session.hyprland.hyprlock.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${config.user.name} = {
|
||||||
# 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 = {
|
||||||
@@ -81,14 +91,36 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# Lock on Super+X.
|
registry.keybind.${config.networking.hostName} = {
|
||||||
# The guard drops the keypress when a locker is already up, so a second
|
hyprlock-lock = {
|
||||||
# hyprlock never stacks over the first.
|
keys = "SUPER + X";
|
||||||
wayland.windowManager.hyprland.settings.bind = [
|
action = {
|
||||||
"$mod, X, exec, ${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}"
|
kind = "exec";
|
||||||
"$mod CTRL, X, exec, ${idleLockToggle}"
|
# The guard drops the call when a locker is already up, so no manual
|
||||||
];
|
# 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,18 +6,20 @@
|
|||||||
}:
|
}:
|
||||||
# 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 = lib.mkEnableOption "greetd with the tuigreet text greeter";
|
options.modules.desktop.session.login.enable =
|
||||||
|
lib.mkEnableOption "greetd with the tuigreet text greeter";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.desktop.session.login.enable {
|
||||||
services.greetd = {
|
services.greetd = {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings.default_session.command =
|
settings.default_session.command = lib.escapeShellArgs [
|
||||||
"${lib.getExe pkgs.tuigreet} --time --remember "
|
(lib.getExe pkgs.tuigreet)
|
||||||
+ "--cmd 'uwsm start -e -D Hyprland hyprland.desktop'";
|
"--time"
|
||||||
|
"--remember"
|
||||||
|
"--cmd"
|
||||||
|
"${lib.getExe config.programs.uwsm.package} start -e -D Hyprland hyprland.desktop"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,29 @@
|
|||||||
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
|
||||||
cfg = config.modules.desktop.session.mako;
|
dndStatus = pkgs.writeShellScript "waybar-dnd-status" ''
|
||||||
user = config.user.name;
|
if ${lib.getExe' pkgs.mako "makoctl"} mode 2>/dev/null | ${lib.getExe pkgs.gnugrep} -qx dnd; then
|
||||||
|
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
|
||||||
|
'';
|
||||||
|
|
||||||
makoctl = "${pkgs.mako}/bin/makoctl";
|
dndToggle = pkgs.writeShellScript "waybar-dnd-toggle" ''
|
||||||
|
${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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.session.mako.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${config.user.name} = {
|
||||||
# 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 = {
|
||||||
@@ -32,13 +41,25 @@ in
|
|||||||
"mode=dnd".invisible = true;
|
"mode=dnd".invisible = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# Recall the last notification from history, keyboard-only like the rest
|
registry.keybind.${config.networking.hostName}.mako-history = {
|
||||||
# of the session.
|
keys = "SUPER + N";
|
||||||
# $mod is defined by the compositor config these binds share.
|
action = {
|
||||||
wayland.windowManager.hyprland.settings.bind = [
|
kind = "exec";
|
||||||
"$mod, N, exec, ${makoctl} restore"
|
command = "${lib.getExe' pkgs.mako "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,13 +1,10 @@
|
|||||||
{ 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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.session.portals.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,47 +6,61 @@
|
|||||||
}:
|
}:
|
||||||
# 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' \
|
||||||
| ${pkgs.rofi}/bin/rofi -dmenu -i -p Power)
|
| ${lib.getExe pkgs.rofi} -dmenu -i -p Power)
|
||||||
case "$chosen" in
|
case "$chosen" in
|
||||||
Lock) ${pkgs.systemd}/bin/loginctl lock-session ;;
|
Lock) ${lib.getExe' pkgs.systemd "loginctl"} lock-session ;;
|
||||||
Suspend) ${pkgs.systemd}/bin/systemctl suspend ;;
|
Suspend) ${lib.getExe' pkgs.systemd "systemctl"} suspend ;;
|
||||||
'Log out') ${pkgs.systemd}/bin/loginctl terminate-session "$XDG_SESSION_ID" ;;
|
'Log out') ${lib.getExe' pkgs.systemd "loginctl"} terminate-session "$XDG_SESSION_ID" ;;
|
||||||
Reboot) ${pkgs.systemd}/bin/systemctl reboot ;;
|
Reboot) ${lib.getExe' pkgs.systemd "systemctl"} reboot ;;
|
||||||
'Shut down') ${pkgs.systemd}/bin/systemctl poweroff ;;
|
'Shut down') ${lib.getExe' pkgs.systemd "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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.session.rofi.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${config.user.name} = hm: {
|
||||||
# No theme is set here: Stylix's rofi target supplies the Nord one.
|
# Stylix supplies the Nord theme.
|
||||||
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.
|
||||||
extraConfig = {
|
settings = {
|
||||||
|
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";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# The launcher and the session menu, kept next to the tool they drive.
|
rofi-power-menu = {
|
||||||
# $mod is defined by the compositor config these binds share.
|
keys = "SUPER + SHIFT + X";
|
||||||
wayland.windowManager.hyprland.settings.bind = [
|
action = {
|
||||||
"$mod, R, exec, ${pkgs.rofi}/bin/rofi -show drun"
|
kind = "exec";
|
||||||
"$mod SHIFT, X, exec, ${powerMenu}"
|
command = "${powerMenu}";
|
||||||
];
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,165 +6,57 @@
|
|||||||
}:
|
}:
|
||||||
# Waybar: a top status bar reading system state at a glance.
|
# Waybar: a top status bar reading system state at a glance.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.session.waybar;
|
moduleNames =
|
||||||
vpnClient = config.modules.network.vpn-client;
|
position:
|
||||||
user = config.user.name;
|
map (entry: entry.name) (
|
||||||
|
lib.sort (left: right: left.module.order < right.module.order) (
|
||||||
|
lib.filter (entry: entry.module.position == position) moduleEntries
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
# A Nerd Font Private-Use-Area codepoint as a literal character, decoded
|
moduleEntries = lib.mapAttrsToList (name: module: {
|
||||||
# through JSON so the glyph survives as bytes rather than an editor paste.
|
inherit name module;
|
||||||
g = code: builtins.fromJSON ''"\u${code}"'';
|
}) modules;
|
||||||
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
|
||||||
|
|
||||||
# Renders and toggles mako's do-not-disturb mode.
|
modules = lib.attrByPath [ config.networking.hostName ] { } config.registry.waybar;
|
||||||
# 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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.session.waybar.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.${user}.programs.waybar = {
|
home-manager.users.${config.user.name}.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;
|
||||||
|
|
||||||
style = ''
|
settings.mainBar = lib.mkMerge [
|
||||||
.modules-right {
|
{
|
||||||
margin-right: 8px;
|
layer = "top";
|
||||||
|
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)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
#custom-dnd {
|
registry.waybar.${config.networking.hostName} = {
|
||||||
margin-right: 8px;
|
clock = {
|
||||||
}
|
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 = {
|
||||||
@@ -173,77 +65,6 @@ 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,12 +1,34 @@
|
|||||||
{ config, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
util,
|
||||||
|
...
|
||||||
|
}:
|
||||||
# PipeWire as the desktop audio server.
|
# PipeWire as the desktop audio server.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.utilities.audio;
|
volumeKey = keys: command: {
|
||||||
|
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.audio.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;
|
||||||
|
|
||||||
@@ -15,5 +37,50 @@ 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,29 +6,23 @@
|
|||||||
}:
|
}:
|
||||||
# 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" ''
|
||||||
${cliphist} list \
|
${lib.getExe pkgs.cliphist} list \
|
||||||
| ${rofi} -dmenu -i -p Clipboard \
|
| ${lib.getExe pkgs.rofi} -dmenu -i -p Clipboard \
|
||||||
| ${cliphist} decode \
|
| ${lib.getExe pkgs.cliphist} decode \
|
||||||
| ${wl-copy}
|
| ${lib.getExe' pkgs.wl-clipboard "wl-copy"}
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.clipboard.enable = lib.mkEnableOption "cliphist clipboard history";
|
options.modules.desktop.utilities.clipboard.enable =
|
||||||
|
lib.mkEnableOption "cliphist clipboard history";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.clipboard.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${config.user.name} = {
|
||||||
# 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
|
||||||
@@ -38,11 +32,14 @@ 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;
|
||||||
|
};
|
||||||
|
|
||||||
# $mod is defined by the compositor config these binds share.
|
registry.keybind.${config.networking.hostName}.clipboard-history = {
|
||||||
wayland.windowManager.hyprland.settings.bind = [
|
keys = "SUPER + SHIFT + V";
|
||||||
"$mod SHIFT, V, exec, ${picker}"
|
action = {
|
||||||
];
|
kind = "exec";
|
||||||
|
command = "${picker}";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,35 +5,52 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# 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 = lib.mkEnableOption "the SwayOSD volume and brightness popup";
|
options.modules.desktop.utilities.osd.enable =
|
||||||
|
lib.mkEnableOption "the SwayOSD volume and brightness popup";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.osd.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.${user}.extraGroups = [ "video" ];
|
users.users.${config.user.name}.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.${user}.systemd.user.services.swayosd = {
|
home-manager.users.${config.user.name}.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 = "${pkgs.swayosd}/bin/swayosd-server";
|
ExecStart = lib.getExe' pkgs.swayosd "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,53 +2,67 @@
|
|||||||
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
|
||||||
cfg = config.modules.desktop.utilities.recording;
|
# A single key starts and stops recording.
|
||||||
user = config.user.name;
|
# Waybar refreshes on state changes instead of polling.
|
||||||
|
recordingToggle = pkgs.writeShellScript "screen-record-toggle" ''
|
||||||
|
refresh_waybar() {
|
||||||
|
${lib.getExe' pkgs.procps "pkill"} -RTMIN+10 waybar >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
slurp = "${pkgs.slurp}/bin/slurp";
|
if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
|
||||||
wf-recorder = "${pkgs.wf-recorder}/bin/wf-recorder";
|
${lib.getExe' pkgs.procps "pkill"} -INT -x 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=$(${slurp}) || exit 0
|
region=$(${lib.getExe pkgs.slurp}) || exit 0
|
||||||
dir="$(${xdgUserDir} VIDEOS)/Recordings"
|
dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} VIDEOS)/Recordings"
|
||||||
${mkdir} -p "$dir"
|
${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
|
||||||
file="$dir/recording-$(${date} +%Y%m%d-%H%M%S).mp4"
|
file="$dir/recording-$(${lib.getExe' pkgs.coreutils "date"} +%Y%m%d-%H%M%S).mp4"
|
||||||
|
|
||||||
${notify-send} -a "Screen recording" "Recording started" "Region capture, no audio."
|
${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording started" "Region capture, no audio."
|
||||||
${wf-recorder} -g "$region" -f "$file"
|
${lib.getExe pkgs.wf-recorder} -g "$region" -f "$file" &
|
||||||
${notify-send} -a "Screen recording" "Recording saved" "$file"
|
recorder=$!
|
||||||
|
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 = lib.mkEnableOption "wf-recorder screen recording";
|
options.modules.desktop.utilities.recording.enable =
|
||||||
|
lib.mkEnableOption "wf-recorder screen recording";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.recording.enable {
|
||||||
home-manager.users.${user} = {
|
registry.keybind.${config.networking.hostName}.recording-toggle = {
|
||||||
# $mod is defined by the compositor config these binds share.
|
keys = "SUPER + SHIFT + R";
|
||||||
wayland.windowManager.hyprland.settings.bind = [
|
action = {
|
||||||
"$mod SHIFT, R, exec, ${toggle}"
|
kind = "exec";
|
||||||
];
|
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 that open in satty for annotation, then land in
|
# Keyboard-driven screenshots copy to the clipboard before Satty opens for
|
||||||
# both the clipboard and a dated file.
|
# annotation. Satty copies annotated screenshots when it saves them.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.utilities.screenshot;
|
screenshotKey = keys: target: {
|
||||||
user = config.user.name;
|
inherit keys;
|
||||||
|
action = {
|
||||||
|
kind = "exec";
|
||||||
|
command = "${capture target}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
grimblast = "${pkgs.grimblast}/bin/grimblast";
|
# Copy the original screenshot before Satty opens.
|
||||||
satty = "${pkgs.satty}/bin/satty";
|
# Satty copies and saves annotated screenshots when the user presses Enter.
|
||||||
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="$(${xdgUserDir} PICTURES)/Screenshots"
|
dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} PICTURES)/Screenshots"
|
||||||
mkdir -p "$dir"
|
${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
|
||||||
${grimblast} save ${target} - \
|
${lib.getExe pkgs.grimblast} save ${target} - \
|
||||||
| ${satty} --filename - \
|
| ${lib.getExe' pkgs.coreutils "tee"} >(${lib.getExe' pkgs.wl-clipboard "wl-copy"}) \
|
||||||
|
| ${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 ${wl-copy} \
|
--copy-command ${lib.getExe' pkgs.wl-clipboard "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 = lib.mkEnableOption "screenshots via grimblast and satty";
|
options.modules.desktop.utilities.screenshot.enable =
|
||||||
|
lib.mkEnableOption "screenshots via grimblast and satty";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.screenshot.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${config.user.name} = {
|
||||||
# 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.
|
||||||
# windowrule v3 syntax: space-separated field/value tokens, comma
|
wayland.windowManager.hyprland.settings.window_rule = [
|
||||||
# separated, with matchers under a match: prefix and effects bare.
|
{
|
||||||
wayland.windowManager.hyprland.settings.windowrule = [
|
match.class = "^(com\\.gabm\\.satty)$";
|
||||||
"float 1, match:class ^(com\\.gabm\\.satty)$"
|
float = true;
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|
||||||
# Print with plain/Shift/Ctrl for region/window/full.
|
registry.keybind.${config.networking.hostName} = {
|
||||||
# Super+L is already the hjkl focus and movement bind, so screenshots take
|
screenshot-area = screenshotKey "Print" "area";
|
||||||
# the Print key instead.
|
screenshot-active = screenshotKey "SHIFT + Print" "active";
|
||||||
wayland.windowManager.hyprland.settings.bind = [
|
screenshot-screen = screenshotKey "CTRL + Print" "screen";
|
||||||
", Print, exec, ${capture "area"}"
|
|
||||||
"SHIFT, Print, exec, ${capture "active"}"
|
|
||||||
"CTRL, Print, exec, ${capture "screen"}"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,10 @@
|
|||||||
}:
|
}:
|
||||||
# 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" { } ''
|
||||||
${pkgs.imagemagick}/bin/magick \
|
${lib.getExe pkgs.imagemagick} \
|
||||||
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
|
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
|
||||||
"$out"
|
"$out"
|
||||||
'';
|
'';
|
||||||
@@ -21,7 +18,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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.theming.enable {
|
||||||
stylix = {
|
stylix = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
@@ -60,7 +57,7 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${config.user.name} = {
|
||||||
# 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;
|
||||||
@@ -69,5 +66,10 @@ 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,15 +1,27 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
# Declares the XDG user directories, so a tool that queries them lands in a
|
# Declares XDG user directories, so tools that query them use predictable
|
||||||
# well-known folder a host can relocate from one place.
|
# folders a host can relocate from one place.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.utilities.userdirs;
|
homeDirectory = "/home/${config.user.name}";
|
||||||
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 cfg.enable {
|
config = lib.mkIf config.modules.desktop.utilities.userdirs.enable {
|
||||||
# Write ~/.config/user-dirs.dirs from the option defaults.
|
home-manager.users.${config.user.name}.xdg.userDirs = {
|
||||||
home-manager.users.${user}.xdg.userDirs.enable = true;
|
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,12 +1,9 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
let
|
|
||||||
cfg = config.modules.guest;
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
options.modules.guest.enable = lib.mkEnableOption "nested-guest base configuration";
|
options.modules.guest.enable = lib.mkEnableOption "nested guest configuration";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.guest.enable {
|
||||||
modules.network.ssh.enable = lib.mkDefault true;
|
modules.network.ssh.enable = lib.mkDefault false;
|
||||||
modules.network.ssh.hostKeys.restore = lib.mkDefault false;
|
modules.network.ssh.hostKeys.restore = lib.mkDefault false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
18
configs/nixos/modules/host/cachyos.nix
Normal file
18
configs/nixos/modules/host/cachyos.nix
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
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="
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
106
configs/nixos/modules/host/default.nix
Normal file
106
configs/nixos/modules/host/default.nix
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
57
configs/nixos/modules/host/zfs.nix
Normal file
57
configs/nixos/modules/host/zfs.nix
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
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,13 +4,10 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# 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 cfg.enable {
|
config = lib.mkIf config.modules.monorepo.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;
|
||||||
|
|||||||
57
configs/nixos/modules/network/default.nix
Normal file
57
configs/nixos/modules/network/default.nix
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
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,343 +4,202 @@
|
|||||||
registry,
|
registry,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# SSH on this machine, in both directions.
|
# Configures inbound and outbound SSH access.
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.ssh;
|
cfg = config.modules.network.ssh;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
|
||||||
inherit (lib)
|
workstationNames = [
|
||||||
concatLists
|
"neogaia"
|
||||||
concatStringsSep
|
"zeus"
|
||||||
elem
|
];
|
||||||
filter
|
serverNames = [ "pikachu" ];
|
||||||
genAttrs
|
|
||||||
hasAttr
|
|
||||||
imap0
|
|
||||||
listToAttrs
|
|
||||||
mapAttrs
|
|
||||||
mapAttrsToList
|
|
||||||
mkIf
|
|
||||||
mkMerge
|
|
||||||
mkOption
|
|
||||||
nameValuePair
|
|
||||||
optional
|
|
||||||
optionalAttrs
|
|
||||||
types
|
|
||||||
unique
|
|
||||||
;
|
|
||||||
|
|
||||||
hostKeySecret = type: "ssh-host-${type}-key";
|
isWorkstation = lib.elem config.networking.hostName workstationNames;
|
||||||
userKeySecret = "ssh-user-ed25519-key";
|
isServer = lib.elem config.networking.hostName serverNames;
|
||||||
|
|
||||||
targetType = types.submodule (
|
clientKeyFor = host: builtins.readFile (../../hosts + "/${host}/ssh_user_ed25519_key.pub");
|
||||||
{ name, ... }:
|
hostKeysFor =
|
||||||
{
|
host:
|
||||||
options = {
|
map builtins.readFile [
|
||||||
hostName = mkOption {
|
(../../hosts + "/${host}/ssh_host_ed25519_key.pub")
|
||||||
type = types.str;
|
(../../hosts + "/${host}/ssh_host_rsa_key.pub")
|
||||||
default = name;
|
];
|
||||||
description = ''
|
|
||||||
The network address OpenSSH connects to for this target.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
user = mkOption {
|
managedTargets = lib.genAttrs (workstationNames ++ serverNames) (name: {
|
||||||
type = types.str;
|
hostName = registry.address.${name}.self;
|
||||||
default = config.user.name;
|
clientKey = clientKeyFor name;
|
||||||
description = ''
|
hostKeys = hostKeysFor name;
|
||||||
The remote login name OpenSSH uses for this target.
|
});
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
port = mkOption {
|
extraTargets = {
|
||||||
type = types.port;
|
# TODO: Pin Gitea's host key after migrating it from Proxmox.
|
||||||
default = 22;
|
gitea = {
|
||||||
description = ''
|
hostName = "git.alexion.dev";
|
||||||
The TCP port OpenSSH uses for this target.
|
port = 2022;
|
||||||
'';
|
user = "gitea";
|
||||||
};
|
aliases = [
|
||||||
|
"gitea"
|
||||||
|
"git.alexion.dev"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
aliases = mkOption {
|
mkTarget = name: target: {
|
||||||
type = types.listOf types.str;
|
hostName = target.hostName or name;
|
||||||
default = [ name ];
|
user = target.user or user;
|
||||||
description = ''
|
port = target.port or 22;
|
||||||
Host patterns written into the generated OpenSSH client block.
|
aliases = target.aliases or [ name ];
|
||||||
'';
|
clientKey = target.clientKey or null;
|
||||||
};
|
hostKeys = target.hostKeys or [ ];
|
||||||
|
};
|
||||||
clientKey = mkOption {
|
targets = lib.mapAttrs mkTarget (managedTargets // extraTargets);
|
||||||
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
|
clientKeysFor workstationNames ++ lib.optional isServer (clientKeysFor serverNames)
|
||||||
++ optional isServer (clientKeysFor serverNames)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
outboundTargetNames =
|
outboundTargetNames =
|
||||||
let
|
let
|
||||||
groupTargets =
|
groupTargets =
|
||||||
if isServer then
|
if isServer then
|
||||||
[ "gitea" ] ++ cfg.hosts.servers
|
builtins.attrNames extraTargets ++ serverNames
|
||||||
else if isWorkstation then
|
else if isWorkstation then
|
||||||
[ "gitea" ] ++ cfg.hosts.servers ++ cfg.hosts.workstations
|
builtins.attrNames extraTargets ++ serverNames ++ workstationNames
|
||||||
else
|
else
|
||||||
[ "gitea" ];
|
builtins.attrNames extraTargets;
|
||||||
in
|
in
|
||||||
filter (name: name != currentHost) (targetNamesIn groupTargets);
|
lib.filter (name: name != config.networking.hostName) groupTargets;
|
||||||
|
|
||||||
sshSettingsFor = name:
|
knownHosts = lib.listToAttrs (
|
||||||
let
|
lib.concatLists (
|
||||||
target = cfg.targets.${name};
|
lib.mapAttrsToList (
|
||||||
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:
|
||||||
imap0 (i: key:
|
lib.imap0 (
|
||||||
nameValuePair "${targetName}-${toString i}" {
|
i: key:
|
||||||
|
lib.nameValuePair "${targetName}-${toString i}" {
|
||||||
hostNames = knownHostNamesFor target;
|
hostNames = knownHostNamesFor target;
|
||||||
publicKey = keyWithoutComment key;
|
publicKey = keyWithoutComment key;
|
||||||
}
|
}
|
||||||
) target.hostKeys
|
) target.hostKeys
|
||||||
) cfg.targets
|
) 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, with host keys restored from secrets";
|
enable = lib.mkEnableOption "the OpenSSH daemon";
|
||||||
|
|
||||||
hosts = {
|
hostKeys.restore = lib.mkOption {
|
||||||
servers = mkOption {
|
type = lib.types.bool;
|
||||||
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 = ''
|
||||||
Restore the host keys from secrets rather than letting the daemon generate its own.
|
Restores host keys from secrets.
|
||||||
A machine with its own identity keeps its fingerprint across a reimage by restoring committed keys.
|
Preserves the fingerprint across reimages.
|
||||||
A guest carries no host identity, so it turns this off and presents a self-generated key instead.
|
Guests disable this option and generate their own host keys.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
hostKeys.sopsFile = mkOption {
|
hostKeys.sopsFile = lib.mkOption {
|
||||||
type = types.nullOr types.path;
|
type = lib.types.nullOr lib.types.path;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Encrypted file holding this host's SSH host private keys, one entry per key type, named `ssh-host-<type>-key`.
|
Sets encrypted file that contains host private keys.
|
||||||
Required when `restore` is on.
|
Names each secret `ssh-host-<type>-key`.
|
||||||
|
Requires a value when `restore` is enabled.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
hostKeys.types = mkOption {
|
userKey.sopsFile = lib.mkOption {
|
||||||
type = types.listOf types.str;
|
type = lib.types.nullOr lib.types.path;
|
||||||
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 = ''
|
||||||
Encrypted file holding this machine's SSH client private key, under the entry `ssh-user-ed25519-key`.
|
Sets encrypted file that contains the SSH client private key.
|
||||||
Left unset on a machine that authenticates to no remote server, such as a guest.
|
Names the secret `ssh-user-ed25519-key`.
|
||||||
|
Guests leave this unset because they do not authenticate to remote servers.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable (
|
config = lib.mkIf cfg.enable (
|
||||||
mkMerge [
|
lib.mkMerge [
|
||||||
{
|
{
|
||||||
services.openssh.enable = true;
|
services.openssh.enable = true;
|
||||||
|
|
||||||
users.users.${user}.openssh.authorizedKeys.keys =
|
users.users.${user}.openssh.authorizedKeys.keys = defaultAuthorizedKeys;
|
||||||
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 =
|
settings = lib.mapAttrs (name: _: sshSettingsFor name) (
|
||||||
mapAttrs (name: _: sshSettingsFor name) (genAttrs outboundTargetNames (name: name))
|
lib.genAttrs outboundTargetNames (name: name)
|
||||||
// cfg.extraSettings;
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
(mkIf cfg.hostKeys.restore {
|
(lib.mkIf cfg.hostKeys.restore {
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = cfg.hostKeys.sopsFile != null;
|
assertion = cfg.hostKeys.sopsFile != null;
|
||||||
message = "modules.network.ssh.hostKeys.restore requires modules.network.ssh.hostKeys.sopsFile to name the encrypted host keys.";
|
message = "Set `modules.network.ssh.hostKeys.sopsFile` when host-key restoration is enabled.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
sops.secrets = genAttrs (map hostKeySecret cfg.hostKeys.types) (_: {
|
sops.secrets = lib.genAttrs (map hostKeySecret hostKeyTypes) (_: {
|
||||||
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 = concatStringsSep "" (
|
services.openssh.extraConfig = lib.concatStringsSep "" (
|
||||||
map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") cfg.hostKeys.types
|
map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") hostKeyTypes
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
(mkIf (cfg.userKey.sopsFile != null) {
|
(lib.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."*".IdentityFile =
|
home-manager.users.${user}.programs.ssh.settings."*" = {
|
||||||
config.sops.secrets.${userKeySecret}.path;
|
IdentityFile = config.sops.secrets.${userKeySecret}.path;
|
||||||
|
IdentitiesOnly = true;
|
||||||
|
};
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,24 +4,17 @@
|
|||||||
util,
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# The host networking foundation: per-VLAN bridges over a tagged trunk.
|
# Configures bridges for tagged VLANs on a physical interface that uses DHCP.
|
||||||
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 = {
|
||||||
enable = lib.mkEnableOption "host VLAN bridges over a tagged trunk";
|
interface = lib.mkOption {
|
||||||
|
|
||||||
trunk = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "enp1s0";
|
example = "enp1s0";
|
||||||
description = ''
|
description = ''
|
||||||
Physical interface that carries 802.1Q-tagged traffic for every VLAN.
|
Names the physical interface that carries untagged DHCP traffic and tagged VLANs.
|
||||||
Each declared VLAN attaches to its own bridge.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,66 +27,37 @@ in
|
|||||||
30
|
30
|
||||||
];
|
];
|
||||||
description = ''
|
description = ''
|
||||||
Tagged VLAN ids to materialize.
|
Creates bridges for the specified tagged VLAN IDs.
|
||||||
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.enable {
|
config = lib.mkIf (cfg.ids != [ ]) {
|
||||||
assertions = [
|
networking = {
|
||||||
{
|
useDHCP = false;
|
||||||
assertion = cfg.management.vlan == null || lib.elem cfg.management.vlan cfg.ids;
|
networkmanager.unmanaged = [
|
||||||
message = "modules.network.vlans.management.vlan (${toString cfg.management.vlan}) must name one of modules.network.vlans.ids.";
|
"interface-name:${cfg.interface}"
|
||||||
}
|
]
|
||||||
{
|
++ map (id: "interface-name:${util.bridgeName id}") cfg.ids
|
||||||
assertion = cfg.management.address == null || cfg.management.vlan != null;
|
++ map (id: "interface-name:vlan${toString id}") cfg.ids;
|
||||||
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-${bridgeName id}" = {
|
"40-${util.bridgeName id}" = {
|
||||||
netdevConfig = {
|
netdevConfig = {
|
||||||
Name = bridgeName id;
|
Name = util.bridgeName id;
|
||||||
Kind = "bridge";
|
Kind = "bridge";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"40-${vlanName id}" = {
|
"40-vlan${toString id}" = {
|
||||||
netdevConfig = {
|
netdevConfig = {
|
||||||
Name = vlanName id;
|
Name = "vlan${toString id}";
|
||||||
Kind = "vlan";
|
Kind = "vlan";
|
||||||
};
|
};
|
||||||
vlanConfig.Id = id;
|
vlanConfig.Id = id;
|
||||||
@@ -103,37 +67,25 @@ in
|
|||||||
|
|
||||||
networks = lib.mkMerge (
|
networks = lib.mkMerge (
|
||||||
[
|
[
|
||||||
# The trunk carries only tagged frames and takes no address.
|
|
||||||
{
|
{
|
||||||
"30-${cfg.trunk}" = {
|
"10-${cfg.interface}" = {
|
||||||
matchConfig.Name = cfg.trunk;
|
matchConfig.Name = cfg.interface;
|
||||||
networkConfig.LinkLocalAddressing = "no";
|
networkConfig.DHCP = "yes";
|
||||||
linkConfig.RequiredForOnline = "no";
|
vlan = map (id: "vlan${toString id}") cfg.ids;
|
||||||
vlan = map vlanName cfg.ids;
|
linkConfig.RequiredForOnline = "routable";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
++ map (id: {
|
++ map (id: {
|
||||||
# Enslaved to its bridge, carrying no address itself.
|
"40-vlan${toString id}" = {
|
||||||
"40-${vlanName id}" = {
|
matchConfig.Name = "vlan${toString id}";
|
||||||
matchConfig.Name = vlanName id;
|
networkConfig.Bridge = util.bridgeName id;
|
||||||
networkConfig.Bridge = bridgeName id;
|
linkConfig.RequiredForOnline = "no";
|
||||||
|
};
|
||||||
|
"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,10 +1,34 @@
|
|||||||
{
|
{
|
||||||
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 = {
|
||||||
@@ -13,61 +37,31 @@ in
|
|||||||
interface = lib.mkOption {
|
interface = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "wg-home";
|
default = "wg-home";
|
||||||
description = "WireGuard interface name.";
|
description = "Names the WireGuard interface.";
|
||||||
};
|
};
|
||||||
|
|
||||||
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 = "Client tunnel address in CIDR notation.";
|
description = "Sets the 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 = "Encrypted secrets file that contains privateKeySecret.";
|
description = "Names the encrypted file that contains the WireGuard private key.";
|
||||||
};
|
};
|
||||||
|
|
||||||
peer = {
|
peer = {
|
||||||
publicKey = lib.mkOption {
|
publicKey = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "WireGuard public key for the remote peer.";
|
description = "Sets the 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 = "WireGuard endpoint for the remote peer.";
|
description = "Sets the 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.";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -76,28 +70,39 @@ in
|
|||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = cfg.sopsFile != null;
|
assertion = cfg.sopsFile != null;
|
||||||
message = "modules.network.vpn-client.sopsFile must name the encrypted secrets file containing the WireGuard private key.";
|
message = "Set modules.network.vpn-client.sopsFile when the VPN client is enabled.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
sops.secrets.${cfg.privateKeySecret}.sopsFile = cfg.sopsFile;
|
sops.secrets.wireguard-home-private-key.sopsFile = cfg.sopsFile;
|
||||||
|
|
||||||
systemd.targets."wireguard-${cfg.interface}" = lib.mkIf (!cfg.autoStart) {
|
registry.shell-shortcut.${config.networking.hostName} = {
|
||||||
wantedBy = lib.mkForce [ ];
|
vpn-on = "sudo systemctl start wireguard-${cfg.interface}.target";
|
||||||
|
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 = cfg.mtu;
|
mtu = 1280;
|
||||||
privateKeyFile = config.sops.secrets.${cfg.privateKeySecret}.path;
|
privateKeyFile = config.sops.secrets.wireguard-home-private-key.path;
|
||||||
|
|
||||||
peers = [
|
peers = [
|
||||||
({
|
{
|
||||||
inherit (cfg.peer) publicKey endpoint allowedIPs;
|
inherit (cfg.peer) publicKey endpoint;
|
||||||
|
allowedIPs = [ "10.23.0.0/16" ];
|
||||||
|
persistentKeepalive = 25;
|
||||||
}
|
}
|
||||||
// lib.optionalAttrs (cfg.peer.persistentKeepalive != null) {
|
|
||||||
inherit (cfg.peer) persistentKeepalive;
|
|
||||||
})
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
{
|
|
||||||
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,138 +2,135 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
|
util,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.copyparty;
|
cfg = config.modules.services.copyparty;
|
||||||
|
|
||||||
volumeType = lib.types.submodule {
|
accountName = config.user.name;
|
||||||
options = {
|
|
||||||
path = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Filesystem path served by this volume.";
|
|
||||||
};
|
|
||||||
|
|
||||||
access = lib.mkOption {
|
serviceGroup = "storage";
|
||||||
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
|
||||||
default = { };
|
|
||||||
example = {
|
|
||||||
A = [ "alexion" ];
|
|
||||||
};
|
|
||||||
description = "Copyparty access map from permission string to account names.";
|
|
||||||
};
|
|
||||||
|
|
||||||
flags = lib.mkOption {
|
serviceStateDir = "/var/lib/copyparty";
|
||||||
type = lib.types.listOf lib.types.str;
|
|
||||||
default = [ ];
|
serviceUser = "copyparty";
|
||||||
example = [ "dthumb" ];
|
|
||||||
description = "Copyparty volume flags.";
|
writableVolumes = lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes);
|
||||||
};
|
|
||||||
};
|
port = 3923;
|
||||||
|
defaultAccess = {
|
||||||
|
A = [ accountName ];
|
||||||
};
|
};
|
||||||
|
|
||||||
renderAccess = permission: accounts: " ${permission}: ${lib.concatStringsSep ", " accounts}";
|
defaultWritableFlags = {
|
||||||
|
d2d = true;
|
||||||
|
d2t = true;
|
||||||
|
dthumb = true;
|
||||||
|
};
|
||||||
|
|
||||||
hasFlag = name: flags: lib.any (flag: lib.hasPrefix "${name}:" flag || lib.hasPrefix "${name}=" flag) flags;
|
credentialHash = builtins.substring 0 16 (builtins.hashString "sha256" accountName);
|
||||||
|
|
||||||
defaultPermissionFlags = flags:
|
credentialName = "password-${credentialHash}";
|
||||||
lib.optionals (!(hasFlag "chmod_d" flags)) [ "chmod_d: 2770" ]
|
|
||||||
++ lib.optionals (!(hasFlag "chmod_f" flags)) [ "chmod_f: 660" ];
|
|
||||||
|
|
||||||
renderFlags = flags: lib.concatMapStringsSep "\n" (flag: " ${flag}") (flags ++ defaultPermissionFlags flags);
|
passwordPlaceholder = "@COPYPARTY_PASSWORD_${credentialHash}@";
|
||||||
|
|
||||||
renderVolume = urlPath: volume: ''
|
globalSettings = {
|
||||||
[${urlPath}]
|
i = "0.0.0.0";
|
||||||
${volume.path}
|
usernames = true;
|
||||||
accs:
|
"http-only" = true;
|
||||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList renderAccess volume.access)}
|
"no-crt" = true;
|
||||||
${lib.optionalString (volume.flags != [ ]) ''
|
"no-thumb" = true;
|
||||||
flags:
|
"no-snap" = true;
|
||||||
${renderFlags volume.flags}
|
"no-rescan" = true;
|
||||||
''}
|
"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";
|
||||||
|
};
|
||||||
|
|
||||||
renderTrustedSource = source: " xff-src: ${source}";
|
globalExtraConfig = lib.optionalString cfg.reverseProxy.enable (
|
||||||
|
lib.concatMapStringsSep "\n" (source: "xff-src: ${source}") cfg.reverseProxy.trustedSources
|
||||||
|
);
|
||||||
|
|
||||||
renderedReverseProxy = lib.optionalString cfg.reverseProxy.enable ''
|
volumeFlags =
|
||||||
rproxy: 1
|
flags:
|
||||||
xff-hdr: x-forwarded-for
|
lib.optionalAttrs (flags != { }) (
|
||||||
${lib.concatStringsSep "\n" (map renderTrustedSource cfg.reverseProxy.trustedSources)}
|
{
|
||||||
'';
|
chmod_d = "2770";
|
||||||
|
chmod_f = "660";
|
||||||
|
}
|
||||||
|
// flags
|
||||||
|
);
|
||||||
|
|
||||||
renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes);
|
renderedVolumes = lib.mapAttrs (_urlPath: volume: {
|
||||||
|
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;
|
||||||
|
|
||||||
configScript = pkgs.writeShellScript "copyparty-config" ''
|
configText = util.formats.copyparty.render {
|
||||||
set -eu
|
inherit globalExtraConfig;
|
||||||
|
accounts.${accountName} = passwordPlaceholder;
|
||||||
|
global = globalSettings;
|
||||||
|
volumes = renderedVolumes;
|
||||||
|
};
|
||||||
|
|
||||||
umask 077
|
configFile = pkgs.writeText "copyparty.conf" configText;
|
||||||
password="$(${pkgs.coreutils}/bin/tr -d '\n' < "$CREDENTIALS_DIRECTORY/password")"
|
|
||||||
|
|
||||||
cat > /run/copyparty/copyparty.conf <<EOF
|
replacePassword = ''
|
||||||
[global]
|
${lib.getExe pkgs.replace-secret} ${lib.escapeShellArg passwordPlaceholder} "$CREDENTIALS_DIRECTORY/${credentialName}" /run/copyparty/copyparty.conf
|
||||||
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";
|
||||||
|
|
||||||
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;
|
|
||||||
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 {
|
passwordFile = lib.mkOption {
|
||||||
type = lib.types.path;
|
|
||||||
description = "Runtime file containing the Copyparty account password.";
|
|
||||||
};
|
|
||||||
|
|
||||||
stateDir = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "/var/lib/copyparty";
|
description = "Sets the runtime file that contains the Copyparty account password.";
|
||||||
description = "Directory where Copyparty stores service state.";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
volumes = lib.mkOption {
|
volumes = lib.mkOption {
|
||||||
type = lib.types.attrsOf volumeType;
|
type = lib.types.attrsOf (
|
||||||
|
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 = "Copyparty volumes keyed by their URL path.";
|
description = "Defines Copyparty volumes by URL path.";
|
||||||
};
|
};
|
||||||
|
|
||||||
reverseProxy = {
|
reverseProxy = {
|
||||||
@@ -142,7 +139,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 = "IP ranges that may supply Copyparty's forwarded client IP header.";
|
description = "Lists proxy sources that can set Copyparty's forwarded client IP header.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -164,43 +161,49 @@ in
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
users.users.${cfg.user} = {
|
users.users.${serviceUser} = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
uid = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
uid = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||||
group = cfg.group;
|
group = serviceGroup;
|
||||||
home = cfg.stateDir;
|
home = serviceStateDir;
|
||||||
};
|
};
|
||||||
|
|
||||||
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 = cfg.user;
|
User = serviceUser;
|
||||||
Group = cfg.group;
|
Group = serviceGroup;
|
||||||
WorkingDirectory = cfg.stateDir;
|
WorkingDirectory = serviceStateDir;
|
||||||
Environment = [
|
Environment = [
|
||||||
"HOME=${cfg.stateDir}"
|
"HOME=${serviceStateDir}"
|
||||||
"XDG_CONFIG_HOME=${cfg.stateDir}"
|
"XDG_CONFIG_HOME=${serviceStateDir}"
|
||||||
];
|
];
|
||||||
StateDirectory = "copyparty";
|
StateDirectory = "copyparty";
|
||||||
RuntimeDirectory = "copyparty";
|
RuntimeDirectory = "copyparty";
|
||||||
RuntimeDirectoryMode = "0700";
|
RuntimeDirectoryMode = "0700";
|
||||||
LoadCredential = [ "password:${cfg.passwordFile}" ];
|
LoadCredential = [ "${credentialName}:${cfg.passwordFile}" ];
|
||||||
ExecStartPre = configScript;
|
ExecStart = "${lib.getExe pkgs.copyparty-min} -c /run/copyparty/copyparty.conf";
|
||||||
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 = [ cfg.stateDir ] ++ map (volume: volume.path) (lib.attrValues cfg.volumes);
|
ReadWritePaths = [
|
||||||
|
serviceStateDir
|
||||||
|
]
|
||||||
|
++ map (volume: volume.path) writableVolumes;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
networking.firewall.allowedTCPPorts = [ port ];
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
157
configs/nixos/modules/services/games/palworld.nix
Normal file
157
configs/nixos/modules/services/games/palworld.nix
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
options.modules.services.games.palworld = {
|
||||||
|
enable = lib.mkEnableOption "Palworld dedicated server";
|
||||||
|
|
||||||
|
steamDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "/data/steamcmd";
|
||||||
|
description = "Directory where SteamCMD stores metadata and cache files.";
|
||||||
|
};
|
||||||
|
|
||||||
|
installDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "/data/servers/palworld/tracion";
|
||||||
|
description = "Directory where SteamCMD installs the Palworld server files.";
|
||||||
|
};
|
||||||
|
|
||||||
|
stateDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "/data/state/palworld/tracion";
|
||||||
|
description = "Directory where Palworld stores durable server state.";
|
||||||
|
};
|
||||||
|
|
||||||
|
serverName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "palworld";
|
||||||
|
description = "Name shown for the Palworld server.";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 8211;
|
||||||
|
description = "UDP port for the Palworld server.";
|
||||||
|
};
|
||||||
|
|
||||||
|
publicIP = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Public IP address advertised by the Palworld server.";
|
||||||
|
};
|
||||||
|
|
||||||
|
maxPlayers = lib.mkOption {
|
||||||
|
type = lib.types.ints.between 1 32;
|
||||||
|
default = 32;
|
||||||
|
description = "Maximum player count for the Palworld server.";
|
||||||
|
};
|
||||||
|
|
||||||
|
publicLobby = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
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 = {
|
||||||
|
enable = lib.mkEnableOption "Palworld RCON" // {
|
||||||
|
default = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 25575;
|
||||||
|
description = "TCP port for Palworld RCON.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "Runtime file containing the Palworld server password.";
|
||||||
|
};
|
||||||
|
|
||||||
|
adminPasswordFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "Runtime file containing the Palworld admin password.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf config.modules.services.games.palworld.enable (
|
||||||
|
let
|
||||||
|
service = config.modules.services.games.palworld;
|
||||||
|
savedDir = "${service.installDir}/Pal/Saved";
|
||||||
|
startScript = pkgs.writeShellScript "palworld-start" ''
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
${lib.getExe' pkgs.coreutils "install"} -d \
|
||||||
|
-m 0770 \
|
||||||
|
${lib.escapeShellArg service.stateDir} \
|
||||||
|
${lib.escapeShellArg (builtins.dirOf savedDir)}
|
||||||
|
|
||||||
|
if [ -L ${lib.escapeShellArg savedDir} ]; then
|
||||||
|
current_target="$(${lib.getExe' pkgs.coreutils "readlink"} ${lib.escapeShellArg savedDir})"
|
||||||
|
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
|
||||||
|
${lib.getExe' pkgs.coreutils "rm"} ${lib.escapeShellArg savedDir}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
|
||||||
|
else
|
||||||
|
${lib.getExe' pkgs.coreutils "cp"} -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
|
||||||
|
${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -e ${lib.escapeShellArg savedDir} ]; then
|
||||||
|
${lib.getExe' pkgs.coreutils "ln"} -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
|
||||||
|
fi
|
||||||
|
|
||||||
|
password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||||
|
admin_password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
|
||||||
|
|
||||||
|
exec ./PalServer.sh \
|
||||||
|
"$@" \
|
||||||
|
"-serverpassword=$password" \
|
||||||
|
"-adminpassword=$admin_password"
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
modules.services.steamcmd = {
|
||||||
|
enable = true;
|
||||||
|
inherit (service) steamDir installDir stateDir;
|
||||||
|
serviceName = "palworld-${lib.toLower service.serverName}";
|
||||||
|
appId = 2394010;
|
||||||
|
inherit (service) autoStart;
|
||||||
|
executable = "${startScript}";
|
||||||
|
arguments = [
|
||||||
|
"-useperfthreads"
|
||||||
|
"-NoAsyncLoadingThread"
|
||||||
|
"-UseMultithreadForDS"
|
||||||
|
"-port=${toString service.port}"
|
||||||
|
"-publicport=${toString service.port}"
|
||||||
|
"-servername=${service.serverName}"
|
||||||
|
"-players=${toString service.maxPlayers}"
|
||||||
|
]
|
||||||
|
++ lib.optional service.publicLobby "-publiclobby"
|
||||||
|
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
|
||||||
|
++ lib.optionals service.rcon.enable [
|
||||||
|
"-rcon"
|
||||||
|
"-RCONPort=${toString service.rcon.port}"
|
||||||
|
];
|
||||||
|
udpPorts = [ service.port ];
|
||||||
|
tcpPorts = lib.optional service.rcon.enable service.rcon.port;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
@@ -37,9 +38,9 @@
|
|||||||
description = "Base UDP port for the Valheim server.";
|
description = "Base UDP port for the Valheim server.";
|
||||||
};
|
};
|
||||||
|
|
||||||
password = lib.mkOption {
|
passwordFile = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.path;
|
||||||
description = "Valheim server password.";
|
description = "Runtime file containing the Valheim server password.";
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -47,6 +48,12 @@
|
|||||||
config = lib.mkIf config.modules.services.games.valheim.enable (
|
config = lib.mkIf config.modules.services.games.valheim.enable (
|
||||||
let
|
let
|
||||||
service = config.modules.services.games.valheim;
|
service = config.modules.services.games.valheim;
|
||||||
|
startScript = pkgs.writeShellScript "valheim-start" ''
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||||
|
exec ./valheim_server.x86_64 "$@" -password "$password"
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
modules.services.steamcmd = {
|
modules.services.steamcmd = {
|
||||||
@@ -54,7 +61,7 @@
|
|||||||
inherit (service) steamDir installDir stateDir;
|
inherit (service) steamDir installDir stateDir;
|
||||||
serviceName = "valheim";
|
serviceName = "valheim";
|
||||||
appId = 896660;
|
appId = 896660;
|
||||||
executable = "./valheim_server.x86_64";
|
executable = "${startScript}";
|
||||||
arguments = [
|
arguments = [
|
||||||
"-nographics"
|
"-nographics"
|
||||||
"-batchmode"
|
"-batchmode"
|
||||||
@@ -66,10 +73,11 @@
|
|||||||
service.serverName
|
service.serverName
|
||||||
"-savedir"
|
"-savedir"
|
||||||
service.stateDir
|
service.stateDir
|
||||||
"-password"
|
|
||||||
service.password
|
|
||||||
"-public"
|
"-public"
|
||||||
"0"
|
"0"
|
||||||
|
"-modifier"
|
||||||
|
"raids"
|
||||||
|
"none"
|
||||||
];
|
];
|
||||||
udpPorts = [
|
udpPorts = [
|
||||||
service.port
|
service.port
|
||||||
|
|||||||
228
configs/nixos/modules/services/homepage.nix
Normal file
228
configs/nixos/modules/services/homepage.nix
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
{
|
||||||
|
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";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
64
configs/nixos/modules/services/navidrome.nix
Normal file
64
configs/nixos/modules/services/navidrome.nix
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
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,71 +5,94 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.reverse-proxy;
|
|
||||||
|
|
||||||
mkRegistryPath = registryName: host: entry: "registry.${registryName}.${host}.${entry}";
|
generatedUdpStreams = lib.concatLists (
|
||||||
|
|
||||||
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 (
|
lib.mapAttrsToList (
|
||||||
host: routes:
|
host: routes:
|
||||||
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry) routes
|
lib.concatLists (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
entry: route: lib.optionals (route.protocol == "udp") (map (mkUdpStream host entry) route.ports)
|
||||||
|
) routes
|
||||||
|
)
|
||||||
) registry.route
|
) registry.route
|
||||||
);
|
);
|
||||||
|
|
||||||
virtualHostsByDomain = lib.foldl' (
|
mkUdpStream =
|
||||||
acc: virtualHost:
|
host: entry: port:
|
||||||
acc
|
let
|
||||||
// {
|
address =
|
||||||
${virtualHost.name} = (acc.${virtualHost.name} or [ ]) ++ [ virtualHost ];
|
compositeEntry.address
|
||||||
}
|
or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
||||||
) { } generatedVirtualHostList;
|
compositeEntry = registry.join host entry;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit
|
||||||
|
host
|
||||||
|
entry
|
||||||
|
port
|
||||||
|
address
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
duplicateDomainMessages = lib.concatLists (
|
generatedStreamConfig = lib.concatMapStringsSep "\n" (stream: ''
|
||||||
|
server {
|
||||||
|
listen ${toString stream.port} udp;
|
||||||
|
proxy_pass ${stream.address}:${toString stream.port};
|
||||||
|
}
|
||||||
|
'') generatedUdpStreams;
|
||||||
|
generatedVirtualHosts = lib.listToAttrs generatedVirtualHostList;
|
||||||
|
|
||||||
|
# Route registry validation already requires unique domains.
|
||||||
|
generatedVirtualHostList = lib.concatLists (
|
||||||
lib.mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
domain: virtualHosts:
|
host: routes:
|
||||||
if lib.length virtualHosts > 1 then
|
lib.mapAttrsToList (entry: route: mkVirtualHost host entry route.protocol) (
|
||||||
[
|
lib.filterAttrs (_entry: route: route.protocol != "udp") routes
|
||||||
"duplicate route domain ${domain} in ${lib.concatMapStringsSep " and " (virtualHost: mkRegistryPath "route" virtualHost.host virtualHost.entry) virtualHosts}"
|
)
|
||||||
]
|
) registry.route
|
||||||
else
|
|
||||||
[ ]
|
|
||||||
) virtualHostsByDomain
|
|
||||||
);
|
);
|
||||||
|
|
||||||
generatedVirtualHosts =
|
mkVirtualHost =
|
||||||
if duplicateDomainMessages != [ ] then
|
host: entry: mode:
|
||||||
throw (builtins.head duplicateDomainMessages)
|
let
|
||||||
else
|
isHttp = mode == "http";
|
||||||
lib.listToAttrs generatedVirtualHostList;
|
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 cfg.enable {
|
config = lib.mkIf config.modules.services.reverse-proxy.enable {
|
||||||
networking.firewall.allowedTCPPorts = [
|
networking.firewall.allowedTCPPorts = [
|
||||||
80
|
80
|
||||||
443
|
443
|
||||||
];
|
];
|
||||||
|
networking.firewall.allowedUDPPorts = map (stream: stream.port) generatedUdpStreams;
|
||||||
|
|
||||||
security.acme = {
|
security.acme = {
|
||||||
acceptTerms = true;
|
acceptTerms = true;
|
||||||
@@ -89,6 +112,7 @@ in
|
|||||||
recommendedOptimisation = true;
|
recommendedOptimisation = true;
|
||||||
recommendedProxySettings = true;
|
recommendedProxySettings = true;
|
||||||
recommendedTlsSettings = true;
|
recommendedTlsSettings = true;
|
||||||
|
streamConfig = lib.mkIf (generatedUdpStreams != [ ]) generatedStreamConfig;
|
||||||
virtualHosts = generatedVirtualHosts;
|
virtualHosts = generatedVirtualHosts;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
serviceName = lib.mkOption {
|
serviceName = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.strMatching "[a-z0-9][a-z0-9-]*";
|
||||||
example = "valheim";
|
example = "valheim";
|
||||||
description = "Name of the systemd service that runs the game server.";
|
description = "Lowercase systemd service basename for the game server.";
|
||||||
};
|
};
|
||||||
|
|
||||||
appId = lib.mkOption {
|
appId = lib.mkOption {
|
||||||
@@ -59,6 +59,18 @@
|
|||||||
default = [ ];
|
default = [ ];
|
||||||
description = "UDP ports opened for the game service.";
|
description = "UDP ports opened for the game service.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tcpPorts = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.port;
|
||||||
|
default = [ ];
|
||||||
|
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 [
|
||||||
@@ -68,14 +80,14 @@
|
|||||||
|
|
||||||
(lib.mkIf config.modules.services.steamcmd.enable (
|
(lib.mkIf config.modules.services.steamcmd.enable (
|
||||||
let
|
let
|
||||||
service = config.modules.services.steamcmd;
|
|
||||||
user = "steamcmd";
|
|
||||||
group = "storage";
|
group = "storage";
|
||||||
|
|
||||||
|
service = config.modules.services.steamcmd;
|
||||||
|
user = "steamcmd";
|
||||||
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
|
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
${pkgs.coreutils}/bin/install -d \
|
${lib.getExe' pkgs.coreutils "install"} -d \
|
||||||
-o ${user} \
|
-o ${user} \
|
||||||
-g ${group} \
|
-g ${group} \
|
||||||
-m 0770 \
|
-m 0770 \
|
||||||
@@ -113,11 +125,11 @@
|
|||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d ${service.steamDir} 0770 ${user} ${group} - -"
|
"d ${service.steamDir} 0770 ${user} ${group} - -"
|
||||||
"z ${service.steamDir} 0770 ${user} ${group} - -"
|
"Z ${service.steamDir} 0770 ${user} ${group} - -"
|
||||||
"d ${service.installDir} 0770 ${user} ${group} - -"
|
"d ${service.installDir} 0770 ${user} ${group} - -"
|
||||||
"z ${service.installDir} 0770 ${user} ${group} - -"
|
"Z ${service.installDir} 0770 ${user} ${group} - -"
|
||||||
"d ${service.stateDir} 0770 ${user} ${group} - -"
|
"d ${service.stateDir} 0770 ${user} ${group} - -"
|
||||||
"z ${service.stateDir} 0770 ${user} ${group} - -"
|
"Z ${service.stateDir} 0770 ${user} ${group} - -"
|
||||||
];
|
];
|
||||||
|
|
||||||
systemd.services = {
|
systemd.services = {
|
||||||
@@ -132,6 +144,18 @@
|
|||||||
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";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,24 +165,25 @@
|
|||||||
"network-online.target"
|
"network-online.target"
|
||||||
"steamcmd-${service.serviceName}-update.service"
|
"steamcmd-${service.serviceName}-update.service"
|
||||||
];
|
];
|
||||||
wants = [ "network-online.target" ];
|
wants = [
|
||||||
wantedBy = [ "multi-user.target" ];
|
"network-online.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 = [
|
ExecStartPre = "+${prepareScript}";
|
||||||
"+${prepareScript}"
|
|
||||||
updateScript
|
|
||||||
];
|
|
||||||
ExecStart = startScript;
|
ExecStart = startScript;
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
|
TimeoutStartSec = "15min";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedUDPPorts = service.udpPorts;
|
networking.firewall.allowedUDPPorts = service.udpPorts;
|
||||||
|
networking.firewall.allowedTCPPorts = service.tcpPorts;
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,14 +5,11 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# 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 = lib.mkEnableOption "Bazel for the primary user's shell environment";
|
options.modules.shell.bazel.enable =
|
||||||
|
lib.mkEnableOption "Bazel for the primary user's shell environment";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.shell.bazel.enable {
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.bazel ];
|
home-manager.users.${config.user.name}.home.packages = [ pkgs.bazel ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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,13 +7,9 @@
|
|||||||
# 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;
|
||||||
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;
|
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
|
||||||
exportEnvVar = name: value: ''
|
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
|
||||||
set -gx ${name} ${lib.escapeShellArg value}
|
user = config.user.name;
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.fish = {
|
options.modules.shell.fish = {
|
||||||
@@ -99,17 +95,14 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
deploy-to = {
|
deploy-to = {
|
||||||
description = "Deploy the current dotfiles branch to a host through target-local nixos-rebuild";
|
description = "Build the current checkout locally, import it on a host, and activate it";
|
||||||
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 = lib.concatStringsSep "\n" (
|
interactiveShellInit = builtins.readFile ./config.fish;
|
||||||
(lib.mapAttrsToList exportEnvVar envVars)
|
|
||||||
++ [ (builtins.readFile ./config.fish) ]
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
if test (count $argv) -lt 1; or test (count $argv) -gt 2
|
if test (count $argv) -ne 1
|
||||||
echo "usage: deploy-to <host> [branch]" >&2
|
echo "usage: deploy-to <host>" >&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
|
||||||
@@ -18,76 +11,85 @@ or begin
|
|||||||
return 2
|
return 2
|
||||||
end
|
end
|
||||||
|
|
||||||
set untracked_lines (git status --porcelain --untracked-files=normal | string match -r '^\?\? ')
|
set repository_root (git rev-parse --show-toplevel)
|
||||||
if test (count $untracked_lines) -gt 0
|
|
||||||
echo "deploy-to: refusing to deploy untracked files" >&2
|
|
||||||
echo "deploy-to: git add new files that Nix must see, or remove them" >&2
|
|
||||||
return 1
|
|
||||||
end
|
|
||||||
|
|
||||||
git fetch origin
|
|
||||||
or return $status
|
or return $status
|
||||||
|
|
||||||
if test -z "$branch"; or not git rev-parse --verify --quiet refs/remotes/origin/$branch >/dev/null
|
set untracked_nix_files (git -C $repository_root ls-files --others --exclude-standard -- '*.nix' flake.nix flake.lock)
|
||||||
if test "$branch_source" = explicit
|
if test (count $untracked_nix_files) -gt 0
|
||||||
echo "deploy-to: origin/$branch does not exist" >&2
|
echo "deploy-to: warning: untracked Nix files are not part of Git flake evaluation" >&2
|
||||||
return 1
|
printf ' %s\n' $untracked_nix_files >&2
|
||||||
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
|
||||||
|
|
||||||
if test -z "$branch"; or not git rev-parse --verify --quiet refs/remotes/origin/$branch >/dev/null
|
set installable "$repository_root#nixosConfigurations.$host.config.system.build.toplevel"
|
||||||
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
|
|
||||||
|
|
||||||
if test -z "$branch"
|
echo "deploy-to: building $installable" >&2
|
||||||
echo "deploy-to: could not determine a deploy branch" >&2
|
set system_path (nix build --no-link --print-out-paths $installable)
|
||||||
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: origin/$branch does not exist" >&2
|
echo "deploy-to: build failed for $host" >&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 has_local_changes false
|
set closure_paths (nix-store -qR $system_path)
|
||||||
if not git diff --quiet $remote_ref --
|
or begin
|
||||||
set has_local_changes true
|
echo "deploy-to: closure query failed for $host" >&2
|
||||||
|
return $status
|
||||||
end
|
end
|
||||||
|
|
||||||
set escaped_branch (string escape -- $branch)
|
echo "deploy-to: checking which closure paths $host is missing" >&2
|
||||||
set escaped_host (string escape -- $host)
|
set missing_paths (printf '%s\n' $closure_paths | ssh $host 'xargs -r nix-store --check-validity --print-invalid')
|
||||||
set remote_command "cd ~/dev && git fetch origin && git switch -- $escaped_branch && git reset --hard origin/$escaped_branch && git clean -fd"
|
or begin
|
||||||
|
echo "deploy-to: remote closure check failed for $host" >&2
|
||||||
|
return $status
|
||||||
|
end
|
||||||
|
|
||||||
set patch_file ""
|
if test (count $missing_paths) -gt 0
|
||||||
set remote_patch_file ""
|
set nar_file (mktemp --suffix=.nar)
|
||||||
if test "$has_local_changes" = true
|
or return $status
|
||||||
set patch_file (mktemp)
|
set remote_nar "/tmp/deploy-to-$host-"(date +%s)"-$fish_pid.nar"
|
||||||
set remote_patch_file "/tmp/deploy-to-$host.patch"
|
|
||||||
git diff --binary $remote_ref -- >$patch_file
|
echo "deploy-to: exporting "(count $missing_paths)" missing closure paths for $system_path" >&2
|
||||||
|
nix-store --export $missing_paths >$nar_file
|
||||||
or begin
|
or begin
|
||||||
rm -f $patch_file
|
set export_status $status
|
||||||
return $status
|
rm -f $nar_file
|
||||||
|
echo "deploy-to: closure export failed for $host" >&2
|
||||||
|
return $export_status
|
||||||
end
|
end
|
||||||
|
|
||||||
scp $patch_file $host:$remote_patch_file
|
echo "deploy-to: copying closure archive to $host:$remote_nar" >&2
|
||||||
|
scp $nar_file $host:$remote_nar
|
||||||
set scp_status $status
|
set scp_status $status
|
||||||
rm -f $patch_file
|
rm -f $nar_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
|
||||||
|
|
||||||
set escaped_remote_patch_file (string escape -- $remote_patch_file)
|
echo "deploy-to: importing closure on $host" >&2
|
||||||
set remote_command "$remote_command && git apply --index $escaped_remote_patch_file && rm -f $escaped_remote_patch_file"
|
ssh -t $host "sudo sh -c 'nix-store --import < $remote_nar' && rm -f $remote_nar"
|
||||||
|
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
|
||||||
|
|
||||||
set remote_command "$remote_command && sudo nixos-rebuild switch --flake .#$escaped_host --print-build-logs"
|
echo "deploy-to: recording $system_path as the system profile on $host" >&2
|
||||||
|
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
|
||||||
|
|
||||||
ssh -t $host $remote_command
|
echo "deploy-to: activating $system_path on $host" >&2
|
||||||
|
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,14 +5,13 @@
|
|||||||
}:
|
}:
|
||||||
# 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 cfg.enable {
|
config = lib.mkIf config.modules.shell.git.enable {
|
||||||
home-manager.users.${user}.programs.git = {
|
home-manager.users.${user}.programs.git = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,13 @@
|
|||||||
}:
|
}:
|
||||||
# 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 cfg.enable {
|
config = lib.mkIf config.modules.shell.gitea-axi.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,42 +6,58 @@
|
|||||||
}:
|
}:
|
||||||
# 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 = lib.mkEnableOption "Herdr, a terminal multiplexer for coding agents";
|
options.modules.shell.herdr.enable =
|
||||||
|
lib.mkEnableOption "Herdr, a terminal multiplexer for coding agents";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.shell.herdr.enable {
|
||||||
registry.shell-shortcut.${config.networking.hostName}.hs = "herdr --session";
|
registry.shell-shortcut.${config.networking.hostName}.hs = "herdr --session";
|
||||||
|
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${user} = lib.mkMerge [
|
||||||
home.packages = [ pkgs.herdr ];
|
{
|
||||||
|
home.packages = [ pkgs.herdr ];
|
||||||
|
|
||||||
xdg.configFile."herdr/config.toml".text = ''
|
xdg.configFile."herdr/config.toml".text = ''
|
||||||
[keys]
|
onboarding = false
|
||||||
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"
|
|
||||||
|
|
||||||
[ui]
|
[keys]
|
||||||
prompt_new_tab_name = false
|
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"
|
||||||
|
|
||||||
[experimental]
|
[ui]
|
||||||
kitty_graphics = true
|
prompt_new_tab_name = false
|
||||||
'';
|
|
||||||
};
|
[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 = lib.mkEnableOption "Neovim, configured declaratively via nixvim";
|
options.modules.shell.nvim.enable =
|
||||||
|
lib.mkEnableOption "Neovim, configured declaratively via nixvim";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.shell.nvim.enable {
|
||||||
registry.shell-shortcut.${config.networking.hostName} = {
|
registry.shell-shortcut.${config.networking.hostName} = {
|
||||||
vi = "nvim";
|
vi = "nvim";
|
||||||
vim = "nvim";
|
vim = "nvim";
|
||||||
@@ -31,6 +31,7 @@ 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 = " ";
|
||||||
@@ -188,6 +189,8 @@ in
|
|||||||
installRustc = false;
|
installRustc = false;
|
||||||
installRustfmt = false;
|
installRustfmt = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
servers.nixd.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
gitsigns = {
|
gitsigns = {
|
||||||
|
|||||||
19
configs/nixos/modules/shell/omp.nix
Normal file
19
configs/nixos/modules/shell/omp.nix
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
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,31 +8,22 @@
|
|||||||
# 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 = {
|
options.modules.shell.pi.enable =
|
||||||
enable = lib.mkEnableOption ''
|
lib.mkEnableOption "Pi, a terminal coding agent, configured through home-manager";
|
||||||
Pi, a terminal coding agent, configured through home-manager'';
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.shell.pi.enable {
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${user} = {
|
||||||
programs.pi-coding-agent = {
|
programs.pi-coding-agent = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = patchedPi;
|
package = pkgs.alexion.pi-coding-agent;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
defaultProvider = "openai-codex";
|
defaultProvider = "openai-codex";
|
||||||
defaultModel = "gpt-5.5";
|
defaultModel = "gpt-5.6-terra";
|
||||||
defaultThinkingLevel = "medium";
|
defaultThinkingLevel = "medium";
|
||||||
theme = "dark";
|
theme = "dark";
|
||||||
enableInstallTelemetry = false;
|
enableInstallTelemetry = false;
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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 = lib.mkEnableOption "Python tools for the primary user's shell environment";
|
options.modules.shell.python.enable =
|
||||||
|
lib.mkEnableOption "Python tools for the primary user's shell environment";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.shell.python.enable {
|
||||||
registry.env-var.${config.networking.hostName}.PYTHON_TOOLCHAIN_ROOT = "${pkgs.python3}";
|
registry.env.${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,8 +6,6 @@
|
|||||||
}:
|
}:
|
||||||
# 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; [
|
||||||
@@ -18,12 +16,14 @@ let
|
|||||||
rustfmt
|
rustfmt
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.rust.enable = lib.mkEnableOption "Rust tools for the primary user's shell environment";
|
options.modules.shell.rust.enable =
|
||||||
|
lib.mkEnableOption "Rust tools for the primary user's shell environment";
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf config.modules.shell.rust.enable {
|
||||||
registry.env-var.${config.networking.hostName}.RUST_TOOLCHAIN_ROOT = "${toolchain}";
|
registry.env.${config.networking.hostName}.RUST_TOOLCHAIN_ROOT = "${toolchain}";
|
||||||
|
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${user} = {
|
||||||
home.packages = [
|
home.packages = [
|
||||||
|
|||||||
@@ -6,13 +6,12 @@
|
|||||||
# 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 cfg.enable {
|
config = lib.mkIf config.modules.shell.tmux.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,13 +6,12 @@
|
|||||||
}:
|
}:
|
||||||
# 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 cfg.enable {
|
config = lib.mkIf config.modules.shell.yazi.enable {
|
||||||
home-manager.users.${user}.programs.yazi = {
|
home-manager.users.${user}.programs.yazi = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enableFishIntegration = true;
|
enableFishIntegration = true;
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
{
|
|
||||||
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, ... }:
|
{ lib, util, ... }:
|
||||||
{
|
{
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
|
|
||||||
checks = [
|
checks = [
|
||||||
{
|
{
|
||||||
assertion = lib.registry.predicates.uniqueValue (entry: entry.value);
|
assertion = util.registry.predicates.uniqueValue (entry: entry.value);
|
||||||
message = "Address registry values must be unique.";
|
message = "Address registry values must be unique.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
45
configs/nixos/registry/homepage.nix
Normal file
45
configs/nixos/registry/homepage.nix
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{ 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.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
112
configs/nixos/registry/keybind.nix
Normal file
112
configs/nixos/registry/keybind.nix
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
{ 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,10 +1,40 @@
|
|||||||
{ lib, ... }:
|
{ lib, util, ... }:
|
||||||
{
|
{
|
||||||
checks = [
|
checks = [
|
||||||
{
|
{
|
||||||
assertion = lib.registry.predicates.uniqueValue (entry: entry.value.domain);
|
assertion = util.registry.predicates.uniqueValue (entry: entry.value.domain);
|
||||||
message = "Route domains must be unique.";
|
message = "Route domains must be unique.";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
assertion =
|
||||||
|
entries:
|
||||||
|
util.registry.predicates.uniqueValue (entry: entry.value) (
|
||||||
|
lib.concatMap (entry: map (port: entry // { value = port; }) entry.value.ports) entries
|
||||||
|
);
|
||||||
|
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 {
|
||||||
@@ -12,13 +42,40 @@
|
|||||||
domain = lib.mkOption {
|
domain = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "files.alexion.dev";
|
example = "files.alexion.dev";
|
||||||
description = "Public domain served by this reverse-proxy route entry.";
|
description = "Records the public domain for this route.";
|
||||||
|
};
|
||||||
|
|
||||||
|
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.ints.between 1 65535;
|
type = lib.types.nullOr lib.types.port;
|
||||||
|
default = null;
|
||||||
example = 3923;
|
example = 3923;
|
||||||
description = "Backend TCP port served by the matching address registry entry.";
|
description = "Sets the HTTP backend port for an HTTP or HTTPS route.";
|
||||||
|
};
|
||||||
|
|
||||||
|
ports = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.port;
|
||||||
|
default = [ ];
|
||||||
|
example = [
|
||||||
|
2456
|
||||||
|
2457
|
||||||
|
];
|
||||||
|
description = "Sets the UDP ports for a UDP route.";
|
||||||
|
};
|
||||||
|
|
||||||
|
clientMaxBodySize = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
example = "1g";
|
||||||
|
description = "Maximum HTTP request body the reverse proxy accepts.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
let
|
|
||||||
entryType = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
command = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "nvim";
|
|
||||||
description = "Command expanded by this shell shortcut.";
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
type = lib.types.coercedTo lib.types.str (command: { inherit command; }) (
|
||||||
|
lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
command = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
example = "nvim";
|
||||||
|
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.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,25 @@
|
|||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
let
|
let
|
||||||
inherit (lib)
|
duplicateUids =
|
||||||
filterAttrs
|
entries:
|
||||||
groupBy
|
lib.filterAttrs (_uid: entryNames: builtins.length entryNames > 1) (entryNamesByUid entries);
|
||||||
mapAttrs
|
|
||||||
unique
|
|
||||||
;
|
|
||||||
|
|
||||||
serviceNamesByUid = entries:
|
entryNamesByUid =
|
||||||
mapAttrs (_uid: uidEntries: unique (map (entry: entry.entry) uidEntries)) (
|
entries:
|
||||||
groupBy (entry: toString entry.value) entries
|
lib.mapAttrs (_uid: uidEntries: lib.unique (map (entry: entry.entry) uidEntries)) (
|
||||||
|
lib.groupBy (entry: toString entry.value) entries
|
||||||
);
|
);
|
||||||
|
|
||||||
duplicateUids = entries:
|
|
||||||
filterAttrs (_uid: serviceNames: builtins.length serviceNames > 1) (serviceNamesByUid entries);
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Next stable UID: 20003
|
# Next stable UID: 20005
|
||||||
replicate = true;
|
|
||||||
|
|
||||||
type = lib.types.ints.positive;
|
type = lib.types.ints.positive;
|
||||||
|
|
||||||
checks = [
|
checks = [
|
||||||
{
|
{
|
||||||
assertion = entries: duplicateUids entries == { };
|
assertion = entries: duplicateUids entries == { };
|
||||||
message = "Stable UID allocations must be unique.";
|
message = "Multiple entries use the same stable UID.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
29
configs/nixos/registry/waybar.nix
Normal file
29
configs/nixos/registry/waybar.nix
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{ 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