Compare commits

..

18 Commits

Author SHA1 Message Date
Joshua Yuen 5364812171
Configure pipewire so that hotplugging into an external display/speaker is recognized properly 2026-10-06 20:21:22 -04:00
Joshua Yuen eb480a205c
Automate Battle.net client tweaks via home activation 2026-10-06 19:42:36 -04:00
Joshua Yuen 79ba19161f
Add Battle.net module enabling Lutris, wine, and winetricks 2026-10-06 19:31:32 -04:00
Joshua Yuen 0a34d3f062
Add gaming profile and make host profile selection explicit 2026-10-06 19:29:25 -04:00
Joshua Yuen 2efa87f90c
Add steam module 2026-09-28 20:42:59 -04:00
Joshua Yuen dfbc3540e6
Add resolved when tailscale is enabled so that DNS works properly 2026-09-28 13:25:14 -04:00
Joshua Yuen 1af0956351
Add nix-ld and uv for Python development 2026-09-13 00:57:16 -04:00
Joshua Yuen 5a08f180b2
Fix opencode permission event name for 1.17.x 2026-09-12 22:58:46 -04:00
Joshua Yuen 1cd43eb487
Theme Firefox with stylix firefox-gnome-theme 2026-09-12 22:47:41 -04:00
Joshua Yuen f599d74e3f
Use custom Blueish base16 scheme with blue-gray base02 2026-09-12 22:47:28 -04:00
Joshua Yuen 60b6b80fc3
Use string-form mkEnableOption for all module options 2026-09-12 21:48:48 -04:00
Joshua Yuen 60fe16d168
Use mkOption for enabled-by-default module options 2026-09-12 21:44:50 -04:00
Joshua Yuen 116e4eff7d
Add host-configurable opencode module with notifications 2026-09-12 21:40:25 -04:00
Joshua Yuen 4e91caa76a
Reduce Hyprland gaps and theme borders via stylix 2026-09-12 21:06:29 -04:00
Joshua Yuen 87c7040239
Add screenshot tool with SUPER+SHIFT+S bind 2026-09-12 20:45:33 -04:00
Joshua Yuen 3db6ad962e
Add function volume and brightness key controls 2026-09-12 13:04:34 -04:00
Joshua Yuen 72d733ba7d
Format Nix files with nixfmt 2026-09-12 12:46:47 -04:00
Joshua Yuen 0efda59204
Add hold-to-talk voice dictation 2026-09-12 12:46:30 -04:00
57 changed files with 705 additions and 476 deletions

View File

@ -1,47 +1,5 @@
{ {
"nodes": { "nodes": {
"ags": {
"inputs": {
"astal": "astal",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1775689345,
"narHash": "sha256-tM3s7CX+tgxlYW0Sk3nzVThg2MHn08foIuMxABupxIs=",
"owner": "aylur",
"repo": "ags",
"rev": "bbee2f18939f1ec7ff720e717cf305e73635628f",
"type": "github"
},
"original": {
"owner": "aylur",
"repo": "ags",
"type": "github"
}
},
"astal": {
"inputs": {
"nixpkgs": [
"ags",
"nixpkgs"
]
},
"locked": {
"lastModified": 1773914523,
"narHash": "sha256-GOL+bR30FPImAzy4NNsTMY1gpoINMsLTXR0WJBRSq30=",
"owner": "aylur",
"repo": "astal",
"rev": "41b50290c6a1cdce7b482897c22fe49286912b9a",
"type": "github"
},
"original": {
"owner": "aylur",
"repo": "astal",
"type": "github"
}
},
"base16": { "base16": {
"inputs": { "inputs": {
"fromYaml": "fromYaml" "fromYaml": "fromYaml"
@ -302,7 +260,6 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"ags": "ags",
"elephant": "elephant", "elephant": "elephant",
"home-manager": "home-manager", "home-manager": "home-manager",
"install-system": "install-system", "install-system": "install-system",

View File

@ -18,11 +18,6 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
ags = {
url = "github:aylur/ags";
inputs.nixpkgs.follows = "nixpkgs";
};
elephant = { elephant = {
url = "github:abenz1267/elephant/master"; url = "github:abenz1267/elephant/master";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
@ -53,7 +48,6 @@
let let
system = "x86_64-linux"; system = "x86_64-linux";
username = "josh"; username = "josh";
profile = "desktop";
homePath = ./. + "/users/${username}/home.nix"; homePath = ./. + "/users/${username}/home.nix";
pkgs = import nixpkgs { pkgs = import nixpkgs {
@ -65,6 +59,7 @@
mkHost = mkHost =
{ {
host, host,
profile,
extraModules ? [ ], extraModules ? [ ],
}: }:
nixpkgs.lib.nixosSystem { nixpkgs.lib.nixosSystem {
@ -84,12 +79,19 @@
in in
{ {
nixosConfigurations = { nixosConfigurations = {
laptop = mkHost { host = "laptop"; }; laptop = mkHost {
host = "laptop";
profile = "gaming";
};
vm = mkHost { host = "vm"; }; vm = mkHost {
host = "vm";
profile = "desktop";
};
live = mkHost { live = mkHost {
host = "installation-iso"; host = "installation-iso";
profile = "desktop";
extraModules = [ extraModules = [
(nixpkgs + "/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix") (nixpkgs + "/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix")
{ environment.systemPackages = [ install-system.packages.${system}.default ]; } { environment.systemPackages = [ install-system.packages.${system}.default ]; }
@ -99,7 +101,8 @@
homeConfigurations.${username} = home-manager.lib.homeManagerConfiguration { homeConfigurations.${username} = home-manager.lib.homeManagerConfiguration {
extraSpecialArgs = { extraSpecialArgs = {
inherit inputs profile username; inherit inputs username;
profile = "desktop";
}; };
inherit pkgs; inherit pkgs;
modules = [ modules = [

View File

@ -1,9 +1,10 @@
{ {
profile,
... ...
}: }:
{ {
imports = [ imports = [
../../profiles/desktop ../../profiles/${profile}
../../modules/nixos ../../modules/nixos
../../modules/drivers ../../modules/drivers

View File

@ -1,9 +1,10 @@
{ {
profile,
... ...
}: }:
{ {
imports = [ imports = [
../../profiles/desktop ../../profiles/${profile}
../../modules/nixos ../../modules/nixos
../../modules/drivers ../../modules/drivers
@ -15,6 +16,9 @@
modules.gpg.signingKey = "0x530DA0331EFE99F5"; modules.gpg.signingKey = "0x530DA0331EFE99F5";
modules.gnomeBoxes.enable = true; modules.gnomeBoxes.enable = true;
modules.opencode.enable = true;
modules.display.scale = 2; modules.display.scale = 2;
modules.steam.remotePlay.enable = true;
} }

View File

@ -1,32 +1,47 @@
# Do not modify this file! It was generated by ‘nixos-generate-config’ # Do not modify this file! It was generated by ‘nixos-generate-config’
# and may be overwritten by future invocations. Please make changes # and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead. # to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }: {
config,
lib,
pkgs,
modulesPath,
...
}:
{ {
imports = imports = [
[ (modulesPath + "/installer/scan/not-detected.nix") (modulesPath + "/installer/scan/not-detected.nix")
]; ];
boot.initrd.availableKernelModules = [ "xhci_pci" "thunderbolt" "nvme" "usb_storage" "sd_mod" ]; boot.initrd.availableKernelModules = [
"xhci_pci"
"thunderbolt"
"nvme"
"usb_storage"
"sd_mod"
];
boot.initrd.kernelModules = [ ]; boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ]; boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ]; boot.extraModulePackages = [ ];
fileSystems."/" = fileSystems."/" = {
{ device = "/dev/disk/by-uuid/041a9bc5-aae2-44ec-bacf-7f6a08a696f9"; device = "/dev/disk/by-uuid/041a9bc5-aae2-44ec-bacf-7f6a08a696f9";
fsType = "ext4"; fsType = "ext4";
}; };
fileSystems."/boot" = fileSystems."/boot" = {
{ device = "/dev/disk/by-uuid/79D1-97BE"; device = "/dev/disk/by-uuid/79D1-97BE";
fsType = "vfat"; fsType = "vfat";
options = [ "fmask=0077" "dmask=0077" ]; options = [
}; "fmask=0077"
"dmask=0077"
swapDevices =
[ { device = "/dev/disk/by-uuid/82b669f5-d624-4a29-bf18-dd8bc453f537"; }
]; ];
};
swapDevices = [
{ device = "/dev/disk/by-uuid/82b669f5-d624-4a29-bf18-dd8bc453f537"; }
];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;

View File

@ -1,9 +1,10 @@
{ {
profile,
... ...
}: }:
{ {
imports = [ imports = [
../../profiles/desktop ../../profiles/${profile}
../../modules/nixos ../../modules/nixos
../../modules/drivers ../../modules/drivers

View File

@ -1,2 +0,0 @@
node_modules/
@girs/

View File

@ -1,10 +0,0 @@
import app from "ags/gtk4/app"
import style from "./style.scss"
import Bar from "./widget/Bar"
app.start({
css: style,
main() {
app.get_monitors().map(Bar)
},
})

View File

@ -1,57 +0,0 @@
{
config,
lib,
pkgs,
inputs,
...
}:
with lib;
let
cfg = config.modules.ags;
hyprlandCfg = config.modules.hyprland;
astalPackages = with inputs.ags.packages.${pkgs.system}; [
io
astal4
battery
powerprofiles
];
extraPackages = astalPackages ++ [
pkgs.libadwaita
pkgs.libsoup_3
];
myShell = pkgs.stdenv.mkDerivation {
name = "my-shell";
src = ./.;
nativeBuildInputs = with pkgs; [
wrapGAppsHook3
gobject-introspection
inputs.ags.packages.${pkgs.system}.default
];
buildInputs = extraPackages ++ [ pkgs.gjs ];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
mkdir -p $out/share
cp -r * $out/share
ags bundle app.ts $out/bin/my-shell -d "SRC='$out/share'"
runHook postInstall
'';
};
in
{
config = mkIf cfg.enable {
home.packages = [ myShell ];
wayland.windowManager.hyprland.settings.exec-once = mkIf hyprlandCfg.enable [
"my-shell"
];
};
}

View File

@ -1,21 +0,0 @@
declare const SRC: string
declare module "inline:*" {
const content: string
export default content
}
declare module "*.scss" {
const content: string
export default content
}
declare module "*.blp" {
const content: string
export default content
}
declare module "*.css" {
const content: string
export default content
}

View File

@ -1,70 +0,0 @@
{
"nodes": {
"ags": {
"inputs": {
"astal": "astal",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1775689345,
"narHash": "sha256-tM3s7CX+tgxlYW0Sk3nzVThg2MHn08foIuMxABupxIs=",
"owner": "aylur",
"repo": "ags",
"rev": "bbee2f18939f1ec7ff720e717cf305e73635628f",
"type": "github"
},
"original": {
"owner": "aylur",
"repo": "ags",
"type": "github"
}
},
"astal": {
"inputs": {
"nixpkgs": [
"ags",
"nixpkgs"
]
},
"locked": {
"lastModified": 1782062955,
"narHash": "sha256-FGZHls4eQJ8y3pvf5h3b83PfXlve3vD/Gj3g1qoAK6o=",
"owner": "aylur",
"repo": "astal",
"rev": "11842ae3045c1367fb3a62a2302dba0d9ccb4a33",
"type": "github"
},
"original": {
"owner": "aylur",
"repo": "astal",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1782467914,
"narHash": "sha256-pGvFkM8N0xEkIIXDe5YYfbEAvHrk4IxBrjB/x8OomhE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e73de5be04e0eff4190a1432b946d469c794e7b4",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"ags": "ags",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,74 +0,0 @@
{
description = "My Awesome Desktop Shell";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
ags = {
url = "github:aylur/ags";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
self,
nixpkgs,
ags,
}: let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
pname = "my-shell";
entry = "app.ts";
astalPackages = with ags.packages.${system}; [
io
astal4 # or astal3 for gtk3
battery
powerprofiles
# notifd tray wireplumber
];
extraPackages =
astalPackages
++ [
pkgs.libadwaita
pkgs.libsoup_3
];
in {
packages.${system} = {
default = pkgs.stdenv.mkDerivation {
name = pname;
src = ./.;
nativeBuildInputs = with pkgs; [
wrapGAppsHook3
gobject-introspection
ags.packages.${system}.default
];
buildInputs = extraPackages ++ [pkgs.gjs];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
mkdir -p $out/share
cp -r * $out/share
ags bundle ${entry} $out/bin/${pname} -d "SRC='$out/share'"
runHook postInstall
'';
};
};
devShells.${system} = {
default = pkgs.mkShell {
buildInputs = [
(ags.packages.${system}.default.override {
inherit extraPackages;
})
];
};
};
};
}

View File

@ -1,10 +0,0 @@
{
"dependencies": {
"ags": "*",
"gnim": "*"
},
"prettier": {
"semi": false,
"tabWidth": 2
}
}

View File

@ -1,20 +0,0 @@
// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss
$fg-color: #{"@theme_fg_color"};
$bg-color: #{"@theme_bg_color"};
window.Bar {
background: transparent;
color: $fg-color;
font-weight: bold;
> centerbox {
background: $bg-color;
border-radius: 10px;
margin: 8px;
}
button {
border-radius: 8px;
margin: 2px;
}
}

View File

@ -1,14 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"module": "ES2022",
"target": "ES2020",
"lib": ["ES2023"],
"moduleResolution": "Bundler",
// "checkJs": true,
// "allowJs": true,
"jsx": "react-jsx",
"jsxImportSource": "ags/gtk4"
}
}

View File

@ -1,33 +0,0 @@
import app from "ags/gtk4/app"
import { Astal, Gtk, Gdk } from "ags/gtk4"
import { createPoll } from "ags/time"
import BatteryWidget from "./Battery";
export default function Bar(gdkmonitor: Gdk.Monitor) {
const time = createPoll("", 1000, "date '+%a %b %d %l:%M:%S %p'");
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor;
return (
<window
visible
name="bar"
class="Bar"
gdkmonitor={gdkmonitor}
exclusivity={Astal.Exclusivity.EXCLUSIVE}
anchor={TOP | LEFT | RIGHT}
application={app}
>
<centerbox cssName="centerbox">
<menubutton $type="center" hexpand halign={Gtk.Align.CENTER}>
<label label={time} />
<popover>
<Gtk.Calendar />
</popover>
</menubutton>
<box $type="end">
<BatteryWidget />
</box>
</centerbox>
</window>
)
}

View File

@ -1,36 +0,0 @@
import AstalBattery from "gi://AstalBattery";
import AstalPowerProfiles from "gi://AstalPowerProfiles";
import { createBinding } from "ags";
import { Gtk } from "ags/gtk4";
export default function BatteryWidget() {
const battery = AstalBattery.get_default()
const powerprofiles = AstalPowerProfiles.get_default()
const percent = createBinding(
battery,
"percentage",
)((p) => `${Math.floor(p * 100)}%`)
const setProfile = (profile: string) => {
powerprofiles.set_active_profile(profile)
}
return (
<menubutton visible={createBinding(battery, "isPresent")}>
<box>
<image iconName={createBinding(battery, "iconName")} />
<label label={percent} />
</box>
<popover>
<box orientation={Gtk.Orientation.VERTICAL}>
{powerprofiles.get_profiles().map(({ profile }) => (
<button onClicked={() => setProfile(profile)}>
<label label={profile} xalign={0} />
</button>
))}
</box>
</popover>
</menubutton>
)
}

View File

@ -0,0 +1,48 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.modules.battlenet;
yq = "${pkgs.yq}/bin/yq";
jq = "${pkgs.jq}/bin/jq";
in
{
config = mkIf cfg.enable {
# Battle.net is installed manually through Lutris, which creates the game
# config and wine prefix outside of Home Manager's control. The Lutris
# installer script writes HardwareAcceleration "false" (software rendering,
# which breaks Battle.net's CEF UI repainting under XWayland), so patch it
# and the game env after every rebuild. Re-run the rebuild after installing
# Battle.net for these tweaks to land.
home.activation.battlenetClientTweaks = hm.dag.entryAfter [ "writeBoundary" ] ''
shopt -s nullglob
for game in "$HOME"/.local/share/lutris/games/battlenet*.yml; do
prefix=$(${yq} -r '.game.prefix // ""' "$game" 2>/dev/null) || continue
for configFile in "$prefix"/drive_c/users/*/AppData/Roaming/Battle.net/Battle.net.config; do
[ -e "$configFile" ] || continue
if [ "$(${jq} -r '.Client.HardwareAcceleration // ""' "$configFile" 2>/dev/null)" != "true" ]; then
${jq} '.Client.HardwareAcceleration = "true"' "$configFile" > "$configFile.tmp" && mv "$configFile.tmp" "$configFile"
fi
done
# GE-Proton's Wayland driver breaks Battle.net's CEF window sizing and
# painting, and Xalia misrenders the client, so pin both off.
if [ "$(${yq} -r '.system.env.PROTON_USE_XALIA // ""' "$game" 2>/dev/null)" != "0" ]; then
${yq} --in-place --yaml-output '.system.env.PROTON_USE_XALIA = "0"' "$game"
fi
if [ "$(${yq} -r '.system.env.WINE_SIMULATE_WRITECOPY // ""' "$game" 2>/dev/null)" != "1" ]; then
${yq} --in-place --yaml-output '.system.env.WINE_SIMULATE_WRITECOPY = "1"' "$game"
fi
if [ -n "$(${yq} -r '.system.env.PROTON_ENABLE_WAYLAND // ""' "$game" 2>/dev/null)" ]; then
${yq} --in-place --yaml-output 'del(.system.env.PROTON_ENABLE_WAYLAND)' "$game"
fi
done
shopt -u nullglob
'';
};
}

View File

@ -3,14 +3,17 @@
}: }:
{ {
imports = [ imports = [
./ags ./battlenet.nix
./direnv.nix ./direnv.nix
./discord.nix ./discord.nix
./dictation.nix
./firefox.nix ./firefox.nix
./gpg.nix ./gpg.nix
./fzf.nix ./fzf.nix
./hyprland ./hyprland
./kitty.nix ./kitty.nix
./opencode
./screenshot.nix
./stylix.nix ./stylix.nix
./vscodium.nix ./vscodium.nix
./walker.nix ./walker.nix

128
modules/home/dictation.nix Normal file
View File

@ -0,0 +1,128 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.modules.dictation;
hyprlandCfg = config.modules.hyprland;
models = {
"tiny.en" = pkgs.fetchurl {
url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en.bin";
hash = "sha256-kh5M+Ghv3Zk9zQgaXaW2w2W/3hFi5ysI11rHUomSCx8=";
};
"base.en" = pkgs.fetchurl {
url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin";
hash = "sha256-oDd5yG3zMjB19eeWyyzlAp8A7Ihp7uP9+4l6/jbG0AI=";
};
"small.en" = pkgs.fetchurl {
url = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.en.bin";
hash = "sha256-xhONbVjsyDIgl+D5h8MvG+i7ChhTKj+I9zTRu/nEHl0=";
};
};
runtimeDeps = with pkgs; [
coreutils
gnused
libnotify
pipewire
whisper-cpp
wtype
];
dictation = pkgs.writeShellScriptBin "dictation" ''
PATH=${lib.makeBinPath runtimeDeps}:''${PATH:-}
set -euo pipefail
model="${models.${cfg.model}}"
dir="''${XDG_RUNTIME_DIR:-/tmp}/dictation"
mkdir -p "$dir"
pidfile="$dir/pid"
wav="$dir/clip.wav"
notify() {
notify-send -t 2500 "Dictation" "$1" 2>/dev/null || true
}
case "''${1:-}" in
start)
if [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then
exit 0
fi
rm -f "$pidfile" "$wav"
notify "Listening..."
pw-record --rate 16000 --channels 1 --format s16 "$wav" >/dev/null 2>&1 &
pid=$!
echo "$pid" > "$pidfile"
disown
sleep 0.3
if ! kill -0 "$pid" 2>/dev/null; then
rm -f "$pidfile"
notify "Microphone unavailable"
fi
;;
stop)
[ -f "$pidfile" ] || exit 0
pid="$(cat "$pidfile")"
rm -f "$pidfile"
if kill -0 "$pid" 2>/dev/null; then
kill -INT "$pid" 2>/dev/null || true
for _ in $(seq 1 40); do
kill -0 "$pid" 2>/dev/null || break
sleep 0.05
done
kill -0 "$pid" 2>/dev/null && kill -KILL "$pid" 2>/dev/null || true
fi
sleep 0.1
if [ ! -f "$wav" ]; then
notify "Recording missing"
exit 0
fi
# Ignore clips shorter than 0.5s (16 kHz, s16, mono: 32000 B/s + 44 B header)
size="$(stat -c %s "$wav" 2>/dev/null || echo 0)"
if [ "$size" -lt 16044 ]; then
rm -f "$wav"
exit 0
fi
if ! text="$(whisper-cli -np -nt -t "$(nproc)" -m "$model" -f "$wav" 2>/dev/null \
| sed -e 's/([^)]*)//g' -e 's/\[[^]]*\]//g' \
| tr '\n' ' ' \
| sed -e 's/ */ /g' -e 's/^ *//' -e 's/ *$//')"; then
rm -f "$wav"
notify "Transcription failed"
exit 1
fi
rm -f "$wav"
if [ -z "$text" ]; then
notify "No speech detected"
exit 0
fi
wtype -- "$text" || notify "Could not type transcription"
;;
*)
echo "usage: dictation {start|stop}" >&2
exit 1
;;
esac
'';
in
{
config = mkIf cfg.enable {
home.packages = [ dictation ];
wayland.windowManager.hyprland = mkIf hyprlandCfg.enable {
settings = {
bind = [ "SUPER, T, exec, dictation start" ];
bindr = [ "SUPER, T, exec, dictation stop" ];
};
};
};
}

View File

@ -51,6 +51,23 @@ in
"$mod SHIFT, 5, movetoworkspace, 5" "$mod SHIFT, 5, movetoworkspace, 5"
]; ];
# Repeatable binds (hold key to keep changing)
bindel = [
# Volume
", XF86AudioRaiseVolume, exec, wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+"
", XF86AudioLowerVolume, exec, wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%-"
# Brightness
", XF86MonBrightnessUp, exec, brightnessctl set +5%"
", XF86MonBrightnessDown, exec, brightnessctl set 5%-"
];
# Binds that also work while screen is locked
bindl = [
", XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"
", XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"
];
bindm = [ bindm = [
"$mod, mouse:272, movewindow" "$mod, mouse:272, movewindow"
"$mod, mouse:273, resizewindow" "$mod, mouse:273, resizewindow"

View File

@ -10,6 +10,10 @@ let
displayCfg = config.modules.display; displayCfg = config.modules.display;
in in
{ {
home.packages = with pkgs; [
brightnessctl
];
wayland.windowManager.hyprland = { wayland.windowManager.hyprland = {
enable = cfg.enable; enable = cfg.enable;
package = pkgs.hyprland; package = pkgs.hyprland;
@ -18,11 +22,9 @@ in
monitor = cfg.monitor; monitor = cfg.monitor;
general = { general = {
gaps_in = 5; gaps_in = 3;
gaps_out = 20; gaps_out = 8;
border_size = 2; border_size = 2;
"col.active_border" = mkForce "rgba(33ccffee) rgba(00ff99ee) 45deg";
"col.inactive_border" = mkForce "rgba(595959aa)";
layout = "dwindle"; layout = "dwindle";
}; };
@ -42,8 +44,6 @@ in
enabled = true; enabled = true;
range = 4; range = 4;
render_power = 3; render_power = 3;
# Home manager hyprland module sets this for some reason
color = mkForce "rgba(1a1a1aee)";
}; };
}; };

View File

@ -0,0 +1,23 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.modules.opencode;
in
{
config = mkIf cfg.enable {
programs.opencode.enable = true;
home.packages = mkIf cfg.notify [ pkgs.libnotify ];
xdg.configFile."opencode/plugin/notify.ts" = mkIf cfg.notify {
source = ./plugin/notify.ts;
};
home.file.".agents/skills/caveman/SKILL.md".source = ./skills/caveman/SKILL.md;
};
}

View File

@ -0,0 +1,15 @@
export default async ({ $ }) => {
const notify = (summary, body) => {
$`notify-send -a opencode ${summary} ${body}`.quiet().nothrow().catch(() => {})
}
return {
event: async ({ event }) => {
if (event.type === "permission.asked") {
notify("opencode needs input", event.properties.title ?? event.properties.type)
} else if (event.type === "session.idle") {
notify("opencode finished", "Agent is idle")
}
},
}
}

View File

@ -0,0 +1,90 @@
---
name: caveman
description: >
Ultra-compressed communication mode that cuts output tokens while keeping
technical accuracy. Levels: lite, full, ultra and the wenyan variants. Use for
/caveman, "caveman mode", "talk like caveman", "be brief" or "less tokens".
---
Respond terse like smart caveman. All technical substance stay. Only fluff die.
## Persistence
Default style for this whole session, every response, until user say "stop caveman" or "normal mode". Keep terse on long sessions no filler drift.
Default: **full**. Switch: `/caveman lite|full|ultra|wenyan-lite|wenyan-full|wenyan-ultra|off`.
## Rules
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). No tool-call narration, no decorative tables/emoji, no dumping long raw error logs unless asked quote shortest decisive line. Standard well-known tech acronyms OK (DB/API/HTTP); never invent new abbreviations (cfg/impl/req/res/fn) tokenizer split them same as full word: zero token saved, reader still decode. Full word cheaper AND clearer. No causal arrows (→) either own token, save nothing. Technical terms exact. Code blocks unchanged. Errors quoted exact.
Never drop not/never/no/only/except flip meaning worse than any token saved. Numbers, units exact.
Never ADD word to sound caveman. Compression only style never grow output. No inserted pronoun or copula to fake broken grammar: "when it not" cost one token more than "when not" and say same thing. Keep correct verb form when correct form cost same "sees" one token, "see" one token, so mangle buy nothing and read worse. Same rule as abbreviations and arrows: if caveman phrasing not shorter than plain phrasing, use plain.
Clarity register: mix ASD-STE100 Simplified Technical English into caveman, always. One idea per sentence. Sentence short, target 20 words max. Active voice. Present tense where true. One word one meaning: same term for same thing every time, no synonym rotation. Instruction = imperative: "Run X", not "X should be run". Noun cluster 3 words max. Pronoun only with one clear referent, else repeat noun. Caveman cut filler; STE keep what make meaning unambiguous. Conflict between them → clarity win.
Tool calls: fire direct. No preamble, plan, or progress note before or between calls. After result: next call direct or final answer never announce next call. Text before call only to clarify, warn security/irreversible, or resolve ambiguity.
Preserve user's dominant language exactly reply in the language user writes, never switch regardless of example text or multilingual context elsewhere. Compress the style, not the language. Every emitted line in that language openings, pre-tool status lines, all not just final reply. ALWAYS keep technical terms, code, API names, CLI commands, commit-type keywords (feat/fix/...), and exact error strings verbatim unless user explicitly ask for translation.
'Drop articles' = article languages only. Where small markers carry case/role (particles, postpositions), keep them grammar, not filler; compress politeness/filler instead.
Answer directly in this style. Skip "caveman mode on", "me caveman think", "Caveman:" prefix or recap redundant with the reply itself. No normal answer plus caveman duplicate. User ask what mode is → say so plainly.
Pattern: `[thing] [action] [reason]. [next step].`
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
## Intensity
| Level | What change |
|-------|------------|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman. No tool-call narration, no decorative tables/emoji, no long raw error-log dumps unless asked. Standard acronyms OK; no invented abbreviations |
| **ultra** | Strip conjunctions when cause-then-effect stay unambiguous. One word when one word enough. State each fact once. NO prose abbreviations (cfg/impl/req/res/fn/auth), NO arrows (X → Y) measured zero token saving under tokenizer, cost decode clarity. Code symbols, function names, API names, error strings: never touch |
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction chars, not tokens. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
Example "Why React component re-render?"
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
- ultra: "Inline obj prop, new ref, re-render. `useMemo`."
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
- wenyan-full: "每繪新生對象參照,故重繪;以 useMemo 包之則免。"
- wenyan-ultra: "新參照則重繪。useMemo 包之。"
Example "Explain database connection pooling."
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
- ultra: "Pool reuse open DB connections. No per-request handshake."
- wenyan-full: "池蓄已開之連,不逐請而新開,省握手之費。"
- wenyan-ultra: "池蓄連,免逐請新開,省握手。"
Classical chars = wenyan modes only. Never swap a word to a classical char to shrink at non-wenyan levels.
## Auto-Clarity
Drop caveman when:
- Security warnings
- Irreversible action confirmations
- Multi-step sequences where fragment order or omitted conjunctions risk misread
- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` order unclear without articles/conjunctions)
- User asks to clarify or repeats question
Resume caveman after clear part done.
Example shows FORMAT only write warning in session language, not example's.
Example destructive op:
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
> ```sql
> DROP TABLE users;
> ```
> Caveman resume. Verify backup exist first.
## Boundaries
Persisted outside chat: write normal prose code, comments, commits, docs, issue/PR/MR/defect/ticket/bug-report text, memory files, third-party messages (/caveman-compress exempt). "Open a defect" or "file a bug" mean the same as "open issue": body go to other humans, so body normal English. "stop caveman" or "normal mode": revert. Level persist until changed or session end.

View File

@ -0,0 +1,58 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.modules.screenshot;
hyprlandCfg = config.modules.hyprland;
runtimeDeps = with pkgs; [
coreutils
grim
libnotify
slurp
util-linux
wl-clipboard
];
screenshot = pkgs.writeShellScriptBin "screenshot" ''
PATH=${lib.makeBinPath runtimeDeps}:''${PATH:-}
set -euo pipefail
pictures_dir="$(xdg-user-dir PICTURES 2>/dev/null || echo "$HOME/Pictures")"
mkdir -p "$pictures_dir"
file="$pictures_dir/screenshot_$(date +%Y-%m-%d_%H-%M-%S).png"
notify() {
notify-send -t 2500 -u low "Screenshot" "$1" 2>/dev/null || true
}
# Pick a region (Escape or a plain click cancels)
geometry="$(slurp 2>/dev/null)" || exit 0
if ! grim -g "$geometry" "$file"; then
rm -f "$file"
notify "Capture failed"
exit 1
fi
wl-copy < "$file" 2>/dev/null || true
notify "Saved to $file (also copied to clipboard)"
'';
in
{
config = mkIf cfg.enable {
home.packages = [ screenshot ];
wayland.windowManager.hyprland = mkIf hyprlandCfg.enable {
settings = {
bind = [ "SUPER SHIFT, S, exec, screenshot" ];
};
};
};
}

View File

@ -6,7 +6,7 @@
{ {
stylix = { stylix = {
enable = true; enable = true;
base16Scheme = "${pkgs.base16-schemes}/share/themes/blueish.yaml"; base16Scheme = ../../modules/stylix/blueish-custom.yaml;
polarity = "dark"; polarity = "dark";
@ -44,7 +44,9 @@
targets = { targets = {
firefox = { firefox = {
profileNames = [ "${username}" ]; profileNames = [ "${username}" ];
firefoxGnomeTheme.enable = true;
}; };
hyprland.enable = true;
}; };
}; };
} }

View File

@ -0,0 +1,23 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.modules.battlenet;
in
{
config = mkIf cfg.enable {
# Battle.net games rely on 32-bit graphics libraries; Steam would
# normally force this on, but enable it independently as a fallback.
hardware.graphics.enable32Bit = mkDefault true;
environment.systemPackages = with pkgs; [
lutris
wineWow64Packages.staging
winetricks
];
};
}

View File

@ -3,7 +3,9 @@
}: }:
{ {
imports = [ imports = [
./battlenet.nix
./boot.nix ./boot.nix
./dev.nix
./fonts.nix ./fonts.nix
./gdm.nix ./gdm.nix
./gnomeBoxes.nix ./gnomeBoxes.nix
@ -16,6 +18,7 @@
./plasma.nix ./plasma.nix
./sddm.nix ./sddm.nix
./services.nix ./services.nix
./steam.nix
./stylix.nix ./stylix.nix
./system.nix ./system.nix
./tailscale.nix ./tailscale.nix

39
modules/nixos/dev.nix Normal file
View File

@ -0,0 +1,39 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.modules.dev;
in
{
config = mkIf cfg.enable {
# Runs dynamically linked executables intended for generic Linux (e.g. Python
# binaries downloaded by uv) by providing the standard loaders in /lib/*
programs.nix-ld = {
enable = true;
libraries = with pkgs; [
zlib
zstd
stdenv.cc.cc
curl
openssl
attr
libssh
bzip2
libxml2
acl
libsodium
util-linux
xz
systemd
glib
libelf
];
};
environment.systemPackages = with pkgs; [ uv ];
};
}

View File

@ -28,6 +28,7 @@ in
imports = [ homePath ]; imports = [ homePath ];
modules.gpg.signingKey = config.modules.gpg.signingKey; modules.gpg.signingKey = config.modules.gpg.signingKey;
modules.display.scale = config.modules.display.scale; modules.display.scale = config.modules.display.scale;
modules.opencode = config.modules.opencode;
}; };
}; };

View File

@ -1,12 +1,19 @@
{ {
host, host,
config,
lib,
pkgs, pkgs,
... ...
}: }:
{ {
networking = { networking = {
hostName = "${host}"; hostName = "${host}";
networkmanager.enable = true; networkmanager = {
enable = true;
# resolved is only enabled when tailscale is; pointing NM at a
# systemd-resolved that isn't running would break DNS entirely
dns = lib.mkIf config.services.resolved.enable "systemd-resolved";
};
wireless.enable = true; wireless.enable = true;
}; };

View File

@ -19,6 +19,27 @@ in
jack.enable = true; jack.enable = true;
}; };
# Re-enable PulseAudio-style auto profile switching (ACP), since
# WirePlumber 0.5 disables it and never reacts to HDMI hotplug events,
# leaving audio stuck on analog when a dock/TV is connected.
services.pipewire.wireplumber.extraConfig."99-acp-auto-profile" = {
"monitor.alsa.rules" = [
{
matches = [
{
"device.name" = "~alsa_card.*";
}
];
actions = {
update-props = {
"api.acp.auto-profile" = true;
"api.acp.auto-port" = true;
};
};
}
];
};
# Allows Pipewire to use the realtime scheduler for increased performance # Allows Pipewire to use the realtime scheduler for increased performance
security.rtkit.enable = true; security.rtkit.enable = true;
}; };

20
modules/nixos/steam.nix Normal file
View File

@ -0,0 +1,20 @@
{
config,
lib,
...
}:
with lib;
let
cfg = config.modules.steam;
in
{
config = mkIf cfg.enable {
programs.steam = {
enable = true;
remotePlay.openFirewall = cfg.remotePlay.enable;
dedicatedServer.openFirewall = cfg.remotePlay.enable;
};
hardware.steam-hardware.enable = true;
};
}

View File

@ -8,7 +8,7 @@
stylix = { stylix = {
enable = true; enable = true;
base16Scheme = "${pkgs.base16-schemes}/share/themes/blueish.yaml"; base16Scheme = ../../modules/stylix/blueish-custom.yaml;
polarity = "dark"; polarity = "dark";
}; };

View File

@ -10,5 +10,12 @@ in
{ {
config = mkIf cfg.enable { config = mkIf cfg.enable {
services.tailscale.enable = true; services.tailscale.enable = true;
# systemd-resolved is needed for tailscale's MagicDNS to work properly
services.resolved.enable = true;
# tailscaled applies its DNS config (split DNS, MagicDNS) to
# systemd-resolved over D-Bus, which is polkit-guarded
security.polkit.enable = true;
}; };
} }

View File

@ -14,6 +14,7 @@ in
initialPassword = "changeme"; initialPassword = "changeme";
extraGroups = [ extraGroups = [
"networkmanager" "networkmanager"
"video"
"wheel" "wheel"
]; ];
shell = pkgs.${shellCfg.defaultShell}; shell = pkgs.${shellCfg.defaultShell};

View File

@ -1,13 +0,0 @@
{
lib,
...
}:
with lib;
{
options.modules.ags = {
enable = mkEnableOption {
default = false;
description = "Enable the AGS shell";
};
};
}

View File

@ -0,0 +1,10 @@
{
lib,
...
}:
with lib;
{
options.modules.battlenet = {
enable = mkEnableOption "Battle.net via Lutris/Wine";
};
}

View File

@ -3,16 +3,21 @@
}: }:
{ {
imports = [ imports = [
./ags.nix ./battlenet.nix
./discord.nix ./discord.nix
./dictation.nix
./dev.nix
./display.nix ./display.nix
./gnomeBoxes.nix ./gnomeBoxes.nix
./gpg.nix ./gpg.nix
./hyprland.nix ./hyprland.nix
./localsend.nix ./localsend.nix
./opencode.nix
./pipewire.nix ./pipewire.nix
./plasma.nix ./plasma.nix
./screenshot.nix
./shell.nix ./shell.nix
./steam.nix
./system.nix ./system.nix
./tailscale.nix ./tailscale.nix
./vm.nix ./vm.nix

10
modules/options/dev.nix Normal file
View File

@ -0,0 +1,10 @@
{
lib,
...
}:
with lib;
{
options.modules.dev = {
enable = mkEnableOption "Python development with uv and nix-ld";
};
}

View File

@ -0,0 +1,21 @@
{
lib,
...
}:
with lib;
{
options.modules.dictation = {
enable = mkEnableOption "hold-to-talk voice dictation (hold SUPER+T)";
model = mkOption {
type = types.enum [
"tiny.en"
"base.en"
"small.en"
];
default = "base.en";
description = ''
The local Whisper model to use for transcription.
'';
};
};
}

View File

@ -5,9 +5,6 @@
with lib; with lib;
{ {
options.modules.discord = { options.modules.discord = {
enable = mkEnableOption { enable = mkEnableOption "Discord";
default = false;
description = "Enable Discord";
};
}; };
} }

View File

@ -5,9 +5,6 @@
with lib; with lib;
{ {
options.modules.gnomeBoxes = { options.modules.gnomeBoxes = {
enable = mkEnableOption { enable = mkEnableOption "GNOME Boxes";
default = false;
description = "Enable GNOME Boxes";
};
}; };
} }

View File

@ -5,10 +5,7 @@
with lib; with lib;
{ {
options.modules.gpg = { options.modules.gpg = {
enable = mkEnableOption { enable = mkEnableOption "gpg-agent";
default = false;
description = "Enable gpg-agent";
};
signingKey = mkOption { signingKey = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
@ -16,4 +13,4 @@ with lib;
description = "The key ID to sign with for git"; description = "The key ID to sign with for git";
}; };
}; };
} }

View File

@ -5,10 +5,7 @@
with lib; with lib;
{ {
options.modules.hyprland = { options.modules.hyprland = {
enable = mkEnableOption { enable = mkEnableOption "Hyprland";
default = false;
description = "Enable Hyprland";
};
terminal = mkOption { terminal = mkOption {
type = types.enum [ type = types.enum [
"kitty" "kitty"

View File

@ -5,9 +5,6 @@
with lib; with lib;
{ {
options.modules.localsend = { options.modules.localsend = {
enable = mkEnableOption { enable = mkEnableOption "LocalSend";
default = false;
description = "Enable LocalSend";
};
}; };
} }

View File

@ -0,0 +1,15 @@
{
lib,
...
}:
with lib;
{
options.modules.opencode = {
enable = mkEnableOption "opencode";
notify = mkOption {
type = types.bool;
default = true;
description = "Desktop notifications when opencode needs input or finishes";
};
};
}

View File

@ -5,7 +5,8 @@
with lib; with lib;
{ {
options.modules.pipewire = { options.modules.pipewire = {
enable = mkEnableOption { enable = mkOption {
type = types.bool;
default = true; default = true;
description = "Enable PipeWire"; description = "Enable PipeWire";
}; };

View File

@ -5,9 +5,6 @@
with lib; with lib;
{ {
options.modules.plasma = { options.modules.plasma = {
enable = mkEnableOption { enable = mkEnableOption "Plasma";
default = false;
description = "Enable Plasma";
};
}; };
} }

View File

@ -0,0 +1,10 @@
{
lib,
...
}:
with lib;
{
options.modules.screenshot = {
enable = mkEnableOption "screenshot capture tool (SUPER+SHIFT+S)";
};
}

13
modules/options/steam.nix Normal file
View File

@ -0,0 +1,13 @@
{
lib,
...
}:
with lib;
{
options.modules.steam = {
enable = mkEnableOption "Steam";
remotePlay = {
enable = mkEnableOption "Steam Remote Play firewall rules";
};
};
}

View File

@ -5,9 +5,6 @@
with lib; with lib;
{ {
options.modules.tailscale = { options.modules.tailscale = {
enable = mkEnableOption { enable = mkEnableOption "Tailscale";
default = false;
description = "Enable Tailscale";
};
}; };
} }

View File

@ -5,9 +5,10 @@
with lib; with lib;
{ {
options.modules.zsh = { options.modules.zsh = {
enable = mkEnableOption { enable = mkOption {
description = "Install Zsh"; type = types.bool;
default = true; default = true;
description = "Install Zsh";
}; };
}; };
} }

View File

@ -0,0 +1,21 @@
system: "base16"
name: "Blueish (custom)"
author: "Ben Mayoras (modified by Joshua Yuen)"
variant: "dark"
palette:
base00: "#182430"
base01: "#243C54"
base02: "#2E4460"
base03: "#616D78"
base04: "#74AFE7"
base05: "#C8E1F8"
base06: "#DDEAF6"
base07: "#8F98A0"
base08: "#4CE587"
base09: "#F6A85C"
base0A: "#F6CD5C"
base0B: "#C3E88D"
base0C: "#5FD1FF"
base0D: "#82AAFF"
base0E: "#FF84DD"
base0F: "#BBD2E8"

View File

@ -15,7 +15,7 @@
modules.localsend.enable = true; modules.localsend.enable = true;
modules.ags.enable = true; modules.dev.enable = true;
modules.zsh.enable = true; modules.zsh.enable = true;
modules.shell.defaultShell = "zsh"; modules.shell.defaultShell = "zsh";
@ -24,5 +24,9 @@
modules.discord.enable = true; modules.discord.enable = true;
modules.dictation.enable = true;
modules.screenshot.enable = true;
modules.tailscale.enable = true; modules.tailscale.enable = true;
} }

View File

@ -0,0 +1,12 @@
{
...
}:
{
imports = [
../desktop
];
modules.steam.enable = true;
modules.battlenet.enable = true;
}