Compare commits

...

29 Commits

Author SHA1 Message Date
primary
c8c1e656c1 archive: repo moved to titaniumtown/nixos 2026-04-18 01:51:22 -04:00
e9a44f677d update
All checks were successful
Build / build (push) Successful in 7m2s
2026-04-17 23:26:43 -04:00
0c881602e9 yarn: fix steamos update flow 2026-04-17 23:26:15 -04:00
7f375e8574 kernel: re-enable SND_PCI
All checks were successful
Build / build (push) Successful in 1h35m51s
2026-04-17 18:26:21 -04:00
577b5eeb77 update
All checks were successful
Build / build (push) Successful in 1h36m48s
2026-04-17 12:33:33 -04:00
91aba32afb pi: update to claude opus 4.7
All checks were successful
Build / build (push) Successful in 4m19s
2026-04-17 00:25:38 -04:00
29e71fb127 ??!?!?!??!
All checks were successful
Build / build (push) Successful in 6m25s
2026-04-16 23:46:13 -04:00
ff94c3b027 steamos-update: exit 0 not 7
All checks were successful
Build / build (push) Successful in 6m14s
2026-04-16 23:05:24 -04:00
0b457b83d3 fix build
All checks were successful
Build / build (push) Successful in 5m49s
2026-04-16 22:53:11 -04:00
c23240c529 yarn: move pull-update into steamos-update script
Some checks failed
Build / build (push) Failing after 1m25s
2026-04-16 22:28:49 -04:00
e40929018f eww: remove
All checks were successful
Build / build (push) Successful in 1m13s
2026-04-16 18:02:02 -04:00
5997c886f6 pull-update: improvement
All checks were successful
Build / build (push) Successful in 3m0s
2026-04-16 17:43:35 -04:00
72d37f57ac update
All checks were successful
Build / build (push) Successful in 12m59s
2026-04-16 16:31:49 -04:00
0718568bec pull-update: forgot lib.getExe 2026-04-16 15:03:06 -04:00
982cc4aebc pull-update: use writeShellApplication instead 2026-04-16 15:02:08 -04:00
d2d25bbdfe omp: remove patch that didn't work 2026-04-16 14:52:51 -04:00
76cdd535c8 gitea workflow: remove notifications
All checks were successful
Build / build (push) Successful in 3m41s
2026-04-16 13:35:26 -04:00
0be90ace43 initrd: fix module loading
Some checks failed
Build / build (push) Failing after 4m55s
2026-04-16 13:04:22 -04:00
13f16fe775 update
Some checks failed
Build / build (push) Failing after 1h39m57s
2026-04-16 11:15:13 -04:00
20df895312 pull-update: update and reboot
Some checks failed
Build / build (push) Failing after 3m42s
2026-04-16 00:50:13 -04:00
4542a5002c fix pull-update 2026-04-16 00:15:29 -04:00
d0d8d5b9d2 ci: prevent gc from deleting 2026-04-15 23:25:45 -04:00
21658b7bc0 update
Some checks failed
Build / build (push) Failing after 1h48m36s
2026-04-15 22:08:59 -04:00
56cda525cd fix gitea workflow
Some checks failed
Build / build (push) Failing after 2m33s
2026-04-15 22:06:22 -04:00
194c66feb4 fix initrd build
Some checks failed
Build / build (push) Failing after 2m10s
2026-04-15 21:57:04 -04:00
7ab17f132e kernel: compile for x86_64-v3 (common target)
Some checks failed
Build / build (push) Failing after 12s
2026-04-15 17:33:21 -04:00
da1bfbb778 update
Some checks failed
Build / build (push) Failing after 3h25m51s
2026-04-15 13:32:34 -04:00
ec42b906d6 update 2026-04-15 09:31:15 -04:00
b050ecc5bf kernel: enable CHROME_PLATFORMS for framework laptop 2026-04-15 09:30:51 -04:00
21 changed files with 249 additions and 917 deletions

View File

@@ -18,28 +18,21 @@ jobs:
- name: Build NixOS configuration (yarn)
run: |
nix build .#nixosConfigurations.yarn.config.system.build.toplevel -L
- name: Record yarn store path for pull-update
continue-on-error: true
run: |
mkdir -p /var/lib/dotfiles-deploy
readlink -f result > /var/lib/dotfiles-deploy/yarn
nix-store --add-root /var/lib/dotfiles-deploy/yarn-gcroot -r "$(readlink -f result)"
- name: Build NixOS configuration (mreow)
run: |
nix build .#nixosConfigurations.mreow.config.system.build.toplevel -L
- name: Notify success
if: success()
run: |
curl -sf -X POST \
"https://ntfy.sigkill.computer/deployments" \
-H "Title: [dotfiles] Build succeeded" \
-H "Priority: default" \
-H "Tags: white_check_mark" \
-d "dotfiles built from commit ${GITHUB_SHA::8}"
- name: Notify failure
if: failure()
- name: Record mreow store path
continue-on-error: true
run: |
curl -sf -X POST \
"https://ntfy.sigkill.computer/deployments" \
-H "Title: [dotfiles] Build FAILED" \
-H "Priority: urgent" \
-H "Tags: rotating_light" \
-d "dotfiles build failed at commit ${GITHUB_SHA::8}"
mkdir -p /var/lib/dotfiles-deploy
readlink -f result > /var/lib/dotfiles-deploy/mreow
nix-store --add-root /var/lib/dotfiles-deploy/mreow-gcroot -r "$(readlink -f result)"

View File

@@ -1,3 +1,10 @@
> **Archived.** These dotfiles have moved into the unified
> [`titaniumtown/nixos`](https://git.sigkill.computer/titaniumtown/nixos) repo
> (merged with `server-config`). The final pre-unify commit is tagged
> `final-before-unify`. No new commits will land here.
---
# My Dotfiles ✨
These are my dotfiles for my laptop and desktop (which I use [NixOS](https://nixos.org/) and [home-manager](https://github.com/nix-community/home-manager) on).

119
flake.lock generated
View File

@@ -12,11 +12,11 @@
]
},
"locked": {
"lastModified": 1771437256,
"narHash": "sha256-bLqwib+rtyBRRVBWhMuBXPCL/OThfokA+j6+uH7jDGU=",
"lastModified": 1776249299,
"narHash": "sha256-Dt9t1TGRmJFc0xVYhttNBD6QsAgHOHCArqGa0AyjrJY=",
"owner": "numtide",
"repo": "blueprint",
"rev": "06ee7190dc2620ea98af9eb225aa9627b68b0e33",
"rev": "56131e8628f173d24a27f6d27c0215eff57e40dd",
"type": "github"
},
"original": {
@@ -46,15 +46,16 @@
]
},
"locked": {
"lastModified": 1770895533,
"narHash": "sha256-v3QaK9ugy9bN9RXDnjw0i2OifKmz2NnKM82agtqm/UY=",
"owner": "nix-community",
"lastModified": 1776182890,
"narHash": "sha256-+/VOe8XGq5klpU+I19D+3TcaR7o+Cwbq67KNF7mcFak=",
"owner": "Mic92",
"repo": "bun2nix",
"rev": "c843f477b15f51151f8c6bcc886954699440a6e1",
"rev": "648d293c51e981aec9cb07ba4268bc19e7a8c575",
"type": "github"
},
"original": {
"owner": "nix-community",
"owner": "Mic92",
"ref": "catalog-support",
"repo": "bun2nix",
"type": "github"
}
@@ -62,11 +63,11 @@
"cachyos-kernel": {
"flake": false,
"locked": {
"lastModified": 1775145950,
"narHash": "sha256-AfVja9nvYHm0BHbuTvn+K8rKfLmPl5QjoiNecp9HOJU=",
"lastModified": 1776183001,
"narHash": "sha256-lvLKB5dTqjO1S/YonS9ZyWemEjO6QXtN4D76rYEYy4s=",
"owner": "CachyOS",
"repo": "linux-cachyos",
"rev": "b91624f68ceaf5394ef1571f60290dca6ba22b45",
"rev": "4224303b6d7a50dd1cc3ffa78864050cc9536eec",
"type": "github"
},
"original": {
@@ -78,11 +79,11 @@
"cachyos-kernel-patches": {
"flake": false,
"locked": {
"lastModified": 1775157685,
"narHash": "sha256-g8HgH7gADoEnrBN30BK3pz7+M2pT/p3xtfRFEuEov5w=",
"lastModified": 1776355454,
"narHash": "sha256-b9Hc0sTxjEzDbphzS9yQqxVha/7bsPIs2cQQQvaG45E=",
"owner": "CachyOS",
"repo": "kernel-patches",
"rev": "c1ba300617a12d257b5721572b9bbe28efae182f",
"rev": "b5e029226df5cc30c103651072d49a7af2878202",
"type": "github"
},
"original": {
@@ -130,11 +131,11 @@
"doomemacs": {
"flake": false,
"locked": {
"lastModified": 1776049930,
"narHash": "sha256-6nuelk+8qSRsh5Ryj9EpWOWXeeh/g3lI5mZsBfiFZQI=",
"lastModified": 1776400245,
"narHash": "sha256-RuQB1PxazI4DOw3O+rEVU2FPT0vP0Xb+Gp/M6Yqer20=",
"owner": "doomemacs",
"repo": "doomemacs",
"rev": "5eb006f455f0558c97210ba7579880aacb045b67",
"rev": "860a91aaac235701f30b70fdc74259d438818968",
"type": "github"
},
"original": {
@@ -153,11 +154,11 @@
]
},
"locked": {
"lastModified": 1776074175,
"narHash": "sha256-8e7+uLslLDZRD8p5QyJV8QSivpB2qMy2XuAcVYbW1f4=",
"lastModified": 1776478519,
"narHash": "sha256-4TWCOVYe0iWEKuW7OH93nRI4Z7u68wNT6k9UJn0FZ5w=",
"owner": "nix-community",
"repo": "emacs-overlay",
"rev": "000ca2cd866f7c37a8c0cc96dd2aff457ee4c865",
"rev": "513e332b074507e1b46992952e7d83f329f2c22c",
"type": "github"
},
"original": {
@@ -174,11 +175,11 @@
},
"locked": {
"dir": "pkgs/firefox-addons",
"lastModified": 1776052978,
"narHash": "sha256-WR0Svwg/JreBNW006qjHET6RRRmmjWCMfrkS5JmDZK8=",
"lastModified": 1776398575,
"narHash": "sha256-WArU6WOdWxzbzGqYk4w1Mucg+bw/SCl6MoSp+/cZMio=",
"owner": "rycee",
"repo": "nur-expressions",
"rev": "6c0e7f01d9315f4806a187c2ec58d0f3b6961876",
"rev": "05815686caf4e3678f5aeb5fd36e567886ab0d30",
"type": "gitlab"
},
"original": {
@@ -306,11 +307,11 @@
]
},
"locked": {
"lastModified": 1776114641,
"narHash": "sha256-VJMt3n9zGRzupzvlhcKIz4SpWflKh0rWfYTgmkmun0Q=",
"lastModified": 1776454077,
"narHash": "sha256-7zSUFWsU0+jlD7WB3YAxQ84Z/iJurA5hKPm8EfEyGJk=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "2de7205ce6e10b031151033e69b7ef89708dc282",
"rev": "565e5349208fe7d0831ef959103c9bafbeac0681",
"type": "github"
},
"original": {
@@ -365,11 +366,11 @@
]
},
"locked": {
"lastModified": 1775841957,
"narHash": "sha256-oHxj9I82v+axW1lj+jUj2t8V++E6A9x54K5lq+liNAk=",
"lastModified": 1776428236,
"narHash": "sha256-+0SyQglnT2xUiyY07155G+O7aUWISELwqtTnfURufRU=",
"owner": "Jovian-Experiments",
"repo": "Jovian-NixOS",
"rev": "67d55e61fe5e4d88d3fb90c0888cfced04a0589d",
"rev": "eac78fc379ca47f7e21be8539c405e5fb489a857",
"type": "github"
},
"original": {
@@ -411,11 +412,11 @@
]
},
"locked": {
"lastModified": 1775866084,
"narHash": "sha256-mWn8D/oXXAaqeFFFRorKHvTLw5V9M8eYzAWRr4iffag=",
"lastModified": 1776248416,
"narHash": "sha256-TC6yzbCAex1pDfqUZv9u8fVm8e17ft5fNrcZ0JRDOIQ=",
"owner": "nix-community",
"repo": "lanzaboote",
"rev": "29d2cca7fc3841708c1d48e2d1272f79db1538b6",
"rev": "18e9e64bae15b828c092658335599122a6db939b",
"type": "github"
},
"original": {
@@ -436,11 +437,11 @@
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1776092696,
"narHash": "sha256-4MQq9lP6b9nBHozK1LHQNXPv72XAgGt8Drb4mQPqd7s=",
"lastModified": 1776482297,
"narHash": "sha256-KmsWPwtbO8vrlH/R9stIun0LKZ4PFSCCEdqWDeLgbTE=",
"owner": "numtide",
"repo": "llm-agents.nix",
"rev": "215193474714ad712668bda23eef596c2656af6b",
"rev": "66c76393570f8fc4730caa2dc2d2c470fe33a3c9",
"type": "github"
},
"original": {
@@ -463,11 +464,11 @@
"xwayland-satellite-unstable": "xwayland-satellite-unstable"
},
"locked": {
"lastModified": 1776109195,
"narHash": "sha256-yug5v5OI5ixCYyAiqCbNrxfiyfvxvlsMr/tj3uyH51c=",
"lastModified": 1776435348,
"narHash": "sha256-qsZnMThxTqxCJZ7DEKu3DD3KjIPcuUBvZ0C9a2uIvaQ=",
"owner": "sodiboo",
"repo": "niri-flake",
"rev": "8fcfcef0fc05ee826adf66225b27716131ed74af",
"rev": "55b5b1fc9481ab267603a1099e5d4b4ebc7394d7",
"type": "github"
},
"original": {
@@ -496,11 +497,11 @@
"niri-unstable": {
"flake": false,
"locked": {
"lastModified": 1775561155,
"narHash": "sha256-TK2IrqQivRcwqJa0suZMbcsN17CtA8Uu0v7CDnLATb0=",
"lastModified": 1776432730,
"narHash": "sha256-Pq1ZVvRGq/IFiFH6vkNwMfZEpWk23NjgGdX50COdj/c=",
"owner": "YaLTeR",
"repo": "niri",
"rev": "599db847f857b8a7ff78ce02f15acab5d5d9fee1",
"rev": "c814c656c53ea9d69f5afb45c88f4dc4d25338cd",
"type": "github"
},
"original": {
@@ -520,11 +521,11 @@
]
},
"locked": {
"lastModified": 1775239578,
"narHash": "sha256-MKJmDHlaxwBcnfCUEA89AwKOOONjOjbjHNNWdSdg5RA=",
"lastModified": 1776386586,
"narHash": "sha256-eVAUaL/6n8mnmBiPpEVW1NDNVSKLWhYVfycG+P0SvWU=",
"owner": "xddxdd",
"repo": "nix-cachyos-kernel",
"rev": "beaf7a533ae106c2681de2624da94707f9857f1f",
"rev": "c65c3faf90ae07bae101c15ef502f0bcb06c5d74",
"type": "github"
},
"original": {
@@ -546,11 +547,11 @@
"systems": "systems_3"
},
"locked": {
"lastModified": 1776078956,
"narHash": "sha256-+JCa+VodMqySrmSBWwNxuUcgLFSDvFA8rR0sY6YC7t0=",
"lastModified": 1776419397,
"narHash": "sha256-vmWJwNYtQFexLG6r/v8Dlou/5z8FbFCLo3QqZ/stLYQ=",
"owner": "marienz",
"repo": "nix-doom-emacs-unstraightened",
"rev": "f4abf68bf74d506ff56af64e7a0b29e1a72d8b57",
"rev": "7623dd4adbdf5f8a8464ecc5fd089e5c5cb5dada",
"type": "github"
},
"original": {
@@ -614,11 +615,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1775710090,
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
"lastModified": 1776169885,
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
"type": "github"
},
"original": {
@@ -651,11 +652,11 @@
"noctalia-qs": "noctalia-qs"
},
"locked": {
"lastModified": 1776043445,
"narHash": "sha256-ie3vFwg0eZTTHBDCRm+ee/PecbtdPn/pyL6hlotAfeQ=",
"lastModified": 1776302695,
"narHash": "sha256-xZc9o1JLQpmWn2Dqui323+Tq2Ai4sSdtdvbFZCs4qLo=",
"owner": "noctalia-dev",
"repo": "noctalia-shell",
"rev": "e56a9db57ed61ea248f109edd60965faf56d3da2",
"rev": "a7c724181fca5d1aff2d47b18fa733504cfdbda2",
"type": "github"
},
"original": {
@@ -739,11 +740,11 @@
]
},
"locked": {
"lastModified": 1776050130,
"narHash": "sha256-/f/6/1WOfBJaGMfqV3VxWD9lpFRbPpF+Cx4MO+0mGok=",
"lastModified": 1776481912,
"narHash": "sha256-Xq7p+Ex3YHFAd+fFFLOYw2Wv67582X7SAmrEDtIDZQ4=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "3c27f4c92a7d977556dd2c10bb564d9c61b375e9",
"rev": "e611106c527e8ab0adbb641183cda284411d575c",
"type": "github"
},
"original": {
@@ -898,11 +899,11 @@
]
},
"locked": {
"lastModified": 1776091817,
"narHash": "sha256-Vwmi3P4LAUmOrE2zc9JpnRrNxNwamDN46hqcXpWTkp0=",
"lastModified": 1776403742,
"narHash": "sha256-ZmGY9XiOsuMS/THsSNkgp2fnc3asXQX/xRrQpWnY9nA=",
"owner": "0xc000022070",
"repo": "zen-browser-flake",
"rev": "4f2e98c1125ab4be758cd1b51b526ad998e9618f",
"rev": "ca7077bea5c830470437ea878da2a1940773324c",
"type": "github"
},
"original": {

View File

@@ -9,9 +9,6 @@
# niri wayland compositor
./progs/niri.nix
# statusbar
# ./progs/eww/eww.nix
# lockscreen
./progs/swaylock.nix
@@ -29,5 +26,4 @@
# used by /etc/nixos logic to launch niri
config.programs.niri.package
];
}

View File

@@ -1,109 +0,0 @@
$background: #1e1e2e;
$pink: #f5c2e7;
$lavendar: #b4befe;
$red: #f38ba8;
$maroon: #eba0ac;
$peach: #fab387;
$yellow: #f9e2af;
$green: #a6e3a1;
$text: #cdd6f4;
$subtext: #a6adc8;
$surface: #585b70;
* {
color: $text;
font-family: CaskaydiaCove Nerd Font Mono;
font-weight: 600;
font-size: 10pt;
padding: 0 1px;
}
.red {
color: $red;
}
.maroon {
color: $maroon;
}
.peach {
color: $peach;
}
.yellow {
color: $yellow;
}
.green {
color: $green;
}
.lavendar {
color: $lavendar;
}
.symbol {
color: $lavendar;
font-size: 20px;
}
.button {
* {
all: unset;
margin: 0 5px;
font-size: 14pt;
transition: color 0.2s ease-in-out;
}
&:hover * {
color: $pink;
}
}
.bluetooth * {
font-size: 10pt;
padding: 0 0.3em;
}
.padded>*:not(:last-child) {
padding: 0 10px;
border-right: 1px solid $surface;
}
.background {
border: 1px solid $pink;
background-color: $background;
border-radius: 12px;
opacity: 0.8;
}
scale trough {
margin: 0 10px;
border: none;
background-color: #FFF;
min-height: 3px;
min-width: 100px;
& slider {
box-shadow: none;
background-image: none;
border: none;
background-color: $pink;
min-width: 5pt;
min-height: 5pt;
margin: -5pt;
}
& highlight {
border: none;
background-color: $lavendar;
}
}
.clipboard {
color: $subtext;
}
.time {
padding-right: 10px;
}

View File

@@ -1 +0,0 @@
(include "./statusbar.yuck")

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env bash
niri_data=$(niri msg --json focused-window)
if [[ "$niri_data" == "null" ]]; then
exit 0
fi
name=$(echo "$niri_data" | jq -r '.["app_id"], .["title"]' | tr '\n' ' ' | sed 's/.$//')
proc_name=$(echo "$name" | head -c 55)
# TODO! fix this logic, add a '...' at the end
if [[ "$name" != "$proc_name" ]]; then
proc_name="$proc_name..."
fi
echo "$proc_name"

View File

@@ -1,3 +0,0 @@
#!/usr/bin/env fish
niri msg --json workspaces | jq -r '.[] | select(.is_focused == true) | .["id"]'

View File

@@ -1,58 +0,0 @@
#!/usr/bin/env rust-script
use std::{fmt, fs::read_to_string, str::FromStr};
const BASE_PATH: &str = "/sys/class/power_supply/BAT1/";
const CURRENT_NOW_PATH: &str = "current_now";
const VOLTAGE_NOW_PATH: &str = "voltage_now";
const STATUS_PATH: &str = "status";
const FACTOR: f32 = 1e6_f32;
#[derive(Debug)]
enum Status {
Charging,
Discharging,
NotCharging,
}
impl FromStr for Status {
type Err = &'static str;
fn from_str(input: &str) -> Result<Status, Self::Err> {
match input {
"Charging" => Ok(Status::Charging),
"Discharging" => Ok(Status::Discharging),
"Not charging" => Ok(Status::NotCharging),
_ => Err("unknown state"),
}
}
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
fn fetch_and_trim_into<T: FromStr<Err = impl fmt::Debug>>(path: &str) -> T {
let mut content = read_to_string(BASE_PATH.to_owned() + path).unwrap();
content.pop();
T::from_str(&content).unwrap()
}
fn fetch_bat_info(path: &str) -> f32 {
let value: f32 = fetch_and_trim_into(path);
value / FACTOR
}
fn main() {
let current_now: f32 = fetch_bat_info(CURRENT_NOW_PATH);
let voltage_now: f32 = fetch_bat_info(VOLTAGE_NOW_PATH);
let watts: f32 = current_now * voltage_now;
let status: Status = fetch_and_trim_into(STATUS_PATH);
println!(
"voltage: {:.4}\ncurrent: {:.4}\nwatts: {:.4}\nstatus: {}",
voltage_now, current_now, watts, status
);
}

View File

@@ -1,2 +0,0 @@
#!/usr/bin/env sh
wpctl inspect @DEFAULT_SINK@ | grep -E "^ +\* node\.description" | cut -d' ' -f6- | tr -d '"'

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env bash
output=$(wpctl get-volume @DEFAULT_SINK@ | cut -d' ' -f2- | sed -E 's/\.//g' | sed 's/^0*//g')
count=$(echo "$output" | awk -F, '{print $1+0}')
muted=$(echo "$output" | cut -d'[' -f2 | cut -d ']' -f1)
# if not muted, set to empty string
if [ "$muted" == "$count" ]; then
muted=""
fi
color="green"
if ((count > 75)); then color="yellow"; fi
if ((count > 90)); then color="peach"; fi
if ((count > 100)); then color="maroon"; fi
if ((count > 110)); then color="red"; fi
output="${count}%"
if [ "$muted" != "" ]; then
output="${output} [${muted}]"
fi
echo "{\"count\":\"${output}\", \"color\":\"${color}\"}"

View File

@@ -1,23 +0,0 @@
#!/usr/bin/env zsh
export CHARSET=ASCII
case $1 in
name)
nmcli -f IN-USE,SSID d w | grep '*' | sed 's/[\* ]//g' | cat
exit 0;;
strength)
str=$(nmcli -f ACTIVE,BARS d w | grep 'yes' | tr -d ' yesno')
case ${str: 0:-1} in
'****')
icon="󰤨"; colour="green";;
'***')
icon="󰤥"; colour="yellow";;
'**')
icon="󰤢"; colour="peach";;
'*')
icon="󰤟"; colour="maroon";;
*)
icon="󰤯"; colour="red";;
esac
echo "{\"icon\":\"$icon\",\"colour\":\"$colour\"}"
exit 0;;
esac

View File

@@ -1,130 +0,0 @@
(defwindow statusbar
:monitor 0
:stacking "fg"
:exclusive true
:geometry (geometry
:y "0.5%"
:width "100%"
:height "24px"
:anchor "top center")
(statusbar))
(defwidget statusbar []
(centerbox
(box :space-evenly false :halign 'start' :class 'padded'
(window-title))
(time)
(box :space-evenly false :halign 'end' :class 'padded'
(brightness-ctl)
(brightness-ctl-opener)
(volume)
(battery)
(bluetooth)
(wifi))))
(defwidget cmd-slider [?symbol value command max color]
(box :space-evenly false
(label :text symbol :class "symbol")
(scale
:min 0 :max max
:value value
:round-digits 0
:timeout "200ms"
:onchange command)
(label :text "${value}%" :class color)))
(defpoll windowtitle :interval "1s" `scripts/currentWindow.sh`)
(defwidget window-title []
(label
:text {windowtitle == "" ? "" : "(${windowtitle})"}))
(defwidget brightness-ctl []
(box :visible brightnessctl-open
(cmd-slider :symbol "󰃠" :value brightness
:command `brightnessctl set {}%`
:max 101 :color {
brightness >= 80 ? "green" :
brightness >= 50 ? "yellow" :
brightness >= 30 ? "peach" :
brightness >= 10 ? "maroon" : "red"
})))
(defpoll brightness :interval "1s" :run-while brightnessctl-open `brightnessctl -m | awk -F, '{print $4+0}'`)
(defvar brightnessctl-open false)
(defwidget brightness-ctl-opener []
(eventbox :class "button"
(button
:onclick `${EWW_CMD} update brightnessctl-open=${!brightnessctl-open}`
"󰃠")))
(defwidget wifi []
(eventbox
:class "button ${wifi-strength.colour}"
(label
:text {wifi-strength.icon}
:tooltip "Connected To: ${wifi-name}")))
(defpoll wifi-strength :interval "10s" `scripts/wifiInfo.zsh strength`)
(defpoll wifi-name :interval "1m" `scripts/wifiInfo.zsh name`)
(defwidget bluetooth []
(eventbox
:class "bluetooth button ${ bluetooth-name != "" ? "green" : "lavendar" }"
:onclick `blueman-manager &`
(label
:text "${bluetooth-name} 󰂯")))
; `FNR == 1 + head -c 30` so the name doesn't explode the screen
(defpoll bluetooth-name :interval "10s" `bluetoothctl devices Connected | awk '$1 == "Device" {print $0}' | cut -d' ' -f3-`)
(defwidget time []
(box
:space-evenly false
:class "time"
:tooltip {time.long}
(label :class "yellow" :text {time.hour})
(label :text ":")
(label :class "yellow" :text {time.minute})))
(defpoll time :interval "1s" `date +'{"long":"%a %b %e %H:%M:%S %Z %Y","hour":"%H","minute":"%M"}'`)
(defpoll powerstats :interval "2s" `power_bat`)
(defwidget battery []
(box :space-evenly false
:tooltip powerstats
(label
:text {EWW_BATTERY.BAT1.status == "Charging" ? "󰂄" :
EWW_BATTERY.BAT1.capacity >= 90 ? "󰁹" :
EWW_BATTERY.BAT1.capacity >= 80 ? "󰂂" :
EWW_BATTERY.BAT1.capacity >= 70 ? "󰂁" :
EWW_BATTERY.BAT1.capacity >= 60 ? "󰂀" :
EWW_BATTERY.BAT1.capacity >= 50 ? "󰁿" :
EWW_BATTERY.BAT1.capacity >= 40 ? "󰁾" :
EWW_BATTERY.BAT1.capacity >= 30 ? "󰁽" :
EWW_BATTERY.BAT1.capacity >= 20 ? "󰁼" :
EWW_BATTERY.BAT1.capacity >= 10 ? "󰁻" : "󰁺"
}
:class {
EWW_BATTERY.BAT1.capacity >= 80 ? "green" :
EWW_BATTERY.BAT1.capacity >= 50 ? "yellow" :
EWW_BATTERY.BAT1.capacity >= 30 ? "peach" :
EWW_BATTERY.BAT1.capacity >= 10 ? "maroon" : "red"
})
(label :text "${EWW_BATTERY.BAT1.capacity}%" :class "yellow")))
(defpoll volumevalue :interval "1s" `scripts/sound/getVolume.sh`)
(defpoll volumesink :interval "1s" `scripts/sound/getSink.sh`)
(defwidget volume []
(eventbox :tooltip volumesink
:onclick `pwvucontrol &`
(label :text "${volumevalue.count}" :class {volumevalue.color})))
(defpoll currentworkspace :interval "1s" `scripts/currentWorkspace.sh`)

View File

@@ -1,40 +0,0 @@
{
pkgs,
lib,
config,
...
}:
{
home.packages = with pkgs; [
zsh
bluez
brightnessctl
(callPackage ./power_bat.nix { })
];
programs.eww = {
enable = true;
configDir = ./config;
};
programs.niri.settings.spawn-at-startup = [
{
command = [
(lib.getExe config.programs.eww.package)
"-c"
"${config.programs.eww.configDir}"
"open"
"statusbar"
];
}
# swaybg works on more than just sway (sets a wallpaper)
{
command = [
(lib.getExe pkgs.swaybg)
"-i"
"${../wallpaper.png}"
];
}
];
}

View File

@@ -1,4 +0,0 @@
{ pkgs, lib, ... }:
pkgs.writeShellScriptBin "power_bat" ''
exec ${lib.getExe pkgs.rust-script} ${./config/scripts/power_bat.rs} "$@"
''

View File

@@ -10,10 +10,10 @@ let
# librarian/explore/quick → smol/commit = haiku
ompSettings = {
modelRoles = {
default = "anthropic/claude-opus-4-6:high";
default = "anthropic/claude-opus-4-7:high";
smol = "anthropic/claude-haiku-4-5:low";
slow = "anthropic/claude-opus-4-6:xhigh";
plan = "anthropic/claude-opus-4-6:high";
slow = "anthropic/claude-opus-4-7:xhigh";
plan = "anthropic/claude-opus-4-7:high";
commit = "anthropic/claude-haiku-4-5:low";
};
};
@@ -38,7 +38,7 @@ in
{
home.packages = [
(inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.omp.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [ ../../patches/omp-fix-auth.patch ];
patches = (old.patches or [ ]) ++ [ ];
}))
];

View File

@@ -1,298 +0,0 @@
commit 02b80dd74e2f962fffc9b0076bb7966eea4e45ff
Author: Simon Gardling <titaniumtown@proton.me>
Date: Wed Apr 8 13:05:09 2026 -0400
Fix key reauth with parallel sessions
diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md
index 3f50b7bd4..248dacbff 100644
--- a/packages/ai/CHANGELOG.md
+++ b/packages/ai/CHANGELOG.md
@@ -5,6 +5,10 @@
- Removed `coerceNullStrings` function and its automatic null-string coercion behavior from JSON parsing
+### Fixed
+
+- Fixed concurrent OAuth refresh token rotation race: when multiple instances share the same credential DB, one instance refreshing a token no longer causes other instances to permanently disable the credential on `invalid_grant` ([#607](https://github.com/can1357/oh-my-pi/issues/607))
+
## [13.19.0] - 2026-04-05
### Fixed
diff --git a/packages/ai/src/auth-storage.ts b/packages/ai/src/auth-storage.ts
index 9fdb4473b..fc81a6f3b 100644
--- a/packages/ai/src/auth-storage.ts
+++ b/packages/ai/src/auth-storage.ts
@@ -1865,7 +1865,35 @@ export class AuthStorage {
});
if (isDefinitiveFailure) {
- // Permanently disable invalid credentials with an explicit cause for inspection/debugging
+ // Before permanently disabling, check if another instance already refreshed
+ // the token in the shared DB. Concurrent instances hold stale in-memory
+ // copies; refresh token rotation by one instance invalidates the token
+ // that other instances still hold. Re-read from DB before giving up.
+ if (/invalid_grant/i.test(errorMsg)) {
+ const entries = this.#getStoredCredentials(provider);
+ const entry = entries[selection.index];
+ if (entry) {
+ const dbCredentials = this.#store.listAuthCredentials(provider);
+ const dbEntry = dbCredentials.find(row => row.id === entry.id);
+ if (
+ dbEntry?.credential.type === "oauth" &&
+ dbEntry.credential.refresh !== selection.credential.refresh
+ ) {
+ // DB has a newer refresh token — another instance refreshed.
+ // Update in-memory state and retry with the fresh credential.
+ logger.warn("Concurrent refresh detected, syncing from DB and retrying", {
+ provider,
+ index: selection.index,
+ rowId: entry.id,
+ });
+ const updated = [...entries];
+ updated[selection.index] = { id: entry.id, credential: dbEntry.credential };
+ this.#setStoredCredentials(provider, updated);
+ return this.getApiKey(provider, sessionId, options);
+ }
+ }
+ }
+ // Genuinely invalid — disable the credential
this.#disableCredentialAt(provider, selection.index, `oauth refresh failed: ${errorMsg}`);
if (this.#getCredentialsForProvider(provider).some(credential => credential.type === "oauth")) {
return this.getApiKey(provider, sessionId, options);
diff --git a/packages/ai/test/auth-storage-refresh-race.test.ts b/packages/ai/test/auth-storage-refresh-race.test.ts
new file mode 100644
index 000000000..5a8098a18
--- /dev/null
+++ b/packages/ai/test/auth-storage-refresh-race.test.ts
@@ -0,0 +1,230 @@
+import { Database } from "bun:sqlite";
+import { afterEach, beforeEach, describe, expect, test, vi } from "bun:test";
+import * as fs from "node:fs/promises";
+import * as os from "node:os";
+import * as path from "node:path";
+import { AuthCredentialStore, AuthStorage, type OAuthCredential } from "../src/auth-storage";
+import * as oauthUtils from "../src/utils/oauth";
+import type { OAuthCredentials } from "../src/utils/oauth/types";
+
+/**
+ * Tests for the concurrent OAuth refresh token rotation race condition.
+ *
+ * When multiple omp instances share the same SQLite credential DB but hold
+ * independent in-memory caches, refresh token rotation by one instance
+ * invalidates the token that other instances still hold. Without the fix,
+ * the stale instance permanently disables the credential on `invalid_grant`
+ * even though a valid refresh token exists in the DB.
+ */
+
+function readDisabledCauses(dbPath: string, provider: string): string[] {
+ const db = new Database(dbPath, { readonly: true });
+ try {
+ const rows = db
+ .prepare(
+ "SELECT disabled_cause FROM auth_credentials WHERE provider = ? AND disabled_cause IS NOT NULL ORDER BY id ASC",
+ )
+ .all(provider) as Array<{ disabled_cause?: string | null }>;
+ return rows.flatMap(row => (typeof row.disabled_cause === "string" ? [row.disabled_cause] : []));
+ } finally {
+ db.close();
+ }
+}
+
+function countActiveCredentials(dbPath: string, provider: string): number {
+ const db = new Database(dbPath, { readonly: true });
+ try {
+ const row = db
+ .prepare("SELECT COUNT(*) AS count FROM auth_credentials WHERE provider = ? AND disabled_cause IS NULL")
+ .get(provider) as { count?: number } | undefined;
+ return row?.count ?? 0;
+ } finally {
+ db.close();
+ }
+}
+
+const EXPIRED_TOKEN_EXPIRES = Date.now() - 60_000;
+const FRESH_TOKEN_EXPIRES = Date.now() + 3_600_000;
+
+function makeOAuthCredential(suffix: string, opts?: { expires?: number }): OAuthCredential {
+ return {
+ type: "oauth",
+ access: `access-${suffix}`,
+ refresh: `refresh-${suffix}`,
+ expires: opts?.expires ?? FRESH_TOKEN_EXPIRES,
+ accountId: "acct-shared",
+ email: "user@example.com",
+ };
+}
+
+describe("AuthStorage concurrent OAuth refresh token rotation race", () => {
+ let tempDir = "";
+ let dbPath = "";
+ let storeA: AuthCredentialStore | null = null;
+ let storeB: AuthCredentialStore | null = null;
+ let authStorageA: AuthStorage | null = null;
+ let authStorageB: AuthStorage | null = null;
+
+ beforeEach(async () => {
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-ai-auth-refresh-race-"));
+ dbPath = path.join(tempDir, "agent.db");
+
+ // Both stores point at the same SQLite DB — simulates two omp instances
+ storeA = await AuthCredentialStore.open(dbPath);
+ storeB = await AuthCredentialStore.open(dbPath);
+ });
+
+ afterEach(async () => {
+ vi.restoreAllMocks();
+ storeA?.close();
+ storeB?.close();
+ storeA = null;
+ storeB = null;
+ authStorageA = null;
+ authStorageB = null;
+ if (tempDir) {
+ await fs.rm(tempDir, { recursive: true, force: true });
+ tempDir = "";
+ }
+ });
+
+ test("instance B recovers from invalid_grant when instance A already refreshed", async () => {
+ if (!storeA || !storeB) throw new Error("test setup failed");
+
+ // Store an expired OAuth credential — both instances will need to refresh
+ const staleCredential = makeOAuthCredential("v1", { expires: EXPIRED_TOKEN_EXPIRES });
+ authStorageA = new AuthStorage(storeA);
+ await authStorageA.set("anthropic", [staleCredential]);
+
+ // Instance B loads same credential from DB into its own in-memory cache
+ authStorageB = new AuthStorage(storeB);
+ await authStorageB.reload();
+
+ // The refreshed credential that instance A will produce
+ const refreshedCredential: OAuthCredentials = {
+ access: "access-v2",
+ refresh: "refresh-v2",
+ expires: FRESH_TOKEN_EXPIRES,
+ accountId: "acct-shared",
+ email: "user@example.com",
+ };
+
+ // Mock both refresh paths:
+ // - refreshOAuthToken: called by pre-refresh in #resolveOAuthApiKey for expired tokens
+ // - getOAuthApiKey: called by #tryOAuthCredential for the actual token exchange
+ const refreshSpy = vi.spyOn(oauthUtils, "refreshOAuthToken");
+ const getApiKeySpy = vi.spyOn(oauthUtils, "getOAuthApiKey");
+
+ // Step 1: Instance A refreshes successfully
+ refreshSpy.mockImplementation(async (_provider, credential) => {
+ return { ...credential, ...refreshedCredential };
+ });
+ getApiKeySpy.mockImplementation(async (_provider, credentials) => {
+ const cred = credentials.anthropic as OAuthCredentials | undefined;
+ if (!cred) return null;
+ return { newCredentials: refreshedCredential, apiKey: "sk-ant-new-key" };
+ });
+
+ const keyA = await authStorageA.getApiKey("anthropic", "session-a");
+ expect(keyA).toBe("sk-ant-new-key");
+
+ // DB now has refreshed credential from instance A.
+ // Instance B still has stale refresh-v1 in memory.
+
+ // Step 2: Instance B tries to refresh with stale token.
+ // The pre-refresh (refreshOAuthToken) runs first, then #tryOAuthCredential calls getOAuthApiKey.
+ // Both throw invalid_grant for stale tokens. After DB re-read, retry with fresh token succeeds.
+ let getApiKeyCallCount = 0;
+ refreshSpy.mockImplementation(async (_provider, credential) => {
+ if (credential.refresh === "refresh-v1") {
+ throw new Error("invalid_grant: Refresh token not found or invalid");
+ }
+ return { ...credential, ...refreshedCredential };
+ });
+ getApiKeySpy.mockImplementation(async (_provider, credentials) => {
+ const cred = credentials.anthropic as OAuthCredentials | undefined;
+ if (!cred) return null;
+ getApiKeyCallCount++;
+
+ if (cred.refresh === "refresh-v1") {
+ // Stale token — Anthropic would return invalid_grant
+ throw new Error("invalid_grant: Refresh token not found or invalid");
+ }
+ if (cred.refresh === "refresh-v2") {
+ // Fresh token from instance A — success
+ return { newCredentials: refreshedCredential, apiKey: "sk-ant-new-key-b" };
+ }
+ return null;
+ });
+
+ const keyB = await authStorageB.getApiKey("anthropic", "session-b");
+
+ // The credential must NOT be disabled — instance B should have recovered
+ expect(keyB).toBeDefined();
+ expect(typeof keyB).toBe("string");
+
+ // DB should still have exactly 1 active credential, none disabled
+ expect(countActiveCredentials(dbPath, "anthropic")).toBe(1);
+ expect(readDisabledCauses(dbPath, "anthropic")).toEqual([]);
+ // The getOAuthApiKey mock must have been called at least twice: once with stale, once with fresh
+ expect(getApiKeyCallCount).toBeGreaterThanOrEqual(2);
+ });
+
+ test("credential is still disabled when DB has same stale token (genuine failure)", async () => {
+ if (!storeA || !storeB) throw new Error("test setup failed");
+
+ // Store an expired credential — only one instance, no concurrent refresh
+ const staleCredential = makeOAuthCredential("v1", { expires: EXPIRED_TOKEN_EXPIRES });
+ authStorageA = new AuthStorage(storeA);
+ await authStorageA.set("anthropic", [staleCredential]);
+
+ // Mock: always fail with invalid_grant (genuinely revoked token)
+ vi.spyOn(oauthUtils, "refreshOAuthToken").mockImplementation(async () => {
+ throw new Error("invalid_grant: Refresh token not found or invalid");
+ });
+ vi.spyOn(oauthUtils, "getOAuthApiKey").mockImplementation(async () => {
+ throw new Error("invalid_grant: Refresh token not found or invalid");
+ });
+
+ const key = await authStorageA.getApiKey("anthropic", "session-a");
+
+ // Should return undefined — no valid credentials
+ expect(key).toBeUndefined();
+
+ // Credential should be disabled in DB
+ const causes = readDisabledCauses(dbPath, "anthropic");
+ expect(causes.length).toBe(1);
+ expect(causes[0]).toContain("invalid_grant");
+ });
+
+ test("terminates when DB token matches stale token (no concurrent refresh)", async () => {
+ if (!storeA || !storeB) throw new Error("test setup failed");
+
+ // Both instances share the same stale credential — nobody refreshed
+ const staleCredential = makeOAuthCredential("v1", { expires: EXPIRED_TOKEN_EXPIRES });
+ authStorageA = new AuthStorage(storeA);
+ await authStorageA.set("anthropic", [staleCredential]);
+
+ // Instance B loads same stale credential
+ authStorageB = new AuthStorage(storeB);
+ await authStorageB.reload();
+
+ // Mock: always fail — the token is genuinely revoked, nobody refreshed
+ vi.spyOn(oauthUtils, "refreshOAuthToken").mockImplementation(async () => {
+ throw new Error("invalid_grant: Refresh token not found or invalid");
+ });
+ vi.spyOn(oauthUtils, "getOAuthApiKey").mockImplementation(async () => {
+ throw new Error("invalid_grant: Refresh token not found or invalid");
+ });
+
+ // Instance B fails. DB has the same token (nobody refreshed), so the
+ // fix correctly falls through to disable instead of retrying forever.
+ const keyB = await authStorageB.getApiKey("anthropic", "session-b");
+ expect(keyB).toBeUndefined();
+
+ // Credential should be disabled — the fix did not prevent a genuine failure
+ const causes = readDisabledCauses(dbPath, "anthropic");
+ expect(causes.length).toBe(1);
+ expect(causes[0]).toContain("invalid_grant");
+ });
+});

View File

@@ -100,6 +100,17 @@
# kernel options
boot = {
# cachyos kernel: bore scheduler, full lto, x86_64-v3 (common to zen 3 + zen 5)
kernelPackages =
let
helpers = pkgs.callPackage "${inputs.nix-cachyos-kernel}/helpers.nix" { };
kernel = pkgs.cachyosKernels.linux-cachyos-bore-lto.override {
lto = "full";
processorOpt = "x86_64-v3";
};
in
helpers.kernelModuleLLVMOverride (pkgs.linuxKernel.packagesFor kernel);
# disable legacy subsystems neither host will ever use
kernelPatches = [
{
@@ -154,7 +165,7 @@
# staging drivers (experimental/unmaintained)
STAGING = lib.mkForce no;
SND_PCI = lib.mkForce no;
# SND_PCI stays — SND_HDA_INTEL (AMD HDA audio) lives under it
ACCESSIBILITY = lib.mkForce no;
MTD = lib.mkForce no;
MEDIA_RC_SUPPORT = lib.mkForce no;
@@ -169,7 +180,7 @@
PPDEV = lib.mkForce no;
PHANTOM = lib.mkForce no;
X86_ANDROID_TABLETS = lib.mkForce no;
CHROME_PLATFORMS = lib.mkForce no;
# CHROME_PLATFORMS stays — Framework laptops use CrOS EC
SURFACE_PLATFORMS = lib.mkForce no;
MCTP = lib.mkForce no;
GPIB = lib.mkForce no;
@@ -266,6 +277,9 @@
lib.filter (m: m != "aes_generic") options.boot.initrd.luks.cryptoModules.default
);
# some default initrd modules (ata_piix etc) don't exist with ATA_SFF=n
initrd.allowMissingModules = true;
lanzaboote = {
enable = true;
# TODO: proper secrets management so this is not stored in nix store

View File

@@ -1,44 +0,0 @@
# Pull-based NixOS updates for hosts that can't be pushed to reliably.
# CI builds the system closure on muffin (which Harmonia serves), then
# records the output store path at /deploy/<hostname>. On boot this
# service fetches that path, pulls the closure from the binary cache,
# and activates it.
{ pkgs, hostname, ... }:
let
deploy-url = "https://nix-cache.sigkill.computer/deploy/${hostname}";
pull-update = pkgs.writeShellScript "pull-update" ''
set -euo pipefail
STORE_PATH=$(${pkgs.lib.getExe pkgs.curl} -sf --max-time 30 "${deploy-url}" || true)
if [ -z "$STORE_PATH" ]; then
echo "Server unreachable or no deployment available, skipping"
exit 0
fi
CURRENT=$(readlink -f /nix/var/nix/profiles/system)
if [ "$CURRENT" = "$STORE_PATH" ]; then
echo "Already on latest configuration"
exit 0
fi
echo "Pulling update: $CURRENT -> $STORE_PATH"
nix-store -r "$STORE_PATH"
nix-env -p /nix/var/nix/profiles/system --set "$STORE_PATH"
"$STORE_PATH/bin/switch-to-configuration" switch
echo "Update applied"
'';
in
{
systemd.services.pull-update = {
description = "Pull latest NixOS configuration from binary cache";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = pull-update;
};
};
}

View File

@@ -14,17 +14,6 @@
inputs.nixos-hardware.nixosModules.framework-amd-ai-300-series
];
# cachyos kernel: bore scheduler, full lto, zen 4 (amd ai 340)
boot.kernelPackages =
let
helpers = pkgs.callPackage "${inputs.nix-cachyos-kernel}/helpers.nix" { };
kernel = pkgs.cachyosKernels.linux-cachyos-bore-lto.override {
lto = "full";
processorOpt = "zen4";
};
in
helpers.kernelModuleLLVMOverride (pkgs.linuxKernel.packagesFor kernel);
hardware.framework.laptop13.audioEnhancement.rawDeviceName =
lib.mkDefault "alsa_output.pci-0000_c1_00.6.analog-stereo";

View File

@@ -11,7 +11,6 @@
./disk_yarn.nix
./common.nix
./impermanence.nix
./pull-update.nix
./no-rgb.nix
./vr.nix
@@ -19,22 +18,6 @@
inputs.jovian-nixos.nixosModules.default
];
# cachyos kernel: bore scheduler, full lto, zen 3 (5800x)
boot.kernelPackages =
let
helpers = pkgs.callPackage "${inputs.nix-cachyos-kernel}/helpers.nix" { };
kernel = pkgs.cachyosKernels.linux-cachyos-bore-lto.override {
lto = "full";
processorOpt = "x86_64-v3";
structuredExtraConfig = with lib.kernel; {
# x86_64-v3 is the ISA level; pin to zen 3 for microarch tuning
GENERIC_CPU = lib.mkForce no;
MZEN3 = lib.mkForce yes;
};
};
in
helpers.kernelModuleLLVMOverride (pkgs.linuxKernel.packagesFor kernel);
fileSystems."/media/games" = {
device = "/dev/disk/by-uuid/1878136e-765d-4784-b204-3536ab4fdac8";
fsType = "f2fs";
@@ -92,12 +75,54 @@
# LACT (Linux AMDGPU Configuration Tool): https://github.com/ilya-zlobintsev/LACT
environment.systemPackages = with pkgs; [
lact
jovian-stubs
];
systemd.packages = with pkgs; [ lact ];
systemd.services.lactd.wantedBy = [ "multi-user.target" ];
systemd.services.lactd.serviceConfig.ExecStartPre = "${lib.getExe pkgs.bash} -c \"sleep 3s\"";
# root-level service that applies a pending update. Triggered by
# steamos-update (via systemctl start) when the user accepts an update.
# Runs as root so it can write the system profile and boot entry.
systemd.services.pull-update-apply = {
description = "Apply pending NixOS update pulled from binary cache";
serviceConfig = {
Type = "oneshot";
ExecStart = pkgs.writeShellScript "pull-update-apply" ''
set -uo pipefail
export PATH=${
pkgs.lib.makeBinPath [
pkgs.curl
pkgs.coreutils
pkgs.nix
]
}
STORE_PATH=$(curl -sf --max-time 30 "https://nix-cache.sigkill.computer/deploy/yarn" || true)
if [ -z "$STORE_PATH" ]; then
echo "server unreachable"
exit 1
fi
echo "applying $STORE_PATH"
nix-store -r "$STORE_PATH" || { echo "fetch failed"; exit 1; }
nix-env -p /nix/var/nix/profiles/system --set "$STORE_PATH" || { echo "profile set failed"; exit 1; }
"$STORE_PATH/bin/switch-to-configuration" boot || { echo "boot entry failed"; exit 1; }
echo "update applied; reboot required"
'';
};
};
# Allow primary user to start pull-update-apply.service without a password
security.polkit.extraConfig = ''
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
action.lookup("unit") == "pull-update-apply.service" &&
subject.user == "${username}") {
return polkit.Result.YES;
}
});
'';
nixpkgs.config.allowUnfreePredicate =
pkg:
builtins.elem (lib.getName pkg) [
@@ -113,19 +138,69 @@
# This prevents Steam from requesting reboots for "system updates"
# Steam client updates will still work normally
nixpkgs.overlays = [
(final: prev: {
(
final: prev:
let
deploy-url = "https://nix-cache.sigkill.computer/deploy/yarn";
steamos-update-script = final.writeShellScript "steamos-update" ''
export PATH=${
final.lib.makeBinPath [
final.curl
final.coreutils
final.systemd
]
}
STORE_PATH=$(curl -sf --max-time 30 "${deploy-url}" || true)
if [ -z "$STORE_PATH" ]; then
>&2 echo "[steamos-update] server unreachable"
exit 7
fi
CURRENT=$(readlink -f /nix/var/nix/profiles/system)
if [ "$CURRENT" = "$STORE_PATH" ]; then
>&2 echo "[steamos-update] no update available"
exit 0
fi
# check-only mode: just report that an update exists
if [ "''${1:-}" = "check" ] || [ "''${1:-}" = "--check-only" ]; then
>&2 echo "[steamos-update] update available"
exit 0
fi
# apply: trigger the root-running systemd service to install the update
>&2 echo "[steamos-update] applying update..."
if systemctl start --wait pull-update-apply.service; then
>&2 echo "[steamos-update] update installed, reboot to apply"
exit 0
else
>&2 echo "[steamos-update] apply failed; see 'journalctl -u pull-update-apply'"
exit 1
fi
'';
in
{
jovian-stubs = prev.stdenv.mkDerivation {
name = "jovian-stubs-no-update";
name = "jovian-stubs";
dontUnpack = true;
installPhase = ''
mkdir -p $out/bin
ln -s ${steamos-update-script} $out/bin/steamos-update
ln -s ${steamos-update-script} $out/bin/steamos-mandatory-update
# steamos-update: always report "no update available" (exit 7)
# This disables the kernel mismatch check that triggers reboot prompts
cat > $out/bin/steamos-update << 'STUB'
# jupiter-initial-firmware-update: no-op (not a real steam deck)
cat > $out/bin/jupiter-initial-firmware-update << 'STUB'
#!/bin/sh
>&2 echo "[JOVIAN] $0: stub called with: $* (system updates disabled)"
exit 7
exit 0
STUB
# jupiter-biosupdate: no-op (not a real steam deck)
cat > $out/bin/jupiter-biosupdate << 'STUB'
#!/bin/sh
exit 0
STUB
# steamos-reboot: reboot the system
@@ -162,16 +237,24 @@
exec /run/wrappers/bin/pkexec "$@"
STUB
# sudo: pass through to doas
# sudo: strip flags and run the command directly (no escalation).
# privileged ops are delegated to root systemd services via systemctl.
cat > $out/bin/sudo << 'STUB'
#!/bin/sh
exec /run/wrappers/bin/doas "$@"
while [ $# -gt 0 ]; do
case "$1" in
-*) shift ;;
*) break ;;
esac
done
exec "$@"
STUB
chmod 755 $out/bin/*
find $out/bin -type f -exec chmod 755 {} +
'';
};
})
}
)
];
jovian = {