216 Commits

Author SHA1 Message Date
686023e006 nixos: update rlru input 2026-06-19 12:08:14 -07:00
f12285b7f7 Add x.com Chrome app scratchpad 2026-06-18 21:27:03 -07:00
0efb55f23f Enable hyprtasking workspace overview 2026-06-18 20:28:53 -07:00
30fb74b13b nixos: update rlru input 2026-06-18 16:19:52 -07:00
9bb090bb35 Update Claude and Codex flakes 2026-06-18 15:57:04 -07:00
9d09b33e3e chore: update keepbook input 2026-06-18 15:49:59 -07:00
8a00f33c75 chore: update keepbook input 2026-06-18 14:45:55 -07:00
7b9031d85f docs: avoid pull request title prefixes 2026-06-18 14:19:24 -07:00
6216b54bfc nixos: update rlru input 2026-06-18 14:17:45 -07:00
01895cf518 nixos: update rlru input 2026-06-18 14:06:08 -07:00
ae67cd4ca3 nixos: add rocket sense privacy page 2026-06-18 13:53:38 -07:00
b41a1d8e36 docs: prefer ready pull requests 2026-06-18 13:53:16 -07:00
9bdc56c207 Update disk cleanup Rust target guidance 2026-06-18 11:43:12 -07:00
428ee71396 Get back on upstream for codex desktop 2026-06-18 01:34:15 -07:00
d936b477cd nixos: use nix store path for zsh lib instead of mutable worktree path
libDir pointed to ${config.dotfiles-worktree}/dotfiles/lib, which gets
evaluated at build time — so running `just switch` from inside a worktree
would bake that worktree path into /etc/zshenv permanently. If the worktree
is later removed, every zsh startup errors with "no matches found".

zshLibDir was already defined via builtins.path (a stable nix store copy)
but unused. Switch all fpath/autoload/PATH references to it and drop libDir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:26:25 -07:00
d552b3f89f claude: make CLAUDE.md an @-import of AGENTS.md
Replace the relative symlink with a regular file using Claude's @-import
so the file resolves to ~/.agents/AGENTS.md regardless of the dotfiles
checkout location.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:12:48 -07:00
f755db41f5 railbird-sf: add DuckDNS dynamic-DNS updater
Keep rocket-sense.duckdns.org pointed at this node's current public IP via
a systemd oneshot + 5min timer that hits the DuckDNS update API with the
source IP auto-detected. The residential WAN IP changes after ISP
failover, which otherwise leaves the public hostname stale.

Adds the agenix-encrypted duckdns-token secret and its key grant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:12:39 -07:00
3144fab895 chore: update claude-desktop-debian input
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:52:02 -07:00
2a3243b240 codex: register nixos MCP server
Inject an mcp_servers.nixos entry (pinned mcp-nixos binary) into the
generated Codex config.toml, threading it through both the awk merge of
the base config and the plain concatenation fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:51:58 -07:00
1869d3af8d nixos: register nixos MCP server for Claude Code
Add a module that exposes the pinned mcp-nixos binary and idempotently
merges a `nixos` stdio server into the user-scope ~/.claude.json on every
switch, giving Claude Code accurate NixOS package/option search.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:51:54 -07:00
fe733a9eb4 chore: update keepbook input 2026-06-16 15:04:43 -07:00
7058c68e56 agents: link Claude skills and root dotfiles at /srv
Wire the shared agent skills library into Claude Code, which only reads
~/.claude/skills. Add dotfiles/claude/skills -> ../agents/skills and an
allowlist exception so the symlink survives the /dotfiles/claude/* ignore
and lands in the flake source for enumeration.

Point the Codex skills module at the live worktree instead of its
~/dotfiles default so ~/.codex/AGENTS.md and ~/.codex/skills/* resolve
when the checkout lives at /srv/dotfiles, and change the worktreeRoot
fallback to /srv/dotfiles so no path defaults under ~/dotfiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:01:22 -07:00
68be1cdd09 chore: update rlru input 2026-06-15 14:11:32 -07:00
da7a946d31 Add Inkscape to ryzen-shine 2026-06-14 04:58:43 -07:00
b0df8ef27f Add backup AI scratchpad binding 2026-06-14 03:32:47 -07:00
4681f49d81 agents: add RLRML project guide 2026-06-14 03:17:29 -07:00
3ee11bcc14 nixos: bump Claude tooling flakes 2026-06-14 02:16:49 -07:00
2cfdb47469 flake.lock: refresh keepbook input 2026-06-14 02:16:38 -07:00
e0905eb651 Update codex desktop 2026-06-13 12:36:35 -07:00
5b4f605145 flake.lock: update keepbook 2026-06-12 18:50:23 -07:00
0336aa6e53 chore(nixos): update rlru 2026-06-12 14:58:01 -07:00
ad567c3e3f Build Heroic account switching app payload 2026-06-12 13:07:02 -07:00
3cb49b51bc Use Heroic account switching build 2026-06-12 04:36:10 -07:00
bd6e8d4e30 rlru: bump to v0.1.9 2026-06-12 01:08:02 -07:00
7713ad07bd taffybar: bump submodule for usage backoff 2026-06-12 00:44:08 -07:00
a5e3e650ae flake: bump rlru input 2026-06-12 00:44:04 -07:00
53becf8cc7 hyprland: disable crashing hyprexpo preview 2026-06-12 00:44:00 -07:00
4e98850276 codex: set desktop CLI package 2026-06-12 00:43:52 -07:00
8054fce6a3 claude: enable notification settings 2026-06-12 00:43:44 -07:00
563208d03b taffybar: point library input to anthropic-usage-rate-limit-backoff PR branch
Pins taffybar to github:taffybar/taffybar/anthropic-usage-rate-limit-backoff
(commit 7beecc89, taffybar/taffybar#681) so the Anthropic usage widget backs
off the OAuth usage endpoint on 429/Retry-After, caches the last good response
during backoff, and recovers immediately when the access token is refreshed —
instead of hammering the endpoint and dropping to the token-count fallback.

Bumps both the imalison-taffybar config flake.lock and the transitive node in
nixos/flake.lock. Revert to master after the PR merges.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 03:12:58 -07:00
d29c361a9e gitignore: untrack nixos/.playwright-cli scratch artifacts
These are throwaway browser-session debug logs that were committed by accident; ignore the directory and drop the tracked files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 02:10:57 -07:00
cd18fc2056 taffybar: bump submodule to 4c612d4
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 02:10:57 -07:00
39b68274e3 flake: bump claude-code-nix to latest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 02:10:51 -07:00
a2609b6f5b hypr-dynamic-cursors: pin to da447486 (pre Hyprland >0.55.2 API requirement)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 02:10:48 -07:00
1955bbed68 hyprland: disable hyprtasking plugin (build failure against current API)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 02:10:33 -07:00
942b987cc7 git-sync: brand tray icons for claude/codex history services
Use the desktop apps' hicolor theme icons (claude-desktop, codex-desktop)
for the git-sync-rs tray indicators instead of the generic git fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 02:09:19 -07:00
fc280676f5 nix-darwin: don't sync codex-history on mac-demarco-mini (disk)
The codex archive is ~1GB and the mac runs near full; cloning it would
break darwin rebuilds. Its codex sessions are already merged into the
repo. Keep claude-history (small) syncing there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 02:01:26 -07:00
dce3666521 codex: stop managing ~/.codex/.gitignore via home-manager
~/.codex is a git-sync-rs checkout of codex-history, which ships its own
real .gitignore. An HM-managed symlink there resolves to a symlink-blob
(the target path), silently disabling all ignore rules — git won't read a
symlinked ignore file. That let a git-sync auto-commit stage auth.json,
*.sqlite and ~540 other state files locally (caught before push; remote
was never affected). Leave .gitignore to the repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 01:58:26 -07:00
f3649945cb git-sync: sync ~/.codex history repo across machines
Mirror the claude-history setup for Codex: ~/.codex is now a git repo
(github.com/colonelpanic8/codex-history) holding session rollouts and
history.jsonl from all machines, synced by git-sync-rs in watch mode.

- Generalize the nixos host gate (claudeHistoryHosts -> aiHistoryHosts)
  and add codex-history alongside claude-history on both NixOS and darwin.
- Drop the old HM-managed dotfiles/codex/.gitignore; the repo ships its
  own real .gitignore (git won't read a symlinked one).
- Shield dotfiles/codex/* in the root .gitignore so nested codex history
  can't be committed into dotfiles on darwin (where ~/.codex resolves
  into dotfiles/codex).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 01:45:47 -07:00
994291b969 taffybar: update to latest master (f160fcd1)
Switch imalison-taffybar flake input from the anthropic-oauth-utilization
branch to upstream master and update both flake.lock files and the
taffybar submodule pointer accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 01:45:47 -07:00
ec00711c85 darwin: migrate ssh config settings 2026-06-10 23:35:40 -07:00
16aa6ed735 darwin: update flake inputs and switch fixes 2026-06-10 23:35:40 -07:00
f36cbe0207 gitignore: shield dotfiles/claude from nested claude-history data
On nix-darwin ~/.claude resolves into dotfiles/claude, so the
claude-history repo lives nested inside this worktree on those
machines; allowlist only the HM-managed config files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:35:26 -07:00
1c0377f3ad nix-darwin: sync ~/.claude history repo on mac-demarco-mini
kat's existing claude history has been merged into the claude-history
repo and the local checkout converted, mirroring the NixOS rollout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:33:39 -07:00
6290679406 git-sync: enable claude-history sync on jay-lenovo and strixi-minaj
Both machines' existing ~/.claude history has been merged into the
claude-history repo and their local checkouts converted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:30:50 -07:00
18ec8a7809 nixos: bump flake inputs (Hyprland v0.55.2 pin, taffybar PR branch)
Updates imalison-taffybar/taffybar to the anthropic-oauth-utilization
branch commit (6e2a1275) and locks Hyprland at v0.55.2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 23:06:54 -07:00
d21177b29c nixos: pin NVIDIA driver to 595.71.05 on ryzen-shine
595.80 (nixpkgs 9b366138, 2026-06-02) introduced a GSP firmware
regression on the RTX 3070 Ti (GA104): random hard freezes with
Xid 119 / "GSP RM heartbeat timed out". 595.71.05 ran cleanly for
4.5d before the bump. Revisit when the 610.x branch stabilises.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 23:06:50 -07:00
395f580645 nixos: add git-sync service for claude-history repo
Syncs ~/.claude to github.com/colonelpanic8/claude-history on
ryzen-shine and railbird-sf. Uses git-sync-rs in watch mode with
--new-files true and a 300s min-interval to handle active sessions
appending to transcripts without spamming pushes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 23:06:43 -07:00
a957e78e25 nixos: pin Hyprland to v0.55.2 to avoid broken Monitor.hpp on main
An untagged main snapshot (68e3e40, 2026-06-10) shipped Monitor.hpp
that #includes MonitorZoomController.hpp which doesn't exist yet,
breaking every plugin build. v0.55.2 is the release the hyprexpo fork
is built against.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 23:06:36 -07:00
a4f648fef8 taffybar: point library input to anthropic-oauth-utilization PR branch
Pins taffybar to github:taffybar/taffybar/anthropic-oauth-utilization
(commit 6e2a1275) so the Anthropic usage widget reads OAuth utilization
percentages instead of raw token counts. Revert to master after the PR
merges.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 23:06:27 -07:00
5eaaa39527 taffybar: add AIUsage stack widget that switches on scratchpad state
Reads $XDG_STATE_HOME/hypr/ai-scratchpad (written by the Super+Alt+C
toggle) to show either the OpenAI or Anthropic usage section. Uses a
Gtk.Stack for instant switching and fsnotify to watch the state file
for live updates without polling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 23:06:21 -07:00
1ec67b7892 nixos: bump keepbook to v0.4.7 (spending tag narrowing)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 22:48:01 -07:00
c75cf6c29c hypr: fix AI desktop apps stealing focus via xdg-activation
claude-desktop and codex-desktop fire xdg-activation requests while
streaming responses. With misc:focus_on_activate=true this yanked focus
(and warped the cursor) to those windows mid-use — most visibly causing
Super+Ctrl+Space to always end up focused on claude-desktop regardless of
which window was active.

Fix in settings.lua: window rules for claude-desktop and codex-desktop
with focus_on_activate=false (dynamic — applies to already-mapped windows
on reload) and suppress_event="activatefocus" (static — applies at map
time). Forced activations from the taskbar/foreign-toplevel-wlr protocol
still focus the window normally.

Secondary fixes in layouts.lua:
- find_tabbed_group_anchor: constrain search to current workspace so the
  binding can't jump across workspaces when a stale anchor exists elsewhere
- restore_workspace_tabbed_group / gather_workspace_into_tabbed_group: use
  the window that was focused at binding invocation time rather than the
  group anchor, so focus is returned to where the user actually was
- Add focus_window_with_cursor helper: focuses the target window and warps
  the cursor to its center when the cursor is outside it, preventing
  follow_mouse=1 from fighting the explicit focus dispatch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 18:22:08 -07:00
5aa543209d Bump flake 2026-06-10 14:27:51 -07:00
7276109962 chore: update rlru to v0.1.5 (remove window decorations) 2026-06-10 13:43:16 -07:00
87422afae3 hypr: make Super+Alt+C toggle a runtime-selectable AI scratchpad
Super+Alt+C now toggles whichever AI app is currently selected (Codex or
Claude Desktop) instead of always Codex. The selection is read from
$XDG_STATE_HOME/hypr/ai-scratchpad at keypress time, so switching is
dynamic and needs no Hyprland reload.

Hyper+C opens a rofi chooser (rofi_ai_scratchpad.sh) to pick between the
two, replacing the old "Open Codex session menu" binding. It writes the
choice and brings the selected scratchpad into view.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 15:57:12 -07:00
ab49f6c079 nixos: point claude-desktop at colonelpanic8 fork my-main branch
Integration branch with 12 merged upstream PRs (Fedora fixes, AppArmor
userns profiles, cowork perms, GNOME Wayland shortcuts, helper cleanup,
AppStream metainfo, etc.) that aaddrick hasn't merged yet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 12:54:38 -07:00
e6e0cd6d5e jay-lenovo: add swapfile 2026-06-09 06:26:10 -07:00
59fc652eab nixos: add claude-remote-control always-on service
Always-on systemd user service running `claude --remote-control` in a
detached tmux session (dedicated socket) pinned to /srv/dotfiles, with a
claude-rc-attach alias. Enabled on railbird-sf. Ad-hoc per-directory
sessions remain handled by the tmclaude shell function.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 05:22:51 -07:00
83ab75a12c nixos: bump codex-desktop-linux to build-info no-shadow fix
Point the codex-desktop-linux input at colonelpanic/linux-build-info-no-shadow
(PR ilysenko/codex-desktop-linux#438), which carries the merged build-info
settings work plus the fix preventing injected helper locals from shadowing
minified bundle bindings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 04:59:35 -07:00
85118f187e nixos/home-manager: materialize ~/.ssh/config as user-owned copy
/nix/store is owned by nobody:nogroup on this host, so the
home-manager-generated ~/.ssh/config symlink resolves to a nobody-owned
store file and OpenSSH rejects it with "Bad owner or permissions",
breaking ssh and git-over-ssh. Add a post-writeBoundary activation step
that replaces the symlink with a real 0600 copy owned by the user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 04:59:35 -07:00
1da7188781 nixos/remote-hyprland: stop seatd restart loop (Type=simple)
seatd's upstream unit is Type=notify wrapped in sdnotify-wrapper, but the
readiness notification never reaches systemd on railbird-sf, so the unit
hits its 90s start timeout and restart-loops indefinitely (restart counter
climbed past 50). Every cycle tears down /run/seatd.sock, which breaks any
libseat client that needs a stable seat (e.g. a headless Hyprland session).

seatd creates its socket immediately on startup, so Type=simple is
sufficient; override it with mkForce. Verified live: active(running),
stable socket, zero restarts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 04:53:45 -07:00
bdd95370bd claude: update local settings handling 2026-06-09 02:01:16 -07:00
48369966b4 git: update shared checkout config 2026-06-09 02:01:12 -07:00
6bdfcb0c8d nixos: remove silver-searcher from essentials 2026-06-09 02:01:04 -07:00
f94572bda0 nixos: allow electron 39 2026-06-09 02:00:39 -07:00
267bd10095 org-agenda-api: point production at railbird-sf 2026-06-09 02:00:29 -07:00
897c97c269 nixos: add claude-desktop flake and fix gpg-agent ordering cycle
Add aaddrick/claude-desktop-debian as a flake input and install its
default (FHS) package system-wide alongside the other LLM tools.

Also fix a home-manager systemd user-session ordering cycle that broke
activation ("Transaction order is cyclic"). The generated
set-SSH_AUTH_SOCK.service was ordered Before=gpg-agent-ssh.socket, which
is pulled into sockets.target that basic.target requires, while the
service implicitly orders After=basic.target -- forming a cycle. Drop
the explicit Before= edge with mkForce; the service is still pulled in
via WantedBy=gpg-agent-ssh.socket (a non-ordering Wants dependency).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 15:30:37 -07:00
d0f500daa8 nixos/desktop: run Claude Desktop natively on Wayland
The claude-desktop-debian flake's launcher ignores NIXOS_OZONE_WL and
ELECTRON_OZONE_PLATFORM_HINT and hardcodes --ozone-platform=x11, so the
app runs under XWayland by default. Its only opt-in for native Wayland is
CLAUDE_USE_WAYLAND=1, so set it as a session variable (trades away global
hotkeys).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 15:11:31 -07:00
e740a657ab Prevent rumno from blocking graphical session startup 2026-06-08 04:02:33 -07:00
1f2a38a8f7 Use named workspaces for Hyprland scratchpads 2026-06-05 08:39:15 -07:00
3da55b59d2 Fix Hyprland scratchpad workspace targeting 2026-06-05 08:27:39 -07:00
6ed6663b72 keepbook: update desktop package check fix 2026-06-05 08:09:58 -07:00
1d22e827b2 spotify: prefer Wayland launcher 2026-06-05 08:00:04 -07:00
11cd44b3f5 codex: point desktop input at chrome fix 2026-06-05 07:59:48 -07:00
8afbbce109 keepbook: update desktop service launch context 2026-06-05 07:57:48 -07:00
415b65d0ee Update keepbook input 2026-06-04 22:29:25 -07:00
66bbdab675 Update keepbook input to 0.4.5 2026-06-04 19:49:19 -07:00
8ed33fc7e8 Add Codex Desktop project launcher binding 2026-06-04 11:22:33 -07:00
ef3d19f1a4 flake: bump keepbook 2026-06-03 17:06:55 -07:00
7e07e768da Propagate taffybar Wayland monitor fix 2026-06-03 16:18:13 -07:00
c368f98e9f Make Hyprland tile binding contextual 2026-06-03 12:22:33 -07:00
aee236e532 Add scratchpad-sized untile binding 2026-06-03 12:20:52 -07:00
acea28cc54 Update codex desktop linux 2026-06-03 11:16:50 -07:00
272e71a37c desktop: improve app activation behavior 2026-06-03 01:28:20 -07:00
8c61bc4cee flake: bump rlru 2026-06-03 01:28:20 -07:00
ce7fd6b7a0 Bump codex-desktop-linux 2026-06-03 01:28:20 -07:00
6d0c29a743 Configure k3s resolv-conf 2026-06-02 12:49:44 -07:00
38a696cff2 taffybar: add tiny jay-lenovo profile 2026-06-02 12:49:44 -07:00
4cc6bee526 nixos: redirect tplinkdns host to rocket-sense 2026-06-02 12:49:44 -07:00
1c9e470ff6 hyprland: start portal backends on dbus activation 2026-06-02 12:49:44 -07:00
495a5cbca2 hyprland: expose density and cursor tuning 2026-06-02 12:49:44 -07:00
964ed7584e codex: patch desktop runtime integration 2026-06-02 12:49:44 -07:00
be1ec8556c nixos: adjust zsh completion setup 2026-06-02 12:49:44 -07:00
b2942e2a07 emacs: stabilize elpaca package recipes 2026-06-02 12:49:44 -07:00
34fd17a6a2 Update keepbook input 2026-06-02 12:49:31 -07:00
4a245306ed nixos: update taffybar locks 2026-06-01 17:31:39 -07:00
30a0ae47a8 nixos: update chrome desktop launchers 2026-06-01 17:30:43 -07:00
2b3a600b1b Update codex desktop linux input 2026-06-01 17:24:23 -07:00
f6ab902015 Bump codex cli flake 2026-06-01 16:52:42 -07:00
7069d0af10 Update dotfiles path references 2026-06-01 13:21:59 -07:00
b1a52b0401 Update keepbook flake inputs 2026-06-01 13:09:00 -07:00
d260b7622c Update rlru input 2026-06-01 12:56:17 -07:00
06eed9281d Update taffybar null monitor fix 2026-05-31 23:51:09 -07:00
38d57d1c0e Keep Chrome launcher new-window action 2026-05-31 23:51:09 -07:00
3e14320f36 Add trackpad-friendly Hyprland resize binding 2026-05-31 23:24:25 -07:00
6018cc6f1d Make dotfiles worktree follow flake checkout 2026-05-31 21:00:50 -07:00
d90a6c7c63 Use codex desktop computer use availability fix 2026-05-30 11:53:45 -07:00
507a306cbf Move dotfiles worktree to shared path 2026-05-30 11:36:11 -07:00
44363cf0fb Update codex desktop linux flake 2026-05-30 11:34:57 -07:00
1b06280cb9 nix: rotate tailscale auth key 2026-05-30 11:25:47 -07:00
7c1185fa6e Avoid duplicate Codex desktop launches 2026-05-30 11:15:34 -07:00
4188a6c0d8 Make jay-lenovo switch activation robust 2026-05-30 07:10:36 -07:00
e6a5464520 Prune heavy editor packages from jay-lenovo 2026-05-30 06:46:55 -07:00
58a55209fa Fix NixOS switch inputs 2026-05-30 06:38:17 -07:00
9baa4c3d44 Fix taffybar weeder flake input 2026-05-29 23:55:52 -07:00
422826a62e git: pin gh credential helper path 2026-05-29 23:19:22 -07:00
32e69cbd01 docs: record disk cleanup findings 2026-05-29 23:19:07 -07:00
59c7d4ba11 nix: add compiler build workarounds 2026-05-29 23:19:02 -07:00
531ad1602b hyprland: include utility packages in bundle 2026-05-29 23:16:36 -07:00
67859a5436 desktop: trim packages and use Adwaita 2026-05-29 23:16:07 -07:00
b8cbf387fa hyprland: expose cacheable package set 2026-05-29 23:13:59 -07:00
29a0af4bde desktop: de-duplicate app desktop entries 2026-05-29 23:12:32 -07:00
03536fbbb1 hyprland: add tasking and dynamic cursor plugins 2026-05-29 23:10:15 -07:00
511d643063 nix: pin rumno package override 2026-05-29 23:07:17 -07:00
81d4496fe4 Fix Codex scratchpad warm start 2026-05-29 23:04:29 -07:00
d6eb0f2e6c Update hyprexpo live preview fix 2026-05-27 03:56:44 -07:00
05bce81158 Remove Happy Coder package 2026-05-27 03:45:31 -07:00
51de81b242 Make hyprexpo active 2026-05-27 03:01:58 -07:00
758a35c836 Update hyprexpo input 2026-05-27 02:45:27 -07:00
187edef30f flake: update nix-darwin inputs 2026-05-26 19:09:15 -07:00
3338b86b48 Update rlru input 2026-05-26 01:48:04 -07:00
63f9ead9a3 Update rlru desktop identity fix 2026-05-25 18:16:46 -07:00
4852985801 Update keepbook desktop init fix 2026-05-25 13:42:09 -07:00
e8612e3df0 Update keepbook desktop identity fix 2026-05-25 13:27:38 -07:00
798d5c0742 Update keepbook input 2026-05-25 10:19:47 -07:00
60fa81fecf flake: update keepbook nix build fix 2026-05-25 03:19:30 -07:00
78842c242d flake: update keepbook desktop fix 2026-05-25 03:07:06 -07:00
e0b3eb9da8 flake: update keepbook dioxus 2026-05-25 02:40:36 -07:00
2acbd0937f keepbook: run dioxus desktop service 2026-05-25 02:02:22 -07:00
fbec3e7380 nixos: update desktop inputs 2026-05-25 02:01:23 -07:00
e4d4547bf1 flake: update keepbook to 0.4.4 2026-05-23 21:32:53 -07:00
a81d1d2caf chore: update rlru flake input 2026-05-23 16:04:15 -07:00
0105b52b7a flake: update rlru input 2026-05-23 10:23:18 -07:00
715eb1e76d nixos: use systemd-resolved for NetworkManager 2026-05-23 10:22:47 -07:00
8360419d2c flake: update rlru input 2026-05-23 03:46:27 -07:00
2bacf623cb nixos: apply nixpkgs PR 523297 2026-05-23 03:46:22 -07:00
443dfb0199 flake: use codex desktop CI branch 2026-05-23 03:46:15 -07:00
07cdc10ef0 disk-space-cleanup: document hypr target false positive 2026-05-23 03:45:49 -07:00
850cddeeb0 taffybar: deprioritize git-sync tray item 2026-05-23 03:45:43 -07:00
eec9f0ba0e rumno: update notification commands 2026-05-23 03:45:40 -07:00
ff23cb8da6 flake: update rlru input 2026-05-23 03:23:16 -07:00
79343c8160 flake: update keepbook input 2026-05-23 02:03:06 -07:00
1df5c22b75 hyprland: provide llvm-nm for plugin symbol lookup 2026-05-22 23:29:23 -07:00
81318ce0be hyprland: disable parallel build for pinned package 2026-05-22 20:30:07 -07:00
680f8b4a91 zsh: preserve larger shared history 2026-05-22 20:29:23 -07:00
b7eb47a71d nixos: disable broad terminfo install 2026-05-22 20:28:36 -07:00
a860b59e3f docs: record rust target cleanup edge cases 2026-05-22 20:27:41 -07:00
dde547f694 Use programs.ssh.settings for Home Manager 2026-05-22 20:01:32 -07:00
bf71d0ee39 nixos: fetch rlru via public GitHub flake 2026-05-22 16:50:17 -07:00
2c22ccd01e nixos: replace rockpload with rlru 2026-05-22 15:53:52 -07:00
348560eefe nixos: consolidate shared nix helpers 2026-05-22 15:12:16 -07:00
2573928706 nixos: use upstream Codex Desktop Linux 2026-05-22 12:58:39 -07:00
18c293ec5f Use upstream hyprutils 2026-05-22 12:56:10 -07:00
2e523750e2 Update hyprexpo input 2026-05-22 06:16:34 -07:00
72c9177f95 Fix hyprexpo Hyprland plugin loading 2026-05-22 05:54:04 -07:00
4360850f82 nixos: use sudo wrapper for switch recipe 2026-05-22 05:02:42 -07:00
d99ccdbd0c nixos: proxy rocket-sense on railbird-sf 2026-05-22 05:02:03 -07:00
a37780c443 nixos: build Hyprland plugins with pinned package set 2026-05-22 05:01:25 -07:00
2c53dda524 Update Codex Desktop Linux input 2026-05-22 04:58:28 -07:00
c39c70f6ac Update hyprexpo 2026-05-22 03:44:06 -07:00
e407f009db flake: update inputs 2026-05-22 02:49:22 -07:00
eee7434aca feat: enable rockpload 2026-05-22 02:49:07 -07:00
54ec7d3f0a feat: add rocket league bakkesmod helper 2026-05-22 02:48:34 -07:00
0ee6c78de3 fix: discard invalid cached chrome favicons 2026-05-22 02:48:24 -07:00
c11f81cbf8 feat: improve hyprland action key labels 2026-05-22 02:45:28 -07:00
deef8b8a07 fix: detect gtk portal file choosers 2026-05-22 02:45:15 -07:00
d57fda3dc9 feat: add discord scratchpads 2026-05-22 02:45:03 -07:00
a8dab69126 Update hyprexpo 2026-05-21 21:51:45 -07:00
2fb8951810 Remove obsolete NixOS bootstrap flake 2026-05-21 20:54:29 -07:00
b74bb07339 Allow Codex scratchpad to tile 2026-05-21 19:14:39 -07:00
87a79e2c8a Bump Codex CLI flake 2026-05-21 14:21:48 -07:00
2d96b71594 Update codex desktop Linux flake input 2026-05-20 15:08:56 -07:00
aed4d24ae7 Default tmcodex to unix remote 2026-05-20 14:37:08 -07:00
f3a10e0b66 Refine rofi candidate separators 2026-05-20 14:25:07 -07:00
ddb854c362 Make rofi selected item more visible 2026-05-20 12:14:31 -07:00
8ae6f0d676 nixos: pin codex desktop fork 2026-05-20 01:17:07 -07:00
0c75df5085 codex: make desktop launch helpers cwd-aware 2026-05-20 01:16:58 -07:00
1c0de36f52 nixos: bump Codex CLI to 0.132.0 2026-05-19 22:33:22 -07:00
874b83259d nixos: set Hyprland DRM device ordering 2026-05-19 22:31:09 -07:00
378fa8df34 docs: clarify tiling wm migration plan 2026-05-19 22:31:09 -07:00
243f64fade code: add node to codex remote control 2026-05-19 22:24:25 -07:00
96b9b5cd85 hyprland: disable hyprexpo focus-follow 2026-05-19 22:23:31 -07:00
f2bb9c8278 Enable hyprexpo window icons 2026-05-19 22:22:43 -07:00
c121e07452 Update hyprexpo follow focus default 2026-05-19 15:40:51 -07:00
6c2183c9ae Fix tmux title fallback for Codex 2026-05-19 15:20:38 -07:00
b72dab7337 Update hyprexpo live preview default 2026-05-19 15:15:42 -07:00
35191bedba Enable hyprexpo live preview focus 2026-05-19 15:11:08 -07:00
ba06ab1e00 Update hyprexpo preview drag behavior 2026-05-19 15:02:21 -07:00
6e21640e58 hyprland: drop unused layout workspace lookup 2026-05-19 15:01:21 -07:00
beef3f8b84 taffybar: include runtime tools on path 2026-05-19 15:00:44 -07:00
875982b6c2 chrome: load favicon dbus extension directly 2026-05-19 15:00:05 -07:00
463cb9d24a codex: preserve launcher working directories 2026-05-19 14:59:02 -07:00
8c31b53e33 rofi: tune apple frost list density 2026-05-19 14:58:50 -07:00
eda407c47d hyprland: add quadrants layout 2026-05-19 14:58:45 -07:00
ce29ee063d hyprland: discover lua modules dynamically 2026-05-19 14:58:40 -07:00
130 changed files with 4512 additions and 1998 deletions

View File

@@ -4,8 +4,11 @@
"Bash(rg:*)",
"Bash(wmctrl:*)",
"Bash(grep:*)",
"Bash(hyprctl:*)"
"Bash(hyprctl:*)",
"Bash(set_multiplexer_title 'dotfiles - Claude desktop icon fix')",
"Bash(nix eval *)",
"Read(//nix/store/xmgdj0242sc04hybgd3x6w0a7cw7kkwl-system-path/share/applications/**)"
],
"deny": []
}
}
}

View File

@@ -1,15 +1,23 @@
name: Build and Push Cachix (imalison-taffybar)
name: Build and Push Cachix
on:
push:
branches: [master]
paths:
- "dotfiles/config/taffybar/**"
- "dotfiles/config/hypr/**"
- "dotfiles/lib/bin/hypr_*"
- "dotfiles/lib/bin/hypr*"
- "nixos/**"
- ".github/workflows/cachix.yml"
pull_request:
branches: [master]
paths:
- "dotfiles/config/taffybar/**"
- "dotfiles/config/hypr/**"
- "dotfiles/lib/bin/hypr_*"
- "dotfiles/lib/bin/hypr*"
- "nixos/**"
- ".github/workflows/cachix.yml"
workflow_dispatch: {}
@@ -87,3 +95,12 @@ jobs:
--no-link \
--print-build-logs \
./dotfiles/config/taffybar#defaultPackage.x86_64-linux
- name: Build Hyprland stuff
run: |
set -euxo pipefail
nix build \
--no-link \
--print-build-logs \
./nixos#hyprland-stuff \
--override-input railbird-secrets path:./nixos/ci/railbird-secrets-stub

24
.gitignore vendored
View File

@@ -49,5 +49,29 @@ gotools
# Local tool state
/.playwright-cli/
/nixos/.playwright-cli/
/nixos/action-cache-dir/
/dotfiles/config/taffybar/dbus-menu/
# On nix-darwin, ~/.claude resolves into dotfiles/claude (HM out-of-store
# symlink), so the claude-history repo and live Claude Code state are nested
# inside this worktree there. Keep everything but the managed config out of
# the dotfiles repo so chat history can never be committed here.
/dotfiles/claude/*
!/dotfiles/claude/CLAUDE.md
!/dotfiles/claude/settings.json
!/dotfiles/claude/settings.local.json
!/dotfiles/claude/settings.local.json.example
# Expose the shared agent skills library to Claude Code, which only reads
# ~/.claude/skills. This is a symlink to ../agents/skills (the canonical
# store, also surfaced at ~/.agents/skills); without the allowlist the
# /dotfiles/claude/* rule above keeps it out of the flake source.
!/dotfiles/claude/skills
# Same story for Codex: ~/.codex resolves into dotfiles/codex on nix-darwin,
# so the codex-history repo and live Codex state nest inside this worktree.
# Allowlist only the HM-managed config.
/dotfiles/codex/*
!/dotfiles/codex/AGENTS.md
!/dotfiles/codex/config.toml
!/dotfiles/codex/skills

View File

@@ -31,9 +31,9 @@ published GitHub Pages site is still generated from that document.
- Container and deployment configuration for personal org-agenda-api instances.
This is not intended to be a generic starter dotfiles repo. Many modules assume
my users, hostnames, hardware, SSH keys, secrets layout, and local checkout path
=(~/dotfiles=). It is still useful as a reference for how the pieces fit
together.
my users, hostnames, hardware, SSH keys, secrets layout, and shared checkout
path =/srv/dotfiles= on NixOS machines. It is still useful as a reference for
how the pieces fit together.
* Layout
@@ -65,7 +65,7 @@ The broad feature set is assembled by [[file:nixos/configuration.nix][nixos/conf
Common workflow:
#+begin_src sh
cd ~/dotfiles/nixos
cd /etc/nixos
just switch
#+end_src
@@ -77,7 +77,7 @@ directly.
Useful variants:
#+begin_src sh
cd ~/dotfiles/nixos
cd /etc/nixos
just switch-remote
just switch-local-taffybar
just remote-switch <host>
@@ -86,8 +86,8 @@ just remote-switch <host>
Build/check examples:
#+begin_src sh
nix flake check ~/dotfiles/nixos
nix build ~/dotfiles/nixos#nixosConfigurations.strixi-minaj.config.system.build.toplevel
nix flake check /etc/nixos
nix build /etc/nixos#nixosConfigurations.strixi-minaj.config.system.build.toplevel
#+end_src
The flake also exposes package/check outputs for Hyprland plugins and a
@@ -102,7 +102,7 @@ nix-darwin, nix-homebrew, Home Manager, agenix, and the shared package list in
Common workflow:
#+begin_src sh
cd ~/dotfiles/nix-darwin
cd /srv/dotfiles/nix-darwin
just switch
#+end_src
@@ -177,7 +177,7 @@ behind nginx with ACME certificates and Podman.
To enter the deployment shell:
#+begin_src sh
nix develop ~/dotfiles/nixos#org-agenda-api
nix develop /etc/nixos#org-agenda-api
#+end_src
* Secrets
@@ -201,7 +201,9 @@ Some third-party or upstream projects are tracked as submodules:
Clone with submodules when bootstrapping a new checkout:
#+begin_src sh
git clone --recurse-submodules git@github.com:IvanMalison/dotfiles.git ~/dotfiles
git clone --recurse-submodules git@github.com:IvanMalison/dotfiles.git /tmp/dotfiles
cd /tmp/dotfiles
just setup-shared-dotfiles
#+end_src
This repo also contains project-local git worktrees under =.worktrees/= during

33
docs/shared-dotfiles.md Normal file
View File

@@ -0,0 +1,33 @@
# Shared Dotfiles Worktree
This repo is intended to live at `/srv/dotfiles` on shared NixOS machines.
Home Manager links user dotfiles to that shared checkout instead of to
`$HOME/dotfiles`, so the links work consistently for every managed user.
Set it up from any existing checkout:
```sh
just setup-shared-dotfiles
```
The setup command:
- copies the current checkout to `/srv/dotfiles` when needed
- makes the checkout readable by everyone
- makes it writable by the `wheel` group
- sets directory setgid/default ACLs so new files stay group-writable
- configures Git for group sharing
- creates `/etc/nixos -> /srv/dotfiles/nixos` when `/etc/nixos` is absent or already a symlink
Use a different target or group when needed:
```sh
just setup-shared-dotfiles --target /srv/dotfiles --group wheel
```
If a machine has a real `/etc/nixos` directory and you want to replace it with
the shared checkout symlink:
```sh
just setup-shared-dotfiles --force-etc-nixos
```

View File

@@ -8,6 +8,65 @@ This document describes the tiling window manager experience I am targeting.
- Important: expected for parity, but a rough first version is acceptable.
- Nice: useful polish or compatibility.
Priority describes the target experience, not implementation order. A first
usable implementation may ship a smaller daily-driver subset as long as it does
not choose designs that block required behavior later.
## Implementation Phases
Phase 1 should establish the core daily-driver loop:
- Global numbered workspaces across monitors.
- Dynamic equal-width columns and tabbed/fullscreen-style layout.
- Directional window focus, directional movement, and directional monitor
focus.
- Direct numbered workspace move/follow bindings.
- Focus-follows-mouse and mouse-follows-focus.
- Basic rofi launcher, terminal, close, reload, and session-exit bindings.
- Basic status-bar workspace and focused-window state.
Phase 2 should restore high-frequency workflow parity:
- Per-monitor workspace history and history cycling.
- Scratchpads.
- Minimization.
- Go-to-window, bring-window, and replace-window pickers.
- Browser raise-or-spawn and class-aware gather workflows.
- Status-bar window lists, class/title/icon metadata, and special-workspace
filtering.
Phase 3 should add visual discovery and polish:
- Visual window overview.
- Visual workspace expose.
- Overview go/bring/replace actions.
- Smart gaps, smart borders, dimming, wallpaper, lock, screenshot, clipboard,
DDC/input switching, and other session utilities.
## Terms and Semantics
- First-class operation means the action has a direct command or binding. It
does not require opening a picker, manually moving focus, or chaining multiple
unrelated commands.
- Preserving useful focus means the operation leaves keyboard focus in a
predictable place. Non-following moves keep focus on the source monitor or
source workspace. Following moves focus the moved window on its destination.
- Directional focus uses visible window geometry when windows have distinct
rectangles. In tabbed or fullscreen-style layouts where geometry overlaps,
directional focus may use a stable logical order instead, but repeated
directional actions must cycle predictably through the windows.
- Near-fullscreen scratchpads are centered floating windows large enough to
dominate the current monitor without taking compositor fullscreen state.
- Robust scratchpad behavior means toggling a named scratchpad finds or
launches the intended app even when the app starts slowly, changes class or
title after launch, is minimized, or is currently on another workspace.
- Approximate window position means enough geometry or ordering information for
status-bar window strips and expose-like previews. Pixel-perfect compositor
geometry is useful but not required.
- Normal workspaces are the bounded user-facing workspaces. Special,
scratchpad, minimized, hidden, internal, and out-of-range workspaces are not
normal workspaces.
## Modifier Terminology
- `Super` names the physical modifier key often labeled Windows, Command, GUI,
@@ -96,18 +155,10 @@ Required behavior:
- Directional monitor focus is available.
- Directional window movement between monitors is available.
- Moving the focused window to an empty workspace on the monitor in a direction
remains required behavior, but it should not require an extra `Hyper`
modifier beyond `Shift`.
- `Super+w/a/s/d` focuses windows directionally.
- `Super+Shift+w/a/s/d` swaps or moves the focused window directionally.
- `Super+Ctrl+w/a/s/d` moves the focused window to the monitor in that
direction while preserving useful focus.
- `Super+Ctrl+Shift+w/a/s/d` moves the focused window to an empty workspace on
the monitor in that direction.
- `Hyper+w/a/s/d` focuses monitors directionally.
- `Hyper+Shift+w/a/s/d` swaps or moves windows between monitors directionally.
- Directional focus in tabbed/fullscreen mode should cycle predictably through
windows even though their screen geometry overlaps.
is available.
- Directional bindings are defined in the Binding Appendix. Required
directional actions must not depend on `Hyper+Ctrl`, because `Ctrl` may
already be part of the fallback `Hyper` chord.
Important behavior:
@@ -207,9 +258,12 @@ Required behavior:
- A named scratchpad exists for spotify.
- A named scratchpad exists for transmission.
- A named scratchpad exists for volume.
- A named scratchpad exists for x.com.
- Scratchpads appear near-fullscreen and centered by default.
- The codex, claude, and x.com scratchpads can be tiled into the normal
workspace when desired, while retaining their summon/dismiss toggles.
- Toggling a scratchpad deactivates fullscreen/tabbed state first.
- Scratchpads are hidden from normal workspace and window listings.
- Floating scratchpads are hidden from normal workspace and window listings.
Important behavior:
@@ -329,8 +383,7 @@ Required behavior:
- `Super+b` opens the bring-window picker.
- `Super+Shift+b` opens the replace-window picker.
- `Super+Shift+e` moves the focused window to the next empty workspace and
follows it. This is the target replacement for the older `Super+Shift+h`
binding.
follows it.
- `Hyper+e` focuses the next empty workspace.
- `Hyper+1` toggles inactive-window opacity reduction for the focused window.
- `Hyper+5` swaps the current workspace with a selected workspace.
@@ -366,10 +419,10 @@ Required behavior:
- `Super+Shift+w/a/s/d` swaps or moves the focused window directionally.
- `Super+Ctrl+w/a/s/d` moves the focused window to the monitor in that
direction while preserving useful focus.
- `Super+Ctrl+Shift+w/a/s/d` moves the focused window to an empty workspace on
the monitor in that direction.
- `Hyper+w/a/s/d` focuses monitors directionally.
- `Hyper+Shift+w/a/s/d` swaps or moves windows between monitors directionally.
- Moving the focused window to an empty workspace on the monitor in a direction
remains required behavior, but it should not require a `Hyper+Ctrl` binding.
- `Super+z` focuses the next monitor.
- `Super+Shift+z` moves the focused window to the next monitor.
@@ -387,13 +440,15 @@ Required behavior:
Required behavior:
- `Super+Alt+c` toggles the codex scratchpad.
- `Super+Alt+c` toggles the primary AI scratchpad.
- `Super+Alt+Shift+c` toggles the backup AI scratchpad.
- `Super+Alt+e` toggles the element scratchpad.
- `Super+Alt+h` toggles the htop scratchpad.
- `Super+Alt+k` toggles the slack scratchpad.
- `Super+Alt+s` toggles the spotify scratchpad.
- `Super+Alt+t` toggles the transmission scratchpad.
- `Super+Alt+v` toggles the volume scratchpad.
- `Super+Alt+x` toggles the x.com scratchpad.
Important behavior:
@@ -412,6 +467,8 @@ Required behavior:
- `Hyper+p` opens the password picker with `rofi-pass`.
- `Hyper+h` opens the screenshot tool with the compositor/session-appropriate
screenshot command.
- `Hyper+n` opens a Codex Desktop project picker and starts a new thread in
the selected saved project root.
- `Hyper+c` opens the Codex launcher with `rofi_tmcodex.sh`.
- `Hyper+Shift+c` opens the Codex launcher with `tmcodex resume`.
- `Hyper+k` opens the process killer with `rofi_kill_process.sh`.
@@ -435,3 +492,8 @@ Important behavior:
compositor-appropriate implementation.
- Session-destructive operations use shifted or otherwise harder-to-hit
variants.
## Migration Notes
- `Super+Shift+e` is the target replacement for the older `Super+Shift+h`
move-to-next-empty-workspace-and-follow binding.

View File

@@ -1,23 +1,9 @@
# Agentic Session Preferences
## Multiplexer session titling
- If the `TMUX` or `ZELLIJ` environment variable is set, treat this chat as the controller for the current tmux or zellij session.
- Use `set_multiplexer_title '<project> - <task>'` to update the title. The command detects tmux vs. zellij internally, prefers tmux when both are present, and no-ops outside a multiplexer.
- Maintain a session/window/pane title that describes the durable purpose of the overall exchange.
- Prefer automatic titling: infer a concise <task> from the current user request and the existing chat context without asking.
- Choose holistic titles over granular turn summaries. The title should answer "what has this chat been for?" rather than describe the latest command, substep, clarification, or follow-up message.
- Preserve the existing <task> when the new user turn is a continuation, status check, refinement, or implementation detail within the same broader objective.
- Title format: "<project> - <task>".
- <project> is the basename of the current project directory.
- Prefer git repo root basename if available; otherwise use basename of the current working directory.
- <task> is a short, user-friendly description of what we are doing.
- Ask for a short descriptive <task> only when the task is ambiguous or you are not confident in an inferred title.
- When the broader objective changes substantially, update the <task> automatically if clear; otherwise ask for an updated <task>.
- When a title is provided or updated, immediately run `set_multiplexer_title '<project> - <task>'`; do not call raw tmux or zellij rename commands unless debugging the helper itself.
- For Claude Code sessions, a UserPromptSubmit hook may initialize titles automatically from the first substantive prompt, but it should not keep overwriting an established same-project title with the latest prompt.
## Pane usage
- Do not create extra panes or windows unless the user asks.
## Sharing dev-server / preview links
- When sharing a local server or preview URL, always prefer this machine's Tailscale address over `127.0.0.1`/`localhost`/LAN IPs, so the link opens from any device on the tailnet.
- Get the address with `tailscale ip -4` (the `100.x.y.z` IP) or the MagicDNS hostname from `tailscale status`. Prefer the `100.x` IP when a server's `allowedHosts` might reject a hostname.
- Start the server bound to all interfaces (e.g. vite's `--host 0.0.0.0` / a `dev:lan` script), not just localhost, or the Tailscale link won't connect. Verify reachability (`curl` the `100.x` URL) before handing it over.
## Git worktrees
- Default to creating git worktrees under a project-local `.worktrees/` directory at the repository root.
@@ -25,8 +11,14 @@
- Create `.worktrees/` if needed before running `git worktree add`.
- Only use a non-`.worktrees/` location when the user explicitly asks for a different path.
## GitHub pull requests
- Default to creating pull requests as ready for review, not drafts.
- Do not add a `[codex]` prefix or any other agent/tool prefix to pull request titles.
- Create a draft pull request only when the user explicitly asks for a draft or when the remote platform requires draft status.
- If using a helper, skill, or CLI wrapper that defaults to draft PRs, override that default before creating the PR.
## NixOS workflow
- This system is managed with a Nix flake at `~/dotfiles/nixos`.
- This system is managed with a Nix flake at `/srv/dotfiles/nixos`.
- Use `just switch` from that directory for rebuilds instead of plain `nixos-rebuild`.
- Host configs live under `machines/`; choose the appropriate host when needed.
@@ -60,23 +52,6 @@
This is an org-mode repository containing personal task management, calendars, habits, and project tracking files. It serves as the central hub for Ivan's personal organization.
## Available Tools
### Chrome DevTools MCP
A browser automation MCP is available for interacting with web pages. Use it to:
- Navigate to websites and fill out forms
- Take screenshots and snapshots of pages
- Click elements, type text, and interact with web UIs
- Read page content and extract information
- Automate multi-step web workflows (booking, purchasing, form submission, etc.)
### Google Workspace CLI (`gws`)
The local `gws` CLI is available for Google Workspace operations. Use it to:
- Search, read, and send Gmail messages
- Manage Gmail labels and filters
- Download attachments and inspect message payloads
- Access Drive, Calendar, Docs, Sheets, and other Google Workspace APIs
## Credentials via `pass`
Many credentials and personal details are stored in `pass` (the standard unix password manager). There are hundreds of entries covering a wide range of things, so always search before asking the user for information. Use `pass find <keyword>` to search and `pass show <entry>` to retrieve values.
@@ -97,8 +72,6 @@ Examples of what's stored:
## Guidelines
- When filling out forms or making purchases, pull personal info from this file and credentials from `pass` rather than asking the user to provide them.
- For web tasks, prefer using the Chrome DevTools MCP to automate interactions directly.
- For email tasks, prefer using `gws gmail` over navigating to Gmail in the browser.
- If a task requires a credential not found in `pass`, ask the user rather than guessing.
- This repo's org files (gtd.org, calendar.org, habits.org, projects.org) contain task and scheduling data. The org-agenda-api skill/service can also be used to query agenda data programmatically.
@@ -124,3 +97,4 @@ Examples of what's stored:
- `./project-guides/taffybar.md`
- `./project-guides/railbird.md`
- `./project-guides/org-emacs-packages.md`
- `./project-guides/subtr-actor-rocket-sense-rlru.md`

View File

@@ -0,0 +1,51 @@
# Subtr Actor / Rocket Sense / rlru constellation
## Scope
- Use this guide for requests involving Rocket League replay parsing, replay analytics, upload flows, or the `rlrml` projects around `subtr-actor`, `rocket-sense`, and `rlru`.
- Primary anchors are `subtr-actor` for replay-domain logic, `rocket-sense` for the hosted analytics service, and `rlru` for local replay discovery/upload and PsyNet integration.
## Related packages/projects (trigger list)
- If any of these names are mentioned, open this guide for context.
- `subtr-actor`: Rocket League replay processing core and source-of-truth replay domain model.
- `rocket-sense`: replay analytics backend and React/Vite web app built on `subtr-actor`.
- `rlru`: Rust-first Rocket League replay uploader and desktop client workspace.
- `psynet`: Psyonix PsyNet client crate inside the `rlru` workspace.
## Package inventory
- `subtr-actor` repo packages:
- Rust crates: `subtr-actor`, `subtr-actor-tools`, `subtr-actor-bakkesmod`, `rl-replay-subtr-actor`.
- Python package/crate: `subtr-actor-py` / `subtr_actor`.
- npm packages: `@rlrml/subtr-actor`, `@rlrml/player`, `@rlrml/stats-player`.
- `rocket-sense` repo packages:
- Rust crates: `rocket-sense-server`, `rocket-sense-db`, `rocket-sense-storage`.
- Web app package: `rocket-sense-web`.
- Vendored packages may appear under `vendor/subtr-actor`; prefer the standalone `subtr-actor` checkout for source-of-truth domain changes unless the user specifically asks about the vendored copy.
- `rlru` repo packages:
- Rust crates: `rlru`, `psynet`, `rlru-dioxus`.
- Apps/binaries: `rlru` CLI and `rlru-dioxus` desktop client.
## Symlink targets
- `./project-links/subtr-actor` -> primary `subtr-actor` repo.
- `./project-links/rocket-sense` -> primary `rocket-sense` repo.
- `./project-links/rlru` -> primary `rlru` repo.
## Discovery hints
- Start from `~/Projects`.
- Common local paths are:
- `~/Projects/subtr-actor`
- `~/Projects/rocket-sense`
- `~/Projects/rlru`
- `rocket-sense` may vendor `subtr-actor` under `vendor/subtr-actor`; prefer the standalone `subtr-actor` checkout for source-of-truth replay-domain changes unless the user specifically asks about the vendored copy.
## Read-first docs
- `./project-links/subtr-actor/AGENTS.md`
- `./project-links/subtr-actor/README.md`
- `./project-links/rocket-sense/AGENTS.md`
- `./project-links/rocket-sense/README.md`
- `./project-links/rlru/README.md`
## Notes
- Treat `subtr-actor` as the source of truth for replay parsing, frame/state extraction, stats calculators, feature matrices, and JS/WASM replay-player data contracts.
- Treat `rocket-sense` as the service/UI layer for replay hosting, metadata, processing state, auth, storage, OpenAPI, and deployed analytics workflows.
- Treat `rlru` as the local uploader/client layer for replay discovery, account/auth state, upload destinations, Dioxus desktop UX, and the reusable `psynet` client.
- For cross-repo work, check each repo's own `AGENTS.md`, `README.md`, and `justfile` before choosing commands.

View File

@@ -19,6 +19,7 @@ Bundled helpers:
- Prioritize easy wins first (`nix-collect-garbage`, container prune, Cargo artifacts).
- Propose destructive actions with expected impact before running them.
- Run destructive actions only after confirmation, unless the user explicitly requests immediate execution of obvious wins.
- For Rust build artifacts, do not repeatedly ask for confirmation before deleting explicit directories literally named `target` after `rust_target_dirs.py delete` validates them. Cargo targets are rebuildable artifacts; when the user asks to clean Rust target directories, validate with the helper, delete with `--yes`, and report the reclaimed space.
- Capture new reusable findings by updating this skill before finishing.
## Workflow
@@ -77,13 +78,13 @@ Do not start with a blind `find ~ -name target` or with hard-coded roots that ma
Inventory the biggest candidates:
```bash
python /home/imalison/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py list --min-size 500M --limit 30
python /srv/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py list --min-size 500M --limit 30
```
Focus on stale targets only:
```bash
python /home/imalison/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py list --min-size 1G --older-than 14 --output tsv
python /srv/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py list --min-size 1G --older-than 14 --output tsv
```
Use `cargo-sweep` when the repo is still active and you want age/toolchain-aware cleanup inside a workspace:
@@ -98,13 +99,13 @@ nix run nixpkgs#cargo-sweep -- sweep -r -i <workspace-root>
Use direct `target/` deletion when inventory shows a discrete stale directory, especially for inactive repos or project-local worktrees. The helper only deletes explicit paths named `target` that are beneath configured roots and a Cargo project:
```bash
python /home/imalison/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py delete /abs/path/to/target
python /home/imalison/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py delete /abs/path/to/target --yes
python /srv/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py delete /abs/path/to/target
python /srv/dotfiles/dotfiles/agents/skills/disk-space-cleanup/scripts/rust_target_dirs.py delete /abs/path/to/target --yes
```
Recommended sequence:
1. Run `rust_target_dirs.py list` to see the largest `target/` directories across `~/Projects`, `~/org`, `~/dotfiles`, and other configured roots.
1. Run `rust_target_dirs.py list` to see the largest `target/` directories across `~/Projects`, `~/org`, `/srv/dotfiles`, and other configured roots.
2. For active repos, prefer `cargo-sweep` from the workspace root.
3. For inactive repos, abandoned branches, and `.worktrees/*/target`, prefer guarded direct deletion of the explicit `target/` directory.
4. Re-run the list command after each deletion round to show reclaimed space.
@@ -113,7 +114,10 @@ Machine-specific note:
- Project-local `.worktrees/*/target` directories are common cleanup wins on this machine and are easy to miss with the old hard-coded workflow.
- `cargo-sweep` is installed through the NixOS `code.nix` package set, but stale manually-installed binaries under `~/.cargo/bin` can shadow `/run/current-system/sw/bin/cargo-sweep`. If `cargo sweep` fails with a missing loader or `No such file or directory`, run `type -a cargo-sweep` and remove the stale `~/.cargo/bin/cargo-sweep` entry.
- `nixos/imalison.nix` defines a daily user timer, `cargo-sweep-rust-targets.timer`, that runs `cargo-sweep sweep -r --hidden --maxsize 15GB` across `/home/imalison/Projects`, `/home/imalison/org`, and `/home/imalison/dotfiles`.
- `cargo-sweep sweep -i/--installed` can fail when `rustup toolchain list` contains stale toolchains whose `rustc` no longer exists. On this machine, `1.68.2-x86_64-unknown-linux-gnu` caused `failed to determine fingerprint ... 'rustc': No such file or directory`.
- `/home/imalison/Projects/codex/codex-rs/target` can be dominated by current-looking `target/debug/incremental` data that `cargo-sweep sweep -a` and `--maxsize` report as not removable. If it is stale and space pressure is high, use the guarded `rust_target_dirs.py delete ... --yes` workflow for that explicit target directory.
- `/home/imalison/Projects/hypr-workspace-history/target` is a small non-Cargo false positive; the guarded delete workflow correctly rejects it because there is no Cargo project above the directory.
- `nixos/imalison.nix` defines a daily user timer, `cargo-sweep-rust-targets.timer`, that runs `cargo-sweep sweep -r --hidden --maxsize 15GB` across `/home/imalison/Projects`, `/home/imalison/org`, and `/srv/dotfiles`.
## Step 4: Investigation with `ncdu` and `du`
@@ -171,6 +175,11 @@ Machine-specific heavy hitters seen in practice:
- Validated cleanup pattern: stop `gitea-runner-nix.service`, remove cache/work directories under `/var/lib/private/gitea-runner` (`.cache`, `.gradle`, `action-cache-dir`, `workspace`, stale nested `gitea-runner`, and nested `nix/.cache`/`nix/.local`), recreate `action-cache-dir`, `workspace`, and `.cache` owned by `gitea-runner:gitea-runner`, then restart the service.
- Preserve registration/config-like files such as `/var/lib/private/gitea-runner/nix/.runner`, `/var/lib/private/gitea-runner/nix/.labels`, `/var/lib/private/gitea-runner/.docker/config.json`, and SSH/Kube material.
- `~/Projects/*/target` directories can dominate home usage. Recent example candidates included stale `target/` directories under `scrobble-scrubber`, `http-client-vcr`, `http-client`, `subtr-actor`, `http-types`, `subtr-actor-py`, `sdk`, and `async-h1`.
- 2026-05-26 cleanup: deleting explicit Cargo-backed targets under `~/Projects/{keepbook,subtr-actor,rlru,rocket-sense,boxcars,rumno}` plus stale `subtr-actor/.worktrees/*/target` reclaimed about 65G by helper sizing and moved `/` from 100% used to 89% used. A final all-depth scan left no `~/Projects` Rust `target/` directories over 500M.
- 2026-05-26 cleanup: when `cargo test` is actively running in `~/Projects/subtr-actor`, leave `subtr-actor/target` alone and delete only inactive Cargo-backed targets. Deleting `keepbook`, `rlru`, `rocket-sense`, `rumno`, and stale `subtr-actor/.worktrees/*/target` reclaimed about 24.5G by helper sizing.
- 2026-05-26 cleanup: `~/Projects/nixpkgs/.worktrees/*/result` symlinks pinned several GiB of Nix closures, and clean registered nixpkgs worktrees were about 460M each. Removing stale `result` symlinks, running GC, and removing clean worktrees while preserving dirty ones moved `/` from 100% used to about 90% used.
- 2026-05-27 cleanup: under `~/Projects`, `hypr-workspace-history/target` can be a Rust-style build cache even though the guarded helper rejects it because no `Cargo.toml` is present; inspect and remove that explicit cache manually if present. Preserve `~/Projects/Hyprland/src/layout/target`, which is source code, not a build artifact.
- 2026-06-18 cleanup: deleting helper-validated Rust targets under `.worktrees/*/target` and `.claude/worktrees/*/target`, plus stale `~/Projects/lastfm-edit/target`, removed 24 target directories totaling 67.1G by helper sizing and moved `/` from 99% used to 90% used. Remaining large targets were top-level project caches under `keepbook`, `rlru`, `subtr-actor`, `rocket-sense`, `rocket-sense-pr-73-ci`, `rocket-sense-subtr-viewer`, `rocket-sense-controlled-plays`, and `boxcars`.
## Step 5: `/nix/store` Deep Dive
@@ -195,7 +204,7 @@ nix-store --gc --print-roots | rg '(ghc|rust)'
Resolve why a path is retained:
```bash
/home/imalison/dotfiles/dotfiles/lib/functions/find_store_path_gc_roots /nix/store/<store-path>
/srv/dotfiles/dotfiles/lib/functions/find_store_path_gc_roots /nix/store/<store-path>
nix why-depends <consumer-store-path> <dependency-store-path>
```
@@ -204,8 +213,9 @@ Common retention pattern on this machine:
- Many `.direnv/flake-profile-*` symlinks under `~/Projects` and worktrees keep `nix-shell-env`/`ghc-shell-*` roots alive.
- Old taffybar constellation repos under `~/Projects` can pin large Haskell closures through `.direnv` and `result` symlinks. Deleting `gtk-sni-tray`, `status-notifier-item`, `dbus-menu`, `dbus-hslogger`, and `gtk-strut` and then rerunning `nix-collect-garbage -d` reclaimed about 11G of store data in one validated run.
- `find_store_path_gc_roots` is especially useful for proving GHC retention: many large `ghc-9.10.3-with-packages` paths are unique per project, while the base `ghc-9.10.3` and docs paths are shared.
- NixOS system generations and a repo-root `nixos/result` symlink can pin multiple Android Studio and Android SDK versions. Check `/nix/var/nix/profiles/system-*-link`, `/run/current-system`, `/run/booted-system`, and `~/dotfiles/nixos/result` before assuming Android paths are pinned by project shells.
- NixOS system generations and a repo-root `nixos/result` symlink can pin multiple Android Studio and Android SDK versions. Check `/nix/var/nix/profiles/system-*-link`, `/run/current-system`, `/run/booted-system`, and `/srv/dotfiles/nixos/result` before assuming Android paths are pinned by project shells.
- `~/Projects/railbird-mobile/.direnv/flake-profile-*` can pin large Android SDK system images. Removing stale direnv profiles there is a more targeted first step than deleting Android store paths directly.
- 2026-05-27 Railbird GHC audit: the Railbird backend flake did not explicitly reference Haskell, but its dev shell had derivation-time GHC edges through `inputs.secrets.devShells.${system}.default -> agenix -> shellcheck -> ShellCheck -> ghc` and through `shell-packages.nix`'s `rdma-core -> pandoc-cli -> ghc`. Railbird Mobile had similar non-app-code GHC edges through `inputs.secrets`/`agenix` and `nixGLIntel -> shellcheck`. The `railbird/gql` and `railbird-mobile/src/gql` shells did not show GHC edges in their derivation graphs, only Rust/Cargo build tooling from packages such as `just`.
- For a repeatable `/nix/store` `ncdu` snapshot without driving the TUI, export and inspect it:
```bash
@@ -240,7 +250,7 @@ nix-store --gc --print-roots | rg '/\\.direnv/flake-profile-' | awk -F' -> ' '{p
- Do not delete user files directly unless explicitly requested.
- Prefer cleanup tools that understand ownership/metadata (`nix`, `docker`, `podman`, `cargo-sweep`) over `rm -rf`.
- For Rust build artifacts, deleting an explicit directory literally named `target` is acceptable when it is discovered by the bundled helper; Cargo will rebuild it.
- For Rust build artifacts, deleting an explicit directory literally named `target` is acceptable when it is discovered and validated by the bundled helper; Cargo will rebuild it. Do not double-check with the user after helper validation when the active request is Rust target cleanup.
- Present a concise “proposed actions” list before high-impact deletes.
- If uncertain whether data is needed, stop at investigation and ask.

View File

@@ -3,4 +3,4 @@
/home/imalison/Projects
/home/imalison/org
/home/imalison/dotfiles
/srv/dotfiles

View File

@@ -7,7 +7,7 @@ description: Use when investigating production org-agenda-api state, testing end
## Overview
Access the production org-agenda-api instance at https://colonelpanic-org-agenda.fly.dev/ for debugging, testing, or verification.
Access the production org-agenda-api instance at https://org-agenda-api.rocket-sense.duckdns.org/ for debugging, testing, or verification.
## Credentials
@@ -20,10 +20,10 @@ Username is currently `imalison`.
## Quick Access with just
This repo includes a `justfile` under `~/dotfiles/org-agenda-api` with pre-configured commands:
This repo includes a `justfile` under `/srv/dotfiles/org-agenda-api` with pre-configured commands:
```bash
cd ~/dotfiles/org-agenda-api
cd /srv/dotfiles/org-agenda-api
just health
just get-all-todos
just get-todays-agenda

View File

@@ -11,16 +11,16 @@ HTTP API for org-mode agenda data. Use this skill when you need to query or modi
Get credentials from pass:
```bash
pass show colonelpanic-org-agenda.fly.dev
pass show org-agenda-api-imalison
```
Returns: password on first line, then `user:` and `url:` fields.
Returns: password on first line. The username is currently `imalison`.
**Note:** The `url` field in pass may be outdated. Use the base URL below.
## Base URL
`https://colonelpanic-org-agenda.fly.dev`
`https://org-agenda-api.rocket-sense.duckdns.org`
All requests use Basic Auth with the credentials from pass.

View File

@@ -46,7 +46,7 @@ cd ~/.config/taffybar/taffybar && nix flake update <pkg>
cd ~/.config/taffybar && nix flake update <pkg> taffybar
# Top:
cd ~/dotfiles/nixos && nix flake update imalison-taffybar
cd /srv/dotfiles/nixos && nix flake update imalison-taffybar
```
Not every change requires touching all three layers. Think about which flake.lock files actually contain stale references:
@@ -58,7 +58,7 @@ Not every change requires touching all three layers. Think about which flake.loc
## Rebuilding
```bash
cd ~/dotfiles/nixos && just switch
cd /srv/dotfiles/nixos && just switch
```
If taffybar seems stale after a rebuild, check whether the flake.lock at each layer actually points at the expected revision — a missed cascade step is the usual cause.

View File

@@ -1,6 +0,0 @@
*
!.gitignore
!CLAUDE.md
!settings.json
!settings.local.json
!settings.local.json.example

View File

@@ -1 +0,0 @@
../agents/AGENTS.md

View File

@@ -0,0 +1 @@
@~/.agents/AGENTS.md

View File

@@ -13,9 +13,12 @@
},
"enabledPlugins": {
"superpowers@superpowers-marketplace": true,
"agent-browser@agent-browser": true
"agent-browser@agent-browser": true,
"chrome-devtools-mcp@claude-plugins-official": true
},
"effortLevel": "high",
"skipDangerousModePermissionPrompt": true,
"remoteControlAtStartup": true
"remoteControlAtStartup": true,
"inputNeededNotifEnabled": true,
"agentPushNotifEnabled": true
}

1
dotfiles/claude/skills Symbolic link
View File

@@ -0,0 +1 @@
../agents/skills

View File

@@ -1,8 +0,0 @@
*
!.gitignore
!AGENTS.md
!config.toml
!skills
# Legacy generated/local Codex state under this repo stays ignored. Active
# host-local Codex fragments now live under ~/.codex.

View File

@@ -9,12 +9,12 @@ suppress_unstable_features_warning = true
# ~/.codex/config.local-state.toml.
[mcp_servers.chrome-devtools]
command = "npx"
args = ["-y", "chrome-devtools-mcp@latest", "--auto-connect"]
command = "/usr/bin/env"
args = ["PATH=/etc/profiles/per-user/imalison/bin:/run/current-system/sw/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin", "npx", "-y", "chrome-devtools-mcp@latest", "--auto-connect"]
[mcp_servers.observability]
command = "npx"
args = ["-y", "@google-cloud/observability-mcp"]
command = "/usr/bin/env"
args = ["PATH=/etc/profiles/per-user/imalison/bin:/run/current-system/sw/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin", "npx", "-y", "@google-cloud/observability-mcp"]
[mcp_servers.openaiDeveloperDocs]
url = "https://developers.openai.com/mcp"

View File

@@ -0,0 +1,37 @@
output HDMI-A-0
off
output DisplayPort-1-1
off
output DisplayPort-1-2
off
output DisplayPort-1-3
off
output DisplayPort-1-4
off
output DisplayPort-0
crtc 1
mode 2560x1440
pos 0x0
rate 144.00
x-prop-colorspace Default
x-prop-max_bpc 16
x-prop-non_desktop 0
x-prop-scaling_mode None
x-prop-tearfree auto
x-prop-underscan off
x-prop-underscan_hborder 0
x-prop-underscan_vborder 0
output eDP
crtc 0
mode 2560x1600
pos 2560x0
primary
rate 165.00
x-prop-colorspace Default
x-prop-max_bpc 16
x-prop-non_desktop 0
x-prop-scaling_mode None
x-prop-tearfree auto
x-prop-underscan off
x-prop-underscan_hborder 0
x-prop-underscan_vborder 0

View File

@@ -0,0 +1,2 @@
DisplayPort-0 00ffffffffffff0009d1767f45540000281d0103803c22782a9325ad4f44a9260d5054a56b80d1fcd1e8d1c0b300a9c08180810081c0f8e300a0a0a032500820980455502100001a000000ff0033414b30313335343031390a20000000fd0028901ede3c000a202020202020000000fc0042656e5120455832373830510a0174020350f1515d5e5f60613f40101f22212004131203012309070783010000e200cf6d030c001000383c20006001020367d85dc401788003681a000001012890e6e305c301e40f180000e60605016262216fc200a0a0a055503020350055502100001e565e00a0a0a029502f20350055502100001a0000000000000000000000bf
eDP 00ffffffffffff0009e59b0a000000001c1e0104b5221578037ce5a4554c9f260f5054000000010101010101010101010101010101016b6e00a0a04084603020360058d71000001a000000fd0c3ca51f1f4e010a202020202020000000fe00424f452043510a202020202020000000fe004e4531363051444d2d4e59310a02d502031d00e3058000e60605016a6a246d1a000002033ca500046a246a240000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff7013790000030114a52f0185ff099f002f001f003f0683000200050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e90

View File

@@ -5,6 +5,6 @@ general {
listener {
timeout = 300
on-timeout = /home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver start
on-resume = /home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver stop
on-timeout = hypr-screensaver start
on-resume = hypr-screensaver stop
}

View File

@@ -19,16 +19,61 @@ package.path = table.concat({
package.path,
}, ";")
local modules = {
"hyprland.state",
"hyprland.scratchpads",
"hyprland.core",
"hyprland.layouts",
"hyprland.windows",
"hyprland.settings",
"hyprland.binds",
"hyprland.events",
}
local function shell_quote(value)
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
end
local function module_load_phase(name)
if name == "state" then
return 0
elseif name == "binds" then
return 20
elseif name == "events" then
return 30
end
return 10
end
local function discover_modules()
local modules_dir = base_dir .. "/hyprland"
local handle = assert(io.popen("find " .. shell_quote(modules_dir) .. " -maxdepth 1 -type f -name '*.lua' -print"))
local discovered = {}
for path in handle:lines() do
local name = path:match("/([^/]+)%.lua$")
if name then
discovered[#discovered + 1] = {
name = name,
module = "hyprland." .. name,
phase = module_load_phase(name),
}
end
end
handle:close()
table.sort(discovered, function(left, right)
if left.phase ~= right.phase then
return left.phase < right.phase
end
return left.name < right.name
end)
local modules = {}
for _, item in ipairs(discovered) do
modules[#modules + 1] = item.module
end
if modules[1] ~= "hyprland.state" then
error("hyprland/state.lua is required")
end
return modules
end
local modules = discover_modules()
for _, module in ipairs(modules) do
package.loaded[module] = nil

View File

@@ -49,8 +49,8 @@ function M.setup(ctx)
bind("XF86AudioRaiseVolume", exec("set_volume --unmute --change-volume +5"), desc("Raise volume", { repeating = true }))
bind("XF86AudioLowerVolume", exec("set_volume --unmute --change-volume -5"), desc("Lower volume", { repeating = true }))
bind("XF86AudioMute", exec("set_volume --toggle-mute"), desc("Toggle mute"))
bind(hyper .. " + O", exec("/home/imalison/dotfiles/dotfiles/lib/functions/rofi_paswitch"), desc("Open PulseAudio output switcher"))
bind(hyper .. " + SHIFT + O", exec("/home/imalison/dotfiles/dotfiles/lib/bin/kef-optical"), desc("Switch KEF speakers to optical input"))
bind(hyper .. " + O", exec("rofi_paswitch"), desc("Open PulseAudio output switcher"))
bind(hyper .. " + SHIFT + O", exec("kef-optical"), desc("Switch KEF speakers to optical input"))
end
local function setup_display_wallpaper_and_capture_bindings()
@@ -58,16 +58,17 @@ function M.setup(ctx)
bind("XF86MonBrightnessDown", exec("brightness.sh down"), desc("Lower display brightness", { repeating = true }))
bind("Print", exec("flameshot gui"), desc("Take screenshot"))
bind(hyper .. " + H", exec("flameshot gui"), desc("Take screenshot"))
bind(hyper .. " + backslash", exec("/home/imalison/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle"), desc("Toggle monitor input"))
bind(hyper .. " + backslash", exec("mpg341cx_input toggle"), desc("Toggle monitor input"))
bind(hyper .. " + comma", exec("rofi_wallpaper.sh"), desc("Open wallpaper menu"))
bind(hyper .. " + SHIFT + comma", exec("/home/imalison/dotfiles/dotfiles/lib/bin/neowall-wallpaper toggle"), desc("Toggle neowall wallpaper"))
bind(hyper .. " + SHIFT + comma", exec("neowall-wallpaper toggle"), desc("Toggle neowall wallpaper"))
end
local function setup_rofi_and_tool_bindings()
bind(main_mod .. " + X", exec("rofi_command.sh"), desc("Open command menu"))
bind(hyper .. " + V", exec([[cliphist list | rofi -dmenu -p "Clipboard" | cliphist decode | wl-copy]]), desc("Open clipboard history"))
bind(hyper .. " + P", exec("rofi-pass"), desc("Open password menu"))
bind(hyper .. " + C", exec("rofi_tmcodex.sh"), desc("Open Codex session menu"))
bind(hyper .. " + N", exec("rofi_codex_desktop_project.sh"), desc("Start Codex Desktop thread from project"))
bind(hyper .. " + C", exec("rofi_ai_scratchpad.sh"), desc("Choose AI scratchpad (Codex/Claude)"))
bind(hyper .. " + SHIFT + C", exec("rofi_tmcodex.sh resume"), desc("Resume Codex session"))
bind(hyper .. " + L", exec("hypr_rofi_layout"), desc("Open Hyprland layout menu"))
bind(hyper .. " + K", exec("rofi_kill_process.sh"), desc("Open process kill menu"))
@@ -87,9 +88,23 @@ function M.setup(ctx)
end
local function setup_window_overview_bindings()
local function toggle_hyprtasking()
if hl.plugin and hl.plugin.hyprtasking and hl.plugin.hyprtasking.toggle then
hl.plugin.hyprtasking.toggle("cursor")
else
hl.notification.create({
text = "hyprtasking is not loaded",
duration = 1800,
icon = notification_icons.warning,
color = "rgba(edb443ff)",
font_size = 13,
})
end
end
bind(main_mod .. " + SHIFT + C", hl.dsp.window.close(), desc("Close active window"))
bind(main_mod .. " + SHIFT + Q", hl.dsp.exit(), desc("Exit Hyprland"))
bind(main_mod .. " + Tab", hyprexpo("toggle"), desc("Toggle hyprexpo workspace overview", overview_bind_opts))
bind(main_mod .. " + Tab", toggle_hyprtasking, desc("Toggle hyprtasking workspace overview", overview_bind_opts))
bind(main_mod .. " + SHIFT + Tab", hyprwinview({
action = "show",
include_current_workspace = false,
@@ -97,8 +112,7 @@ function M.setup(ctx)
default_action = "bring",
}), desc("Show all-workspace window overview", overview_bind_opts))
bind(main_mod .. " + SHIFT + slash", hyprwinview({ action = "toggle-filter" }), desc("Toggle window overview filter", overview_bind_opts))
bind("ALT + Tab", hyprexpo("toggle"), desc("Toggle hyprexpo workspace overview", overview_bind_opts))
bind("ALT + SHIFT + Tab", hyprexpo("on"), desc("Open hyprexpo workspace overview", overview_bind_opts))
bind("ALT + Tab", toggle_hyprtasking, desc("Toggle hyprtasking workspace overview", overview_bind_opts))
bind(main_mod .. " + G", hyprwinview({
action = "show",
start_in_filter_mode = true,
@@ -227,7 +241,9 @@ function M.setup(ctx)
bind(main_mod .. " + CTRL + Space", gather_workspace_into_tabbed_group, desc("Gather workspace into tabbed group"))
bind(main_mod .. " + bracketright", monocle_next, desc("Focus next monocle window"))
bind(main_mod .. " + bracketleft", monocle_prev, desc("Focus previous monocle window"))
bind(main_mod .. " + T", hl.dsp.window.float({ action = "disable" }), desc("Tile active window"))
bind(main_mod .. " + F", toggle_active_window_real_fullscreen, desc("Toggle active window real fullscreen"))
bind(main_mod .. " + SHIFT + F", toggle_active_window_gaming_mode, desc("Toggle active window gaming fullscreen"))
bind(main_mod .. " + T", tile_or_float_active_window, desc("Tile or float active window"))
bind(main_mod .. " + O", toggle_pinned_active_window, desc("Toggle pinned active window"))
bind(main_mod .. " + M", minimize_active_window, desc("Minimize active window"))
bind(main_mod .. " + SHIFT + M", restore_last_minimized, desc("Restore last minimized window"))
@@ -249,9 +265,11 @@ function M.setup(ctx)
local function setup_scratchpad_bindings()
bind(main_mod .. " + SHIFT + X", hl.dsp.workspace.toggle_special("NSP"), desc("Toggle NSP special workspace"))
bind(mod_alt .. " + C", function()
toggle_scratchpad("codex")
end, desc("Toggle Codex scratchpad"))
bind(mod_alt .. " + C", toggle_active_ai_scratchpad, desc("Toggle AI scratchpad (Codex/Claude)"))
bind(mod_alt .. " + SHIFT + C", toggle_backup_ai_scratchpad, desc("Toggle backup AI scratchpad (Codex/Claude)"))
bind(mod_alt .. " + D", function()
toggle_scratchpad("discord")
end, desc("Toggle Discord scratchpad"))
bind(mod_alt .. " + E", function()
toggle_scratchpad("element")
end, desc("Toggle Element scratchpad"))
@@ -273,6 +291,9 @@ function M.setup(ctx)
bind(mod_alt .. " + V", function()
toggle_scratchpad("volume")
end, desc("Toggle volume scratchpad"))
bind(mod_alt .. " + X", function()
toggle_scratchpad("x_com")
end, desc("Toggle X scratchpad"))
bind(mod_alt .. " + grave", function()
toggle_scratchpad("dropdown")
end, desc("Toggle dropdown scratchpad"))
@@ -312,6 +333,7 @@ function M.setup(ctx)
local function setup_mouse_bindings()
bind(main_mod .. " + mouse:272", float_and_drag_active_window, desc("Float and drag active window"))
bind(main_mod .. " + mouse:273", float_and_resize_active_window, desc("Float and resize active window"))
bind(hyper .. " + mouse:272", float_and_resize_active_window, desc("Float and resize active window"))
end
local function setup_internal_window_manager_bindings()

View File

@@ -198,7 +198,7 @@ function M.setup(ctx)
end
local function apply_hyprexpo_config()
if verify_config or not enable_hyprexpo then
if verify_config or not enable_hyprexpo or enable_hyprtasking then
return
end
@@ -216,10 +216,18 @@ function M.setup(ctx)
keynav_wrap_h = 1,
keynav_wrap_v = 1,
keynav_reading_order = 0,
live_preview_follow_focus = 0,
border_width = 2,
border_color_current = "rgb(66ccff)",
border_color_focus = "rgb(edb443)",
border_color_hover = "rgb(aabbcc)",
window_icon_enable = 1,
window_icon_position = "bottom-right",
window_icon_size = 32,
window_icon_offset_x = 8,
window_icon_offset_y = 8,
window_icon_bg_enable = 1,
window_icon_bg_color = 0x88000000,
tile_rounding = 5,
tile_rounding_power = 2.0,
label_enable = 1,

View File

@@ -26,7 +26,7 @@ function M.setup(ctx)
}
fullscreen_states[address] = current
if window.floating or current_layout == monocle_layout then
if window.floating or current_layout == monocle_layout or is_game_like_window(window) then
return
end
@@ -41,6 +41,7 @@ function M.setup(ctx)
apply_hyprexpo_config()
apply_hyprwinview_config()
apply_hyprwobbly_config()
apply_dynamic_cursors_config()
apply_hyprglass_config()
apply_visual_performance_mode()
apply_rules()
@@ -60,6 +61,7 @@ function M.setup(ctx)
hl.on("config.reloaded", apply_hyprexpo_config)
hl.on("config.reloaded", apply_hyprwinview_config)
hl.on("config.reloaded", apply_hyprwobbly_config)
hl.on("config.reloaded", apply_dynamic_cursors_config)
hl.on("config.reloaded", apply_hyprglass_config)
hl.on("config.reloaded", apply_visual_performance_mode)
hl.on("config.reloaded", apply_rules)
@@ -83,7 +85,6 @@ function M.setup(ctx)
hl.on("window.move_to_workspace", update_monocle_notice)
hl.on("window.fullscreen", reconcile_fullscreen_state)
hl.on("window.update_rules", reconcile_fullscreen_state)
hl.on("window.open", adopt_matching_scratchpad_window)
hl.on("window.class", adopt_matching_scratchpad_window)
hl.on("window.title", adopt_matching_scratchpad_window)

View File

@@ -2,18 +2,63 @@ local M = {}
function M.setup(ctx)
local _ENV = ctx
local configure_quadrants_master
local focus_workspace
local move_window_to_workspace
local function is_nstack_layout(layout)
return layout == columns_layout or layout == grid_layout
end
local function hyprland_layout(layout)
if layout == grid_layout then
if layout == quadrants_layout then
return large_main_layout
elseif layout == grid_layout then
return columns_layout
end
return layout
end
configure_quadrants_master = function()
if quadrants_arranging or current_layout ~= quadrants_layout then
return
end
local workspace = active_workspace()
if not is_normal_workspace(workspace) then
return
end
local windows = tiled_windows(workspace)
if #windows == 0 then
return
end
sort_windows_by_visual_position(windows)
quadrants_arranging = true
dispatch(hl.dsp.focus({ window = window_selector(windows[1]) }))
dispatch(hl.dsp.layout("orientationleft"))
dispatch(hl.dsp.layout("mfact exact 0.5"))
for _ = 1, #windows do
dispatch(hl.dsp.layout("removemaster"))
end
if #windows >= 3 then
dispatch(hl.dsp.layout("addmaster"))
end
quadrants_arranging = false
focus_workspace(workspace.id)
end
local function update_nstack_count()
if current_layout == quadrants_layout then
configure_quadrants_master()
return
end
if not enable_nstack or not is_nstack_layout(current_layout) then
return
end
@@ -98,7 +143,10 @@ function M.setup(ctx)
hl.config({ general = { layout = hyprland_layout(layout) } })
write_layout_state()
if is_nstack_layout(layout) then
if layout == quadrants_layout then
dismiss_monocle_notice()
schedule_nstack_count_update()
elseif is_nstack_layout(layout) then
dismiss_monocle_notice()
schedule_nstack_count_update()
else
@@ -127,7 +175,10 @@ function M.setup(ctx)
hl.config({ general = { layout = hyprland_layout(current_layout) } })
write_layout_state()
if is_nstack_layout(current_layout) then
if current_layout == quadrants_layout then
dismiss_monocle_notice()
schedule_nstack_count_update()
elseif is_nstack_layout(current_layout) then
dismiss_monocle_notice()
schedule_nstack_count_update()
else
@@ -210,11 +261,11 @@ function M.setup(ctx)
dispatch(hl.dsp.window.swap({ direction = direction }))
end
local function focus_workspace(workspace_id)
focus_workspace = function(workspace_id)
dispatch(hl.dsp.focus({ workspace = tostring(workspace_id), on_current_monitor = true }))
end
local function move_window_to_workspace(workspace_id, follow, window)
move_window_to_workspace = function(workspace_id, follow, window)
local target_window = window or hl.get_active_window()
local target_selector = window_selector(target_window)
dispatch(hl.dsp.window.move({ workspace = tostring(workspace_id), follow = false, window = target_selector }))
@@ -261,6 +312,50 @@ function M.setup(ctx)
return false
end
local function window_contains_point(window, x, y)
local at = window and window.at
local size = window and window.size
if not at or not size then
return false
end
local left = tonumber(at.x or at[1])
local top = tonumber(at.y or at[2])
local width = tonumber(size.x or size[1])
local height = tonumber(size.y or size[2])
if not left or not top or not width or not height then
return false
end
return x >= left and x < left + width and y >= top and y < top + height
end
-- With follow_mouse=1, a bare focus dispatch does not survive the next
-- pointer motion unless the cursor already sits inside the target window,
-- so warp the cursor into the window when needed.
local function focus_window_with_cursor(window)
local selector = window_selector(window)
if not selector then
return false
end
local live = type(hl.get_window) == "function" and hl.get_window(selector) or window
if not live then
return false
end
dispatch(hl.dsp.focus({ window = selector }))
local cursor = hl.get_cursor_pos and hl.get_cursor_pos()
if cursor and window_contains_point(live, cursor.x, cursor.y) then
return true
end
local center_x, center_y = window_center(live)
dispatch(hl.dsp.cursor.move({ x = math.floor(center_x), y = math.floor(center_y) }))
return true
end
local function find_tabbed_group_anchor(state)
local active = hl.get_active_window()
if active and active.group and active.group.size and active.group.size > 1 then
@@ -271,8 +366,16 @@ function M.setup(ctx)
return nil
end
local workspace = active_workspace()
for _, window in ipairs(hl.get_windows()) do
if window and window.address == state.anchor and window.group and window.group.size and window.group.size > 1 then
if
window
and window.address == state.anchor
and same_workspace(window.workspace, workspace)
and window.group
and window.group.size
and window.group.size > 1
then
return window
end
end
@@ -327,6 +430,7 @@ function M.setup(ctx)
local function restore_workspace_tabbed_group()
local key = workspace_key()
local state = tabbed_workspace_groups[key]
local entry_focused = hl.get_active_window()
local anchor = find_tabbed_group_anchor(state)
local anchor_selector = window_selector(anchor)
local target_workspace_id = anchor and anchor.workspace and anchor.workspace.id
@@ -343,7 +447,9 @@ function M.setup(ctx)
tabbed_workspace_groups[key] = nil
set_layout(columns_layout)
restore_tabbed_group_window_order(state, target_workspace_id)
dispatch(hl.dsp.focus({ window = anchor_selector }))
if not focus_window_with_cursor(entry_focused) then
focus_window_with_cursor(anchor)
end
schedule_nstack_count_update()
end
@@ -418,6 +524,9 @@ function M.setup(ctx)
dispatch(hl.dsp.focus({ window = anchor_selector }))
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
notify_tabbed_group("Unable to group tiled windows")
if not focus_window_with_cursor(focused) then
focus_window_with_cursor(anchor)
end
return
elseif grouped_count < #candidates then
notify_tabbed_group("Grouped " .. tostring(grouped_count) .. " of " .. tostring(#candidates) .. " tiled windows")
@@ -428,7 +537,9 @@ function M.setup(ctx)
order = original_order,
windows = candidate_addresses,
}
dispatch(hl.dsp.focus({ window = anchor_selector }))
if not focus_window_with_cursor(focused) then
focus_window_with_cursor(anchor)
end
end
local function force_columns_layout()
@@ -556,6 +667,7 @@ function M.setup(ctx)
ctx.is_nstack_layout = is_nstack_layout
ctx.hyprland_layout = hyprland_layout
ctx.configure_quadrants_master = configure_quadrants_master
ctx.update_nstack_count = update_nstack_count
ctx.schedule_nstack_count_update = schedule_nstack_count_update
ctx.dismiss_monocle_notice = dismiss_monocle_notice

View File

@@ -15,11 +15,21 @@ function M.setup(ctx)
codex = {
command = "codex_desktop_scratchpad",
class = "codex-desktop",
allow_tiling = true,
},
claude = {
command = "claude-desktop",
class = "claude-desktop",
allow_tiling = true,
},
htop = {
command = "alacritty --class htop-scratch --title htop -e htop",
class = "htop-scratch",
},
discord = {
command = "discord",
class = "discord",
},
volume = {
command = "pavucontrol",
class = "org.pulseaudio.pavucontrol",
@@ -41,6 +51,12 @@ function M.setup(ctx)
command = "google-chrome-stable --profile-directory=Default --app=https://messages.google.com/web/conversations",
class = "chrome-messages.google.com",
},
x_com = {
command = "x-com-pwa",
classes = { "x-com-pwa", "chrome-x.com" },
title = "X",
allow_tiling = true,
},
transmission = {
command = "transmission-gtk",
class = "transmission-gtk",
@@ -82,9 +98,13 @@ function M.setup(ctx)
and lower_contains(window.title, def.title)
end
local function tiled_scratchpad_is_normal_window(window, def)
return def.allow_tiling and window and window.floating == false
end
local function is_scratchpad_window(window)
for _, def in pairs(scratchpads) do
if scratchpad_window_matches(window, def) then
if scratchpad_window_matches(window, def) and not tiled_scratchpad_is_normal_window(window, def) then
return true
end
end
@@ -101,7 +121,7 @@ function M.setup(ctx)
end
local function scratchpad_workspace(name)
return "special:scratch-" .. name
return "name:scratch-hidden-" .. name
end
local function as_number(value, default)
@@ -238,6 +258,24 @@ function M.setup(ctx)
return windows
end
local function default_scratchpad_geometry(target_monitor)
local monitor = target_monitor or hl.get_active_monitor()
if not monitor then
return
end
local workarea = monitor_workarea(monitor)
local width = math.floor(workarea.width * scratchpad_size_ratio)
local height = math.floor(workarea.height * scratchpad_size_ratio)
return {
width = width,
height = height,
x = workarea.x + math.floor((workarea.width - width) / 2),
y = workarea.y + math.floor((workarea.height - height) / 2),
}
end
local function scratchpad_geometry(name, target_monitor, position)
local def = scratchpads[name]
local monitor = target_monitor or hl.get_active_monitor()
@@ -261,10 +299,7 @@ function M.setup(ctx)
y = position
end
else
width = math.floor(workarea.width * scratchpad_size_ratio)
height = math.floor(workarea.height * scratchpad_size_ratio)
x = workarea.x + math.floor((workarea.width - width) / 2)
y = workarea.y + math.floor((workarea.height - height) / 2)
return default_scratchpad_geometry(monitor)
end
return {
@@ -275,6 +310,23 @@ function M.setup(ctx)
}
end
local function should_apply_scratchpad_geometry(name, window, opts)
local def = scratchpads[name]
if not def then
return false
end
return (opts and opts.force_geometry) or def.dropdown or not tiled_scratchpad_is_normal_window(window, def)
end
local function refreshed_window(window)
if not window or not window.address or type(hl.get_window) ~= "function" then
return window
end
return hl.get_window(window_selector(window)) or window
end
local function apply_scratchpad_geometry(name, window, target_monitor, position)
local def = scratchpads[name]
if not def or not window then
@@ -298,9 +350,12 @@ function M.setup(ctx)
end
end
local function schedule_scratchpad_geometry(name, window, target_monitor, position, timeout)
local function schedule_scratchpad_geometry(name, window, target_monitor, position, timeout, opts)
hl.timer(function()
apply_scratchpad_geometry(name, window, target_monitor, position)
local current_window = refreshed_window(window)
if should_apply_scratchpad_geometry(name, current_window, opts) then
apply_scratchpad_geometry(name, current_window, target_monitor, position)
end
end, { timeout = timeout or 50, type = "oneshot" })
end
@@ -332,8 +387,17 @@ function M.setup(ctx)
move_window_to_workspace(scratchpad_workspace(name), false, window)
end
local function show_scratchpad_window(name, window, workspace, target_monitor)
local function scratchpad_show_workspace(workspace)
workspace = workspace or active_workspace()
if is_normal_workspace(workspace) then
return workspace
end
return hl.get_workspace(tostring(active_workspace_id()))
end
local function show_scratchpad_window(name, window, workspace, target_monitor, opts)
workspace = scratchpad_show_workspace(workspace)
if not workspace then
return
end
@@ -346,8 +410,8 @@ function M.setup(ctx)
dispatch(hl.dsp.focus({ window = window_selector(window) }))
if scratchpads[name] and scratchpads[name].dropdown then
animate_dropdown_scratchpad_down(name, window, target_monitor or hl.get_active_monitor())
else
schedule_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor())
elseif should_apply_scratchpad_geometry(name, window, opts) then
schedule_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor(), nil, nil, opts)
end
end
@@ -362,7 +426,12 @@ function M.setup(ctx)
local windows = {}
for _, window in ipairs(hl.get_windows()) do
local name = matching_scratchpad_name(window)
if name and name ~= except_name and scratchpad_is_visible(window) then
if
name
and name ~= except_name
and scratchpad_is_visible(window)
and not tiled_scratchpad_is_normal_window(window, scratchpads[name])
then
windows[#windows + 1] = {
name = name,
window = window,
@@ -404,7 +473,9 @@ function M.setup(ctx)
if scratchpad_pending[name] then
local pending = scratchpad_pending[name]
scratchpad_pending[name] = nil
show_scratchpad_window(name, window, pending.workspace or active_workspace(), pending.monitor or hl.get_active_monitor())
show_scratchpad_window(name, window, pending.workspace or active_workspace(), pending.monitor or hl.get_active_monitor(), {
force_geometry = true,
})
elseif scratchpad_is_visible(window) then
schedule_scratchpad_geometry(name, window, hl.get_active_monitor())
end
@@ -424,10 +495,12 @@ function M.setup(ctx)
local windows = matching_scratchpad_windows(name)
if #windows == 0 then
local workspace = scratchpad_show_workspace()
local target_monitor = hl.get_active_monitor()
hide_active_scratchpads(name)
scratchpad_pending[name] = {
monitor = hl.get_active_monitor(),
workspace = active_workspace(),
monitor = target_monitor,
workspace = workspace,
}
hl.exec_cmd(def.command)
return
@@ -446,18 +519,73 @@ function M.setup(ctx)
hide_scratchpad_window(name, window)
end
else
hide_active_scratchpads(name)
local workspace = active_workspace()
local workspace = scratchpad_show_workspace()
local target_monitor = hl.get_active_monitor()
hide_active_scratchpads(name)
for _, window in ipairs(windows) do
show_scratchpad_window(name, window, workspace, target_monitor)
end
end
end
-- Which AI scratchpad SUPER+ALT+C targets. Selected at runtime (no reload)
-- by rofi_ai_scratchpad.sh, which writes the chosen name to this file.
local ai_scratchpad_default = "codex"
local function ai_scratchpad_state_path()
local base = os.getenv("XDG_STATE_HOME") or ((os.getenv("HOME") or "") .. "/.local/state")
return base .. "/hypr/ai-scratchpad"
end
local function active_ai_scratchpad()
local file = io.open(ai_scratchpad_state_path(), "r")
if not file then
return ai_scratchpad_default
end
local value = file:read("*l")
file:close()
value = value and value:gsub("%s+", "")
if scratchpads[value] then
return value
end
return ai_scratchpad_default
end
local function toggle_active_ai_scratchpad()
toggle_scratchpad(active_ai_scratchpad())
end
local function backup_ai_scratchpad()
if active_ai_scratchpad() == "codex" then
return "claude"
end
return "codex"
end
local function toggle_backup_ai_scratchpad()
toggle_scratchpad(backup_ai_scratchpad())
end
-- Used by rofi_ai_scratchpad.sh after a selection: bring the chosen
-- scratchpad into view if it isn't already, without hiding it when it is.
local function show_active_ai_scratchpad()
local name = active_ai_scratchpad()
for _, window in ipairs(matching_scratchpad_windows(name)) do
if scratchpad_is_visible(window) then
return
end
end
toggle_scratchpad(name)
end
_G.im_hyprland_toggle_ai_scratchpad = toggle_active_ai_scratchpad
_G.im_hyprland_show_ai_scratchpad = show_active_ai_scratchpad
ctx.lower_contains = lower_contains
ctx.lower_contains_any = lower_contains_any
ctx.scratchpad_window_matches = scratchpad_window_matches
ctx.tiled_scratchpad_is_normal_window = tiled_scratchpad_is_normal_window
ctx.is_scratchpad_window = is_scratchpad_window
ctx.matching_scratchpad_name = matching_scratchpad_name
ctx.scratchpad_workspace = scratchpad_workspace
@@ -470,12 +598,16 @@ function M.setup(ctx)
ctx.refresh_monitor_reserved_cache = refresh_monitor_reserved_cache
ctx.monitor_workarea = monitor_workarea
ctx.scratchpad_geometry = scratchpad_geometry
ctx.should_apply_scratchpad_geometry = should_apply_scratchpad_geometry
ctx.refreshed_window = refreshed_window
ctx.matching_scratchpad_windows = matching_scratchpad_windows
ctx.default_scratchpad_geometry = default_scratchpad_geometry
ctx.apply_scratchpad_geometry = apply_scratchpad_geometry
ctx.schedule_scratchpad_geometry = schedule_scratchpad_geometry
ctx.dropdown_spring_progress = dropdown_spring_progress
ctx.animate_dropdown_scratchpad_down = animate_dropdown_scratchpad_down
ctx.hide_scratchpad_window = hide_scratchpad_window
ctx.scratchpad_show_workspace = scratchpad_show_workspace
ctx.show_scratchpad_window = show_scratchpad_window
ctx.scratchpad_is_visible = scratchpad_is_visible
ctx.active_scratchpad_windows = active_scratchpad_windows
@@ -485,6 +617,11 @@ function M.setup(ctx)
ctx.refresh_shell_workarea_and_scratchpads = refresh_shell_workarea_and_scratchpads
ctx.adopt_matching_scratchpad_window = adopt_matching_scratchpad_window
ctx.toggle_scratchpad = toggle_scratchpad
ctx.active_ai_scratchpad = active_ai_scratchpad
ctx.backup_ai_scratchpad = backup_ai_scratchpad
ctx.toggle_active_ai_scratchpad = toggle_active_ai_scratchpad
ctx.toggle_backup_ai_scratchpad = toggle_backup_ai_scratchpad
ctx.show_active_ai_scratchpad = show_active_ai_scratchpad
end
return M

View File

@@ -2,6 +2,7 @@ local M = {}
function M.setup(ctx)
local _ENV = ctx
local file_chooser_class_rule = "^(xdg-desktop-portal-gtk|org\\.freedesktop\\.impl\\.portal\\.desktop\\.gtk)$"
local file_chooser_title_rule = "^(Open File|Open Files|Save File|Save Files|Save As|Select File|Select Files|Choose File|Choose Files|File Upload|Upload File|Upload Files|Select Folder|Choose Folder|Open Folder|Save Folder)$"
local function lower_string(value)
@@ -41,9 +42,20 @@ function M.setup(ctx)
or title:find("file picker", 1, true) ~= nil
end
local function class_indicates_file_chooser(class)
class = lower_string(class)
return class == "xdg-desktop-portal-gtk"
or class == "org.freedesktop.impl.portal.desktop.gtk"
end
local function is_file_chooser_window(window)
return window
and (title_indicates_file_chooser(window.title) or title_indicates_file_chooser(window.initial_title))
and (
title_indicates_file_chooser(window.title)
or title_indicates_file_chooser(window.initial_title)
or class_indicates_file_chooser(window.class)
or class_indicates_file_chooser(window.initial_class)
)
end
local function raise_file_chooser_window(window)
@@ -72,24 +84,31 @@ function M.setup(ctx)
if enable_nstack and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprNStack.so")
end
if enable_hyprexpo and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprexpo.so")
end
if enable_hyprwinview and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprwinview.so")
end
if enable_hyprtasking and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprtasking.so")
os.execute("hyprctl eval 'hl.config({plugin={hyprtasking={full_render=true}}})' >/dev/null 2>&1 || true")
end
if enable_hyprexpo and not enable_hyprtasking and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprexpo.so")
end
if enable_workspace_history and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhypr-workspace-history.so")
end
if enable_hyprwobbly and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprwobbly.so")
end
if enable_dynamic_cursors and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhypr-dynamic-cursors.so")
end
if enable_hyprglass and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/hyprglass.so")
end
hl.env("XCURSOR_SIZE", "24")
hl.env("HYPRCURSOR_SIZE", "24")
hl.env("XCURSOR_SIZE", tostring(hyprland_cursor_size))
hl.env("HYPRCURSOR_SIZE", tostring(hyprland_cursor_size))
hl.env("QT_QPA_PLATFORMTHEME", "qt5ct")
hl.env("HYPR_MAX_WORKSPACE", "9")
@@ -110,8 +129,8 @@ function M.setup(ctx)
persistent_warps = true,
},
general = {
gaps_in = 5,
gaps_out = 10,
gaps_in = hyprland_gaps_enabled and 5 or 0,
gaps_out = hyprland_gaps_enabled and 10 or 0,
border_size = 2,
col = {
active_border = { colors = { "rgba(3b82f6ee)", "rgba(33ccffee)" }, angle = 45 },
@@ -166,6 +185,7 @@ function M.setup(ctx)
force_default_wallpaper = 0,
disable_hyprland_logo = true,
exit_window_retains_fullscreen = true,
focus_on_activate = true,
},
})
@@ -275,6 +295,45 @@ function M.setup(ctx)
})
end
local function apply_dynamic_cursors_config()
if verify_config or not enable_dynamic_cursors then
return
end
hl.config({
plugin = {
dynamic_cursors = {
enabled = true,
mode = "tilt",
threshold = 2,
tilt = {
limit = 5000,
activation = "negative_quadratic",
window = 100,
full = 60,
},
shake = {
enabled = true,
threshold = 6.0,
base = 4.0,
speed = 4.0,
influence = 0.0,
limit = 0.0,
timeout = 2000,
effects = true,
ipc = false,
},
hyprcursor = {
nearest = 1,
enabled = true,
resolution = -1,
fallback = "clientside",
},
},
},
})
end
local function apply_visual_performance_mode()
if verify_config then
return
@@ -323,6 +382,18 @@ function M.setup(ctx)
hl.workspace_rule({ workspace = "w[tv1]s[false]", gaps_out = 0, gaps_in = 0 })
hl.workspace_rule({ workspace = "f[1]s[false]", gaps_out = 0, gaps_in = 0 })
hl.window_rule({
name = "tagged-gaming-window",
match = { tag = gaming_window_tag },
idle_inhibit = "fullscreen",
opaque = true,
no_blur = true,
no_shadow = true,
no_anim = true,
rounding = 0,
border_size = 0,
})
hl.window_rule({ match = { class = "^()$", title = "^()$" }, float = true })
hl.window_rule({ match = { title = "^(Picture-in-Picture)$" }, float = true })
hl.window_rule({
@@ -349,8 +420,30 @@ function M.setup(ctx)
focus_on_activate = true,
stay_focused = true,
})
hl.window_rule({
name = "portal-gtk-dialogs",
match = { class = file_chooser_class_rule },
float = true,
center = true,
focus_on_activate = true,
stay_focused = true,
})
hl.window_rule({ match = { title = "^(Confirm)$" }, float = true })
-- The AI desktop apps fire xdg-activation requests while streaming
-- responses; with misc:focus_on_activate=true that steals focus from
-- whatever window the user is actually working in. focus_on_activate is
-- a dynamic rule (applies to already-mapped windows on reload);
-- suppress_event only applies at map time.
for index, class in ipairs({ "^(claude-desktop)$", "^(codex-desktop)$" }) do
hl.window_rule({
name = "ai-app-no-activate-focus-" .. tostring(index),
match = { class = class },
focus_on_activate = false,
suppress_event = "activatefocus",
})
end
for index, match in ipairs({
{ class = "^(flameshot)$" },
{ title = "^(flameshot)$" },
@@ -412,6 +505,7 @@ function M.setup(ctx)
ctx.apply_rules = apply_rules
ctx.apply_hyprglass_config = apply_hyprglass_config
ctx.apply_hyprwobbly_config = apply_hyprwobbly_config
ctx.apply_dynamic_cursors_config = apply_dynamic_cursors_config
ctx.apply_visual_performance_mode = apply_visual_performance_mode
ctx.is_file_chooser_window = is_file_chooser_window
ctx.raise_file_chooser_window = raise_file_chooser_window

View File

@@ -2,6 +2,7 @@ local shell_ui_command = "hypr_shell_ui"
local columns_layout = "nStack"
local large_main_layout = "master"
local grid_layout = "grid"
local quadrants_layout = "quadrants"
local monocle_layout = "monocle"
return {
@@ -33,29 +34,50 @@ return {
columns_layout = columns_layout,
large_main_layout = large_main_layout,
grid_layout = grid_layout,
quadrants_layout = quadrants_layout,
monocle_layout = monocle_layout,
layout_cycle = { columns_layout, large_main_layout, grid_layout },
layout_cycle = { columns_layout, large_main_layout, quadrants_layout, grid_layout },
layout_names = {
[columns_layout] = "Columns",
[large_main_layout] = "Large main",
[quadrants_layout] = "Quadrants",
[grid_layout] = "Grid",
[monocle_layout] = "Monocle",
},
minimized_workspace = "special:minimized",
inactive_opacity_override_tag = "no-inactive-opacity",
gaming_window_tag = "gaming",
gaming_window_disabled_tag = "no-gaming",
gaming_window_class_patterns = {},
gaming_window_title_patterns = {},
gaming_window_excluded_class_patterns = {
"^[Hh]eroic$",
"^[Ss]team$",
},
gaming_window_excluded_title_patterns = {
"^Heroic Games Launcher$",
"^Steam$",
},
tabbed_group_restore_workspace_prefix = "special:tabbed-monocle-restore-",
current_layout = columns_layout,
enable_nstack = true,
enable_hyprexpo = true,
-- Disabled 2026-06-11: live-preview backend SEGVs Hyprland (shouldRenderWindow
-- hook fires on every popup commit). Re-enable once fixed upstream.
enable_hyprexpo = false,
enable_hyprwinview = true,
enable_hyprtasking = true,
enable_workspace_history = true,
enable_hyprwobbly = true,
enable_dynamic_cursors = true,
enable_hyprglass = false,
hyprland_gaps_enabled = os.getenv("IMALISON_HYPRLAND_GAPS") ~= "0",
hyprland_cursor_size = tonumber(os.getenv("IMALISON_HYPRLAND_CURSOR_SIZE")) or 24,
hypr_visual_performance_mode = false,
configure_nstack_plugin_from_lua = false,
workspace_layouts = {},
minimized_windows = {},
tabbed_workspace_groups = {},
quadrants_arranging = false,
window_picker_mode = nil,
window_picker_candidates = {},
stack_update_timer = nil,

View File

@@ -122,6 +122,44 @@ function M.setup(ctx)
dispatch(hl.dsp.window.resize())
end
local function float_active_window_to_default_scratchpad_geometry()
local window = hl.get_active_window()
local selector = window_selector(window)
if not selector then
return
end
local geometry = default_scratchpad_geometry(hl.get_active_monitor())
if not geometry then
return
end
dispatch(hl.dsp.window.fullscreen_state({
internal = 0,
client = 0,
action = "set",
window = selector,
}))
dispatch(hl.dsp.window.float({ action = "enable", window = selector }))
dispatch(hl.dsp.window.resize({ x = geometry.width, y = geometry.height, relative = false, window = selector }))
dispatch(hl.dsp.window.move({ x = geometry.x, y = geometry.y, relative = false, window = selector }))
end
local function tile_or_float_active_window()
local window = hl.get_active_window()
local selector = window_selector(window)
if not selector then
return
end
if window.floating then
dispatch(hl.dsp.window.float({ action = "disable", window = selector }))
return
end
float_active_window_to_default_scratchpad_geometry()
end
local function toggle_pinned_active_window()
local window = hl.get_active_window()
local selector = window_selector(window)
@@ -365,6 +403,108 @@ function M.setup(ctx)
return false
end
local function value_matches_any_pattern(value, patterns)
value = tostring(value or "")
if value == "" then
return false
end
for _, pattern in ipairs(patterns or {}) do
if value:find(pattern) then
return true
end
end
return false
end
local function is_game_like_window(window)
if not window then
return false
end
if window_has_tag(window, gaming_window_disabled_tag) then
return false
end
if
value_matches_any_pattern(window.class, gaming_window_excluded_class_patterns)
or value_matches_any_pattern(window.initial_class, gaming_window_excluded_class_patterns)
or value_matches_any_pattern(window.title, gaming_window_excluded_title_patterns)
or value_matches_any_pattern(window.initial_title, gaming_window_excluded_title_patterns)
then
return false
end
return tostring(window.content_type or window.content or "") == "game"
or window_has_tag(window, gaming_window_tag)
or value_matches_any_pattern(window.class, gaming_window_class_patterns)
or value_matches_any_pattern(window.initial_class, gaming_window_class_patterns)
or value_matches_any_pattern(window.title, gaming_window_title_patterns)
or value_matches_any_pattern(window.initial_title, gaming_window_title_patterns)
end
local function set_window_gaming_mode(window, enabled, opts)
local selector = window_selector(window)
if not selector then
return
end
if enabled then
dispatch(hl.dsp.window.tag({ tag = "-" .. gaming_window_disabled_tag, window = selector }))
dispatch(hl.dsp.window.tag({ tag = "+" .. gaming_window_tag, window = selector }))
dispatch(hl.dsp.window.fullscreen_state({
internal = 2,
client = 2,
action = "set",
window = selector,
}))
else
dispatch(hl.dsp.window.tag({ tag = "-" .. gaming_window_tag, window = selector }))
dispatch(hl.dsp.window.tag({ tag = "+" .. gaming_window_disabled_tag, window = selector }))
dispatch(hl.dsp.window.fullscreen_state({
internal = 0,
client = 0,
action = "set",
window = selector,
}))
end
if not (opts and opts.quiet) then
hl.notification.create({
text = "Gaming fullscreen: " .. (enabled and "on" or "off"),
duration = 1600,
icon = enabled and notification_icons.ok or notification_icons.info,
color = enabled and "rgba(33ccffee)" or "rgba(edb443ff)",
font_size = 13,
})
end
end
local function toggle_active_window_gaming_mode()
local window = hl.get_active_window()
if not window then
return
end
set_window_gaming_mode(window, not window_has_tag(window, gaming_window_tag))
end
local function toggle_active_window_real_fullscreen()
local window = hl.get_active_window()
if not window then
return
end
local fullscreen = tonumber(window.fullscreen) or 0
local fullscreen_client = tonumber(window.fullscreen_client) or 0
local enabling = fullscreen ~= 2 or fullscreen_client ~= 2
dispatch(hl.dsp.window.fullscreen_state({
internal = enabling and 2 or 0,
client = enabling and 2 or 0,
action = "set",
window = window_selector(window),
}))
end
local function toggle_inactive_opacity_for_active_window()
local window = hl.get_active_window()
local selector = window_selector(window)
@@ -481,6 +621,8 @@ function M.setup(ctx)
ctx.float_active_window_preserving_tiled_geometry = float_active_window_preserving_tiled_geometry
ctx.float_and_drag_active_window = float_and_drag_active_window
ctx.float_and_resize_active_window = float_and_resize_active_window
ctx.float_active_window_to_default_scratchpad_geometry = float_active_window_to_default_scratchpad_geometry
ctx.tile_or_float_active_window = tile_or_float_active_window
ctx.toggle_pinned_active_window = toggle_pinned_active_window
ctx.current_minimized_windows = current_minimized_windows
ctx.restore_minimized_window = restore_minimized_window
@@ -490,6 +632,12 @@ function M.setup(ctx)
ctx.gather_focused_class = gather_focused_class
ctx.focus_next_class = focus_next_class
ctx.show_active_window_info = show_active_window_info
ctx.window_has_tag = window_has_tag
ctx.value_matches_any_pattern = value_matches_any_pattern
ctx.is_game_like_window = is_game_like_window
ctx.set_window_gaming_mode = set_window_gaming_mode
ctx.toggle_active_window_gaming_mode = toggle_active_window_gaming_mode
ctx.toggle_active_window_real_fullscreen = toggle_active_window_real_fullscreen
ctx.toggle_inactive_opacity_for_active_window = toggle_inactive_opacity_for_active_window
ctx.raise_or_spawn = raise_or_spawn
ctx.minimize_active_window = minimize_active_window

View File

@@ -147,6 +147,7 @@ spawnBindings =
, key (super .|. alt) xK_s (toggleScratchpad "spotify")
, key (super .|. alt) xK_t (toggleScratchpad "transmission")
, key (super .|. alt) xK_v (toggleScratchpad "volume")
, key (super .|. alt) xK_x (toggleScratchpad "x-com")
, key (super .|. alt) xK_c (spawnAction "google-chrome-stable")
, key super xK_e (spawnAction "emacsclient --eval '(emacs-everywhere)'")
, key (super .|. ctrl) xK_e (shiftFocusedToNextEmptyWorkspace False)
@@ -165,7 +166,7 @@ spawnBindings =
, key (hyper .|. shift) xK_k (spawnAction "rofi_kill_all.sh")
, key hyper xK_r (spawnAction "rofi_systemd_mono")
, key hyper xK_9 (spawnAction "start_synergy.sh")
, key hyper xK_backslash (spawnAction "$HOME/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle")
, key hyper xK_backslash (spawnAction "mpg341cx_input toggle")
, key hyper xK_i (spawnAction "rofi_select_input.hs")
, key hyper xK_o (spawnAction "rofi_paswitch")
, key hyper xK_comma (spawnAction "rofi_wallpaper.sh")
@@ -297,6 +298,8 @@ scratchpadDefinitions =
anyMatcher [titleContains "Transmission", appIdContains "transmission"]
, ScratchpadDefinition "volume" "pavucontrol" $
anyMatcher [appIdMatches "Pavucontrol", appIdContains "pavucontrol"]
, ScratchpadDefinition "x-com" "x-com-pwa" $
anyMatcher [appIdMatches "x-com-pwa", appIdContains "chrome-x.com"]
]
anyMatcher :: [RiverWMWindowState -> Bool] -> RiverWMWindowState -> Bool

View File

@@ -15,8 +15,11 @@ configuration {
backdrop: #0b102026;
panel: #00000000;
control: #ffffffe0;
candidate-soft: #0b102018;
candidate-frost: #ffffff12;
candidate: #18203372;
candidate-active:#2430489c;
candidate-active:#0a84ffd9;
candidate-line: #ffffff16;
text: #111827ff;
text-muted: #667085ff;
text-on-dark: #f8fafcff;
@@ -53,6 +56,7 @@ mainbox {
inputbar {
background-color: @control;
text-color: @text;
height: 50px;
children: [ prompt, entry ];
border: 1px;
border-color: @hairline;
@@ -80,11 +84,10 @@ entry {
listview {
background-color: @bg;
columns: 1;
lines: 10;
lines: 17;
spacing: 0px;
border: 1px;
border-color: @border;
border-radius: 14px;
border: 0px;
border-radius: 0px 0px 14px 14px;
cycle: false;
dynamic: true;
layout: vertical;
@@ -96,12 +99,24 @@ element {
text-color: @text-on-dark;
orientation: horizontal;
border: 0px 0px 1px 0px;
border-color: @hairline;
border-color: @candidate-line;
border-radius: 0px;
padding: 11px 11px;
/*
* Rofi percentages are monitor-relative. Derive padding from:
* 78% - 176px mainbox padding - 50px inputbar - 10px spacing - 2px border.
*/
padding: calc( ( 78% - 238px ) / 34 - 12px ) 11px calc( ( 78% - 238px ) / 34 - 13px ) 11px;
spacing: 10px;
}
element normal {
background-color: @candidate-soft;
}
element alternate {
background-color: @candidate-soft;
}
element-icon {
background-color: @bg;
text-color: inherit;
@@ -117,9 +132,10 @@ element-text {
}
element selected {
background-color: @candidate;
background-color: @candidate-active;
text-color: @text-on-dark;
border-color: @border;
border: 0px 0px 1px 4px;
border-color: @accent-soft;
}
element selected element-text {

View File

@@ -23,7 +23,7 @@
- Do not create extra panes or windows unless the user asks.
## NixOS workflow
- This system is managed with a Nix flake at `~/dotfiles/nixos`.
- This system is managed with a Nix flake at `/etc/nixos` (`/srv/dotfiles/nixos`).
- Use `just switch` from that directory for rebuilds instead of plain `nixos-rebuild`.
- Host configs live under `machines/`; choose the appropriate host when needed.

View File

@@ -0,0 +1,116 @@
{-# LANGUAGE OverloadedStrings #-}
-- | A usage widget that follows the Hyprland AI scratchpad selection.
--
-- The Hyprland config (scratchpads.lua) lets SUPER+ALT+C toggle whichever AI
-- app is currently selected via rofi_ai_scratchpad.sh, which records the
-- choice in $XDG_STATE_HOME/hypr/ai-scratchpad. This widget reads the same
-- state file and shows the matching provider's usage section (OpenAI for
-- "codex", Anthropic for "claude"), switching live when the file changes.
module TaffybarConfig.AIUsage
( aiUsageWidget,
)
where
import Control.Exception (IOException, try)
import Control.Monad (void)
import Control.Monad.IO.Class (liftIO)
import Data.Text (Text)
import qualified Data.Text as T
import qualified GI.Gtk as Gtk
import qualified System.FSNotify as FSNotify
import System.Directory (createDirectoryIfMissing, getHomeDirectory)
import System.Environment (lookupEnv)
import System.FilePath (takeFileName, (</>))
import System.Taffybar.Context (TaffyIO)
import System.Taffybar.Util (postGUIASync)
import System.Taffybar.Widget.AnthropicUsage
( AnthropicUsageDisplayMode (AnthropicUsageDisplayRemaining),
AnthropicUsageStackConfig (..),
anthropicUsageSectionNewWith,
defaultAnthropicUsageStackConfig,
)
import System.Taffybar.Widget.OpenAIUsage
( OpenAIUsageDisplayMode (OpenAIUsageDisplayRemaining),
OpenAIUsageStackConfig (..),
defaultOpenAIUsageStackConfig,
openAIUsageSectionNewWith,
)
import TaffybarConfig.WidgetUtil (decorateWithClassAndBox, usageLogoWidget)
codexChild, claudeChild :: Text
codexChild = "codex"
claudeChild = "claude"
aiScratchpadStateDir :: IO FilePath
aiScratchpadStateDir = do
stateHome <- lookupEnv "XDG_STATE_HOME"
base <- case stateHome of
Just dir | not (null dir) -> pure dir
_ -> (</> ".local/state") <$> getHomeDirectory
pure (base </> "hypr")
aiScratchpadStateFile :: FilePath
aiScratchpadStateFile = "ai-scratchpad"
-- | Read the currently selected AI scratchpad, defaulting to codex like the
-- Hyprland side does.
readActiveAIScratchpad :: IO Text
readActiveAIScratchpad = do
dir <- aiScratchpadStateDir
result <- try (readFile (dir </> aiScratchpadStateFile)) :: IO (Either IOException String)
pure $ case result of
Right contents
| T.strip (T.pack contents) == claudeChild -> claudeChild
_ -> codexChild
openAIUsageSection :: TaffyIO Gtk.Widget
openAIUsageSection = do
iconWidget <- liftIO $ usageLogoWidget "openai-symbol.svg" "OpenAI usage"
openAIUsageSectionNewWith
iconWidget
defaultOpenAIUsageStackConfig
{ openAIUsageStackDefaultDisplayMode = OpenAIUsageDisplayRemaining
}
anthropicUsageSection :: TaffyIO Gtk.Widget
anthropicUsageSection = do
iconWidget <- liftIO $ usageLogoWidget "claude-symbol.svg" "Claude usage"
anthropicUsageSectionNewWith
iconWidget
defaultAnthropicUsageStackConfig
{ anthropicUsageStackDefaultDisplayMode = AnthropicUsageDisplayRemaining
}
-- | Show usage for whichever AI app the Hyprland AI scratchpad currently
-- targets, switching live when the selection changes.
aiUsageWidget :: TaffyIO Gtk.Widget
aiUsageWidget = do
openAIWidget <- openAIUsageSection
anthropicWidget <- anthropicUsageSection
stackWidget <- liftIO $ do
stack <- Gtk.stackNew
Gtk.stackAddNamed stack openAIWidget codexChild
Gtk.stackAddNamed stack anthropicWidget claudeChild
readActiveAIScratchpad >>= Gtk.stackSetVisibleChildName stack
let syncVisibleChild =
readActiveAIScratchpad
>>= \name -> postGUIASync (Gtk.stackSetVisibleChildName stack name)
void $ Gtk.onWidgetRealize stack $ do
stateDir <- aiScratchpadStateDir
createDirectoryIfMissing True stateDir
manager <- FSNotify.startManager
void $
FSNotify.watchDir
manager
stateDir
((== aiScratchpadStateFile) . takeFileName . FSNotify.eventPath)
(const syncVisibleChild)
syncVisibleChild
void $ Gtk.onWidgetUnrealize stack $ FSNotify.stopManager manager
Gtk.widgetShowAll stack
Gtk.toWidget stack
decorateWithClassAndBox "ai-usage" stackWidget

View File

@@ -10,8 +10,8 @@ module TaffybarConfig.ChromeFavicons
)
where
import Control.Exception (IOException, try)
import Control.Monad (unless, when)
import Control.Exception (IOException, SomeException, try)
import Control.Monad (unless, void)
import Control.Monad.IO.Class (liftIO)
import Data.Char (isAlphaNum)
import Data.Int (Int32)
@@ -24,6 +24,7 @@ import System.Directory
( createDirectoryIfMissing,
doesFileExist,
getFileSize,
removeFile,
renameFile,
)
import System.Environment.XDG.BaseDir (getUserCacheDir)
@@ -204,10 +205,11 @@ loadCachedFavicon size url = do
path <- ensureCachedFavicon url
case path of
Just faviconPath ->
try @IOException (Gdk.pixbufNewFromFileAtScale faviconPath size size True) >>= \case
Right (Just pixbuf) -> pure (Just pixbuf)
Right Nothing -> pure Nothing
Left _ -> pure Nothing
loadPixbuf faviconPath size >>= \case
Just pixbuf -> pure (Just pixbuf)
Nothing -> do
removeCachedFavicon faviconPath
pure Nothing
Nothing -> pure Nothing
ensureCachedFavicon :: Text -> IO (Maybe FilePath)
@@ -254,21 +256,28 @@ faviconExtension url =
downloadFavicon :: Text -> FilePath -> IO ()
downloadFavicon url path = do
let tmp = path <> ".tmp"
(code, _, _) <-
readProcessWithExitCode
"curl"
[ "-fsSL",
"--max-time",
"10",
"--retry",
"1",
"-o",
tmp,
T.unpack url
]
""
when (code == ExitSuccess) $
renameFile tmp path
removeCachedFavicon tmp
result <-
try @IOException $
readProcessWithExitCode
"curl"
[ "-fsSL",
"--max-time",
"10",
"--retry",
"1",
"-o",
tmp,
T.unpack url
]
""
case result of
Right (ExitSuccess, _, _) -> do
mPixbuf <- loadPixbuf tmp 1
case mPixbuf of
Just _ -> renameFile tmp path
Nothing -> removeCachedFavicon tmp
_ -> removeCachedFavicon tmp
nonEmptyFileExists :: FilePath -> IO Bool
nonEmptyFileExists path = do
@@ -318,3 +327,13 @@ composeChromeFavicon cfg size favicon chromeIcon = do
scalePixbuf :: Int32 -> Gdk.Pixbuf -> IO Gdk.Pixbuf
scalePixbuf size pixbuf =
fromMaybe pixbuf <$> Gdk.pixbufScaleSimple pixbuf size size GdkPixbuf.InterpTypeBilinear
loadPixbuf :: FilePath -> Int32 -> IO (Maybe Gdk.Pixbuf)
loadPixbuf path size =
try @SomeException (Gdk.pixbufNewFromFileAtScale path size size True) >>= \case
Right pixbuf -> pure pixbuf
Left _ -> pure Nothing
removeCachedFavicon :: FilePath -> IO ()
removeCachedFavicon path =
void $ try @IOException (removeFile path)

View File

@@ -3,7 +3,7 @@ module TaffybarConfig.Config
)
where
import TaffybarConfig.Host (compactBarHosts, smallBarHosts)
import TaffybarConfig.Host (compactBarHosts, smallBarHosts, tinyBarHosts)
import TaffybarConfig.Widgets (clockWidget, endWidgetsForHost, startWidgetsForHostAndBackend)
import System.Taffybar.Context (Backend)
import System.Taffybar.SimpleConfig
@@ -18,18 +18,24 @@ mkSimpleTaffyConfig hostName backend cssFiles =
barPosition = Top,
widgetSpacing = 0,
barPadding =
if hostName `elem` smallBarHosts
then 1
if hostName `elem` tinyBarHosts
then 0
else
if hostName `elem` compactBarHosts
then 2
else 4,
if hostName `elem` smallBarHosts
then 1
else
if hostName `elem` compactBarHosts
then 2
else 4,
barHeight =
if hostName `elem` smallBarHosts
then ScreenRatio $ 1 / 48
if hostName `elem` tinyBarHosts
then ScreenRatio $ 1 / 90
else
if hostName `elem` compactBarHosts
then ScreenRatio $ 1 / 40
else ScreenRatio $ 1 / 33,
if hostName `elem` smallBarHosts
then ScreenRatio $ 1 / 72
else
if hostName `elem` compactBarHosts
then ScreenRatio $ 1 / 60
else ScreenRatio $ 2 / 99,
cssPaths = cssFiles
}

View File

@@ -3,6 +3,7 @@ module TaffybarConfig.Host
cssFilesForHost,
laptopHosts,
smallBarHosts,
tinyBarHosts,
)
where
@@ -16,7 +17,8 @@ defaultCssFiles = ["taffybar.css"]
cssFilesByHostname :: [(String, [FilePath])]
cssFilesByHostname =
[ ("ryzen-shine", ["ryzen-shine.css"]),
[ ("jay-lenovo", ["jay-lenovo.css"]),
("ryzen-shine", ["ryzen-shine.css"]),
("strixi-minaj", ["strixi-minaj.css"])
]
@@ -28,6 +30,10 @@ smallBarHosts :: [String]
smallBarHosts =
["strixi-minaj"]
tinyBarHosts :: [String]
tinyBarHosts =
["jay-lenovo"]
laptopHosts :: [String]
laptopHosts =
[ "adell",

View File

@@ -33,12 +33,6 @@ import qualified System.Taffybar.Widget.Audio as Audio
import System.Taffybar.Widget.CPUMonitor (cpuMonitorNew)
import System.Taffybar.Widget.Generic.Graph (GraphConfig (..), GraphDirection (..), GraphStyle (..), defaultGraphConfig)
import qualified System.Taffybar.Widget.NetworkManager as NetworkManager
import System.Taffybar.Widget.OpenAIUsage
( OpenAIUsageDisplayMode (OpenAIUsageDisplayRemaining),
OpenAIUsageStackConfig (..),
defaultOpenAIUsageStackConfig,
openAIUsageSectionNewWith,
)
import System.Taffybar.Widget.SNIMenu (withNmAppletMenu)
import System.Taffybar.Widget.SNITray
( CollapsibleSNITrayParams (..),
@@ -60,6 +54,7 @@ import System.Taffybar.Widget.Util
)
import qualified System.Taffybar.Widget.Wlsunset as Wlsunset
import qualified System.Taffybar.Widget.Workspaces as Workspaces
import TaffybarConfig.AIUsage (aiUsageWidget)
import TaffybarConfig.Host (laptopHosts)
import TaffybarConfig.WidgetUtil
( decorateWithClassAndBox,
@@ -355,9 +350,9 @@ omniMenuWidget =
OmniMenuSection
"System"
[ omniMenuItem "Lock" "system-lock-screen" "loginctl lock-session",
omniMenuItem "Toggle screensaver" "video-display" "/home/imalison/dotfiles/dotfiles/lib/bin/hypr-screensaver toggle",
omniMenuItem "Toggle screensaver" "video-display" "hypr-screensaver toggle",
omniMenuItem "Reload WM" "view-refresh" "sh -lc 'hyprctl reload || xmonad --restart || river-xmonad-restart'",
omniMenuItem "Restart taffybar" "view-refresh-symbolic" "/home/imalison/dotfiles/dotfiles/config/taffybar/scripts/taffybar-restart",
omniMenuItem "Restart taffybar" "view-refresh-symbolic" "/srv/dotfiles/dotfiles/config/taffybar/scripts/taffybar-restart",
omniMenuItem "Logout" "system-log-out" "sh -lc 'hyprctl dispatch exit || riverctl exit'",
omniMenuItem "Suspend" "media-playback-pause" "systemctl suspend",
omniMenuItem "Reboot" "system-reboot" "systemctl reboot",
@@ -375,16 +370,6 @@ usageSectionWidget klass iconFile tooltip stackBuilder =
section <- buildIconLabelBox iconWidget stack
widgetSetClassGI section "usage-section"
openAIUsageWidget :: TaffyIO Gtk.Widget
openAIUsageWidget = do
iconWidget <- liftIO $ usageLogoWidget "openai-symbol.svg" "OpenAI usage"
decorateWithClassAndBoxM "openai-usage" $
openAIUsageSectionNewWith
iconWidget
defaultOpenAIUsageStackConfig
{ openAIUsageStackDefaultDisplayMode = OpenAIUsageDisplayRemaining
}
sniPriorityVisibilityThresholdDefault :: Int
sniPriorityVisibilityThresholdDefault = 0
@@ -445,7 +430,7 @@ endWidgetsForHost hostName =
let baseEndWidgets =
[ sniTrayWidget,
audioWidget,
openAIUsageWidget,
aiUsageWidget,
cpuWidget,
ramSwapWidget,
diskUsageWidget,
@@ -458,7 +443,7 @@ endWidgetsForHost hostName =
sniTrayWidget,
asusDiskUsageWidget,
audioBacklightWidget,
openAIUsageWidget,
aiUsageWidget,
cpuWidget,
ramSwapWidget,
sunLockWidget,

View File

@@ -191,6 +191,5 @@ workspaceWindowIconGetter :: Workspaces.WindowIconPixbufGetter
workspaceWindowIconGetter =
chromeFaviconIconGetter chromeFaviconConfig
<|||> workspaceManualIconGetter
<|||> Workspaces.getWindowIconPixbufFromChrome
<|||> Workspaces.defaultGetWindowIconPixbuf
<|||> workspaceFallbackIcon

View File

@@ -136,15 +136,16 @@
"xmonad-contrib": "xmonad-contrib"
},
"locked": {
"lastModified": 1778673962,
"narHash": "sha256-GmHRMdrUIQpMf6k5gRjP9Mvx2WO0FvIEF1SPlxEpnas=",
"lastModified": 1781172310,
"narHash": "sha256-mBd3obUUS+ICqL+U2bOanGwaGl2rfbMZdGzAFiqRSaE=",
"owner": "taffybar",
"repo": "taffybar",
"rev": "08125b267c03232c560fce6259264cc9283d582e",
"rev": "7beecc89928df669281977e41ceed213c5ede88f",
"type": "github"
},
"original": {
"owner": "taffybar",
"ref": "anthropic-usage-rate-limit-backoff",
"repo": "taffybar",
"type": "github"
}

View File

@@ -3,8 +3,13 @@
taffybar = {
# Keep the default source usable in CI. Local iteration uses
# IMALISON_TAFFYBAR_LIVE_CHECKOUT below via `just switch-local-taffybar`.
url = "github:taffybar/taffybar";
inputs.weeder-nix.inputs.pre-commit-hooks.inputs.nixpkgs.follows = "nixpkgs";
# Pinned to the rate-limit-backoff PR branch (taffybar/taffybar#681);
# revert to master after it merges.
url = "github:taffybar/taffybar/anthropic-usage-rate-limit-backoff";
inputs.weeder-nix = {
url = "github:NorfairKing/weeder-nix";
inputs.pre-commit-hooks.inputs.nixpkgs.follows = "nixpkgs";
};
};
# Follow the vendored taffybar flake's pins so the config shell and the
# library shell mostly share their nixpkgs/Haskell dependency graph.

View File

@@ -13,7 +13,8 @@ cabal-version: >=1.10
executable taffybar
hs-source-dirs: .
main-is: taffybar.hs
other-modules: TaffybarConfig.Config
other-modules: TaffybarConfig.AIUsage
, TaffybarConfig.Config
, TaffybarConfig.ChromeFavicons
, TaffybarConfig.Host
, TaffybarConfig.Widgets
@@ -27,6 +28,7 @@ executable taffybar
, containers
, directory
, filepath
, fsnotify
, gi-gdk3
, gi-gtk3
, gi-gdkpixbuf

View File

@@ -0,0 +1,68 @@
@import url("taffybar.css");
/* Host-specific density tweak for jay-lenovo. */
.taffy-box {
font-size: 8.5pt;
border-radius: 7px;
}
.outer-pad,
.workspaces .outer-pad {
border-radius: 7px;
margin: 2px 3px;
}
.inner-pad,
.workspaces .inner-pad {
border-radius: 6px;
padding-top: 0px;
padding-bottom: 0px;
}
.inner-pad {
padding-left: 8px;
padding-right: 8px;
}
.workspaces .inner-pad {
padding-left: 8px;
padding-right: 2px;
}
.workspaces .contents {
border-radius: 6px;
padding: 0px 2px;
}
.workspace-label {
font-size: 8pt;
}
.workspaces .overlay-box .workspace-label {
padding: 0px 3px 3px 9px;
}
.visible .contents,
.workspaces .window-icon-container,
.workspaces .window-icon-container.active {
padding-top: 0px;
padding-bottom: 0px;
}
.auto-size-image,
.sni-tray {
padding-top: 0px;
padding-bottom: 0px;
}
.icon-label > .icon,
.usage-section.icon-label > .icon,
.ram-swap .icon-label > .icon {
padding-right: 6px;
min-width: 18px;
}
.sun-lock .wlsunset,
.sun-lock .screen-lock {
padding: 0px 3px;
}

View File

@@ -21,7 +21,7 @@ priorities:
priority: 0
- key: item-id:flameshot
priority: 0
- key: item-id:keepbook-sync-daemon
- key: item-id:keepbook-dioxus
priority: 0
- key: item-id:udiskie
priority: 0
@@ -41,6 +41,8 @@ priorities:
priority: -1
- key: item-id:tailscale
priority: -1
- key: process:git-sync-rs
priority: -1
- key: process:tailscale
priority: -1
- key: item-id:spotify-client

View File

@@ -237,12 +237,18 @@ chromeSelectorBase = isChromeClass <$> className
chromeSelector = chromeSelectorBase
codexSelector = className =? "codex-desktop"
discordSelector = className =? "Discord" <||> className =? "discord"
elementSelector = className =? "Element"
emacsSelector = className =? "Emacs"
slackSelector = className =? "Slack"
spotifySelector = className =? "Spotify"
transmissionSelector = fmap (isPrefixOf "Transmission") title
volumeSelector = className =? "Pavucontrol"
xComSelector =
className =? "x-com-pwa"
<||> fmap ("chrome-x.com" `isInfixOf`) className
<||> (chromeSelectorBase <&&> title =? "X")
<||> fmap ("x.com" `isInfixOf`) title
virtualClasses =
[ (chromeSelector, "Chrome")
@@ -252,6 +258,7 @@ virtualClasses =
-- Commands
codexCommand = "codex_desktop_scratchpad"
discordCommand = "discord"
elementCommand = "element-desktop"
emacsCommand = "emacsclient -c"
htopCommand = "ghostty --title=htop -e htop"
@@ -259,6 +266,7 @@ slackCommand = "slack"
spotifyCommand = "spotify"
transmissionCommand = "transmission-gtk"
volumeCommand = "pavucontrol"
xComCommand = "x-com-pwa"
-- Startup hook
@@ -802,12 +810,14 @@ nearFullFloat = customFloating $ W.RationalRect l t w h
scratchpads =
[ NS "codex" codexCommand codexSelector nearFullFloat
, NS "discord" discordCommand discordSelector nearFullFloat
, NS "element" elementCommand elementSelector nearFullFloat
, NS "htop" htopCommand (title =? "htop") nearFullFloat
, NS "slack" slackCommand slackSelector nearFullFloat
, NS "spotify" spotifyCommand spotifySelector nearFullFloat
, NS "transmission" transmissionCommand transmissionSelector nearFullFloat
, NS "volume" volumeCommand volumeSelector nearFullFloat
, NS "x-com" xComCommand xComSelector nearFullFloat
]
@@ -1008,12 +1018,14 @@ addKeys conf@XConfig { modMask = modm } =
-- ScratchPads
[ ((modalt, xK_c), doScratchpad "codex")
, ((modalt, xK_d), doScratchpad "discord")
, ((modalt, xK_e), doScratchpad "element")
, ((modalt, xK_h), doScratchpad "htop")
, ((modalt, xK_k), doScratchpad "slack")
, ((modalt, xK_s), doScratchpad "spotify")
, ((modalt, xK_t), doScratchpad "transmission")
, ((modalt, xK_v), doScratchpad "volume")
, ((modalt, xK_x), doScratchpad "x-com")
-- Specific program spawning
@@ -1071,6 +1083,7 @@ addKeys conf@XConfig { modMask = modm } =
, ((hyper, xK_p), spawn "rofi-pass")
, ((0, xK_Print), spawn "flameshot gui")
, ((hyper, xK_h), spawn "flameshot gui")
, ((hyper, xK_n), spawn "rofi_codex_desktop_project.sh")
, ((hyper, xK_c), spawn "rofi_tmcodex.sh")
, ((hyper .|. shiftMask, xK_c), spawn "rofi_tmcodex.sh resume")
, ((hyper .|. shiftMask, xK_l), spawn "dm-tool lock")
@@ -1080,11 +1093,11 @@ addKeys conf@XConfig { modMask = modm } =
, ((hyper, xK_r), spawn "rofi_systemd_mono")
, ((hyper, xK_9), spawn "start_synergy.sh")
, ((hyper, xK_slash), spawn "toggle_taffybar")
, ((hyper, xK_backslash), spawn "$HOME/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle")
, ((hyper, xK_backslash), spawn "mpg341cx_input toggle")
, ((hyper, xK_space), spawn "skippy-xd")
, ((hyper, xK_i), spawn "rofi_select_input.hs")
, ((hyper, xK_o), spawn "rofi_paswitch")
, ((hyper .|. shiftMask, xK_o), spawn "$HOME/dotfiles/dotfiles/lib/bin/kef-optical")
, ((hyper .|. shiftMask, xK_o), spawn "kef-optical")
, ((hyper, xK_comma), spawn "rofi_wallpaper.sh")
, ((hyper, xK_y), spawn "rofi_agentic_skill")
, ((modm, xK_e), spawn "emacsclient --eval '(emacs-everywhere)'")

View File

@@ -13,7 +13,7 @@ keybinds {
tmux {
// Ctrl-b C: start a Codex pane from the current zellij tab.
bind "C" {
Run "codex" "--dangerously-bypass-approvals-and-sandbox" {
Run "codex" "--dangerously-bypass-approvals-and-sandbox" "--cd" "." {
name "codex"
}
SwitchToMode "Normal"

View File

@@ -1383,8 +1383,12 @@ Paradox is a package.el extension. I have no use for it now that I use straight.
#+END_SRC
** load-dir
#+BEGIN_SRC emacs-lisp
(elpaca `(load-dir :host github :repo "emacs-straight/load-dir"
:branch "master"
:protocol https
:wait t))
(use-package load-dir
:ensure (:host github :repo "emacs-straight/load-dir")
:ensure nil
:demand t
:config
(progn
@@ -1462,7 +1466,8 @@ The file server file for this emacs instance no longer exists.")
#+END_SRC
** discover-my-major
#+BEGIN_SRC emacs-lisp
(use-package discover-my-major)
(use-package discover-my-major
:disabled t)
#+END_SRC
** refine
#+BEGIN_SRC emacs-lisp
@@ -4037,6 +4042,7 @@ This is useful with server mode when editing gmail messages. I think that it is
** android-mode
#+BEGIN_SRC emacs-lisp
(use-package android-mode
:ensure (:protocol https)
:defer t
:config
(progn
@@ -4110,7 +4116,8 @@ Ensure all themes that I use are installed:
forest-blue-theme flatland-theme afternoon-theme
cyberpunk-theme dracula-theme))
(mapcar #'elpaca-try packages-appearance)
(dolist (package packages-appearance)
(eval `(use-package ,package :defer t) t))
(use-package doom-themes
:defer t)
@@ -4282,8 +4289,22 @@ load-theme hook (See the heading below).
#+BEGIN_SRC emacs-lisp
(defvar imalison:appearance-setup-done nil)
(defun imalison:ensure-theme-load-path (theme)
(when (boundp 'elpaca-builds-directory)
(when-let ((theme-dir
(seq-find
(lambda (dir)
(file-exists-p
(expand-file-name (format "%s-theme.el" theme) dir)))
(directory-files
elpaca-builds-directory
t
directory-files-no-dot-files-regexp))))
(add-to-list 'custom-theme-load-path theme-dir))))
(defun imalison:appearance-setup-hook (&rest args)
(unless imalison:appearance-setup-done
(imalison:ensure-theme-load-path imalison:dark-theme)
(unless (member imalison:dark-theme custom-enabled-themes)
(load-theme imalison:dark-theme t))
(apply 'imalison:appearance args)

View File

@@ -135,13 +135,18 @@
;; Some split packages fall through the active menus in this config. Give
;; Elpaca an explicit source so startup doesn't get stuck on recipe lookup or
;; stale branch-mapped clones.
(elpaca `(queue :host github :repo "emacs-straight/queue"))
(elpaca `(queue :host github :repo "emacs-straight/queue"
:branch "master"
:protocol https))
(elpaca `(with-editor :host github :repo "magit/with-editor"
:branch "main"))
:branch "main"
:protocol https))
(elpaca `(git-commit :host github :repo "magit/magit"
:files ("lisp/git-commit.el" "lisp/git-commit-pkg.el")))
:files ("lisp/git-commit.el" "lisp/git-commit-pkg.el")
:protocol https))
(elpaca `(magit-section :host github :repo "magit/magit"
:files ("lisp/magit-section.el" "lisp/magit-section-pkg.el")))
:files ("lisp/magit-section.el" "lisp/magit-section-pkg.el")
:protocol https))
(use-package gh
:defer t

View File

@@ -102,10 +102,10 @@
required = true
[credential "https://github.com"]
helper =
helper = !/usr/bin/env gh auth git-credential
helper = !/nix/store/fxvyz1dx5wp87qgbd6dfkmqqb4fypm3b-gh-2.93.0/bin/.gh-wrapped auth git-credential
[credential "https://gist.github.com"]
helper =
helper = !/usr/bin/env gh auth git-credential
helper = !/nix/store/fxvyz1dx5wp87qgbd6dfkmqqb4fypm3b-gh-2.93.0/bin/.gh-wrapped auth git-credential
[includeIf "gitdir:~/Projects/org-agenda-api/"]
path = ~/.gitconfig.org-agenda-api
[includeIf "gitdir:~/Projects/dotfiles/org-agenda-api/"]
@@ -116,3 +116,6 @@
directory = /tmp/tmp.zfvv44RquC/runtime/data/org
directory = /tmp/tmp.zFdvVnKk4B/runtime/data/org
directory = /tmp/tmp.PQTdI3UzS3/runtime/data/org
directory = /srv/dotfiles/dotfiles/config/taffybar/taffybar
directory = /srv/dotfiles
directory = /srv/dotfiles/.worktrees/taffybar-week-old

View File

@@ -80,3 +80,5 @@ cabal.project.local
/untracked
railbird-infra-*.json
**/.claude/settings.local.json

View File

@@ -348,7 +348,7 @@ local function toggleMonitorInput()
end
end, {
"-lc",
"export PATH=\"$HOME/.nix-profile/bin:/run/current-system/sw/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH\"; \"$HOME/dotfiles/dotfiles/lib/functions/mpg341cx_input\" toggle",
"export PATH=\"$HOME/.nix-profile/bin:/run/current-system/sw/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH\"; \"${DOTFILES_WORKTREE:-$HOME/dotfiles}/dotfiles/lib/functions/mpg341cx_input\" toggle",
}):start()
end

View File

@@ -135,7 +135,7 @@ sendNotification brightness = do
rawSystem "sh" ["-c", "command -v rumno >/dev/null 2>&1"]
if rumnoExists
then do
_ <- readProcess "rumno" ["notify", "-t", timeoutSeconds, "-b", show brightness] ""
_ <- readProcess "rumno" ["-t", timeoutSeconds, "-b", show brightness] ""
return ()
else putStrLn (show brightness)

View File

@@ -87,7 +87,7 @@ RUMNO_TIMEOUT="${RUMNO_TIMEOUT:-2.5}"
# Show notification if rumno is available
if command -v rumno &> /dev/null; then
rumno notify -t "$RUMNO_TIMEOUT" -b "$BRIGHTNESS"
rumno -t "$RUMNO_TIMEOUT" -b "$BRIGHTNESS"
else
echo "$BRIGHTNESS"
fi

View File

@@ -17,6 +17,218 @@ pid_is_alive() {
[ -e "/proc/$pid/exe" ]
}
pid_is_current_user() {
local pid="${1:-}"
local uid
uid="$(stat -c %u "/proc/$pid" 2>/dev/null || true)"
[ -n "$uid" ] && [ "$uid" = "$(id -u)" ]
}
pid_cmdline() {
local pid="$1"
tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true
}
pid_is_main_codex_desktop() {
local pid="$1"
local exe
local cmdline
pid_is_alive "$pid" || return 1
pid_is_current_user "$pid" || return 1
exe="$(readlink -f "/proc/$pid/exe" 2>/dev/null || true)"
[ -n "$exe" ] || return 1
[ "$(basename "$exe")" = "electron" ] || return 1
[ -x "$(dirname "$exe")/start.sh" ] || return 1
cmdline="$(pid_cmdline "$pid")"
case " $cmdline " in
*" --class=$app_id "*) ;;
*) return 1 ;;
esac
case " $cmdline " in
*" --app-id=$app_id "*) ;;
*) return 1 ;;
esac
case " $cmdline " in
*" --type="*) return 1 ;;
esac
}
find_running_app() {
local proc_exe
local pid
local exe
for proc_exe in /proc/[0-9]*/exe; do
[ -e "$proc_exe" ] || continue
pid="${proc_exe#/proc/}"
pid="${pid%/exe}"
if pid_is_main_codex_desktop "$pid"; then
exe="$(readlink -f "$proc_exe" 2>/dev/null || true)"
printf '%s\t%s\n' "$pid" "$(dirname "$exe")"
return 0
fi
done
return 1
}
find_recorded_running_app() {
local pid
local exe
[ -r "$pid_file" ] || return 1
pid="$(cat "$pid_file" 2>/dev/null || true)"
pid_is_main_codex_desktop "$pid" || return 1
exe="$(readlink -f "/proc/$pid/exe" 2>/dev/null || true)"
[ -n "$exe" ] || return 1
printf '%s\t%s\n' "$pid" "$(dirname "$exe")"
}
dbus_names_for_pid() {
local pid="$1"
command -v busctl >/dev/null 2>&1 || return 0
busctl --user list --no-legend 2>/dev/null \
| awk -v pid="$pid" '$2 == pid && ($1 ~ /^:/ || $1 ~ /^org[.]kde[.]StatusNotifierItem-/) { print $1 }'
}
dbus_property_object_path() {
local service="$1"
local path="$2"
local interface="$3"
local property="$4"
timeout 3 busctl --user call "$service" "$path" org.freedesktop.DBus.Properties Get ss "$interface" "$property" 2>/dev/null \
| awk '$1 == "v" && $2 == "o" { gsub(/"/, "", $3); print $3; exit }'
}
dbus_name_has_status_notifier_item() {
local service="$1"
timeout 1 busctl --user tree "$service" 2>/dev/null | grep -q '/StatusNotifierItem'
}
open_codex_menu_item_id() {
local layout
layout="$(cat)"
CODEX_DBUSMENU_LAYOUT="$layout" python3 - <<'PY'
import os
import re
layout = os.environ.get("CODEX_DBUSMENU_LAYOUT", "")
for record in layout.split("(ia{sv}av)")[1:]:
item_id = re.match(r"\s+([0-9]+)\s+", record)
if item_id and re.search(r'"label"\s+s\s+"Open Codex"(?:\s|$)', record):
print(item_id.group(1))
break
PY
}
activate_codex_dbus_menu_item_for_service() {
local service="$1"
local menu_path
local layout
local item_id
command -v busctl >/dev/null 2>&1 || return 1
command -v timeout >/dev/null 2>&1 || return 1
menu_path="$(dbus_property_object_path "$service" /StatusNotifierItem org.kde.StatusNotifierItem Menu || true)"
[ -n "$menu_path" ] || menu_path=/com/canonical/dbusmenu
layout="$(timeout 3 busctl --user call "$service" "$menu_path" com.canonical.dbusmenu GetLayout iias 0 -- -1 0 2>/dev/null || true)"
[ -n "$layout" ] || return 1
item_id="$(printf '%s\n' "$layout" | open_codex_menu_item_id)"
[ -n "$item_id" ] || return 1
timeout 3 busctl --user call "$service" "$menu_path" com.canonical.dbusmenu Event isvu "$item_id" clicked v i 0 0 >/dev/null 2>&1
}
activate_codex_tray_menu_item() {
local pid="$1"
local service
while IFS= read -r service; do
[ -n "$service" ] || continue
dbus_name_has_status_notifier_item "$service" || continue
if activate_codex_dbus_menu_item_for_service "$service"; then
return 0
fi
done < <(dbus_names_for_pid "$pid" | awk '!seen[$0]++')
return 1
}
activate_status_notifier_item() {
local pid="$1"
local qdbus_cmd
local service
local path
qdbus_cmd="$(command -v qdbus || command -v qdbus6 || true)"
[ -n "$qdbus_cmd" ] || return 1
while IFS= read -r service; do
[ -n "$service" ] || continue
while IFS= read -r path; do
case "$path" in
*/StatusNotifierItem)
if "$qdbus_cmd" "$service" "$path" org.kde.StatusNotifierItem.Activate 0 0 >/dev/null 2>&1; then
return 0
fi
;;
esac
done < <("$qdbus_cmd" "$service" 2>/dev/null || true)
done < <("$qdbus_cmd" 2>/dev/null | grep -E "^org\\.kde\\.StatusNotifierItem-${pid}-" || true)
return 1
}
focus_hyprland_window() {
local pid="$1"
local address
local output
command -v hyprctl >/dev/null 2>&1 || return 1
address="$(
HYPR_FOCUS_PID="$pid" python3 - <<'PY'
import json
import os
import subprocess
import sys
target_pid = int(os.environ["HYPR_FOCUS_PID"])
try:
clients = json.loads(subprocess.check_output(["hyprctl", "clients", "-j"], text=True, stderr=subprocess.DEVNULL))
except Exception:
raise SystemExit(1)
for client in clients:
if client.get("pid") == target_pid and client.get("address") and client.get("mapped", True) and not client.get("hidden", False):
print(client["address"])
break
else:
raise SystemExit(1)
PY
)" || return 1
[ -n "$address" ] || return 1
output="$(hyprctl dispatch "hl.dsp.focus({ window = \"address:$address\" })" 2>&1)" || return 1
[ "$output" = "ok" ] || return 1
}
start_second_instance_handoff() {
(
exec codex-desktop "$@"
) >/dev/null 2>&1 &
}
running_app_is_alive() {
local pid
@@ -56,4 +268,17 @@ if send_launch_action "$@"; then
exit 0
fi
if running_app="$(find_recorded_running_app || find_running_app)"; then
running_pid="${running_app%% *}"
mkdir -p "$state_dir"
printf '%s\n' "$running_pid" > "$pid_file"
send_launch_action "$@" && exit 0
activate_codex_tray_menu_item "$running_pid" && exit 0
activate_status_notifier_item "$running_pid" && exit 0
focus_hyprland_window "$running_pid" && exit 0
start_second_instance_handoff "$@" && exit 0
notify-send "Codex is already running" "No warm-start socket, tray activation, or second-instance restore path was available." >/dev/null 2>&1 || true
exit 0
fi
exec codex-desktop "$@"

View File

@@ -7,6 +7,8 @@ from pathlib import Path
PROMPT = "Hyprland action"
HYPER_MODIFIERS = {"SUPER", "CTRL", "ALT"}
MODIFIER_ORDER = ["SHIFT", "SUPER", "CTRL", "ALT"]
def ensure_hyprland_instance():
@@ -51,6 +53,32 @@ def rofi_index(entries):
return None
def display_keys(keys):
parts = [part.strip() for part in keys.split("+")]
parts = [part for part in parts if part]
modifier_counts = {}
non_modifiers = []
for part in parts:
canonical = part.upper()
if canonical in MODIFIER_ORDER:
modifier_counts[canonical] = modifier_counts.get(canonical, 0) + 1
else:
non_modifiers.append(part)
modifiers = set(modifier_counts)
if not HYPER_MODIFIERS.issubset(modifiers):
return keys
remaining_modifiers = [
modifier
for modifier in MODIFIER_ORDER
if modifier not in HYPER_MODIFIERS and modifier in modifiers
]
display_parts = ["Hyper", *remaining_modifiers, *non_modifiers]
return " + ".join(display_parts)
def notify(message):
if shutil.which("notify-send"):
subprocess.run(["notify-send", "Hyprland action", message], check=False)
@@ -104,7 +132,7 @@ def main():
return 1
width = min(max(len(action["description"]) for action in actions), 48)
labels = [f"{action['description']:<{width}} {action['keys']}" for action in actions]
labels = [f"{action['description']:<{width}} {display_keys(action['keys'])}" for action in actions]
index = rofi_index(labels)
if index is None:
return 0

View File

@@ -36,6 +36,7 @@ ensure_hyprland_instance
layouts=(
"nStack Columns"
"master Large main"
"quadrants Quadrants"
"grid Grid"
"monocle Monocle"
)
@@ -64,7 +65,7 @@ for entry in "${layouts[@]}"; do
label="${entry#*$'\t'}"
if [[ "$label" == "$selection" ]]; then
hyprctl dispatch "_G.im_hyprland_set_layout(\"$layout\")" >/dev/null
hyprctl -q eval "_G.im_hyprland_set_layout(\"$layout\")"
exit 0
fi
done

View File

@@ -11,7 +11,7 @@ tray_services=(
flameshot.service
kanshi-sni.service
kdeconnect-indicator.service
keepbook-sync-daemon.service
keepbook-dioxus.service
network-manager-applet.service
notifications-tray-icon-gitea.service
notifications-tray-icon-github.service

View File

@@ -0,0 +1,241 @@
#!/usr/bin/env bash
set -euo pipefail
app_name="${HEROIC_ROCKET_LEAGUE_APP_NAME:-Sugar}"
heroic_config_dir="${HEROIC_CONFIG_DIR:-$HOME/.config/heroic}"
game_config="${HEROIC_ROCKET_LEAGUE_CONFIG:-$heroic_config_dir/GamesConfig/$app_name.json}"
installed_json="${HEROIC_LEGENDARY_INSTALLED_JSON:-$heroic_config_dir/legendaryConfig/legendary/installed.json}"
download_url="${BAKKESMOD_SETUP_URL:-https://github.com/bakkesmodorg/BakkesModInjectorCpp/releases/latest/download/BakkesModSetup.zip}"
cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/rocket-league-bakkesmod"
die() {
printf 'rocket-league-bakkesmod: %s\n' "$*" >&2
exit 1
}
need_command() {
command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
}
jq_value() {
local filter="$1"
local file="$2"
jq -er "$filter // empty" "$file" 2>/dev/null || true
}
heroic_value() {
local filter="$1"
jq_value ".$app_name.$filter" "$game_config"
}
installed_value() {
jq_value ".$app_name.$1" "$installed_json"
}
require_heroic_metadata() {
[ -f "$game_config" ] || die "missing Heroic game config: $game_config"
[ -f "$installed_json" ] || die "missing Legendary installed metadata: $installed_json"
}
rocket_league_dir() {
local value
value="${ROCKET_LEAGUE_DIR:-}"
if [ -z "$value" ]; then
value="$(installed_value install_path)"
fi
if [ -z "$value" ]; then
value="$HOME/Games/Heroic/rocketleague"
fi
printf '%s\n' "$value"
}
rocket_league_prefix() {
local value
value="${ROCKET_LEAGUE_PREFIX:-}"
if [ -z "$value" ]; then
value="$(heroic_value winePrefix)"
fi
[ -n "$value" ] || die "could not determine Heroic wine prefix from $game_config"
printf '%s\n' "$value"
}
rocket_league_proton() {
local value
value="${ROCKET_LEAGUE_PROTON:-}"
if [ -z "$value" ]; then
value="$(heroic_value 'wineVersion.bin')"
fi
[ -n "$value" ] || die "could not determine Heroic Proton binary from $game_config"
printf '%s\n' "$value"
}
ensure_paths() {
need_command jq
require_heroic_metadata
rl_dir="$(rocket_league_dir)"
rl_prefix="$(rocket_league_prefix)"
proton_bin="$(rocket_league_proton)"
[ -d "$rl_dir" ] || die "Rocket League install directory does not exist: $rl_dir"
[ -d "$rl_prefix" ] || die "Rocket League prefix does not exist: $rl_prefix"
[ -x "$proton_bin" ] || die "Proton binary is not executable: $proton_bin"
}
proton_run() {
export STEAM_COMPAT_DATA_PATH="$rl_prefix"
export STEAM_COMPAT_CLIENT_INSTALL_PATH="${STEAM_COMPAT_CLIENT_INSTALL_PATH:-$HOME/.steam/root}"
export WINEESYNC="${WINEESYNC:-1}"
export WINEFSYNC="${WINEFSYNC:-1}"
if command -v steam-run >/dev/null 2>&1; then
steam-run "$proton_bin" run "$@"
else
NIXPKGS_ALLOW_UNFREE=1 nix run --impure nixpkgs#steam-run -- "$proton_bin" run "$@"
fi
}
bakkesmod_exe() {
local candidates=(
"$rl_prefix/pfx/drive_c/Program Files/BakkesMod/BakkesMod.exe"
"$rl_prefix/drive_c/Program Files/BakkesMod/BakkesMod.exe"
"$rl_prefix/pfx/drive_c/users/$USER/AppData/Roaming/bakkesmod/bakkesmod/BakkesMod.exe"
"$rl_prefix/drive_c/users/$USER/AppData/Roaming/bakkesmod/bakkesmod/BakkesMod.exe"
)
local candidate
for candidate in "${candidates[@]}"; do
if [ -f "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
find "$rl_prefix" -iname BakkesMod.exe -print -quit 2>/dev/null
}
write_bat_launcher() {
local bat_path="$rl_dir/run_with_bakkesmod.bat"
local bakkes_dir="C:\\Program Files\\BakkesMod"
cat >"$bat_path" <<EOF
@echo off
set RL_PATH=%cd%\\Binaries\\Win64
echo Launching BakkesMod...
C:
cd "$bakkes_dir"
start BakkesMod.exe
echo Launching Rocket League without Easy Anti-Cheat: %RL_PATH%
Z:
cd %RL_PATH%
Launcher.exe -noeac %*
EOF
printf '%s\n' "$bat_path"
}
download_setup() {
need_command wget
need_command unzip
local zip setup
mkdir -p "$cache_dir"
rm -rf "$cache_dir/setup"
zip="$cache_dir/BakkesModSetup.zip"
wget -q --no-server-response -O "$zip" "$download_url"
unzip -oq "$zip" -d "$cache_dir/setup"
setup="$(find "$cache_dir/setup" -iname 'BakkesModSetup.exe' -o -iname 'BakkesMod.exe' | head -1)"
[ -n "$setup" ] || die "download did not contain BakkesModSetup.exe or BakkesMod.exe"
printf '%s\n' "$setup"
}
status() {
ensure_paths
printf 'Rocket League dir: %s\n' "$rl_dir"
printf 'Heroic prefix: %s\n' "$rl_prefix"
printf 'Proton: %s\n' "$proton_bin"
printf 'No-EAC launcher: %s\n' "$rl_dir/run_with_bakkesmod.bat"
local bakkes
bakkes="$(bakkesmod_exe)"
if [ -n "$bakkes" ]; then
printf 'BakkesMod exe: %s\n' "$bakkes"
else
printf 'BakkesMod exe: not installed in this prefix\n'
fi
}
setup() {
ensure_paths
write_bat_launcher >/dev/null
status
}
install() {
ensure_paths
write_bat_launcher >/dev/null
local silent=false
if [ "${1:-}" = "--silent" ]; then
silent=true
shift
fi
local setup_exe
setup_exe="$(download_setup)"
printf 'Running BakkesMod installer in Rocket League prefix...\n'
if $silent; then
if ! proton_run "$setup_exe" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART "$@"; then
die "installer failed; use Heroic > Rocket League > Settings > Wine > Run EXE in Prefix and select: $setup_exe"
fi
else
if ! proton_run "$setup_exe" "$@"; then
die "installer failed; use Heroic > Rocket League > Settings > Wine > Run EXE in Prefix and select: $setup_exe"
fi
fi
}
run() {
ensure_paths
local bat_path
bat_path="$(write_bat_launcher)"
local bakkes
bakkes="$(bakkesmod_exe)"
[ -n "$bakkes" ] || die "BakkesMod is not installed in the Rocket League prefix; run: rocket-league-bakkesmod install"
(
cd "$rl_dir"
proton_run "$bat_path" "$@"
)
}
usage() {
cat <<'EOF'
Usage: rocket-league-bakkesmod <command> [rocket-league-args...]
Commands:
status Show detected Heroic Rocket League, Proton, and BakkesMod paths.
setup Write the Heroic-compatible no-EAC BakkesMod .bat launcher.
install Download and run the BakkesMod installer inside the Rocket League prefix.
Use install --silent for unattended Inno Setup installation.
run Start BakkesMod, then launch Rocket League with -noeac for offline/local play.
EOF
}
command_name="${1:-status}"
if [ "$#" -gt 0 ]; then
shift
fi
case "$command_name" in
status) status "$@" ;;
setup) setup "$@" ;;
install) install "$@" ;;
run) run "$@" ;;
help|--help|-h) usage ;;
*) usage >&2; exit 2 ;;
esac

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env zsh
set -euo pipefail
# Choose which AI app SUPER+ALT+C toggles as a scratchpad: Codex or Claude
# Desktop. The choice is written to a state file that the Hyprland Lua config
# reads at keypress time, so switching is dynamic and needs no reload.
state_file="${XDG_STATE_HOME:-$HOME/.local/state}/hypr/ai-scratchpad"
mkdir -p "${state_file:h}"
names=(codex claude)
labels=("Codex" "Claude Desktop")
current=codex
[[ -r "$state_file" ]] && current="$(<"$state_file")"
menu=""
for i in {1..${#names}}; do
marker=" "
[[ "${names[$i]}" == "$current" ]] && marker="● "
menu+="${marker}${labels[$i]}\n"
done
index="$(printf "$menu" | rofi -dmenu -i -p "AI scratchpad" -format i)" || exit 0
[[ -n "$index" ]] || exit 0
selected="${names[$((index + 1))]}"
[[ -n "$selected" ]] || exit 0
print -r -- "$selected" > "$state_file"
# Bring the freshly selected scratchpad into view (no-op if already visible).
if command -v hyprctl >/dev/null 2>&1; then
hyprctl -q eval "_G.im_hyprland_show_ai_scratchpad()" >/dev/null 2>&1 || true
fi
if command -v notify-send >/dev/null 2>&1; then
notify-send "AI scratchpad" "Super+Alt+C now toggles ${labels[$((index + 1))]}" || true
fi

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env zsh
set -euo pipefail
# Pick a saved Codex Desktop project root via rofi, then start a new desktop
# thread in that project using the documented codex:// deep link.
codex_home="${CODEX_HOME:-$HOME/.codex}"
state_file="${CODEX_DESKTOP_STATE_FILE:-$codex_home/.codex-global-state.json}"
prompt="${CODEX_DESKTOP_PROJECT_PROMPT:-Codex project}"
notify() {
if command -v notify-send >/dev/null 2>&1; then
notify-send "Codex Desktop launcher" "$1"
else
printf '%s\n' "$1" >&2
fi
}
emit_candidates() {
if [[ ! -r "$state_file" ]]; then
notify "Cannot read Codex Desktop state: $state_file"
return 1
fi
jq -r '
def local_paths($key):
.[$key] // []
| .[]?
| select(type == "string" and startswith("/"));
local_paths("pinned-project-ids"),
local_paths("electron-saved-workspace-roots"),
local_paths("project-order")
' "$state_file"
}
dedup() {
awk 'NF && !seen[$0]++'
}
existing_dirs() {
local dir
while IFS= read -r dir; do
[[ -d "$dir" ]] && printf '%s\n' "$dir"
done
}
if [[ "${1:-}" == "--print-candidates" ]]; then
emit_candidates | dedup | existing_dirs
exit 0
fi
selected_dir="$(
emit_candidates | dedup | existing_dirs | rofi -dmenu -i -p "$prompt" || true
)"
[[ -n "$selected_dir" ]] || exit 0
case "$selected_dir" in
"~"|"~/"*)
selected_dir="$HOME${selected_dir:1}"
;;
esac
if command -v realpath >/dev/null 2>&1; then
selected_dir="$(realpath -m -- "$selected_dir" 2>/dev/null || printf '%s' "$selected_dir")"
fi
if [[ ! -d "$selected_dir" ]]; then
notify "Directory not found: $selected_dir"
exit 1
fi
encoded_path="$(jq -rn --arg path "$selected_dir" '$path | @uri')"
if ! command -v xdg-open >/dev/null 2>&1; then
notify "xdg-open is not available"
exit 1
fi
xdg-open "codex://threads/new?path=$encoded_path" >/dev/null 2>&1 &!

View File

@@ -10,6 +10,7 @@ state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/rofi-tmcodex"
history_file="$state_dir/dirs"
codex_home="${CODEX_HOME:-$HOME/.codex}"
terminal="${TMCODEX_TERMINAL:-${TERMINAL:-ghostty}}"
dotfiles_root="${DOTFILES_WORKTREE:-/srv/dotfiles}"
debug_log="$state_dir/debug.log"
tmcodex_args=("$@")
mkdir -p "$state_dir"
@@ -29,8 +30,8 @@ emit_candidates() {
# 2) A few common roots. Keep these before slow/best-effort discovery so
# rofi still has useful entries if a metadata scan breaks.
for d in \
"$HOME/dotfiles" \
"$HOME/dotfiles/nixos" \
"$dotfiles_root" \
"$dotfiles_root/nixos" \
"$HOME/Projects" \
"$HOME/config" \
"$HOME/org"
@@ -43,7 +44,7 @@ emit_candidates() {
# 4) Shallow git repo discovery under a few likely roots.
if command -v fd >/dev/null 2>&1; then
for root in "$HOME/Projects" "$HOME/dotfiles" "$HOME/config" "$HOME/org"; do
for root in "$HOME/Projects" "$dotfiles_root" "$HOME/config" "$HOME/org"; do
[[ -d "$root" ]] || continue
# Find ".git" directories; print their parent (repo root).
# Keep it shallow for speed.

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: setup-shared-dotfiles [--target /srv/dotfiles] [--group wheel] [--no-etc-nixos] [--force-etc-nixos]
Copies this dotfiles checkout to an absent or empty shared machine-local
worktree, then configures permissions so everyone can read it and sudoers can
edit it without sudo. When the target already contains a git checkout, this
repairs permissions/config only and does not overwrite local changes.
Defaults:
target: /srv/dotfiles
group: wheel
The script also creates /etc/nixos -> <target>/nixos when /etc/nixos is absent
or already a symlink. Use --force-etc-nixos to replace an existing /etc/nixos
path with the symlink.
EOF
}
target=/srv/dotfiles
group=wheel
manage_etc_nixos=1
force_etc_nixos=0
while [[ $# -gt 0 ]]; do
case "$1" in
--target)
target="$2"
shift 2
;;
--group)
group="$2"
shift 2
;;
--no-etc-nixos)
manage_etc_nixos=0
shift
;;
--force-etc-nixos)
force_etc_nixos=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source_root="$(git -C "$script_dir" rev-parse --show-toplevel)"
target="$(realpath -m "$target")"
if ! getent group "$group" >/dev/null; then
echo "Group '$group' does not exist" >&2
exit 1
fi
sudo install -d -m 2775 -o root -g "$group" "$(dirname "$target")"
if [[ "$source_root" != "$target" ]]; then
target_has_content=0
if [[ -d "$target" ]] && find "$target" -mindepth 1 -maxdepth 1 -print -quit | grep -q .; then
target_has_content=1
fi
if [[ "$target_has_content" == 1 && ! -d "$target/.git" ]]; then
echo "Refusing to copy into non-empty non-git target: $target" >&2
exit 1
fi
if [[ "$target_has_content" == 0 ]]; then
sudo install -d -m 2775 -o root -g "$group" "$target"
sudo rsync -a --chown="root:$group" "$source_root/" "$target/"
else
echo "Existing git checkout found at $target; repairing permissions only."
fi
fi
sudo chown -R "root:$group" "$target"
sudo find "$target" -type d -exec chmod 2775 {} +
sudo find "$target" -type f -exec chmod u+rw,g+rw,o+r {} +
if command -v setfacl >/dev/null; then
sudo setfacl -R -m "g::rwX,o::rX,m::rwX" "$target"
sudo setfacl -R -d -m "g::rwX,o::rX,m::rwX" "$target"
fi
sudo git -C "$target" config core.sharedRepository group
if ! sudo git config --system --get-all safe.directory 2>/dev/null | grep -Fx -- "$target" >/dev/null; then
sudo git config --system --add safe.directory "$target" 2>/dev/null || true
fi
if [[ "$manage_etc_nixos" == 1 ]]; then
nixos_target="$target/nixos"
if [[ ! -d "$nixos_target" ]]; then
echo "Expected NixOS flake directory is missing: $nixos_target" >&2
exit 1
fi
if [[ -L /etc/nixos || ! -e /etc/nixos ]]; then
sudo rm -rf /etc/nixos
sudo ln -s "$nixos_target" /etc/nixos
elif [[ "$force_etc_nixos" == 1 ]]; then
backup="/etc/nixos.backup.$(date +%Y%m%d-%H%M%S)"
sudo mv /etc/nixos "$backup"
sudo ln -s "$nixos_target" /etc/nixos
echo "Moved previous /etc/nixos to $backup"
else
echo "Leaving existing /etc/nixos in place; pass --force-etc-nixos to replace it." >&2
fi
fi
echo "Shared dotfiles worktree ready: $target"
echo "Nix/Home Manager should use dotfiles-worktree = \"$target\"."

View File

@@ -3,12 +3,12 @@
function pashowvolume {
timeout="${RUMNO_TIMEOUT:-2.5}"
if paismuted; then
rumno notify -t "$timeout" -m
rumno -t "$timeout" -m true
else
actual=$(pavolume)
max=100
show=$(( actual < max ? actual : max ))
rumno notify -t "$timeout" -v "$show"
rumno -t "$timeout" -v "$show"
fi
}

View File

@@ -10,16 +10,50 @@ set_multiplexer_title() {
title="$*"
tmux_socket=""
tmux_session_target=""
tmux_window_target=""
tmux_pane_target=""
if [ -n "${TMUX:-}" ]; then
multiplexer="tmux"
tmux_socket=${TMUX%%,*}
elif [ -n "${ZELLIJ:-}" ]; then
multiplexer="zellij"
else
return 0
tmux_socket="/tmp/tmux-$(id -u)/default"
if command -v tmux >/dev/null 2>&1 && [ -S "$tmux_socket" ]; then
# Newer Codex tool calls may be serviced by an app-server process
# that is not a child of the visible tmux pane, so TMUX is absent.
# Recover the likely pane by matching attached Codex panes in the
# current working directory and taking the newest matching client.
tmux_client=$(
tmux -S "$tmux_socket" list-clients -F '#{session_id}|#{window_id}|#{pane_id}|#{pane_pid}|#{pane_current_path}|#{pane_current_command}' 2>/dev/null |
awk -F '|' -v cwd="$PWD" '$5 == cwd && ($6 == "codex" || $6 == "codex-raw") { line = $0 } END { print line }'
)
if [ -n "$tmux_client" ]; then
multiplexer="tmux"
tmux_session_target=${tmux_client%%|*}
rest=${tmux_client#*|}
tmux_window_target=${rest%%|*}
rest=${rest#*|}
tmux_pane_target=${rest%%|*}
else
return 0
fi
else
return 0
fi
fi
state_dir="${HOME}/.agents/state"
state_file="$state_dir/${multiplexer}-title"
state_key="$multiplexer"
if [ -n "$tmux_pane_target" ]; then
state_key="${state_key}-${tmux_pane_target#%}"
elif [ -n "$tmux_socket" ]; then
state_key="${state_key}-$(printf '%s' "$tmux_socket" | tr '/.,:' '____')"
fi
state_file="$state_dir/${state_key}-title"
mkdir -p "$state_dir"
if [ -f "$state_file" ]; then
@@ -30,7 +64,14 @@ set_multiplexer_title() {
fi
if [ "$multiplexer" = "tmux" ]; then
tmux rename-session "$title" \; rename-window "$title" \; select-pane -T "$title"
if [ -n "$tmux_pane_target" ]; then
tmux -S "$tmux_socket" \
rename-session -t "$tmux_session_target" "$title" \; \
rename-window -t "$tmux_window_target" "$title" \; \
select-pane -t "$tmux_pane_target" -T "$title"
else
tmux rename-session "$title" \; rename-window "$title" \; select-pane -T "$title"
fi
else
zellij action rename-session "$title" &&
zellij action rename-tab "$title" &&

View File

@@ -1,22 +1,93 @@
#!/usr/bin/env sh
function _tmcodex_expand_dir {
case "$1" in
"~")
printf '%s\n' "$HOME"
;;
"~/"*)
printf '%s\n' "$HOME/${1#"~/"}"
;;
*)
printf '%s\n' "$1"
;;
esac
}
function _tmcodex_resolve_dir {
dir="$(_tmcodex_expand_dir "$1")"
if [ ! -d "$dir" ]; then
echo "tmcodex: directory not found: $1" >&2
return 1
fi
cd -- "$dir" && pwd -P
}
function _tmcodex_has_remote_arg {
while [ "$#" -gt 0 ]; do
case "$1" in
--remote|--remote=*)
return 0
;;
esac
shift
done
return 1
}
function tmcodex {
launch_dir="$PWD"
case "${1:-}" in
-C|--cd)
if [ -z "${2:-}" ]; then
echo "tmcodex: $1 requires a directory" >&2
return 2
fi
launch_dir="$2"
shift 2
;;
--cd=*)
launch_dir="${1#--cd=}"
shift
;;
*)
expanded_first_arg="$(_tmcodex_expand_dir "${1:-}")"
if [ -n "${1:-}" ] && [ -d "$expanded_first_arg" ]; then
launch_dir="$1"
shift
fi
;;
esac
launch_dir="$(_tmcodex_resolve_dir "$launch_dir")" || return
# Record launch directories so rofi_tmcodex can offer good defaults.
state_dir="${XDG_STATE_HOME:-$HOME/.local/state}/rofi-tmcodex"
history_file="$state_dir/dirs"
mkdir -p "$state_dir" 2>/dev/null || true
if [ -d "$PWD" ]; then
if [ -d "$launch_dir" ]; then
tmp="$(mktemp 2>/dev/null || true)"
if [ -n "$tmp" ]; then
{ printf '%s\n' "$PWD"; cat "$history_file" 2>/dev/null || true; } \
{ printf '%s\n' "$launch_dir"; cat "$history_file" 2>/dev/null || true; } \
| awk 'NF && !seen[$0]++' \
| head -n 200 >"$tmp" 2>/dev/null || true
mv -f "$tmp" "$history_file" 2>/dev/null || true
else
printf '%s\n' "$PWD" >>"$history_file" 2>/dev/null || true
printf '%s\n' "$launch_dir" >>"$history_file" 2>/dev/null || true
fi
fi
trw codex --dangerously-bypass-approvals-and-sandbox "$@"
(
cd -- "$launch_dir" || exit
if _tmcodex_has_remote_arg "$@"; then
trw codex --dangerously-bypass-approvals-and-sandbox --cd "$launch_dir" "$@"
else
trw codex --remote "${TMCODEX_REMOTE:-unix://}" --dangerously-bypass-approvals-and-sandbox --cd "$launch_dir" "$@"
fi
)
}
tmcodex "$@"

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env sh
function windows_toast {
powershell.exe -File ~/dotfiles/dotfiles/lib/bin/windows-toast.ps1 -Title "$@" 2>/dev/null
powershell.exe -File "${DOTFILES_WORKTREE:-/srv/dotfiles}/dotfiles/lib/bin/windows-toast.ps1" -Title "$@" 2>/dev/null
}
windows_toast "$@"

View File

@@ -17,7 +17,7 @@ function zcodex {
fi
fi
ZRW_NAME=codex zrw codex --dangerously-bypass-approvals-and-sandbox "$@"
ZRW_NAME=codex zrw codex --dangerously-bypass-approvals-and-sandbox --cd "$PWD" "$@"
}
zcodex "$@"

View File

@@ -1,10 +1,10 @@
# Create a new Codex session from the current pane path and switch to it.
# Prefix + C starts a new session without prompting for a name.
bind-key C new-session -c '#{pane_current_path}' 'codex --dangerously-bypass-approvals-and-sandbox'
bind-key C new-session -c '#{pane_current_path}' 'codex --dangerously-bypass-approvals-and-sandbox --cd "$PWD"'
source-file -q /etc/tmux-host-style.conf
set -g status-right '#($HOME/dotfiles/dotfiles/lib/functions/multiplexer_host_label --tmux 2>/dev/null || multiplexer_host_label --tmux 2>/dev/null || hostname -s) #{?window_bigger,[#{window_offset_x}#,#{window_offset_y}] ,}"#{=21:pane_title}" %H:%M %d-%b-%y'
set -g status-right '#(multiplexer_host_label --tmux 2>/dev/null || hostname -s) #{?window_bigger,[#{window_offset_x}#,#{window_offset_y}] ,}"#{=21:pane_title}" %H:%M %d-%b-%y'
set -g status-right-length 150
set -g set-titles on
set -g set-titles-string '#{?#{==:#{session_name},#{window_name}},#{session_name},#{session_name}:#{window_name}}#{?pane_title, - #{pane_title},}'

View File

@@ -6,7 +6,7 @@ set -g status-interval 2
set -g status-justify left
set -g status-left-length 150
set -g status-right-length 150
set -g status-right '#($HOME/dotfiles/dotfiles/lib/functions/multiplexer_host_label --tmux 2>/dev/null || multiplexer_host_label --tmux 2>/dev/null || hostname -s) #(eval $POWERLINE_COMMAND tmux right -R pane_id=`tmux display -p "#D"`)'
set -g status-right '#(multiplexer_host_label --tmux 2>/dev/null || hostname -s) #(eval $POWERLINE_COMMAND tmux right -R pane_id=`tmux display -p "#D"`)'
set -g window-status-format "#[fg=white] #[fg=white,bg=black]#I #[fg=white] #[default]#W "
set -g window-status-current-format "#[fg=black,bg=blue]#[fg=white,bg=blue] #I  #[fg=white,bold]#W #[fg=blue,bg=black,nobold]"
set-window-option -g window-status-fg white

View File

@@ -53,3 +53,12 @@ cachix-auth-from-clipboard:
if command -v wl-paste >/dev/null; then wl-paste --no-newline | cachix authtoken --stdin; printf '' | wl-copy; \
elif command -v xclip >/dev/null; then xclip -o -selection clipboard | tr -d '\n' | cachix authtoken --stdin; printf '' | xclip -selection clipboard; \
else echo "No clipboard tool found (expected wl-paste or xclip)." >&2; exit 1; fi
# Install or re-permission the repo as a shared machine-local checkout.
#
# Usage:
# - `just setup-shared-dotfiles`
# - `just setup-shared-dotfiles --target /srv/dotfiles --group wheel`
# - `just setup-shared-dotfiles --force-etc-nixos`
setup-shared-dotfiles *args:
./dotfiles/lib/bin/setup-shared-dotfiles {{args}}

62
nix-darwin/flake.lock generated
View File

@@ -67,16 +67,16 @@
"brew-src_2": {
"flake": false,
"locked": {
"lastModified": 1778146321,
"narHash": "sha256-HeBwuJmuBioZHyZqDOcf7W/xsMFupSD583v6I5Cl7a8=",
"lastModified": 1779646357,
"narHash": "sha256-rnnAaESXxItX4D9xCMGvs3hfDBjbbTYht7OluRcvT8k=",
"owner": "Homebrew",
"repo": "brew",
"rev": "af835384ac574f76025adb38b292b04cecee1f1f",
"rev": "10a163ac127624caa80cc5cc5a705e97f3615b0e",
"type": "github"
},
"original": {
"owner": "Homebrew",
"ref": "5.1.10",
"ref": "5.1.14",
"repo": "brew",
"type": "github"
}
@@ -89,11 +89,11 @@
]
},
"locked": {
"lastModified": 1778621214,
"narHash": "sha256-a01yQHAvpKSEgo22bKBtHOWtDb49U92dIBDV7WVIaEA=",
"lastModified": 1780440050,
"narHash": "sha256-GUwh7tKnK1ZibdzRIbZ2CrKz9/PJ6BQUXW6Ru3rn56g=",
"owner": "sadjow",
"repo": "claude-code-nix",
"rev": "6c8a73cc749fff4b45ae26d86dbfc82e2093d12c",
"rev": "fcf0beee92892f9193ad52549ed265091bbefde7",
"type": "github"
},
"original": {
@@ -110,11 +110,11 @@
]
},
"locked": {
"lastModified": 1778282716,
"narHash": "sha256-fHo9PlYWu970hmdVkDB2Jqeu0VmhmqQ5iKOnPjf/I1E=",
"lastModified": 1780345858,
"narHash": "sha256-t3dxFjzEFbuMd7o8LWtbnqfOkXDKjzvHXUiXQwvwXtk=",
"owner": "sadjow",
"repo": "codex-cli-nix",
"rev": "7f0f3802287581e04501e2fea26b56d63df18ebd",
"rev": "ea8119de14a2263330da99363e6303db10a0f84b",
"type": "github"
},
"original": {
@@ -377,11 +377,11 @@
]
},
"locked": {
"lastModified": 1778628724,
"narHash": "sha256-VNG6hJ146VEenXcDrB3t6MVnrMx+gtyCWTCDkzOp9Qs=",
"lastModified": 1780515920,
"narHash": "sha256-8KX2hEeOX6KP3hBBJJI8dGWVrzbOOf1rBPmg/GUG24U=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "6a0bbd6b4720da1c9ce7ebf35ff5c41a82db367a",
"rev": "4c5c1e8ba14f1c7475fa31ff11bc1c19cd220974",
"type": "github"
},
"original": {
@@ -415,11 +415,11 @@
"homebrew-cask": {
"flake": false,
"locked": {
"lastModified": 1778665161,
"narHash": "sha256-iffsbk5uf+xkPJY0DQzfBCTo58lCFxoLSmtFdhKwAuo=",
"lastModified": 1780507130,
"narHash": "sha256-C9O84NflGNor0YiyBdm6+YhaoXU1TrAFFdub9upbfx0=",
"owner": "homebrew",
"repo": "homebrew-cask",
"rev": "41670e2c6ba14d92acf86c22fccc26d9bd972212",
"rev": "dd4fb4043764201c941e8b992942330fc3d60175",
"type": "github"
},
"original": {
@@ -431,11 +431,11 @@
"homebrew-core": {
"flake": false,
"locked": {
"lastModified": 1778667551,
"narHash": "sha256-1KftBpiQRi6t2/jn7kiCXWp9Ov4FVD19wrHNvJVmDeI=",
"lastModified": 1780517143,
"narHash": "sha256-ktdHmOdXbpvI4JGd3K3Z7dHAFrqkycJSRqIuPR9YHvw=",
"owner": "homebrew",
"repo": "homebrew-core",
"rev": "ec249d9026c15d9444d27346352ff8b228ad4af9",
"rev": "750a96ef95383f9da433dc665bd441eb4e796f01",
"type": "github"
},
"original": {
@@ -455,11 +455,11 @@
]
},
"locked": {
"lastModified": 1778406464,
"narHash": "sha256-xCzb78zzv3DJbA5+/NyA8WVUzWwWyHCginbFN7AoIHo=",
"lastModified": 1780477777,
"narHash": "sha256-0tkMA17QnFbr/8G9kkak8Y7vE6LGBJgbO1W/8jfmpvo=",
"owner": "colonelpanic8",
"repo": "keepbook",
"rev": "b591f97904a3fe0d89516efbf6f4fee1abc58e8c",
"rev": "dda99c6e00d9c99744ff9c9dd1fd5a64436e05b7",
"type": "github"
},
"original": {
@@ -475,11 +475,11 @@
]
},
"locked": {
"lastModified": 1777780666,
"narHash": "sha256-8wURyQMdDkGUarSTKOGdCuFfYiwa3HbzwscUfn3STDE=",
"lastModified": 1779036909,
"narHash": "sha256-zXcwYQGCT6pzinK+1dBB2ekTVtfxGZAapb3Evdcu4fY=",
"owner": "LnL7",
"repo": "nix-darwin",
"rev": "8c62fba0854ba15c8917aed18894dbccb48a3777",
"rev": "56c666e108467d87d13508936aade6d567f2a501",
"type": "github"
},
"original": {
@@ -493,11 +493,11 @@
"brew-src": "brew-src_2"
},
"locked": {
"lastModified": 1778332591,
"narHash": "sha256-ctJ3ADtugrnbMfMBobA645gCqXVIyHnsCNMkVaIuSiM=",
"lastModified": 1780492467,
"narHash": "sha256-zMEJwtQPmsPPgPczFkyjWHgd1z0HagOPS2Wt2WDYLJY=",
"owner": "zhaofengli-wip",
"repo": "nix-homebrew",
"rev": "7d0038b5bb60568ec41f5f4ef5067cd221ca7c0d",
"rev": "562332f97de9f5ba51aa647d70462e88222b2988",
"type": "github"
},
"original": {
@@ -524,11 +524,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1778580735,
"narHash": "sha256-t+8AVV8ExvOmslz2sLIgw/hJBKlyl65rJvxjvvjHgpE=",
"lastModified": 1780336545,
"narHash": "sha256-vhVhuXzFrIOfcssC/9hDHx7MHzDKjF3keHuREOQqQiQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "48d91f2c0ce7b9e589f967d4f685153dd765dcdd",
"rev": "4df1b885d76a54e1aa1a318f8d16fd6005b6401f",
"type": "github"
},
"original": {

View File

@@ -284,7 +284,16 @@
(final: prev: {
codex = inputs.codex-cli-nix.packages.${prev.stdenv.hostPlatform.system}.default;
claude-code = inputs.claude-code-nix.packages.${prev.stdenv.hostPlatform.system}.default;
git-sync-rs = git-sync-rs.packages.${prev.stdenv.hostPlatform.system}.default;
git-sync-rs = git-sync-rs.packages.${prev.stdenv.hostPlatform.system}.default.overrideAttrs (old: {
checkFlags =
(old.checkFlags or [])
++ [
# Git can auto-detect the Darwin Nix build user's identity, so this
# test does not exercise git-sync-rs's missing-identity fallback here.
"--skip"
"sync::transport::tests::commit_retries_with_fallback_identity_when_git_identity_missing"
];
});
})
];
environment.systemPackages =
@@ -316,9 +325,6 @@
"spotify"
"vlc"
];
masApps = {
Xcode = 497799835;
};
greedyCasks = true;
onActivation = {
cleanup = "zap";

View File

@@ -66,7 +66,7 @@
--passphrase-file "$passphrase_path" \
--import "$normalized_key_file"
'';
multiplexerAliases = import ../../shared/multiplexer-aliases.nix;
multiplexerAliases = import ../../nix-shared/multiplexer-aliases.nix;
excludedTopLevelEntries = [
"codex"
@@ -200,18 +200,18 @@ in {
programs.ssh = {
enable = true;
enableDefaultConfig = false;
matchBlocks = {
settings = {
"*" = {
forwardAgent = true;
addKeysToAgent = "no";
compression = false;
serverAliveInterval = 0;
serverAliveCountMax = 3;
hashKnownHosts = false;
userKnownHostsFile = "~/.ssh/known_hosts";
controlMaster = "no";
controlPath = "~/.ssh/master-%r@%n:%p";
controlPersist = "no";
ForwardAgent = true;
AddKeysToAgent = "no";
Compression = false;
ServerAliveInterval = 0;
ServerAliveCountMax = 3;
HashKnownHosts = false;
UserKnownHostsFile = "~/.ssh/known_hosts";
ControlMaster = "no";
ControlPath = "~/.ssh/master-%r@%n:%p";
ControlPersist = "no";
};
};
};
@@ -283,6 +283,7 @@ in {
programs.zsh = {
enable = true;
dotDir = "${config.home.homeDirectory}/.zsh";
autosuggestion.enable = true;
oh-my-zsh = {
enable = true;

View File

@@ -10,6 +10,7 @@
else pkgs.git-sync;
orgPath = "${config.home.homeDirectory}/org";
passwordStorePath = "${config.home.homeDirectory}/.password-store";
claudePath = "${config.home.homeDirectory}/.claude";
in {
services.git-sync = {
enable = true;
@@ -24,6 +25,16 @@ in {
path = passwordStorePath;
uri = "git@github.com:colonelpanic8/.password-store.git";
};
claude-history = {
path = claudePath;
uri = "git@github.com:colonelpanic8/claude-history.git";
interval = 600;
};
# NB: codex-history is intentionally NOT synced on mac-demarco-mini.
# The codex archive is ~1GB and this machine runs chronically near full
# (APFS container ~94% used); cloning it would break every darwin
# rebuild. mac's own Codex sessions are already merged into the repo —
# it just doesn't receive. Re-enable once the disk has headroom.
};
};
@@ -33,5 +44,9 @@ in {
lib.mkForce ["${gitSyncPackage}/bin/git-sync" "-d" orgPath];
git-sync-password-store.config.ProgramArguments =
lib.mkForce ["${gitSyncPackage}/bin/git-sync" "-d" passwordStorePath];
# Live Claude sessions append to their transcript constantly; sync
# untracked session files and throttle event-driven syncs.
git-sync-claude-history.config.ProgramArguments =
lib.mkForce ["${gitSyncPackage}/bin/git-sync-rs" "-d" claudePath "watch" "--new-files" "true" "--min-interval" "300"];
};
}

View File

@@ -6,6 +6,10 @@
}: let
cfg = config.myModules.codexGeneratedSkills;
oos = config.lib.file.mkOutOfStoreSymlink;
managedConfig = pkgs.writeText "codex-managed-config.toml" ''
[mcp_servers.nixos]
command = "${lib.getExe pkgs.mcp-nixos}"
'';
in {
options.myModules.codexGeneratedSkills = {
enable = lib.mkEnableOption "Codex home setup";
@@ -22,6 +26,12 @@ in {
description = "Codex dotfiles directory in the live worktree.";
};
sourceCodexDir = lib.mkOption {
type = lib.types.str;
default = "${cfg.worktreeCodexDir}";
description = "Readable fallback Codex dotfiles directory from the flake source.";
};
localConfig = lib.mkOption {
type = lib.types.str;
default = "${cfg.codexHome}/config.local.toml";
@@ -49,10 +59,11 @@ in {
config = lib.mkIf cfg.enable {
home.file = {
".codex/.gitignore" = {
force = true;
source = oos "${cfg.worktreeCodexDir}/.gitignore";
};
# NB: ~/.codex/.gitignore is intentionally NOT managed here. ~/.codex is
# a git-sync-rs checkout of the codex-history repo, which ships its own
# real .gitignore — git refuses to read a symlinked ignore file, so an
# HM-managed symlink here would silently disable ignore rules and risk
# committing auth.json/sqlite state. Leave it to the repo.
".codex/AGENTS.md" = {
force = true;
@@ -103,6 +114,8 @@ in {
home.activation.generateCodexConfig = lib.hm.dag.entryAfter ["writeBoundary"] ''
codex_home=${lib.escapeShellArg cfg.codexHome}
base=${lib.escapeShellArg "${cfg.worktreeCodexDir}/config.toml"}
source_base=${lib.escapeShellArg "${cfg.sourceCodexDir}/config.toml"}
managed_config=${lib.escapeShellArg managedConfig}
local_config=${lib.escapeShellArg cfg.localConfig}
local_state_config=${lib.escapeShellArg cfg.generatedStateConfig}
target="$codex_home/config.toml"
@@ -115,8 +128,12 @@ in {
)}
if [ ! -r "$base" ]; then
echo "Missing shared Codex config at $base" >&2
exit 1
if [ -r "$source_base" ]; then
base="$source_base"
else
echo "Missing shared Codex config at $base and $source_base" >&2
exit 1
fi
fi
mkdir -p "$codex_home"
@@ -129,7 +146,7 @@ in {
-v begin_marker="$begin_marker" \
-v end_marker="$end_marker" \
-v rejected_prefixes="$rejected_project_prefixes" '
FNR == NR {
ARGIND < ARGC - 1 {
if ($0 ~ /^\[[^]]+\]$/) {
base_sections[$0] = 1
}
@@ -183,7 +200,7 @@ in {
END {
flush_block()
}
' "$base" "$target" \
' "$base" "$managed_config" "$target" \
| ${lib.getExe pkgs.perl} -0pe 's/\n{3,}/\n\n/g' \
> "$local_state"
@@ -200,6 +217,8 @@ in {
chmod 600 "$tmp"
cat "$base" > "$tmp"
printf '\n' >> "$tmp"
cat "$managed_config" >> "$tmp"
if [ -r "$local_config" ]; then
printf '\n' >> "$tmp"
cat "$local_config" >> "$tmp"
@@ -221,30 +240,37 @@ in {
home.activation.linkCodexDotfileSkills = lib.hm.dag.entryAfter ["writeBoundary"] ''
skills_dir=${lib.escapeShellArg cfg.skillsDir}
worktree_skills=${lib.escapeShellArg "${cfg.worktreeCodexDir}/skills"}
source_skills=${lib.escapeShellArg "${cfg.sourceCodexDir}/skills"}
if [ ! -d "$worktree_skills" ]; then
echo "Skipping Codex dotfile skills setup because $worktree_skills is not a directory" >&2
exit 1
if [ -d "$source_skills" ]; then
worktree_skills="$source_skills"
else
echo "Skipping Codex dotfile skills setup because neither $worktree_skills nor $source_skills is a directory" >&2
worktree_skills=
fi
fi
mkdir -p "$skills_dir"
for skill in "$worktree_skills"/*; do
[ -d "$skill" ] || continue
[ -r "$skill/SKILL.md" ] || continue
if [ -n "$worktree_skills" ]; then
for skill in "$worktree_skills"/*; do
[ -d "$skill" ] || continue
[ -r "$skill/SKILL.md" ] || continue
name="$(basename "$skill")"
case "$name" in
.system|codex-primary-runtime) continue ;;
esac
name="$(basename "$skill")"
case "$name" in
.system|codex-primary-runtime) continue ;;
esac
target="$skills_dir/$name"
if [ -L "$target" ] || [ ! -e "$target" ]; then
ln -sfn "$skill" "$target"
elif [ ! -d "$target" ]; then
echo "Skipping Codex skill $name because $target exists and is not a directory" >&2
fi
done
target="$skills_dir/$name"
if [ -L "$target" ] || [ ! -e "$target" ]; then
ln -sfn "$skill" "$target"
elif [ ! -d "$target" ]; then
echo "Skipping Codex skill $name because $target exists and is not a directory" >&2
fi
done
fi
'';
home.activation.setupCodexGeneratedSkills = lib.hm.dag.entryAfter ["linkCodexDotfileSkills"] ''

View File

@@ -1,5 +1,4 @@
final: prev:
let
final: prev: {
# XXX: codex and claude-code are now provided by dedicated flakes in nix.nix:
# - inputs.codex-cli-nix (github:sadjow/codex-cli-nix)
# - inputs.claude-code-nix (github:sadjow/claude-code-nix)
@@ -30,46 +29,6 @@ let
# hash = "sha256-OqvLiwB5TwZaxDvyN/+/+eueBdWNaYxd81cd5AZK/mA=";
# npmDepsHash = "sha256-vy7osk3UAOEgsJx9jdcGe2wICOk5Urzxh1WLAHyHM+U=";
# };
# Chrome 136+ ignores remote debugging switches on the default profile.
# Keep the wrapper in place, but do not inject remote debugging flags into
# the normal Chrome launcher. The supported path for a real profile is the
# Chrome remote debugging permission flow used by chrome-devtools-mcp
# --auto-connect.
chromeRemoteDebuggingFlags = [];
placeholder = null; # Dummy binding to keep let block valid
in
{
google-chrome = prev.symlinkJoin {
name = prev.google-chrome.name;
paths = [ prev.google-chrome ];
nativeBuildInputs = [ final.makeWrapper ];
postBuild = ''
rm "$out/bin/google-chrome" "$out/bin/google-chrome-stable"
makeWrapper ${prev.google-chrome}/bin/google-chrome "$out/bin/google-chrome" \
${final.lib.concatMapStringsSep " " (flag: "--add-flags ${final.lib.escapeShellArg flag}") chromeRemoteDebuggingFlags}
makeWrapper ${prev.google-chrome}/bin/google-chrome-stable "$out/bin/google-chrome-stable" \
${final.lib.concatMapStringsSep " " (flag: "--add-flags ${final.lib.escapeShellArg flag}") chromeRemoteDebuggingFlags}
for desktopName in google-chrome.desktop com.google.Chrome.desktop; do
desktopFile="$out/share/applications/$desktopName"
if [ -f "$desktopFile" ]; then
rm "$desktopFile"
cp "${prev.google-chrome}/share/applications/$desktopName" "$desktopFile"
substituteInPlace "$desktopFile" \
--replace-fail "${prev.google-chrome}/bin/google-chrome-stable" "$out/bin/google-chrome-stable"
substituteInPlace "$desktopFile" \
--replace-fail "image/gif;" "" \
--replace-fail "image/jpeg;" "" \
--replace-fail "image/png;" "" \
--replace-fail "image/webp;" ""
fi
done
'';
meta = prev.google-chrome.meta;
};
# Fix poetry pbs-installer version constraint issue
poetry = prev.poetry.overrideAttrs (oldAttrs: {
dontCheckRuntimeDeps = true;
@@ -88,6 +47,18 @@ in
oldAttrs.preConfigure;
});
vte = prev.vte.overrideAttrs (oldAttrs: {
# The termite compatibility patch in nixpkgs still uses a helper that VTE
# removed. VTE 0.84 builds as C++23 and already uses std::to_underlying.
postPatch = (oldAttrs.postPatch or "") + ''
if grep -q "vte::to_integral(vte::platform::ClipboardType::PRIMARY)" src/vtegtk.cc; then
substituteInPlace src/vtegtk.cc \
--replace-fail "vte::to_integral(vte::platform::ClipboardType::PRIMARY)" \
"std::to_underlying(vte::platform::ClipboardType::PRIMARY)"
fi
'';
});
# XXX: codex and claude-code are now provided by flakes in nix.nix
# See the overlay at the end of nixpkgs.overlays in nix.nix
@@ -311,7 +282,6 @@ from transformers import (/' \
'';
});
happy-coder = final.callPackage ../../nixos/packages/happy-coder { };
playwright-cli = final.callPackage ../../nixos/packages/playwright-cli { };
t3code = final.callPackage ../../nixos/packages/t3code { };
# Custom Waybar fork for workspace taskbar support + external SNI watcher option.

View File

@@ -60,7 +60,6 @@
pstree
rclone
ripgrep
silver-searcher
skim
tmux
zellij

View File

@@ -7,7 +7,7 @@ description: Use when user asks to bump, update, or upgrade claude-code or codex
## Overview
Updates claude-code and/or codex to latest versions in `~/dotfiles/nixos/overlay.nix`. Nix requires correct hashes which must be discovered through failed builds.
Updates claude-code and/or codex to latest versions in `/etc/nixos/overlay.nix`. Nix requires correct hashes which must be discovered through failed builds.
## Quick Reference
@@ -30,7 +30,7 @@ curl -s "https://api.github.com/repos/openai/codex/releases/latest" | jq -r '.ta
### 2. Update Version and Clear Hashes
In `~/dotfiles/nixos/overlay.nix`:
In `/etc/nixos/overlay.nix`:
**For claude-code:**
```nix
@@ -77,4 +77,4 @@ enableClaudeCodeOverride = true; # Set false to use nixpkgs claude-code
## File Location
`~/dotfiles/nixos/overlay.nix`
`/etc/nixos/overlay.nix`

View File

@@ -1 +0,0 @@
[ 305ms] [ERROR] Failed to load resource: the server responded with a status of 403 () @ https://www.reddit.com/r/hyprland/comments/1t74dt6/pre055_discussion_share_your_new_lua_scripts_that/?solution=c0ac3501a8dec997c0ac3501a8dec997&js_challenge=1&token=bbbe4bf1c9a2b5160829c4be34da586130c27f161a9c565bc73f58c2dec5bfa9&jsc_orig_r=:0

View File

@@ -1,26 +0,0 @@
- generic [active] [ref=e1]:
- link [ref=e3] [cursor=pointer]:
- /url: https://www.reddit.com
- img [ref=e4]
- generic [ref=e5]:
- img [ref=e6]
- heading "Prove your humanity" [level=1] [ref=e8]
- paragraph [ref=e9]: Were committed to safety and security. But not for bots. Complete the challenge below and let us know youre a real person.
- iframe [ref=e14]:
- generic [ref=f1e2]:
- generic [ref=f1e3]:
- checkbox "I'm not a robot" [ref=f1e7]
- generic [ref=f1e11]: I'm not a robot
- generic [ref=f1e15]: reCAPTCHA
- generic [ref=e15]:
- link "Reddit, Inc. © \"2026\". All rights reserved." [ref=e16] [cursor=pointer]:
- /url: https://www.redditinc.com/
- generic [ref=e17]:
- link "User Agreement" [ref=e18] [cursor=pointer]:
- /url: https://www.reddit.com/help/useragreement
- link "Privacy Policy" [ref=e19] [cursor=pointer]:
- /url: https://www.reddit.com/help/privacypolicy
- link "Content Policy" [ref=e20] [cursor=pointer]:
- /url: https://www.reddit.com/help/contentpolicy
- link "Help" [ref=e21] [cursor=pointer]:
- /url: https://support.reddithelp.com/hc/en-us

View File

@@ -1,9 +0,0 @@
- generic [ref=e3]:
- img [ref=e5]
- generic [ref=e7]:
- generic [ref=e8]: You've been blocked by network security.
- generic [ref=e10]:
- text: If you think you've been blocked by mistake, file a ticket below and we'll look into it.
- link "File a ticket" [ref=e12] [cursor=pointer]:
- /url: https://support.reddithelp.com/hc/en-us/requests/new?ticket_form_id=21879292693140
- generic [ref=e14]: File a ticket

View File

@@ -1,9 +0,0 @@
- generic [ref=e3]:
- img [ref=e5]
- generic [ref=e7]:
- generic [ref=e8]: You've been blocked by network security.
- generic [ref=e10]:
- text: If you think you've been blocked by mistake, file a ticket below and we'll look into it.
- link "File a ticket" [ref=e12] [cursor=pointer]:
- /url: https://support.reddithelp.com/hc/en-us/requests/new?ticket_form_id=21879292693140
- generic [ref=e14]: File a ticket

View File

@@ -1,6 +1,6 @@
# Agent Notes (dotfiles/nixos)
This repository is a single git repo rooted at `~/dotfiles`. This `nixos/` directory is the NixOS flake, but most "user command" scripts and shell functions live outside of it.
This repository is a single git repo rooted at `/srv/dotfiles` on NixOS machines. This `nixos/` directory is the NixOS flake, but most "user command" scripts and shell functions live outside of it.
## Where To Put Things
@@ -13,7 +13,7 @@ Avoid dropping scripts in `~/bin` or `~/.local/bin` unless the user explicitly a
## NixOS Rebuild Workflow
- Run `just switch` from `~/dotfiles/nixos` (not `nixos-rebuild` directly).
- Run `just switch` from `/etc/nixos` or `/srv/dotfiles/nixos` (not `nixos-rebuild` directly).
- Host configs live under `machines/`.
## Rofi/Tmux Integration Pointers

View File

@@ -10,6 +10,7 @@ makeEnable config "myModules.base" true {
"electron-12.2.3"
"electron-19.1.9"
"electron-32.3.3"
"electron-39.8.10"
"etcher"
"nix-2.16.2"
"openssl-1.0.2u"
@@ -39,10 +40,12 @@ makeEnable config "myModules.base" true {
networking.nameservers = ["8.8.8.8" "8.8.4.4"];
networking.networkmanager = {
enable = true;
dns = "systemd-resolved";
plugins = [pkgs.networkmanager-l2tp pkgs.networkmanager-openvpn];
settings.main.rc-manager = "symlink";
};
networking.resolvconf.enable = false;
services.resolved.enable = true;
services.mullvad-vpn.enable = true;
# Audio

View File

@@ -1,64 +0,0 @@
{pkgs, ...}: {
imports = [
../nix-shared/system/essential.nix
];
environment.systemPackages = with pkgs; [
emacs-auto
];
programs.zsh.enable = true;
networking.firewall.enable = false;
networking.networkmanager = {
enable = true;
extraConfig = ''
[main]
rc-manager=resolvconf
'';
};
nixpkgs.config.allowUnfree = true;
services.xserver = {
exportConfiguration = true;
enable = true;
layout = "us";
desktopManager = {
plasma6.enable = true;
};
displayManager = {
sddm = {
enable = true;
};
sessionCommands = ''
systemctl --user import-environment GDK_PIXBUF_MODULE_FILE DBUS_SESSION_BUS_ADDRESS PATH
'';
setupCommands = ''
autorandr -c
systemctl restart autorandr.service
'';
};
};
nix = {
extraOptions = ''
experimental-features = nix-command flakes
'';
};
users.users = {
imalison = {
extraGroups = [
"audio"
"adbusers"
"disk"
"docker"
"networkmanager"
"openrazer"
"plugdev"
"syncthing"
"systemd-journal"
"video"
"wheel"
];
group = "users";
isNormalUser = true;
createHome = true;
shell = pkgs.zsh;
};
};
}

View File

@@ -1,985 +0,0 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nixified-ai",
"nixpkgs"
]
},
"locked": {
"lastModified": 1677714448,
"narHash": "sha256-Hq8qLs8xFu28aDjytfxjdC96bZ6pds21Yy09mSC156I=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "dc531e3a9ce757041e1afaff8ee932725ca60002",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1673362319,
"narHash": "sha256-Pjp45Vnj7S/b3BRpZEVfdu8sqqA6nvVjvYu59okhOyI=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "82c16f1682cf50c01cb0280b38a1eed202b3fe9f",
"type": "github"
},
"original": {
"id": "flake-parts",
"type": "indirect"
}
},
"flake-parts_3": {
"inputs": {
"nixpkgs-lib": [
"nixified-ai",
"hercules-ci-effects",
"hercules-ci-agent",
"nixpkgs"
]
},
"locked": {
"lastModified": 1666885127,
"narHash": "sha256-uXA/3lhLhwOTBMn9a5zJODKqaRT+SuL5cpEmOz2ULoo=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "0e101dbae756d35a376a5e1faea532608e4a4b9a",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": [
"systems"
]
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1667077288,
"narHash": "sha256-bdC8sFNDpT0HK74u9fUkpbf1MEzVYJ+ka7NXCdgBoaA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "6ee9ebb6b1ee695d2cacc4faa053a7b9baa76817",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_4": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_5": {
"inputs": {
"systems": "systems_4"
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"git-ignore-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1660459072,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"git-ignore-nix_2": {
"inputs": {
"nixpkgs": "nixpkgs_6"
},
"locked": {
"lastModified": 1660459072,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"ref": "master",
"repo": "gitignore.nix",
"type": "github"
}
},
"git-ignore-nix_3": {
"inputs": {
"nixpkgs": "nixpkgs_8"
},
"locked": {
"lastModified": 1660459072,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"ref": "master",
"repo": "gitignore.nix",
"type": "github"
}
},
"gtk-sni-tray": {
"inputs": {
"flake-utils": [
"taffybar",
"flake-utils"
],
"git-ignore-nix": [
"taffybar",
"git-ignore-nix"
],
"nixpkgs": [
"taffybar",
"nixpkgs"
],
"status-notifier-item": [
"taffybar",
"status-notifier-item"
]
},
"locked": {
"lastModified": 1663379298,
"narHash": "sha256-m18+G7V1N+g/pPeKJG9hkblGA5c8QTnUYnsU5t14sOw=",
"owner": "taffybar",
"repo": "gtk-sni-tray",
"rev": "1927d86308d34b5d21a709cf8ff5332ec5d37de4",
"type": "github"
},
"original": {
"owner": "taffybar",
"ref": "master",
"repo": "gtk-sni-tray",
"type": "github"
}
},
"gtk-strut": {
"inputs": {
"flake-utils": [
"taffybar",
"flake-utils"
],
"git-ignore-nix": [
"taffybar",
"git-ignore-nix"
],
"nixpkgs": [
"taffybar",
"nixpkgs"
]
},
"locked": {
"lastModified": 1663377859,
"narHash": "sha256-UrBd+R3NaJIDC2lt5gMafS3KBeLs83emm2YorX2cFCo=",
"owner": "taffybar",
"repo": "gtk-strut",
"rev": "d946eb230cdccf5afc063642b3215723e555990b",
"type": "github"
},
"original": {
"owner": "taffybar",
"ref": "master",
"repo": "gtk-strut",
"type": "github"
}
},
"hercules-ci-agent": {
"inputs": {
"flake-parts": "flake-parts_3",
"nix-darwin": "nix-darwin",
"nixpkgs": "nixpkgs_2",
"pre-commit-hooks-nix": "pre-commit-hooks-nix"
},
"locked": {
"lastModified": 1673183923,
"narHash": "sha256-vb+AEQJAW4Xn4oHsfsx8H12XQU0aK8VYLtWYJm/ol28=",
"owner": "hercules-ci",
"repo": "hercules-ci-agent",
"rev": "b3f8aa8e4a8b22dbbe92cc5a89e6881090b933b3",
"type": "github"
},
"original": {
"id": "hercules-ci-agent",
"type": "indirect"
}
},
"hercules-ci-effects": {
"inputs": {
"flake-parts": "flake-parts_2",
"hercules-ci-agent": "hercules-ci-agent",
"nixpkgs": [
"nixified-ai",
"nixpkgs"
]
},
"locked": {
"lastModified": 1676558019,
"narHash": "sha256-obUHCMMWbffb3k0b9YIChsJ2Z281BcDYnTPTbJRP6vs=",
"owner": "hercules-ci",
"repo": "hercules-ci-effects",
"rev": "fdbc15b55db8d037504934d3af52f788e0593380",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "hercules-ci-effects",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1692503956,
"narHash": "sha256-MOA6FKc1YgfGP3ESnjSYfsyJ1BXlwV5pGlY/u5XdJfY=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "958c06303f43cf0625694326b7f7e5475b1a2d5c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"invokeai-src": {
"flake": false,
"locked": {
"lastModified": 1677475057,
"narHash": "sha256-REtyVcyRgspn1yYvB4vIHdOrPRZRNSSraepHik9MfgE=",
"owner": "invoke-ai",
"repo": "InvokeAI",
"rev": "650f4bb58ceca458bff1410f35cd6d6caad399c6",
"type": "github"
},
"original": {
"owner": "invoke-ai",
"ref": "v2.3.1.post2",
"repo": "InvokeAI",
"type": "github"
}
},
"koboldai-src": {
"flake": false,
"locked": {
"lastModified": 1668957963,
"narHash": "sha256-fKQ/6LiMmrfSWczC5kcf6M9cpuF9dDYl2gJ4+6ZLSdY=",
"owner": "koboldai",
"repo": "koboldai-client",
"rev": "f2077b8e58db6bd47a62bf9ed2649bb0711f9678",
"type": "github"
},
"original": {
"owner": "koboldai",
"ref": "1.19.2",
"repo": "koboldai-client",
"type": "github"
}
},
"lowdown-src": {
"flake": false,
"locked": {
"lastModified": 1633514407,
"narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=",
"owner": "kristapsdz",
"repo": "lowdown",
"rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8",
"type": "github"
},
"original": {
"owner": "kristapsdz",
"repo": "lowdown",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": "flake-compat",
"lowdown-src": "lowdown-src",
"nixpkgs": "nixpkgs",
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1690515062,
"narHash": "sha256-PyvvANcbsjHAjvUsrGDyxk0b/CVExcrJAlCEQRp9HWc=",
"owner": "IvanMalison",
"repo": "nix",
"rev": "bedf108a183191519fdfa99a913f766090515d34",
"type": "github"
},
"original": {
"owner": "IvanMalison",
"ref": "my2.15.1",
"repo": "nix",
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
"nixified-ai",
"hercules-ci-effects",
"hercules-ci-agent",
"nixpkgs"
]
},
"locked": {
"lastModified": 1667419884,
"narHash": "sha256-oLNw87ZI5NxTMlNQBv1wG2N27CUzo9admaFlnmavpiY=",
"owner": "LnL7",
"repo": "nix-darwin",
"rev": "cfc0125eafadc9569d3d6a16ee928375b77e3100",
"type": "github"
},
"original": {
"owner": "LnL7",
"repo": "nix-darwin",
"type": "github"
}
},
"nixified-ai": {
"inputs": {
"flake-parts": "flake-parts",
"hercules-ci-effects": "hercules-ci-effects",
"invokeai-src": "invokeai-src",
"koboldai-src": "koboldai-src",
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1685671845,
"narHash": "sha256-qVA3wIxPb9PIFqa9Wf2a9jRMeMhE4kWw2y3oPSuRHU4=",
"owner": "nixified-ai",
"repo": "flake",
"rev": "0c58f8cba3fb42c54f2a7bf9bd45ee4cbc9f2477",
"type": "github"
},
"original": {
"owner": "nixified-ai",
"repo": "flake",
"type": "github"
}
},
"nixos-hardware": {
"locked": {
"lastModified": 1692373088,
"narHash": "sha256-EPgCecdc9I8aTdmDNoO1l7R72r2WPhZRcesV4nzxBj8=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "7f1836531b126cfcf584e7d7d71bf8758bb58969",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixos-hardware",
"type": "github"
}
},
"nixos-wsl": {
"inputs": {
"flake-compat": "flake-compat_2",
"flake-utils": "flake-utils_3",
"nixpkgs": "nixpkgs_4"
},
"locked": {
"lastModified": 1692543835,
"narHash": "sha256-1fR7+IhSSEHRbRW1w3nXb38/4kFfpmCDzMsK+ApqZCk=",
"owner": "nix-community",
"repo": "NixOS-WSL",
"rev": "faab3194692c5b6b351e33fc8d5e7f15f22d1d15",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "NixOS-WSL",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1670461440,
"narHash": "sha256-jy1LB8HOMKGJEGXgzFRLDU1CBGL0/LlkolgnqIsF0D8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "04a75b2eecc0acf6239acf9dd04485ff8d14f425",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-22.11-small",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1672350804,
"narHash": "sha256-jo6zkiCabUBn3ObuKXHGqqORUMH27gYDIFFfLq5P4wg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "677ed08a50931e38382dbef01cba08a8f7eac8f6",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1672262501,
"narHash": "sha256-ZNXqX9lwYo1tOFAqrVtKTLcJ2QMKCr3WuIvpN8emp7I=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e182da8622a354d44c39b3d7a542dc12cd7baa5f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1677932085,
"narHash": "sha256-+AB4dYllWig8iO6vAiGGYl0NEgmMgGHpy9gzWJ3322g=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3c5319ad3aa51551182ac82ea17ab1c6b0f0df89",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1690470004,
"narHash": "sha256-l57RmPhPz9r1LGDg/0v8bYgJO8R+GGTQZtkIxE7negU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9462344318b376e157c94fa60c20a25b913b2381",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1692447944,
"narHash": "sha256-fkJGNjEmTPvqBs215EQU4r9ivecV5Qge5cF/QDLVn3U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d680ded26da5cf104dd2735a51e88d2d8f487b4d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_6": {
"locked": {
"lastModified": 1632846328,
"narHash": "sha256-sFi6YtlGK30TBB9o6CW7LG9mYHkgtKeWbSLAjjrNTX0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2b71ddd869ad592510553d09fe89c9709fa26b2b",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_7": {
"locked": {
"lastModified": 1692557222,
"narHash": "sha256-TCOtZaioLf/jTEgfa+nyg0Nwq5Uc610Z+OFV75yUgGw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0b07d4957ee1bd7fd3bdfd12db5f361bd70175a6",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_8": {
"locked": {
"lastModified": 1632846328,
"narHash": "sha256-sFi6YtlGK30TBB9o6CW7LG9mYHkgtKeWbSLAjjrNTX0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2b71ddd869ad592510553d09fe89c9709fa26b2b",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_9": {
"locked": {
"lastModified": 1692557222,
"narHash": "sha256-TCOtZaioLf/jTEgfa+nyg0Nwq5Uc610Z+OFV75yUgGw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0b07d4957ee1bd7fd3bdfd12db5f361bd70175a6",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"notifications-tray-icon": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"git-ignore-nix": [
"git-ignore-nix"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1688066969,
"narHash": "sha256-h0ENXHgNMUgjD14ceNPWeNU6+cDR+6itQpfobf/CVUA=",
"owner": "IvanMalison",
"repo": "notifications-tray-icon",
"rev": "e3bae70029b7b4be8385ceccd89ad67c334071c2",
"type": "github"
},
"original": {
"owner": "IvanMalison",
"repo": "notifications-tray-icon",
"type": "github"
}
},
"pre-commit-hooks-nix": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixified-ai",
"hercules-ci-effects",
"hercules-ci-agent",
"nixpkgs"
]
},
"locked": {
"lastModified": 1667760143,
"narHash": "sha256-+X5CyeNEKp41bY/I1AJgW/fn69q5cLJ1bgiaMMCKB3M=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "06f48d63d473516ce5b8abe70d15be96a0147fcd",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"git-ignore-nix": "git-ignore-nix",
"home-manager": "home-manager",
"nix": "nix",
"nixified-ai": "nixified-ai",
"nixos-hardware": "nixos-hardware",
"nixos-wsl": "nixos-wsl",
"nixpkgs": "nixpkgs_5",
"notifications-tray-icon": "notifications-tray-icon",
"systems": "systems_2",
"taffybar": "taffybar",
"xmonad": "xmonad",
"xmonad-contrib": "xmonad-contrib"
}
},
"status-notifier-item": {
"inputs": {
"flake-utils": [
"taffybar",
"flake-utils"
],
"git-ignore-nix": [
"taffybar",
"git-ignore-nix"
],
"nixpkgs": [
"taffybar",
"nixpkgs"
]
},
"locked": {
"lastModified": 1770953113,
"narHash": "sha256-E9HHKMMZStzKeXqKLPh32fA1q0aOrHg+v+gBw3dNwR4=",
"ref": "refs/heads/master",
"rev": "d62b44beb1b189bf4a97b7f632b2cb41d3addacb",
"revCount": 117,
"type": "git",
"url": "file:///home/imalison/Projects/status-notifier-item"
},
"original": {
"owner": "taffybar",
"repo": "status-notifier-item",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_4": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"taffybar": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"git-ignore-nix": [
"git-ignore-nix"
],
"gtk-sni-tray": "gtk-sni-tray",
"gtk-strut": "gtk-strut",
"nixpkgs": [
"nixpkgs"
],
"status-notifier-item": "status-notifier-item",
"xmonad": [
"xmonad"
]
},
"locked": {
"lastModified": 1690672871,
"narHash": "sha256-BlSP4JJ1pYvGtiuvYh7royLWoyC9xts6WS28c4KeIgQ=",
"owner": "taffybar",
"repo": "taffybar",
"rev": "175f0ee5c8c599cb72332c42516ef59ed6189e66",
"type": "github"
},
"original": {
"owner": "taffybar",
"repo": "taffybar",
"type": "github"
}
},
"unstable": {
"locked": {
"lastModified": 1692447944,
"narHash": "sha256-fkJGNjEmTPvqBs215EQU4r9ivecV5Qge5cF/QDLVn3U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d680ded26da5cf104dd2735a51e88d2d8f487b4d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"unstable_2": {
"locked": {
"lastModified": 1692447944,
"narHash": "sha256-fkJGNjEmTPvqBs215EQU4r9ivecV5Qge5cF/QDLVn3U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d680ded26da5cf104dd2735a51e88d2d8f487b4d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"xmonad": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"git-ignore-nix": [
"git-ignore-nix"
],
"nixpkgs": [
"nixpkgs"
],
"unstable": "unstable"
},
"locked": {
"lastModified": 1691842937,
"narHash": "sha256-dOrvPpypuNn/fAWY2XjMacpsAXEiMZ4Dll3Ot81iQL4=",
"owner": "xmonad",
"repo": "xmonad",
"rev": "5c2ba069026666998a8932832bc8f3fce24f42e9",
"type": "github"
},
"original": {
"owner": "xmonad",
"repo": "xmonad",
"type": "github"
}
},
"xmonad-contrib": {
"inputs": {
"flake-utils": "flake-utils_4",
"git-ignore-nix": "git-ignore-nix_2",
"nixpkgs": "nixpkgs_7",
"xmonad": "xmonad_2"
},
"locked": {
"lastModified": 1691842946,
"narHash": "sha256-XEZ+Z/23ZueKygLgg/ps4KD9lgiBSxh7/WygqAbZsq0=",
"owner": "xmonad",
"repo": "xmonad-contrib",
"rev": "2df26cf9f8d93d3b3fc2b1ac853d31280e9fa916",
"type": "github"
},
"original": {
"owner": "xmonad",
"repo": "xmonad-contrib",
"type": "github"
}
},
"xmonad_2": {
"inputs": {
"flake-utils": "flake-utils_5",
"git-ignore-nix": "git-ignore-nix_3",
"nixpkgs": "nixpkgs_9",
"unstable": "unstable_2"
},
"locked": {
"lastModified": 1691842937,
"narHash": "sha256-dOrvPpypuNn/fAWY2XjMacpsAXEiMZ4Dll3Ot81iQL4=",
"owner": "xmonad",
"repo": "xmonad",
"rev": "5c2ba069026666998a8932832bc8f3fce24f42e9",
"type": "github"
},
"original": {
"owner": "xmonad",
"repo": "xmonad",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,122 +0,0 @@
{
inputs = {
nixos-hardware = {url = github:NixOS/nixos-hardware;};
nixpkgs = {url = github:NixOS/nixpkgs/nixos-unstable;};
home-manager = {
url = github:nix-community/home-manager;
inputs.nixpkgs.follows = "nixpkgs";
};
nix = {
url = github:IvanMalison/nix/my2.15.1;
};
flake-utils = {
url = github:numtide/flake-utils;
inputs.systems.follows = "systems";
};
systems = {url = github:nix-systems/default;};
git-ignore-nix = {
url = github:hercules-ci/gitignore.nix;
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-wsl = {url = github:nix-community/NixOS-WSL;};
taffybar = {
url = "github:taffybar/taffybar";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
git-ignore-nix.follows = "git-ignore-nix";
xmonad.follows = "xmonad";
};
};
xmonad = {
url = "github:xmonad/xmonad";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
git-ignore-nix.follows = "git-ignore-nix";
};
};
xmonad-contrib = {
url = "github:xmonad/xmonad-contrib";
};
notifications-tray-icon = {
url = "github:IvanMalison/notifications-tray-icon";
inputs.flake-utils.follows = "flake-utils";
inputs.git-ignore-nix.follows = "git-ignore-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
nixified-ai = {url = "github:nixified-ai/flake";};
};
outputs = inputs @ {
self,
nixpkgs,
nixos-hardware,
home-manager,
nix,
...
}: let
machinesPath = ../machines;
machineFilenames = builtins.attrNames (builtins.readDir machinesPath);
machineNameFromFilename = filename: builtins.head (builtins.split "\\." filename);
machineNames = map machineNameFromFilename machineFilenames;
mkConfigurationParams = filename: {
name = machineNameFromFilename filename;
value = {
modules = [(machinesPath + ("/" + filename))];
};
};
defaultConfigurationParams =
builtins.listToAttrs (map mkConfigurationParams machineFilenames);
customParams = {
biskcomp = {
system = "aarch64-linux";
};
air-gapped-pi = {
system = "aarch64-linux";
};
};
mkConfig = args @ {
system ? "x86_64-linux",
baseModules ? [],
modules ? [],
specialArgs ? {},
...
}:
nixpkgs.lib.nixosSystem (args
// {
inherit system;
modules = baseModules ++ modules;
specialArgs =
rec {
inherit inputs machineNames;
makeEnable = (import ../make-enable.nix) nixpkgs.lib;
realUsers = ["root" "imalison" "kat" "dean" "alex" "ben"];
}
// specialArgs // (import ../keys.nix);
});
in {
nixosConfigurations =
builtins.mapAttrs (
machineName: params: let
machineParams =
if builtins.hasAttr machineName customParams
then (builtins.getAttr machineName customParams)
else {};
in
mkConfig (params // machineParams)
)
defaultConfigurationParams;
};
}

View File

@@ -72,6 +72,12 @@ in
recursive = true;
};
home.file."chrome-favicon-dbus-extension" = {
source = extensionSource;
recursive = true;
force = true;
};
xdg.configFile."google-chrome/External Extensions/${extensionId}.json".text = builtins.toJSON {
external_crx = "${extensionPackage}/chrome-favicon-dbus.crx";
external_version = extensionVersion;

58
nixos/claude-mcp.nix Normal file
View File

@@ -0,0 +1,58 @@
{
pkgs,
config,
lib,
makeEnable,
...
}: let
# The MCP-NixOS server (https://mcp-nixos.io) — gives Claude accurate NixOS
# package/option/Home-Manager/flake search instead of hallucinated names.
# Pinned by Nix from nixpkgs, so no runtime `uvx`/`nix run` fetch is needed.
mcpNixosBin = "${pkgs.mcp-nixos}/bin/mcp-nixos";
# Claude Code reads MCP server *definitions* from the top-level `mcpServers`
# key of ~/.claude.json (the user scope). That is the only scope that applies
# to every project without an approval prompt while remaining additive:
# - settings.json (our nix-managed dotfiles file) cannot define servers,
# only filter them (enabled/disabledMcpjsonServers).
# - /etc/claude-code/managed-mcp.json would take *exclusive* control and
# disable every other server (per-project playwright, future `mcp add`).
# So we merge into the user config rather than owning a whole file.
serverJson = builtins.toJSON {
type = "stdio";
command = mcpNixosBin;
};
in
makeEnable config "myModules.claudeMcpNixos" true {
# Also expose the pinned binary on PATH for manual `claude mcp` use / Codex.
environment.systemPackages = [pkgs.mcp-nixos];
# Use a module function so `lib` here is home-manager's lib (which carries
# `lib.hm.dag`), not the plain NixOS lib.
home-manager.users.imalison = {lib, ...}: {
# ~/.claude.json is mutable state owned by Claude Code, so it can't be
# managed as a whole file. Instead idempotently merge our server into it
# on every switch with jq, preserving all other (per-project, user-added)
# servers and state. This module is the declarative source of truth:
# `claude mcp remove nixos` is re-applied on the next switch.
home.activation.registerMcpNixos = lib.hm.dag.entryAfter ["writeBoundary"] ''
config="$HOME/.claude.json"
server=${lib.escapeShellArg serverJson}
if [ -f "$config" ]; then
tmp="$(mktemp)"
if ${pkgs.jq}/bin/jq --argjson srv "$server" \
'.mcpServers = ((.mcpServers // {}) + {nixos: $srv})' \
"$config" > "$tmp"; then
mv -f "$tmp" "$config"
else
rm -f "$tmp"
echo "claude-mcp: failed to update $config; left unchanged" >&2
fi
else
${pkgs.jq}/bin/jq -n --argjson srv "$server" \
'{mcpServers: {nixos: $srv}}' > "$config"
chmod 600 "$config"
fi
'';
};
}

View File

@@ -0,0 +1,64 @@
{
pkgs,
config,
lib,
makeEnable,
...
}: let
# Working directory the always-on session is pinned to. Ad-hoc sessions in
# other directories are handled by the `tmclaude` shell function instead.
workingDirectory = "/srv/dotfiles";
# Dedicated tmux socket + session name so the service owns its own tmux
# server (independent of the interactive one). Attach locally with:
# tmux -L claude-rc attach -t claude-rc
socket = "claude-rc";
sessionName = "claude-rc";
# Name the session registers under for native Remote Control (phone/web).
remoteName = config.networking.hostName;
# claude shells out to these for its tools; give the service a clean PATH.
servicePath = lib.makeBinPath (with pkgs; [
claude-code
tmux
bashInteractive
coreutils
findutils
git
gnugrep
gnused
nix
nodejs
openssh
ripgrep
zsh
]);
in
makeEnable config "myModules.claudeRemoteControl" false {
home-manager.users.imalison = {
systemd.user.services.claude-remote-control = {
Unit = {
Description = "Claude Code remote-control session";
After = ["network.target"];
};
Service = {
# tmux new-session -d daemonizes the server and returns.
Type = "forking";
Environment = ["PATH=${servicePath}"];
ExecStart = lib.concatStringsSep " " [
"${pkgs.tmux}/bin/tmux -L ${socket} new-session -d"
"-s ${sessionName} -c ${workingDirectory}"
"${pkgs.claude-code}/bin/claude --remote-control ${remoteName} --dangerously-skip-permissions"
];
ExecStop = "${pkgs.tmux}/bin/tmux -L ${socket} kill-server";
Restart = "on-failure";
RestartSec = 5;
};
Install.WantedBy = ["default.target"];
};
# Convenience: attach to the always-on session from any directory.
home.shellAliases.claude-rc-attach = "tmux -L ${socket} attach -t ${sessionName}";
};
}

View File

@@ -7,8 +7,35 @@
...
}:
let
codexDesktop =
inputs.codex-desktop-linux.packages.${pkgs.stdenv.hostPlatform.system}."codex-desktop-computer-use-ui-remote-mobile-control";
codexDesktopLinuxSource = pkgs.applyPatches {
name = "codex-desktop-linux-patched";
src = inputs.codex-desktop-linux;
patches = [ ./patches/codex-desktop-linux-gsettings-schemas.patch ];
};
claudeDesktopSource = inputs.claude-desktop;
claudeDesktopNodePty = pkgs.callPackage "${claudeDesktopSource}/nix/node-pty.nix" {};
claudeDesktop = pkgs.callPackage "${claudeDesktopSource}/nix/claude-desktop.nix" {
node-pty = claudeDesktopNodePty;
};
claudeDesktopFhs = pkgs.callPackage "${claudeDesktopSource}/nix/fhs.nix" {
claude-desktop = claudeDesktop;
};
codexDesktopLinux =
let
flake = import "${codexDesktopLinuxSource}/flake.nix";
self' =
(flake.outputs {
self = self';
nixpkgs = inputs.nixpkgs;
flake-utils = inputs.flake-utils;
})
// {
outPath = "${codexDesktopLinuxSource}";
rev = inputs.codex-desktop-linux.rev or "";
lastModified = inputs.codex-desktop-linux.lastModified or 1;
};
in
self';
in
makeEnable config "myModules.code" true {
programs.direnv = {
@@ -24,6 +51,7 @@ makeEnable config "myModules.code" true {
};
home-manager.sharedModules = lib.mkIf config.myModules.desktop.enable [
codexDesktopLinux.homeManagerModules.default
{
home.sessionVariables.YDOTOOL_SOCKET = "/run/ydotoold/socket";
systemd.user.sessionVariables.YDOTOOL_SOCKET = "/run/ydotoold/socket";
@@ -40,6 +68,16 @@ makeEnable config "myModules.code" true {
programs.codex = {
enable = true;
package = pkgs.codex;
};
programs.codexDesktopLinux = {
enable = true;
# Bake CODEX_CLI_PATH into the launcher so Codex Desktop always finds this
# CLI, regardless of how it is started (GUI autostart, app launcher,
# terminal, or warm-start handoff) and without needing a re-login.
cliPackage = pkgs.codex;
computerUseUi.enable = true;
remoteMobileControl.enable = true;
remoteControl = {
enable = true;
package = pkgs.codex;
@@ -51,7 +89,10 @@ makeEnable config "myModules.code" true {
gnugrep
gnused
nix
nodejs
openssh
ripgrep
zsh
];
listen = "unix://";
};
@@ -61,12 +102,11 @@ makeEnable config "myModules.code" true {
environment.systemPackages = with pkgs;
[
# LLM Tools
antigravity
# antigravity
claude-code
claudeDesktopFhs
codex
codexDesktop
gemini-cli
happy-coder
opencode
t3code

View File

@@ -12,6 +12,8 @@
./cache-server.nix
./cache.nix
./chrome-favicon-dbus.nix
./claude-mcp.nix
./claude-remote-control.nix
./code.nix
./cua.nix
./desktop.nix
@@ -67,6 +69,10 @@
{
system.autoUpgrade.flake = "github:colonelpanic8/dotfiles?dir=nixos#${config.networking.hostName}";
}
(lib.mkIf config.services.rumno.enable {
# Do not let rumno's forking/PIDFile startup gate the whole graphical session.
systemd.user.services.rumno.unitConfig.After = lib.mkForce ["graphical-session.target"];
})
(lib.mkIf config.features.full.enable {
myModules.base.enable = true;
myModules.desktop.enable = true;

View File

@@ -18,25 +18,170 @@
exec ${../dotfiles/lib/bin/desktop_shell_ui} "$@"
'';
};
googleChrome = pkgs.symlinkJoin {
name = "google-chrome-wayland-fractional-scale-workaround";
paths = [pkgs.google-chrome];
nativeBuildInputs = [pkgs.makeWrapper];
postBuild = ''
wrapProgram "$out/bin/google-chrome-stable" \
--add-flags "--disable-features=WaylandFractionalScaleV1"
chromeCommandLineFlags =
[
"--disable-features=WaylandFractionalScaleV1"
]
++ lib.optionals config.myModules.chrome-favicon-dbus.enable [
"--load-extension=${inputs.chrome-favicon-dbus}/extension"
];
googleChromeWrapperArgs = lib.concatMapStringsSep " " (flag: "--add-flags ${lib.escapeShellArg flag}") chromeCommandLineFlags;
googleChromeCommandWrappers = pkgs.runCommand "google-chrome-command-wrappers" {nativeBuildInputs = [pkgs.makeWrapper];} ''
mkdir -p "$out/bin"
makeWrapper ${pkgs.google-chrome}/bin/google-chrome "$out/bin/google-chrome" \
${googleChromeWrapperArgs}
makeWrapper ${pkgs.google-chrome}/bin/google-chrome-stable "$out/bin/google-chrome-stable" \
${googleChromeWrapperArgs}
'';
googleChromeProfileWindow = pkgs.writeShellApplication {
name = "google-chrome-profile-window";
runtimeInputs = [
googleChromeCommandWrappers
pkgs.gawk
pkgs.jq
pkgs.rofi
];
text = ''
if [ "$#" -gt 0 ]; then
exec google-chrome-stable "$@"
fi
desktop_file="$out/share/applications/google-chrome.desktop"
rm "$desktop_file"
cp "${pkgs.google-chrome}/share/applications/google-chrome.desktop" "$desktop_file"
chmod u+w "$desktop_file"
local_state="''${CHROME_USER_DATA_DIR:-$HOME/.config/google-chrome}/Local State"
substituteInPlace "$desktop_file" \
--replace-fail \
"Exec=${pkgs.google-chrome}/bin/google-chrome-stable" \
"Exec=$out/bin/google-chrome-stable"
if [ ! -r "$local_state" ]; then
exec google-chrome-stable --new-window
fi
profiles="$(
jq -r '
(.profile.info_cache // {})
| to_entries
| sort_by(if .key == "Default" then 0 else 1 end, -(.value.active_time // 0))[]
| [.value.name, .value.user_name, .key]
| @tsv
' "$local_state" \
| awk -F '\t' '{
label = $1
if ($2 != "") {
label = label " <" $2 ">"
}
print label "\t" $3
}'
)"
if [ -z "$profiles" ]; then
exec google-chrome-stable --new-window
fi
selection="$(printf '%s\n' "$profiles" | rofi -dmenu -i -p 'Chrome profile' || true)"
if [ -z "$selection" ]; then
exit 0
fi
profile_dir="$(printf '%s\n' "$selection" | awk -F '\t' '{print $NF}')"
if [ -z "$profile_dir" ]; then
exit 0
fi
exec google-chrome-stable --profile-directory="$profile_dir" --new-window
'';
};
xComPwa = pkgs.writeShellApplication {
name = "x-com-pwa";
runtimeInputs = [
googleChromeCommandWrappers
pkgs.jq
];
text = ''
profile_args=()
local_state="''${CHROME_USER_DATA_DIR:-$HOME/.config/google-chrome}/Local State"
if [ -r "$local_state" ]; then
profile_dir="$(
jq -r '
(.profile.info_cache // {})
| to_entries
| sort_by(if .key == "Default" then 0 else 1 end, -(.value.active_time // 0))
| .[0].key // empty
' "$local_state" 2>/dev/null || true
)"
if [ -n "$profile_dir" ]; then
profile_args+=(--profile-directory="$profile_dir")
fi
fi
exec google-chrome-stable "''${profile_args[@]}" --class=x-com-pwa --app=https://x.com/
'';
};
googleChromeDesktopEntries = pkgs.runCommand "google-chrome-desktop-entries" {nativeBuildInputs = [pkgs.gnused];} ''
mkdir -p "$out/share/applications"
for desktop_name in google-chrome.desktop com.google.Chrome.desktop; do
source_file="${pkgs.google-chrome}/share/applications/$desktop_name"
if [ -f "$source_file" ]; then
desktop_file="$out/share/applications/$desktop_name"
cp "$source_file" "$desktop_file"
chmod u+w "$desktop_file"
substituteInPlace "$desktop_file" \
--replace-fail "${pkgs.google-chrome}/bin/google-chrome-stable" "google-chrome-stable"
${pkgs.gnused}/bin/sed -i \
-e 's,application/pdf;,,g' \
-e 's,image/gif;,,g' \
-e 's,image/jpeg;,,g' \
-e 's,image/png;,,g' \
-e 's,image/webp;,,g' \
"$desktop_file"
${pkgs.gnused}/bin/sed -i \
-e 's#^Exec=.*google-chrome-stable *%U$#Exec=google-chrome-profile-window %U#' \
-e '/^\[Desktop Action new-window\]/,/^\[Desktop Action / s#^Exec=.*google-chrome-stable.*$#Exec=google-chrome-profile-window#' \
"$desktop_file"
fi
done
'';
spotifyWaylandFlags = [
"--enable-features=UseOzonePlatform,WaylandWindowDecorations"
"--ozone-platform=wayland"
"--enable-wayland-ime=true"
];
spotifyWaylandWrapperArgs = lib.concatMapStringsSep " " (flag: "--add-flags ${lib.escapeShellArg flag}") spotifyWaylandFlags;
spotifyWaylandPatch = lib.hiPrio (pkgs.runCommand "${pkgs.spotify.name}-wayland-patch" {
nativeBuildInputs = [
pkgs.gnused
pkgs.makeWrapper
];
} ''
mkdir -p "$out/bin" "$out/share/applications"
makeWrapper ${pkgs.spotify}/bin/spotify "$out/bin/spotify" \
--unset NIXOS_OZONE_WL \
${spotifyWaylandWrapperArgs}
cp ${pkgs.spotify}/share/applications/spotify.desktop "$out/share/applications/spotify.desktop"
chmod u+w "$out/share/applications/spotify.desktop"
${pkgs.gnused}/bin/sed -i \
-e "s#^TryExec=.*spotify\$#TryExec=$out/bin/spotify#" \
-e "s#^Exec=.*spotify\\( .*\\)\\?\$#Exec=$out/bin/spotify\\1#" \
"$out/share/applications/spotify.desktop"
'');
rlruPackages = inputs.rlru.packages.${pkgs.stdenv.hostPlatform.system};
rlruDioxusDesktopBase = rlruPackages.rlru-dioxus-desktop.overrideAttrs (_: {
# Rust 1.95 can otherwise ICE/SEGV while compiling rlru's desktop dependency
# graph in release mode.
RUST_MIN_STACK = "2147483648";
});
rlruDioxusDesktop = pkgs.symlinkJoin {
name = "${rlruDioxusDesktopBase.name}-single-desktop-entry";
paths = [rlruDioxusDesktopBase];
postBuild = ''
rm -f "$out/share/applications/rlru-dioxus.desktop"
'';
meta = rlruDioxusDesktopBase.meta;
};
enabledModule = makeEnable config "myModules.desktop" true {
services.greenclip.enable = true;
imports = [
@@ -66,19 +211,35 @@
environment.sessionVariables = {
# This is for the benefit of VSCODE running natively in wayland
NIXOS_OZONE_WL = "1";
# Claude Desktop's launcher (claude-desktop-debian flake) ignores
# NIXOS_OZONE_WL/ELECTRON_OZONE_PLATFORM_HINT and hardcodes
# --ozone-platform=x11 by default. This is the only knob it honors;
# it switches the launcher to native Wayland (loses global hotkeys).
CLAUDE_USE_WAYLAND = "1";
IM_HYPRLAND_SHELL_UI = cfg.shellUi;
};
system.activationScripts.playwrightChromeCompat.text = lib.optionalString (pkgs.stdenv.hostPlatform.system == "x86_64-linux") ''
# Playwright's Chrome channel lookup expects the FHS path below.
mkdir -p /opt/google/chrome
ln -sfn ${googleChrome}/bin/google-chrome-stable /opt/google/chrome/chrome
ln -sfn ${googleChromeCommandWrappers}/bin/google-chrome-stable /opt/google/chrome/chrome
'';
services.gnome.at-spi2-core.enable = true;
services.gnome.gnome-keyring.enable = true;
home-manager.users.imalison = {
imports = [
inputs.rlru.homeManagerModules.default
];
services.rlru = {
enable = true;
package = rlruDioxusDesktop;
};
};
home-manager.sharedModules = [
{
imports = [./dunst.nix];
@@ -109,6 +270,21 @@
};
};
xdg.desktopEntries.x-com-pwa = {
name = "X";
genericName = "Social Network";
comment = "Open x.com in a dedicated Chrome app window";
icon = "google-chrome";
terminal = false;
type = "Application";
categories = ["Network"];
startupNotify = true;
exec = "${xComPwa}/bin/x-com-pwa";
settings = {
StartupWMClass = "x-com-pwa";
};
};
xdg.configFile."ghostty/config" = {
force = true;
text = ''
@@ -182,7 +358,7 @@
pinentry-gnome3
# mission-center
quassel
remmina
# remmina
rofi
wofi
rofi-pass
@@ -212,13 +388,17 @@
if pkgs.stdenv.hostPlatform.system == "x86_64-linux"
then
with pkgs; [
googleChrome
googleChromeCommandWrappers
googleChromeDesktopEntries
googleChromeProfileWindow
pommed_light
slack
spicetify-cli
spotify
spotifyWaylandPatch
tor-browser
vscode
xComPwa
# vscode
zulip
]
else []

View File

@@ -1,10 +1,11 @@
{
config,
lib,
nixos,
...
}: let
# Replicate the useful part of rcm/rcup:
# - dotfiles live in ~/dotfiles/dotfiles (no leading dots in the repo)
# - dotfiles live in <dotfiles-worktree>/dotfiles (no leading dots in the repo)
# - links in $HOME add a leading '.' to the first path component
# - link files individually so unmanaged state can coexist (e.g. ~/.cabal/store)
#
@@ -13,11 +14,15 @@
oos = config.lib.file.mkOutOfStoreSymlink;
# Where the checked-out repo lives at runtime (activation time).
worktreeDotfiles = "${config.home.homeDirectory}/dotfiles/dotfiles";
# Keep this outside individual home directories so links work for every
# managed user on a shared machine.
worktreeRoot = nixos.config.dotfiles-worktree or "/srv/dotfiles";
worktreeDotfiles = "${worktreeRoot}/dotfiles";
# Use the flake source for enumeration (pure), but point links at the worktree.
srcDotfiles = ../dotfiles;
srcConfig = srcDotfiles + "/config";
srcCodex = srcDotfiles + "/codex";
excludedTop = [
# Managed by nix-shared/home-manager/codex-generated-skills.nix so
@@ -86,6 +91,11 @@ in {
builtins.listToAttrs (map mkConfigDir configDirNames);
myModules.codexGeneratedSkills.enable = true;
myModules.codexGeneratedSkills.sourceCodexDir = "${srcCodex}";
# Point the Codex module at the live worktree (e.g. /srv/dotfiles) like the
# links above, not its ~/dotfiles default. Without this, ~/.codex/AGENTS.md
# and ~/.codex/skills/* dangle when the checkout lives outside ~/dotfiles.
myModules.codexGeneratedSkills.worktreeCodexDir = "${worktreeDotfiles}/codex";
# Home Manager directory links for .emacs.d resolve through the store on this
# machine, which breaks Elpaca's writable state under ~/.emacs.d/elpaca.

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