mirror of
https://github.com/nmasur/dotfiles
synced 2026-11-13 14:45:02 +00:00
Compare commits
17
Commits
06c99f5e98
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75e7ddb104 | ||
|
|
0d96b8fbed | ||
|
|
314298134d | ||
|
|
a8dc2f0d62 | ||
|
|
7404d10533 | ||
|
|
9a458530d0 | ||
|
|
b3d9938f10 | ||
|
|
542e39a2aa | ||
|
|
c757493b35 | ||
|
|
744afbb2d8 | ||
|
|
6edfa03206 | ||
|
|
25f7af63ad | ||
|
|
fde3b0214a | ||
|
|
612ffdaaaf | ||
|
|
bc43f3dfaf | ||
|
|
33c426bc8e | ||
|
|
da7f8639ae |
No files matched your search
@@ -1,5 +1,57 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-10-25: fix pgweb/mathesar postgresql.target dependency and refresh all DB collations
|
||||||
|
|
||||||
|
- **pgweb & mathesar**: Added `requires = [ "postgresql.target" ];` to `systemd.services.pgweb` (`platforms/nixos/modules/nmasur/presets/services/pgweb.nix`) and `"postgresql.target"` to `systemd.services.mathesar.requires` (`platforms/nixos/modules/nmasur/presets/services/mathesar/mathesar.nix`). Previously they only set `after = [ "postgresql.target" ]`, allowing `switch-to-configuration` to start `pgweb.service` before `postgresql.target` was queued and causing `pgweb` to fail with `connect: no such file or directory` on `/run/postgresql/.s.PGSQL.5432`.
|
||||||
|
- **PostgreSQL collation version refresh**: Updated `systemd.services.postgresql-setup.preStart` (`platforms/nixos/modules/nmasur/presets/services/postgresql.nix`) to iterate over all databases with `datallowconn` in `pg_database` and run `ALTER DATABASE ... REFRESH COLLATION VERSION;` instead of only refreshing `template1` and `postgres`.
|
||||||
|
|
||||||
|
## 2026-10-24: fix build failures for NixOS swan
|
||||||
|
|
||||||
|
- **Nextcloud**: Upstream NixOS removed `services.nextcloud.config.adminpassFile` and `services.nextcloud.config.adminuser`. Removed `adminpassFile` from `services.nextcloud.config`, updated `services.prometheus.exporters.nextcloud` to reference `config.secrets.nextcloud.dest` directly as `passwordFile` and set `username = "root"`, and added ordering dependencies on `nextcloud-secret` for `prometheus-nextcloud-exporter.service`.
|
||||||
|
- **Bazarr**: Replaced obsolete `services.bazarr.listenPort` with `services.bazarr.settings.general.port` in Caddy reverse proxy upstream (`arr.nix`) and Grafana dashboard matcher (`grafana.nix`).
|
||||||
|
- **pyopenjtalk-plus**: Added `overlays/pyopenjtalk-plus.nix` disabling `test_run_njd_from_mecab_invalid_input_should_not_break_next_call`, which fails under Python 3.14 due to an exact string match assertion on the `TypeError` message ("Expected str, got int" vs expected "must be str").
|
||||||
|
- **calibre**: Added `overlays/calibre.nix` setting `doInstallCheck = false`, bypassing upstream failure in `test_speech_dispatcher` caused by `speechd-minimal` omitting Python bindings in nixpkgs.
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-10-17: removed post-TUI lag tooling
|
||||||
|
|
||||||
|
- With the root cause fixed (starship notifications, below), removed the `lag-triage` module: `lag-triage`, `unlag`, `lag-sample`, `term-probe`, the PTY repro script, the flight recorder filter and the Ctrl-b `heal-autosuggest` binding. Also deleted its `power-user.nix` enable and `~/.local/state/lag-triage`.
|
||||||
|
- The zellij `fish-no-query-term` wrapper now only exports `fish_features=no-query-term` before starting fish; the recorder is gone.
|
||||||
|
|
||||||
|
## 2026-10-17: post-TUI lag ROOT CAUSE — starship cmd_duration notifications
|
||||||
|
|
||||||
|
- The first timestamped episode (fish 4.9.3, fresh zellij session) showed fish handling keys in ~1ms, but every prompt took **~2s**, blocked on one child process. After Ctrl-b, prompts dropped to ~50ms.
|
||||||
|
- Cause: `starship.nix` set `cmd_duration.show_notifications = true` on darwin only (`min_time_to_notify = 30000`). On macOS, sending that notification blocks `starship prompt` for ~2s (measured: 0.03s with `--cmd-duration=100` vs 2.02s with `--cmd-duration=600000`). fish keeps `$CMD_DURATION` until another command actually executes. So after any command over 30s (i.e. every TUI session), every prompt redraw re-sent the notification and froze the shell for 2s: Enter on an empty line, vi mode switches, repaints.
|
||||||
|
- This explains every observation: macOS-only (Linux had notifications off), "post-TUI" (TUIs are the long commands), and why the toggle "cured" it. Executing *any* command resets `$CMD_DURATION`; autosuggestions were never involved. The 2026-08/09 entries blaming fish autosuggestions, query-term and thread pools were wrong.
|
||||||
|
- Fix: `show_notifications = false` everywhere. Verified: the real config with a 10-minute duration renders in 0.05s and still shows the duration.
|
||||||
|
|
||||||
|
## 2026-10-17: disable atuin tests
|
||||||
|
|
||||||
|
- Added `overlays/atuin.nix` to disable atuin 18.23.0's `checkPhase`. Its
|
||||||
|
PTY-driven e2e tests (`tests/e2e_capture.rs`, `tests/e2e_pty.rs`, notably the
|
||||||
|
bash-preexec variants) time out in the darwin build sandbox ("timed out
|
||||||
|
waiting for shell session ID", `captured: []`), which broke
|
||||||
|
`nh home switch` for `lookingglass` after the flake update. atuin isn't in
|
||||||
|
the binary cache for aarch64-darwin, so it gets built locally. Skipping
|
||||||
|
individual tests wasn't viable: cargo stops at the first failing test binary,
|
||||||
|
hiding later failures behind ~13-minute rebuilds. The 554 unit tests passed.
|
||||||
|
|
||||||
|
## 2026-10-17: fix flake check for AWS-only arrow
|
||||||
|
|
||||||
|
- `nix flake check` (run by the weekly Update Flake workflow) failed evaluating `packages.x86_64-linux.arrow-iso`: arrow now imports `amazon-image.nix` directly, which conflicts with the `iso` and `qcow-efi` generator formats (`boot.loader.timeout` 1 vs 10, and similar).
|
||||||
|
- `flake.nix` now has an `awsOnlyHosts = [ "arrow" ]` list; those hosts only get the `-aws` image. `arrow-iso` and `arrow-qcow` are no longer exported.
|
||||||
|
|
||||||
|
## 2026-10-15: post-TUI lag — re-investigation, timestamped recorder
|
||||||
|
|
||||||
|
- **Status: root cause still unproven.** Not reproducible in fresh shells: a PTY harness (with and without a real zellij 0.45.1 session, fish 4.9.3, the real config and a copy of the real history) showed no echo-latency change after hx (with nixd/marksman/rumdl running), jjui, yazi, vim, less, lazygit or Claude Code, nor with huge history autosuggestions or concurrent history writers.
|
||||||
|
- New facts:
|
||||||
|
- Timeline: fish 4.8.0 first landed in the store on 2026-07-05 and the first lag fix attempt was 2026-08-08. 4.8.0 changed history timestamps/merging (fish #10300), which is the data autosuggestions search on every keystroke.
|
||||||
|
- Today's (2026-10-15) lag was in a 36-day-old fish **4.8.1** pane. 57 of 85 running fish processes are still 4.8.1 (started before 4.9.3 was installed on 2026-09-24); there is no evidence yet either way that 4.9.3 shells lag. Long-lived shells are the main difference from every failed reproduction.
|
||||||
|
- In the shell history, the command run right before a heal was `hx` in 4 of the 5 cases with usable context (Helix, not previously on the trigger list).
|
||||||
|
- Corrections to earlier entries: the thread-pool starvation theory (2026-08-31) is not supported. In all September flight logs every enqueued work item was picked up by a worker (2090/2090). The September logs had no timestamps, so they cannot show whether fish was slow to handle keys or the delay was downstream (zellij/ghostty). In most pre-heal windows fish handled keys one at a time with a repaint after each (no input backlog), which weakly points away from fish's input loop.
|
||||||
|
- Recorder upgrade (`zellij.nix` `fish-no-query-term`, `lag-triage/flight_filter.pl`): when `~/.local/state/lag-triage/RECORD` exists, new pane shells stream `FISH_DEBUG` through a FIFO into a Perl filter that adds millisecond timestamps and redacts typed/pasted content and command lines (the September logs contained pasted secrets in clear text). Added `reader-render`, `complete`, `autoload`, `history` and `uvar-notifier` categories. `config.fish` now drops `FISH_DEBUG`/`FISH_DEBUG_OUTPUT` so subshells and `fish -c` children no longer write into the recorder's path (they previously truncated the same log file).
|
||||||
|
- `heal-autosuggest` (Ctrl-b) now first copies the last 20k lines of the pane's timestamped log to `~/.local/state/lag-triage/episodes/`, so pressing it during a lag preserves the evidence. Next step: analyze the first episode (time from each `Read byte` to the following `Writing terminal command` decides fish-side versus downstream).
|
||||||
|
|
||||||
## 2026-10-11
|
## 2026-10-11
|
||||||
|
|
||||||
- **Configured `arrow` host for AWS EC2 deployment**:
|
- **Configured `arrow` host for AWS EC2 deployment**:
|
||||||
@@ -11,6 +63,21 @@
|
|||||||
|
|
||||||
## 2026-09-29
|
## 2026-09-29
|
||||||
|
|
||||||
|
- **Added Valheim dedicated server package, service, and preset**:
|
||||||
|
- Added `steam-fetcher` (`github:nix-community/steam-fetcher`) flake input with `inputs.nixpkgs.follows = "nixpkgs"`.
|
||||||
|
- Added `inputs.steam-fetcher.overlay` to system overlays in `lib/default.nix`.
|
||||||
|
- Added `pkgs/valheim-server/package.nix` wrapping the Valheim dedicated server with modern `buildFHSEnv`, updated to latest release 1.0.16 (Steam depot `896661`, manifest `1285123405092214913`), with graceful fallback for non-x86_64-linux platforms.
|
||||||
|
- Added `overlays/valheim.nix` exposing `valheim-server`, `valheim-server-unwrapped`, and `fetchValheimThunderstoreMod` for compatibility.
|
||||||
|
- Added NixOS service module `platforms/nixos/modules/services/valheim.nix` defining `services.valheim`:
|
||||||
|
- Manages dedicated user/group `valheim` and systemd service `valheim.service`.
|
||||||
|
- Supports runtime decryption/loading of server password via `passwordFile` (avoiding plaintext secrets in the Nix store) as well as fallback `password` string option.
|
||||||
|
- Configures firewall rules for Valheim UDP ports (`port`, `port + 1`, `port + 2`).
|
||||||
|
- Configures admin, permitted, and banned Steam ID list files.
|
||||||
|
- Added preset module `platforms/nixos/modules/nmasur/presets/services/valheim/valheim.nix` with options for `nmasur.presets.services.valheim.enable`:
|
||||||
|
- Wires up `secrets.valheim-password` with encrypted secret file `valheim-password.age` and sets `systemd.services.valheim-password-secret` dependency before `valheim.service`.
|
||||||
|
- Adds `valheim = "valheim.masu.rs"` hostname to `flake.nix` and configures `nmasur.presets.services.cloudflare.noProxyDomains = [ hostnames.valheim ]` for DNS-only UDP routing.
|
||||||
|
- Adds `valheim.enable = lib.mkDefault false;` under `services` in `platforms/nixos/modules/nmasur/profiles/communications.nix`.
|
||||||
|
|
||||||
- **Separated Restic backup repositories per host**:
|
- **Separated Restic backup repositories per host**:
|
||||||
- Updated `services.restic.backups.default.repository` in `platforms/nixos/modules/nmasur/presets/services/restic/restic.nix` to append `/${config.networking.hostName}`, isolating backups by host in S3 (e.g., `.../restic/flame` and `.../restic/swan`).
|
- Updated `services.restic.backups.default.repository` in `platforms/nixos/modules/nmasur/presets/services/restic/restic.nix` to append `/${config.networking.hostName}`, isolating backups by host in S3 (e.g., `.../restic/flame` and `.../restic/swan`).
|
||||||
- Enabled `services.restic.backups.default.initialize = true` to automatically initialize the repository in S3 if it does not already exist.
|
- Enabled `services.restic.backups.default.initialize = true` to automatically initialize the repository in S3 if it does not already exist.
|
||||||
|
|||||||
Generated
+54
-33
@@ -49,11 +49,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1786845137,
|
"lastModified": 1793418699,
|
||||||
"narHash": "sha256-oQFip+v0luP8NIxJzmiW4Wu8bILsbFWom5l0zonl8hQ=",
|
"narHash": "sha256-+RpTlcdh9KqZ+ZiLkNSTvibQWm935EcIdBzRPCTFsrQ=",
|
||||||
"owner": "lnl7",
|
"owner": "lnl7",
|
||||||
"repo": "nix-darwin",
|
"repo": "nix-darwin",
|
||||||
"rev": "4cff07de74b50e64bdd68cd4e722ab5b6b35ee48",
|
"rev": "ff0e21a414d4547183396da1b44a66f2577b65e9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -70,11 +70,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1789770686,
|
"lastModified": 1792526520,
|
||||||
"narHash": "sha256-uZkBR7yHdIKUFB5SZdfgh1qkGfI3XmYmI/lTiquxbck=",
|
"narHash": "sha256-3hSJlv8mUVnZPMWK2+CM7a9O+nR6Z4mCh27NfLCmTPc=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "disko",
|
"repo": "disko",
|
||||||
"rev": "725ea35e410ad83be4931d1bff7e090eacaf3563",
|
"rev": "cedd626e11b635470fa6f9fdb8ad79b0075dd7f9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -234,11 +234,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1791600313,
|
"lastModified": 1793979366,
|
||||||
"narHash": "sha256-/2gb2WTztITD/JU1XUCg63vDsIfJGLr0LDsjSCTTJc8=",
|
"narHash": "sha256-7OoWDsYO9teiPxl0QdlS5lYE20Pue8/5mpscSL32FUA=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "427c74a88a93cc9f856c8bb99bf6b4a5187a6b33",
|
"rev": "2c0c1f01521469cf0271180c453c79782d53c2f6",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -277,11 +277,11 @@
|
|||||||
"nixpkgs": "nixpkgs_5"
|
"nixpkgs": "nixpkgs_5"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1784936738,
|
"lastModified": 1792092848,
|
||||||
"narHash": "sha256-X4w9BrFkisfzla3m1WwDJUicUbI7JRdhzq5aS7rrB0k=",
|
"narHash": "sha256-L8uKQaS6wqZxXm7ULQynxnw8m5nZlqaJcbZ2+1ECXbE=",
|
||||||
"owner": "MichailiK",
|
"owner": "MichailiK",
|
||||||
"repo": "nix-caddy-withplugins",
|
"repo": "nix-caddy-withplugins",
|
||||||
"rev": "70307cfc568e5434cf42067d797b27c38ea69944",
|
"rev": "5181c7953264f637f7455cc95c84cf92346c8098",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -298,11 +298,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1791105314,
|
"lastModified": 1793543282,
|
||||||
"narHash": "sha256-oTM7QFgRQ+vxBpMBMc7hEYpqBtpXAzXu2xuB/tru84U=",
|
"narHash": "sha256-PHtGxKYM99f8WhGR5YD7iiCiwemKa2wUD4yVI2oALFg=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nix-index-database",
|
"repo": "nix-index-database",
|
||||||
"rev": "e740bc4e9d8918d7819a51486d5099930c65c72e",
|
"rev": "cee0f879d338cf08f814210fa6c2b280594ade0a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -401,11 +401,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-stable": {
|
"nixpkgs-stable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1791426274,
|
"lastModified": 1793865203,
|
||||||
"narHash": "sha256-MWhktfFtFE8Jl7bf9Pkp7TR5MSdYCjCswSo0Gkb4ywc=",
|
"narHash": "sha256-/cpdPHaySDIyCgZI6EWngiM6jK3YLUNzv1b7lTzy6Bk=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "7c8764b7c7b09b34f632464276218ef9090eaa11",
|
"rev": "c56a87d8e41ce69bd2af57eb35dcc117a9dfca33",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -465,11 +465,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_5": {
|
"nixpkgs_5": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781074563,
|
"lastModified": 1791956154,
|
||||||
"narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=",
|
"narHash": "sha256-jBYGPi4udQ9uo4KQp9m2J0546nJVQitG15gvZSaVVAs=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca",
|
"rev": "55bfb4d7437bd4e09ab7cba58a43bd68eb310044",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -481,11 +481,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_6": {
|
"nixpkgs_6": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1791456040,
|
"lastModified": 1793876309,
|
||||||
"narHash": "sha256-H83yz/do+hjOO0uujHyH8RgtIQP2v7Mw+cLwcZK/xKc=",
|
"narHash": "sha256-Wojg9lIH/hDH91iDVH2/EPw8oDSpxjxhXONbU1yf4Zg=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e7439b6b14ad3cc35d05608ebca9bce01a25f5f8",
|
"rev": "f9a75f41be3a74eab55d3a7679d34ad6cc0da977",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -519,11 +519,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1791626603,
|
"lastModified": 1794047569,
|
||||||
"narHash": "sha256-I16LCKG4Bh3WaMFHppz7gbF70smYOicr/DalSleXatU=",
|
"narHash": "sha256-q15JEuh1qY0tBZcKLH3o2erKR2ofC9uLD8b/HTyWuyI=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nur",
|
"repo": "nur",
|
||||||
"rev": "241ca011a9fcefa68b42e8d5bd55aac59f6d9fe4",
|
"rev": "739e65d0caedf0af153f34b8f20798d776afe3bd",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -547,6 +547,7 @@
|
|||||||
"nixpkgs-stable": "nixpkgs-stable",
|
"nixpkgs-stable": "nixpkgs-stable",
|
||||||
"nur": "nur",
|
"nur": "nur",
|
||||||
"rust-overlay": "rust-overlay",
|
"rust-overlay": "rust-overlay",
|
||||||
|
"steam-fetcher": "steam-fetcher",
|
||||||
"wsl": "wsl",
|
"wsl": "wsl",
|
||||||
"zellij-switch": "zellij-switch",
|
"zellij-switch": "zellij-switch",
|
||||||
"zenyd-mpv-scripts": "zenyd-mpv-scripts"
|
"zenyd-mpv-scripts": "zenyd-mpv-scripts"
|
||||||
@@ -557,11 +558,11 @@
|
|||||||
"nixpkgs": "nixpkgs_7"
|
"nixpkgs": "nixpkgs_7"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1791620365,
|
"lastModified": 1794040562,
|
||||||
"narHash": "sha256-CVZXIDNw7HEwyD8X6q9Ln370Sc0ZqRlTzDRwOZ1hJMY=",
|
"narHash": "sha256-7eEiezSku3lbZPUOXW8we1a7+xd5axkCQdyI3rzOKjo=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "e90fafa71b440fabcd52dd7fb017ab2da81c781a",
|
"rev": "d3fe74269f2a6652437e21e78eb7e83be81f6982",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -570,6 +571,26 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"steam-fetcher": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1758622040,
|
||||||
|
"narHash": "sha256-BRUZZoQDCD1qByJjHm4YKipQlB2PburWLG9Iaggr8XQ=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "steam-fetcher",
|
||||||
|
"rev": "ab5e3a0828b4b179feb548c57be584ce812956a8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "steam-fetcher",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"systems": {
|
"systems": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1681028828,
|
"lastModified": 1681028828,
|
||||||
@@ -689,11 +710,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1789164534,
|
"lastModified": 1792982269,
|
||||||
"narHash": "sha256-DoYGPM6QpnYBLWj9gGw6ZwAzIX+HrAVov1BoT+8Jixo=",
|
"narHash": "sha256-5R5CFIKj06qTQevhg/rCFUSAMxfNgdn0oIBL57R7u0Y=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "NixOS-WSL",
|
"repo": "NixOS-WSL",
|
||||||
"rev": "72c92b11bb8289e6651c7fef29cc0a885fd6a255",
|
"rev": "e45b24b7df093299072b13bf3144ed0ee340e769",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -92,6 +92,12 @@
|
|||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Steam depot fetcher
|
||||||
|
steam-fetcher = {
|
||||||
|
url = "github:nix-community/steam-fetcher";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
# # Text editor
|
# # Text editor
|
||||||
# helix = {
|
# helix = {
|
||||||
# url = "github:helix-editor/helix";
|
# url = "github:helix-editor/helix";
|
||||||
@@ -166,6 +172,7 @@
|
|||||||
status = "status.${baseName}";
|
status = "status.${baseName}";
|
||||||
stream = "stream.${baseName}";
|
stream = "stream.${baseName}";
|
||||||
transmission = "transmission.${baseName}";
|
transmission = "transmission.${baseName}";
|
||||||
|
valheim = "valheim.${baseName}";
|
||||||
};
|
};
|
||||||
|
|
||||||
lib = import ./lib inputs;
|
lib = import ./lib inputs;
|
||||||
@@ -183,27 +190,35 @@
|
|||||||
root = import ./hosts/x86_64-linux/swan/root.nix;
|
root = import ./hosts/x86_64-linux/swan/root.nix;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
awsOnlyHosts = [ "arrow" ];
|
||||||
|
|
||||||
generators = builtins.mapAttrs (
|
generators = builtins.mapAttrs (
|
||||||
# x86_64-linux = { arrow = ...; swan = ...; }
|
# x86_64-linux = { arrow = ...; swan = ...; }
|
||||||
system: hosts:
|
system: hosts:
|
||||||
(lib.concatMapAttrs (name: module: {
|
(lib.concatMapAttrs (
|
||||||
"${name}-aws" = lib.generateImage {
|
name: module:
|
||||||
inherit system module;
|
{
|
||||||
format = "amazon";
|
"${name}-aws" = lib.generateImage {
|
||||||
specialArgs = { inherit hostnames; };
|
inherit system module;
|
||||||
};
|
format = "amazon";
|
||||||
"${name}-iso" = lib.generateImage {
|
specialArgs = { inherit hostnames; };
|
||||||
inherit system module;
|
};
|
||||||
format = "iso";
|
}
|
||||||
specialArgs = { inherit hostnames; };
|
# Hosts that import amazon-image.nix directly conflict with other formats
|
||||||
};
|
// lib.optionalAttrs (!builtins.elem name awsOnlyHosts) {
|
||||||
"${name}-qcow" = lib.generateImage {
|
"${name}-iso" = lib.generateImage {
|
||||||
inherit system module;
|
inherit system module;
|
||||||
format = "qcow-efi";
|
format = "iso";
|
||||||
specialArgs = { inherit hostnames; };
|
specialArgs = { inherit hostnames; };
|
||||||
# extraModules = [ "${nixpkgs}/nixos/modules/virtualisation/oci-image.nix" ];
|
};
|
||||||
};
|
"${name}-qcow" = lib.generateImage {
|
||||||
}) hosts)
|
inherit system module;
|
||||||
|
format = "qcow-efi";
|
||||||
|
specialArgs = { inherit hostnames; };
|
||||||
|
# extraModules = [ "${nixpkgs}/nixos/modules/virtualisation/oci-image.nix" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) hosts)
|
||||||
) lib.linuxHosts # x86_64-linux = { arrow = ...; swan = ...; }
|
) lib.linuxHosts # x86_64-linux = { arrow = ...; swan = ...; }
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ lib
|
|||||||
inputs.nur.overlays.default
|
inputs.nur.overlays.default
|
||||||
inputs.nix2vim.overlay
|
inputs.nix2vim.overlay
|
||||||
inputs.api.overlays.default
|
inputs.api.overlays.default
|
||||||
|
inputs.steam-fetcher.overlay
|
||||||
(final: prev: {
|
(final: prev: {
|
||||||
zellij-switch = inputs.zellij-switch.packages.${prev.stdenv.hostPlatform.system}.default;
|
zellij-switch = inputs.zellij-switch.packages.${prev.stdenv.hostPlatform.system}.default;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
_inputs: _final: prev: {
|
||||||
|
|
||||||
|
# atuin 18.23.0's PTY-driven e2e tests (e2e_capture, e2e_pty) time out in the
|
||||||
|
# darwin build sandbox: shell hooks never report a session ID or history
|
||||||
|
# entry. Unit tests pass, but cargo aborts on the first failing e2e binary.
|
||||||
|
atuin = prev.atuin.overrideAttrs (_old: {
|
||||||
|
doCheck = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Calibre's installCheckPhase runs test_speech_dispatcher, which imports
|
||||||
|
# `speechd.client.SSIPClient`. Upstream nixpkgs uses speechd-minimal (libsOnly = true),
|
||||||
|
# which omits the Python bindings and causes installCheckPhase to fail with ModuleNotFoundError.
|
||||||
|
_inputs: _final: prev: {
|
||||||
|
calibre = prev.calibre.overrideAttrs (_old: {
|
||||||
|
doInstallCheck = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Upstream pyopenjtalk-plus has a test (test_run_njd_from_mecab_invalid_input_should_not_break_next_call)
|
||||||
|
# that asserts the exact TypeError exception string "must be str", which changed to "Expected str, got int"
|
||||||
|
# under Python 3.14 / Cython.
|
||||||
|
_inputs: _final: prev: {
|
||||||
|
pythonPackagesExtensions = prev.pythonPackagesExtensions ++ [
|
||||||
|
(_pyfinal: pyprev: {
|
||||||
|
pyopenjtalk-plus = pyprev.pyopenjtalk-plus.overridePythonAttrs (old: {
|
||||||
|
disabledTests = (old.disabledTests or [ ]) ++ [
|
||||||
|
"test_run_njd_from_mecab_invalid_input_should_not_break_next_call"
|
||||||
|
];
|
||||||
|
});
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
_inputs: final: prev:
|
||||||
|
|
||||||
|
{
|
||||||
|
fetchValheimThunderstoreMod =
|
||||||
|
{
|
||||||
|
owner,
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
hash,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
depString = "${owner}-${name}";
|
||||||
|
in
|
||||||
|
prev.fetchzip {
|
||||||
|
inherit version hash;
|
||||||
|
name = "valheim-thunderstore-${depString}";
|
||||||
|
url = "https://thunderstore.io/package/download/${owner}/${name}/${version}/";
|
||||||
|
extension = "zip";
|
||||||
|
stripRoot = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// prev.lib.optionalAttrs (prev.stdenv.hostPlatform.isLinux && prev.stdenv.hostPlatform.isx86_64) {
|
||||||
|
valheim-server = final.nmasur.valheim-server;
|
||||||
|
valheim-server-unwrapped = final.nmasur.valheim-server.unwrapped;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
stdenv,
|
||||||
|
fetchSteam,
|
||||||
|
buildFHSEnv,
|
||||||
|
writeScript,
|
||||||
|
steamworks-sdk-redist,
|
||||||
|
zlib,
|
||||||
|
pulseaudio,
|
||||||
|
}:
|
||||||
|
|
||||||
|
if !stdenv.hostPlatform.isLinux || !stdenv.hostPlatform.isx86_64 then
|
||||||
|
null
|
||||||
|
else
|
||||||
|
let
|
||||||
|
valheim-server-unwrapped = stdenv.mkDerivation (finalAttrs: {
|
||||||
|
pname = "valheim-server-unwrapped";
|
||||||
|
version = "1.0.16";
|
||||||
|
|
||||||
|
src = fetchSteam {
|
||||||
|
name = "valheim-server";
|
||||||
|
appId = "896660";
|
||||||
|
depotId = "896661";
|
||||||
|
manifestId = "1285123405092214913";
|
||||||
|
hash = "sha256-AH9yU0YW0RpgkD9VSGFLj+qIXk3UlvrAgIu9fy/D+N8=";
|
||||||
|
};
|
||||||
|
|
||||||
|
dontBuild = true;
|
||||||
|
dontConfigure = true;
|
||||||
|
dontFixup = true;
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
|
||||||
|
mkdir -p $out
|
||||||
|
shopt -s nullglob
|
||||||
|
cp -r *.so *.debug valheim_server.x86_64 valheim_server_Data $out
|
||||||
|
chmod +x $out/valheim_server.x86_64
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = with lib; {
|
||||||
|
description = "Valheim dedicated server (unwrapped)";
|
||||||
|
homepage = "https://steamdb.info/app/896660/";
|
||||||
|
changelog = "https://store.steampowered.com/news/app/892970?updates=true";
|
||||||
|
sourceProvenance = with sourceTypes; [
|
||||||
|
binaryBytecode
|
||||||
|
binaryNativeCode
|
||||||
|
];
|
||||||
|
license = licenses.unfree;
|
||||||
|
platforms = [ "x86_64-linux" ];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
in
|
||||||
|
buildFHSEnv {
|
||||||
|
name = "valheim-server";
|
||||||
|
pname = "valheim-server";
|
||||||
|
version = valheim-server-unwrapped.version;
|
||||||
|
|
||||||
|
runScript = writeScript "valheim-server-wrapper" ''
|
||||||
|
export LD_LIBRARY_PATH=${steamworks-sdk-redist}/lib:$LD_LIBRARY_PATH
|
||||||
|
export SteamAppId=892970
|
||||||
|
exec ${valheim-server-unwrapped}/valheim_server.x86_64 "$@"
|
||||||
|
'';
|
||||||
|
|
||||||
|
targetPkgs = pkgs: [
|
||||||
|
valheim-server-unwrapped
|
||||||
|
steamworks-sdk-redist
|
||||||
|
zlib
|
||||||
|
pulseaudio
|
||||||
|
];
|
||||||
|
|
||||||
|
passthru = {
|
||||||
|
unwrapped = valheim-server-unwrapped;
|
||||||
|
};
|
||||||
|
|
||||||
|
meta = valheim-server-unwrapped.meta // {
|
||||||
|
description = "Valheim dedicated server";
|
||||||
|
mainProgram = "valheim-server";
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ in
|
|||||||
# size = 11.0;
|
# size = 11.0;
|
||||||
};
|
};
|
||||||
services.polybar.config."bar/main".font-0 = "Hack Nerd Font:size=10;2";
|
services.polybar.config."bar/main".font-0 = "Hack Nerd Font:size=10;2";
|
||||||
programs.rofi.font = "Hack Nerd Font 14";
|
programs.rofi.settings.font = "Hack Nerd Font 14";
|
||||||
programs.alacritty.settings.font.normal.family = "Maple Mono NF";
|
programs.alacritty.settings.font.normal.family = "Maple Mono NF";
|
||||||
programs.kitty.font.name = "Maple Mono NF";
|
programs.kitty.font.name = "Maple Mono NF";
|
||||||
nmasur.presets.programs.wezterm.font = "Maple Mono NF";
|
nmasur.presets.programs.wezterm.font = "Maple Mono NF";
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ in
|
|||||||
extensions = [
|
extensions = [
|
||||||
pkgs.nmasur.gh-collaborators
|
pkgs.nmasur.gh-collaborators
|
||||||
pkgs.gh-dash
|
pkgs.gh-dash
|
||||||
|
pkgs.gh-actions-lock
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ in
|
|||||||
cask "topnotch" # Darkens the menu bar to complete black
|
cask "topnotch" # Darkens the menu bar to complete black
|
||||||
cask "ghostty" # Terminal application (not buildable on Nix on macOS)
|
cask "ghostty" # Terminal application (not buildable on Nix on macOS)
|
||||||
cask "discord" # nixpkgs bundle breaks its notarization seal on macOS 26+
|
cask "discord" # nixpkgs bundle breaks its notarization seal on macOS 26+
|
||||||
|
cask "vorssaint" # Customizations and automations for macOS
|
||||||
|
|
||||||
# Mac App Store apps (requires 'mas' CLI, optional)
|
# Mac App Store apps (requires 'mas' CLI, optional)
|
||||||
# mas "Tailscale", id: 1475387142
|
# mas "Tailscale", id: 1475387142
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.nmasur.presets.programs.lag-triage;
|
|
||||||
|
|
||||||
term-probe = pkgs.writeScriptBin "term-probe" ''
|
|
||||||
#!${lib.getExe pkgs.python3}
|
|
||||||
${builtins.readFile ./term_probe.py}
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
options.nmasur.presets.programs.lag-triage.enable =
|
|
||||||
lib.mkEnableOption "Terminal input-lag triage tools";
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
|
|
||||||
home.packages = [ term-probe ];
|
|
||||||
|
|
||||||
# Ctrl-b: EXECUTE the proven manual cure as a real commandline. The cure
|
|
||||||
# is not the variable's end value (it starts and ends at 1) — it is the
|
|
||||||
# reader fully exiting readline and re-entering, which only command
|
|
||||||
# EXECUTION does. Setting the variable inline (a plain binding body, or the
|
|
||||||
# old fish_postexec/fish_cancel hook) never makes the reader exit/re-enter,
|
|
||||||
# so it never cured and, with extra repaints on a wedged reader, made it
|
|
||||||
# worse. `commandline -f execute` reproduces exactly what typing the cure
|
|
||||||
# and pressing Enter does — indistinguishable to fish from the manual cure.
|
|
||||||
# Note: this submits the current commandline, so it runs the cure in place
|
|
||||||
# of whatever is typed (fine for a rescue key hit at an empty prompt).
|
|
||||||
# Ctrl-b chosen because it is otherwise unbound (Ctrl-g is taken).
|
|
||||||
nmasur.presets.programs.fish.fish_user_key_bindings = # fish
|
|
||||||
''
|
|
||||||
for mode in insert default visual
|
|
||||||
bind -M $mode \cb heal-autosuggest
|
|
||||||
end
|
|
||||||
'';
|
|
||||||
|
|
||||||
programs.fish.functions = {
|
|
||||||
lag-triage = {
|
|
||||||
description = "Diagnose post-TUI typing lag in the current shell";
|
|
||||||
body = builtins.readFile ./lag-triage.fish;
|
|
||||||
};
|
|
||||||
unlag = {
|
|
||||||
description = "Reset terminal state left behind by a TUI";
|
|
||||||
body = builtins.readFile ./unlag.fish;
|
|
||||||
};
|
|
||||||
lag-sample = {
|
|
||||||
description = "Stack-sample fish and zellij while typing lag is happening";
|
|
||||||
body = builtins.readFile ./lag-sample.fish;
|
|
||||||
};
|
|
||||||
heal-autosuggest = {
|
|
||||||
description = "Heal post-TUI typing lag by executing the autosuggestion-toggle cure (bind to a key)";
|
|
||||||
# Replace the commandline with the exact cure the user runs by hand and
|
|
||||||
# execute it. Executing (not inline-setting) is what cures: it forces
|
|
||||||
# the reader to leave and re-enter readline. Runs in place of whatever
|
|
||||||
# is currently typed.
|
|
||||||
body = # fish
|
|
||||||
''
|
|
||||||
commandline -r 'set -g fish_autosuggestion_enabled 0; and set -g fish_autosuggestion_enabled 1'
|
|
||||||
commandline -f execute
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# Capture stack samples of this fish process, the zellij server, and the
|
|
||||||
# zellij client WHILE the typing lag is happening. This names the guilty
|
|
||||||
# component directly: if fish's main thread is busy/blocked per keystroke the
|
|
||||||
# stacks show exactly where; if fish is idle while typing feels laggy, the
|
|
||||||
# delay is in zellij's render path instead.
|
|
||||||
#
|
|
||||||
# CAUTION (learned 2026-08-30): attaching the sampler to a lagging fish CURES
|
|
||||||
# the lag (thread suspend/resume unwedges it), so run this from a DIFFERENT
|
|
||||||
# pane with the lagging shell's pid: `lag-sample <pid>` (get it in the lagging
|
|
||||||
# shell with the builtin-only `echo $fish_pid`). Have someone type in the
|
|
||||||
# lagging pane while sampling runs — the first samples may catch the wedge.
|
|
||||||
# With no argument it samples the current shell.
|
|
||||||
|
|
||||||
set -l target $fish_pid
|
|
||||||
if test (count $argv) -ge 1; and test -n "$argv[1]"
|
|
||||||
set target $argv[1]
|
|
||||||
end
|
|
||||||
|
|
||||||
set -l outdir ~/.local/state/lag-triage
|
|
||||||
mkdir -p $outdir
|
|
||||||
set -l ts (date +%Y%m%d-%H%M%S)
|
|
||||||
set -l dur 8
|
|
||||||
|
|
||||||
set -l fishfile $outdir/sample-$ts-fish-$target.txt
|
|
||||||
/usr/bin/sample $target $dur 1 -file $fishfile &>/dev/null &
|
|
||||||
disown
|
|
||||||
|
|
||||||
# this session's zellij server (socket path ends in the session name)
|
|
||||||
set -l serverpid (pgrep -f "zellij --server.*/$ZELLIJ_SESSION_NAME\$")
|
|
||||||
test -z "$serverpid"; and set serverpid (pgrep -f "zellij --server" | head -3)
|
|
||||||
for pid in $serverpid
|
|
||||||
/usr/bin/sample $pid $dur 1 -file $outdir/sample-$ts-zellij-server-$pid.txt &>/dev/null &
|
|
||||||
disown
|
|
||||||
end
|
|
||||||
|
|
||||||
# zellij clients (attached to ghostty): named zellij but without --server args
|
|
||||||
set -l allserver (pgrep -f "zellij --server")
|
|
||||||
set -l clientpid
|
|
||||||
for pid in (pgrep -x zellij)
|
|
||||||
contains $pid $allserver; or set -a clientpid $pid
|
|
||||||
end
|
|
||||||
for pid in $clientpid[1..3]
|
|
||||||
/usr/bin/sample $pid $dur 1 -file $outdir/sample-$ts-zellij-client-$pid.txt &>/dev/null &
|
|
||||||
disown
|
|
||||||
end
|
|
||||||
|
|
||||||
# notify when done, without occupying the commandline
|
|
||||||
fish -c "sleep (math $dur + 2); echo; echo '== lag-sample done: '$outdir'/sample-$ts-*.txt =='" &
|
|
||||||
disown
|
|
||||||
|
|
||||||
echo "Sampling fish (pid $target), zellij server(s) [$serverpid], client(s) [$clientpid] for $dur s."
|
|
||||||
echo ">>> TYPE CONTINUOUSLY IN THE LAGGING PANE NOW (junk text is fine) <<<"
|
|
||||||
echo "Files: $outdir/sample-$ts-*.txt"
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
# Guided diagnosis for the post-TUI typing-lag problem (Ghostty + Zellij + fish).
|
|
||||||
# Run this IN THE LAGGING SHELL the moment you notice the lag, BEFORE starting
|
|
||||||
# a new shell. It captures evidence, then applies targeted resets one at a time
|
|
||||||
# so the stage that cures the lag identifies the layer holding stuck state.
|
|
||||||
# Everything is logged for filing an upstream issue.
|
|
||||||
|
|
||||||
set -l logdir ~/.local/state/lag-triage
|
|
||||||
mkdir -p $logdir
|
|
||||||
set -l logfile $logdir/(date +%Y%m%d-%H%M%S).log
|
|
||||||
|
|
||||||
function _lt --inherit-variable logfile
|
|
||||||
echo $argv | tee -a $logfile
|
|
||||||
end
|
|
||||||
|
|
||||||
function _lt_ask --inherit-variable logfile
|
|
||||||
# usage: _lt_ask VARNAME prompt... -> sets global $VARNAME (default: skip)
|
|
||||||
set -l __name $argv[1]
|
|
||||||
read -g -P "$argv[2..] " $__name
|
|
||||||
or set -g $__name skip
|
|
||||||
test -z "$$__name"; and set -g $__name skip
|
|
||||||
echo "ANSWER $__name: $$__name" >>$logfile
|
|
||||||
end
|
|
||||||
|
|
||||||
_lt "== lag-triage "(date)" =="
|
|
||||||
_lt "Log: $logfile"
|
|
||||||
_lt "Answer y / n, or press Enter to skip a question."
|
|
||||||
_lt ""
|
|
||||||
|
|
||||||
# ---- 1. Context -------------------------------------------------------------
|
|
||||||
_lt_ask ans_tui "Which TUI did you just exit (nvim/jjui/yazi/other)?"
|
|
||||||
_lt_ask ans_launch "Launched via (f)loating-pane keybind or (c)ommand typed in this shell?"
|
|
||||||
|
|
||||||
# ---- 2. Snapshot ------------------------------------------------------------
|
|
||||||
begin
|
|
||||||
echo "-- snapshot --"
|
|
||||||
fish --version
|
|
||||||
echo "fish pid: $fish_pid, started: "(ps -o lstart= -p $fish_pid 2>/dev/null)
|
|
||||||
zellij --version 2>/dev/null
|
|
||||||
echo "escape delay: '$fish_escape_delay_ms' sequence delay: '$fish_sequence_key_delay_ms'"
|
|
||||||
env | grep -iE '^(TERM|ZELLIJ|GHOSTTY|COLORTERM)' | sort
|
|
||||||
echo "-- status features --"
|
|
||||||
status features
|
|
||||||
echo "-- stty -a --"
|
|
||||||
stty -a
|
|
||||||
end >>$logfile 2>&1
|
|
||||||
_lt "Captured shell + environment snapshot."
|
|
||||||
|
|
||||||
# Proven root cause of the 2026-08 lag (see docs/CHANGELOG.md 2026-08-29):
|
|
||||||
# fish latches feature flags from its startup env before config.fish runs, so
|
|
||||||
# a shell with query-term ON sends terminal queries after every command; one
|
|
||||||
# reply zellij fails to relay permanently degrades this process's reader.
|
|
||||||
if status features | string match -qr '^query-term\s+on'
|
|
||||||
_lt ""
|
|
||||||
_lt "!! query-term is ON in this shell: fish did NOT get fish_features="
|
|
||||||
_lt "!! no-query-term in its STARTUP environment (config.fish is too late)."
|
|
||||||
_lt "!! This is the proven root cause of the post-TUI lag — a query reply"
|
|
||||||
_lt "!! lost by zellij permanently degrades this fish process's reader."
|
|
||||||
_lt "!! Fix: spawn fish with the variable exported (zellij default_shell"
|
|
||||||
_lt "!! wrapper fish-no-query-term). Subshells are immune because they"
|
|
||||||
_lt "!! inherit the exported variable — that's why a new shell 'fixes' it."
|
|
||||||
else
|
|
||||||
_lt "query-term is off in this shell (good — the known root cause is ruled out)."
|
|
||||||
end
|
|
||||||
|
|
||||||
# ---- 3. Terminal state below the shell --------------------------------------
|
|
||||||
_lt ""
|
|
||||||
_lt "Querying terminal state (takes a few seconds)..."
|
|
||||||
term-probe report 2>&1 | tee -a $logfile
|
|
||||||
_lt ""
|
|
||||||
_lt " ^ Things to look for: kitty flags with a reply > 0, modifyOtherKeys > 1,"
|
|
||||||
_lt " any mouse/alternate-screen mode SET while at a shell prompt, or a slow"
|
|
||||||
_lt " DA1 round-trip (> 100 ms means the input path itself is delayed)."
|
|
||||||
|
|
||||||
# ---- 4. Raw keystroke capture (bypasses fish entirely) ----------------------
|
|
||||||
_lt ""
|
|
||||||
_lt "Raw input capture: type ~10 characters at a steady pace, including one"
|
|
||||||
_lt "ESC press and one arrow key. This shows the exact bytes this pane delivers"
|
|
||||||
_lt "and their timing, with fish's input handling out of the picture."
|
|
||||||
term-probe keylog 2>&1 | tee -a $logfile
|
|
||||||
_lt_ask ans_keylog_instant "Did each keypress appear INSTANTLY in the capture? (y/n)"
|
|
||||||
_lt_ask ans_keylog_plain "Were plain letters single plain bytes like b'a' (not escape sequences)? (y/n)"
|
|
||||||
|
|
||||||
# ---- 5. Scope ---------------------------------------------------------------
|
|
||||||
_lt ""
|
|
||||||
_lt_ask ans_scope_pane "Optional: open a NEW zellij pane/tab and type — laggy there too? (y/n)"
|
|
||||||
_lt_ask ans_scope_window "Optional: type in a separate Ghostty window (outside this zellij session) — laggy? (y/n)"
|
|
||||||
|
|
||||||
# ---- 6. Staged resets -------------------------------------------------------
|
|
||||||
# Each stage resets one category of state a TUI could have left behind.
|
|
||||||
# The first stage that cures the lag names the culprit.
|
|
||||||
set -l fixed none
|
|
||||||
|
|
||||||
_lt ""
|
|
||||||
_lt "Now applying resets one at a time. After each, type into the test prompt"
|
|
||||||
_lt "to judge whether the lag is gone."
|
|
||||||
_lt "CAVEAT: fish's read prompt may NOT exhibit lag even when the main"
|
|
||||||
_lt "commandline does. If typing at these test prompts never feels laggy at"
|
|
||||||
_lt "all, answer 'u' (unsure) instead of 'y' — a 'y' here is only meaningful"
|
|
||||||
_lt "if you could feel the lag at the test prompts before the reset."
|
|
||||||
|
|
||||||
if test $fixed = none
|
|
||||||
_lt ""
|
|
||||||
_lt "Stage A - kitty keyboard protocol: pop stack + clear all flags"
|
|
||||||
printf '\e[<9u\e[=0;1u'
|
|
||||||
_lt_ask ans_stage_a " Test typing here, then Enter — lag gone? (y/n)"
|
|
||||||
test "$ans_stage_a" = y; and set fixed "A (kitty keyboard state)"
|
|
||||||
end
|
|
||||||
|
|
||||||
if test $fixed = none
|
|
||||||
_lt "Stage B - modifyOtherKeys off"
|
|
||||||
printf '\e[>4;0m'
|
|
||||||
_lt_ask ans_stage_b " Test typing here, then Enter — lag gone? (y/n)"
|
|
||||||
test "$ans_stage_b" = y; and set fixed "B (modifyOtherKeys)"
|
|
||||||
end
|
|
||||||
|
|
||||||
if test $fixed = none
|
|
||||||
_lt "Stage C - normal keypad + normal cursor keys"
|
|
||||||
printf '\e>\e[?1l'
|
|
||||||
_lt_ask ans_stage_c " Test typing here, then Enter — lag gone? (y/n)"
|
|
||||||
test "$ans_stage_c" = y; and set fixed "C (application keypad/cursor mode)"
|
|
||||||
end
|
|
||||||
|
|
||||||
if test $fixed = none
|
|
||||||
_lt "Stage D - disable mouse, focus reporting, synchronized output"
|
|
||||||
printf '\e[?1000l\e[?1001l\e[?1002l\e[?1003l\e[?1005l\e[?1006l\e[?1015l\e[?1016l\e[?1004l\e[?2026l'
|
|
||||||
_lt_ask ans_stage_d " Test typing here, then Enter — lag gone? (y/n)"
|
|
||||||
test "$ans_stage_d" = y; and set fixed "D (mouse/focus/sync modes)"
|
|
||||||
end
|
|
||||||
|
|
||||||
if test $fixed = none
|
|
||||||
_lt "Stage E - leave alternate screen"
|
|
||||||
printf '\e[?1049l'
|
|
||||||
_lt_ask ans_stage_e " Test typing here, then Enter — lag gone? (y/n)"
|
|
||||||
test "$ans_stage_e" = y; and set fixed "E (alternate screen)"
|
|
||||||
end
|
|
||||||
|
|
||||||
if test $fixed = none
|
|
||||||
_lt "Stage F - stty sane (line-discipline reset)"
|
|
||||||
stty sane
|
|
||||||
_lt_ask ans_stage_f " Test typing here, then Enter — lag gone? (y/n)"
|
|
||||||
test "$ans_stage_f" = y; and set fixed "F (termios/line discipline)"
|
|
||||||
end
|
|
||||||
|
|
||||||
if test $fixed = none
|
|
||||||
_lt "Stage G - DECSTR soft terminal reset"
|
|
||||||
printf '\e[!p'
|
|
||||||
_lt_ask ans_stage_g " Test typing here, then Enter — lag gone? (y/n)"
|
|
||||||
test "$ans_stage_g" = y; and set fixed "G (DECSTR-resettable mode)"
|
|
||||||
end
|
|
||||||
|
|
||||||
# ---- 7. Verdict --------------------------------------------------------------
|
|
||||||
_lt ""
|
|
||||||
_lt "== Verdict =="
|
|
||||||
if test $fixed != none
|
|
||||||
_lt "Lag cleared by stage $fixed."
|
|
||||||
_lt "That state was stuck BELOW fish — in the Zellij pane or relayed to"
|
|
||||||
_lt "Ghostty — and the TUI you exited ($ans_tui) failed to restore it, or"
|
|
||||||
_lt "Zellij failed to restore it when the pane closed."
|
|
||||||
_lt "Re-probing terminal state after the fix for comparison:"
|
|
||||||
term-probe report 2>&1 | tee -a $logfile
|
|
||||||
_lt ""
|
|
||||||
_lt "-> File this log against zellij (or ghostty, if a separate window also"
|
|
||||||
_lt " lagged). The before/after probe diff pinpoints the exact stuck mode."
|
|
||||||
else if test "$ans_keylog_instant" = y; and test "$ans_keylog_plain" = y
|
|
||||||
_lt "Raw input reaches this pane instantly as plain bytes, and no terminal"
|
|
||||||
_lt "state reset helps: the lag lives INSIDE this fish process (reader state)."
|
|
||||||
_lt "Confirm now: run 'exec fish' — if that cures it, it is fish-internal."
|
|
||||||
_lt ""
|
|
||||||
_lt "-> To catch it in the act, run your next long-lived shell as:"
|
|
||||||
_lt " FISH_DEBUG='reader,term-support' FISH_DEBUG_OUTPUT=$logdir/fish-debug.log fish"
|
|
||||||
_lt " then re-run lag-triage when it recurs and file both logs to fish-shell."
|
|
||||||
else
|
|
||||||
_lt "Keystrokes were delayed or arrived as escape sequences BEFORE fish saw"
|
|
||||||
_lt "them: the problem is in Zellij (client stdin parser / server) or Ghostty."
|
|
||||||
_lt " new pane also laggy: $ans_scope_pane (y -> session-wide, not this pane)"
|
|
||||||
_lt " separate window laggy: $ans_scope_window (y -> Ghostty itself)"
|
|
||||||
_lt "-> File this log against zellij; include the keylog byte capture."
|
|
||||||
end
|
|
||||||
_lt ""
|
|
||||||
_lt "Full log: $logfile"
|
|
||||||
|
|
||||||
functions -e _lt _lt_ask
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
"""Probe the terminal state of the current pane, below the shell.
|
|
||||||
|
|
||||||
Modes:
|
|
||||||
report - query kitty-keyboard flags, modifyOtherKeys, and DEC private
|
|
||||||
modes directly on /dev/tty, reporting each reply (or lack of
|
|
||||||
one) and its round-trip latency. Answers may come from Zellij
|
|
||||||
(pane state) or be relayed from Ghostty (window state).
|
|
||||||
keylog - raw-mode keystroke capture: prints the exact bytes and
|
|
||||||
inter-key latency for every keypress, bypassing the shell's
|
|
||||||
input machinery entirely. Press q to finish.
|
|
||||||
|
|
||||||
Used by the `lag-triage` fish function to pin down which layer
|
|
||||||
(fish / zellij / ghostty) is holding stuck state when typing lags
|
|
||||||
after a TUI exits.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import select
|
|
||||||
import sys
|
|
||||||
import termios
|
|
||||||
import time
|
|
||||||
import tty
|
|
||||||
|
|
||||||
# DECRQM reply values
|
|
||||||
DECRQM_VALUES = {
|
|
||||||
"0": "not recognized",
|
|
||||||
"1": "SET",
|
|
||||||
"2": "reset",
|
|
||||||
"3": "permanently set",
|
|
||||||
"4": "permanently reset",
|
|
||||||
}
|
|
||||||
|
|
||||||
DEC_MODES = [
|
|
||||||
(1, "application cursor keys (DECCKM)"),
|
|
||||||
(25, "cursor visible"),
|
|
||||||
(1000, "mouse click reporting"),
|
|
||||||
(1002, "mouse drag reporting"),
|
|
||||||
(1003, "mouse all-motion reporting"),
|
|
||||||
(1004, "focus reporting"),
|
|
||||||
(1006, "SGR mouse encoding"),
|
|
||||||
(1049, "alternate screen"),
|
|
||||||
(2004, "bracketed paste"),
|
|
||||||
(2026, "synchronized output"),
|
|
||||||
(2031, "color theme reporting"),
|
|
||||||
]
|
|
||||||
|
|
||||||
QUERIES = [
|
|
||||||
("kitty keyboard flags (\\e[?u)", b"\x1b[?u", rb"\x1b\[\?(\d+)u", None),
|
|
||||||
("modifyOtherKeys (XTQMODKEYS)", b"\x1b[?4m", rb"\x1b\[>4;(\d+)m", None),
|
|
||||||
("background color (OSC 11)", b"\x1b]11;?\x1b\\", rb"\x1b\]11;([^\x07\x1b]+)", None),
|
|
||||||
] + [
|
|
||||||
(
|
|
||||||
f"DEC mode {num} — {desc}",
|
|
||||||
b"\x1b[?%d$p" % num,
|
|
||||||
rb"\x1b\[\?%d;(\d+)\$y" % num,
|
|
||||||
DECRQM_VALUES,
|
|
||||||
)
|
|
||||||
for num, desc in DEC_MODES
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def read_for(fd, seconds):
|
|
||||||
buf = b""
|
|
||||||
end = time.monotonic() + seconds
|
|
||||||
while True:
|
|
||||||
remaining = end - time.monotonic()
|
|
||||||
if remaining <= 0:
|
|
||||||
break
|
|
||||||
r, _, _ = select.select([fd], [], [], remaining)
|
|
||||||
if not r:
|
|
||||||
break
|
|
||||||
buf += os.read(fd, 4096)
|
|
||||||
return buf
|
|
||||||
|
|
||||||
|
|
||||||
def report(fd):
|
|
||||||
lines = []
|
|
||||||
raw_dump = b""
|
|
||||||
for label, query, pattern, value_names in QUERIES:
|
|
||||||
raw_dump += read_for(fd, 0.02) # drain stragglers
|
|
||||||
start = time.monotonic()
|
|
||||||
os.write(fd, query)
|
|
||||||
buf = b""
|
|
||||||
match = None
|
|
||||||
deadline = time.monotonic() + 0.35
|
|
||||||
while time.monotonic() < deadline:
|
|
||||||
buf += read_for(fd, 0.05)
|
|
||||||
match = re.search(pattern, buf)
|
|
||||||
if match:
|
|
||||||
break
|
|
||||||
raw_dump += buf
|
|
||||||
if match:
|
|
||||||
latency = (time.monotonic() - start) * 1000
|
|
||||||
value = match.group(1).decode("ascii", "replace")
|
|
||||||
if value_names:
|
|
||||||
value = f"{value} ({value_names.get(value, '?')})"
|
|
||||||
lines.append(f" {label:45s} = {value:24s} [{latency:6.1f} ms]")
|
|
||||||
else:
|
|
||||||
lines.append(f" {label:45s} = (no reply)")
|
|
||||||
|
|
||||||
# DA1 as a fence: every terminal answers it, so its round-trip time
|
|
||||||
# measures the whole input path (ghostty -> zellij -> pane -> here).
|
|
||||||
start = time.monotonic()
|
|
||||||
os.write(fd, b"\x1b[c")
|
|
||||||
buf = b""
|
|
||||||
match = None
|
|
||||||
deadline = time.monotonic() + 2.0
|
|
||||||
while time.monotonic() < deadline:
|
|
||||||
buf += read_for(fd, 0.05)
|
|
||||||
match = re.search(rb"\x1b\[\?([0-9;]*)c", buf)
|
|
||||||
if match:
|
|
||||||
break
|
|
||||||
raw_dump += buf
|
|
||||||
if match:
|
|
||||||
latency = (time.monotonic() - start) * 1000
|
|
||||||
lines.append(
|
|
||||||
f" {'device attributes (DA1) round-trip':45s} = "
|
|
||||||
f"{match.group(1).decode():24s} [{latency:6.1f} ms]"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
lines.append(f" {'device attributes (DA1) round-trip':45s} = (NO REPLY in 2s!)")
|
|
||||||
lines.append(f" raw bytes received: {raw_dump!r}")
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
def keylog(fd):
|
|
||||||
sys.stdout.write("keylog: capturing raw bytes from the tty. Press q to finish.\r\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
last = time.monotonic()
|
|
||||||
while True:
|
|
||||||
select.select([fd], [], [], None)
|
|
||||||
data = os.read(fd, 4096)
|
|
||||||
now = time.monotonic()
|
|
||||||
delta_ms = (now - last) * 1000
|
|
||||||
last = now
|
|
||||||
sys.stdout.write(f" +{delta_ms:8.1f} ms {data!r} hex={data.hex(' ')}\r\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
if data in (b"q", b"\x03", b"\x04"):
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
mode = sys.argv[1] if len(sys.argv) > 1 else "report"
|
|
||||||
fd = os.open("/dev/tty", os.O_RDWR)
|
|
||||||
old = termios.tcgetattr(fd)
|
|
||||||
lines = None
|
|
||||||
try:
|
|
||||||
tty.setraw(fd)
|
|
||||||
if mode == "report":
|
|
||||||
lines = report(fd)
|
|
||||||
elif mode == "keylog":
|
|
||||||
keylog(fd)
|
|
||||||
else:
|
|
||||||
raise SystemExit(f"unknown mode: {mode}")
|
|
||||||
finally:
|
|
||||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
||||||
os.close(fd)
|
|
||||||
if lines:
|
|
||||||
print("terminal state as seen from this pane:")
|
|
||||||
print("\n".join(lines))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# One-shot reset of terminal state a TUI may have left behind (kitty keyboard
|
|
||||||
# flags, modifyOtherKeys, application keypad/cursor, mouse/focus reporting,
|
|
||||||
# alternate screen, termios). fish re-enables the modes it wants at the next
|
|
||||||
# prompt, so this is safe to run any time.
|
|
||||||
#
|
|
||||||
# Diagnostic value: if this cures the lag, the stuck state was below fish
|
|
||||||
# (run lag-triage next time to find which mode). If only `exec fish` cures
|
|
||||||
# it, the lag is inside the fish process itself.
|
|
||||||
printf '\e[<9u\e[=0;1u'
|
|
||||||
printf '\e[>4;0m'
|
|
||||||
printf '\e>\e[?1l'
|
|
||||||
printf '\e[?1000l\e[?1001l\e[?1002l\e[?1003l\e[?1005l\e[?1006l\e[?1015l\e[?1016l\e[?1004l\e[?2026l'
|
|
||||||
printf '\e[?1049l'
|
|
||||||
stty sane
|
|
||||||
echo "terminal state reset — if typing still lags, run lag-triage (before exec fish!)"
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Deterministic repro of permanent fish reader degradation (fish 4.8.1).
|
|
||||||
|
|
||||||
This is evidence for an upstream fish-shell report, and the proof behind the
|
|
||||||
fish-no-query-term wrapper in presets/programs/zellij.nix. Not installed by
|
|
||||||
the nix module; run directly: python3 upstream_repro.py [queryterm|noqueryterm]
|
|
||||||
|
|
||||||
Finding: with the query-term feature enabled (latched from the startup env,
|
|
||||||
which is fish's default), fish sends OSC 11 + CPR (\e[6n) + DA1 (\e[0c)
|
|
||||||
after every external command and waits for the replies. If the terminal
|
|
||||||
fails to reply during ONE such cycle -- even though it answered every query
|
|
||||||
before and answers every query after -- that fish process's interactive
|
|
||||||
reader is PERMANENTLY degraded: keystrokes are no longer echoed (>3s each,
|
|
||||||
never recovers). In production this happens when zellij drops/mis-relays a
|
|
||||||
reply during TUI teardown or heavy output (cf. zellij-org/zellij#5158), and
|
|
||||||
it presents as permanent typing lag cured only by replacing the process.
|
|
||||||
With fish_features=no-query-term in the startup environment, the same
|
|
||||||
sequence has zero effect (~35ms echo throughout).
|
|
||||||
|
|
||||||
Phases:
|
|
||||||
A. terminal answers all queries -> echo ~35ms (both variants)
|
|
||||||
B. replies dropped for one command -> queryterm: echo dead, permanently
|
|
||||||
C. replies restored, another command -> queryterm: still dead
|
|
||||||
"""
|
|
||||||
import os, pty, re, select, subprocess, sys, time, fcntl, termios
|
|
||||||
|
|
||||||
VARIANT = sys.argv[1] if len(sys.argv) > 1 else "queryterm"
|
|
||||||
|
|
||||||
env = dict(os.environ)
|
|
||||||
env["TERM"] = "xterm-ghostty"
|
|
||||||
env["ZELLIJ"] = "0"
|
|
||||||
env["ZELLIJ_SESSION_NAME"] = "repro"
|
|
||||||
env["FISH_DEBUG"] = "term-support"
|
|
||||||
env["FISH_DEBUG_OUTPUT"] = f"/tmp/fish-lagrepro-{VARIANT}.log"
|
|
||||||
env.pop("fish_features", None)
|
|
||||||
if VARIANT == "noqueryterm":
|
|
||||||
env["fish_features"] = "no-query-term"
|
|
||||||
|
|
||||||
master, slave = pty.openpty()
|
|
||||||
# give it a size
|
|
||||||
fcntl.ioctl(master, termios.TIOCSWINSZ, b"\x00\x28\x00\x78\x00\x00\x00\x00")
|
|
||||||
proc = subprocess.Popen(
|
|
||||||
["fish", "-i", "--no-config"],
|
|
||||||
stdin=slave, stdout=slave, stderr=slave, env=env,
|
|
||||||
preexec_fn=lambda: (os.setsid(), fcntl.ioctl(0, termios.TIOCSCTTY, 0)),
|
|
||||||
close_fds=True,
|
|
||||||
)
|
|
||||||
os.close(slave)
|
|
||||||
|
|
||||||
RESPOND = True
|
|
||||||
transcript = []
|
|
||||||
|
|
||||||
def respond(data):
|
|
||||||
"""Answer terminal queries the way a well-behaved terminal would."""
|
|
||||||
out = b""
|
|
||||||
for m in re.finditer(rb"\x1b\[6n", data):
|
|
||||||
out += b"\x1b[40;1R" # CPR
|
|
||||||
for m in re.finditer(rb"\x1b\[0?c", data):
|
|
||||||
out += b"\x1b[?62;22c" # DA1
|
|
||||||
for m in re.finditer(rb"\x1b\[\?u", data):
|
|
||||||
out += b"\x1b[?0u" # kitty flags
|
|
||||||
for m in re.finditer(rb"\x1b\]11;\?", data):
|
|
||||||
out += b"\x1b]11;rgb:2828/2828/2828\x1b\\" # OSC 11
|
|
||||||
for m in re.finditer(rb"\x1b\[>0?q", data):
|
|
||||||
out += b"\x1bP>|ghostty 1.3.1\x1b\\" # XTVERSION
|
|
||||||
for m in re.finditer(rb"\x1bP\+q[0-9a-fA-F;]+\x1b\\", data):
|
|
||||||
out += b"\x1bP0+r\x1b\\" # XTGETTCAP: not found
|
|
||||||
for m in re.finditer(rb"\x1b\[\?(\d+)\$p", data):
|
|
||||||
out += b"\x1b[?%s;2$y" % m.group(1) # DECRQM: reset
|
|
||||||
return out
|
|
||||||
|
|
||||||
def pump(timeout):
|
|
||||||
"""Read fish output for `timeout` seconds, answering queries if RESPOND."""
|
|
||||||
buf = b""
|
|
||||||
end = time.monotonic() + timeout
|
|
||||||
while time.monotonic() < end:
|
|
||||||
r, _, _ = select.select([master], [], [], 0.03)
|
|
||||||
if r:
|
|
||||||
try:
|
|
||||||
data = os.read(master, 65536)
|
|
||||||
except OSError:
|
|
||||||
return buf
|
|
||||||
buf += data
|
|
||||||
transcript.append(data)
|
|
||||||
if RESPOND:
|
|
||||||
reply = respond(data)
|
|
||||||
if reply:
|
|
||||||
os.write(master, reply)
|
|
||||||
return buf
|
|
||||||
|
|
||||||
def send(s):
|
|
||||||
os.write(master, s if isinstance(s, bytes) else s.encode())
|
|
||||||
|
|
||||||
def measure_echo(chars, settle=0.1):
|
|
||||||
"""Send chars one at a time; measure time until each is echoed."""
|
|
||||||
results = []
|
|
||||||
for ch in chars:
|
|
||||||
pump(settle)
|
|
||||||
t0 = time.monotonic()
|
|
||||||
send(ch)
|
|
||||||
deadline = time.monotonic() + 3.0
|
|
||||||
latency = None
|
|
||||||
buf = b""
|
|
||||||
while time.monotonic() < deadline:
|
|
||||||
buf += pump(0.02)
|
|
||||||
if ch.encode() in buf:
|
|
||||||
latency = (time.monotonic() - t0) * 1000
|
|
||||||
break
|
|
||||||
results.append((ch, latency))
|
|
||||||
return results
|
|
||||||
|
|
||||||
print(f"=== variant: {VARIANT} ===")
|
|
||||||
pump(1.2) # startup, queries answered
|
|
||||||
|
|
||||||
send("echo warmup\r")
|
|
||||||
pump(0.8)
|
|
||||||
|
|
||||||
print("phase A: terminal responsive, echo latency per key:")
|
|
||||||
for ch, ms in measure_echo("abcde"):
|
|
||||||
print(f" {ch}: {ms:.0f} ms" if ms else f" {ch}: NO ECHO in 3s")
|
|
||||||
send("\x15") # ctrl-u clear line
|
|
||||||
pump(0.3)
|
|
||||||
|
|
||||||
# Run external command, then STOP answering queries (simulate lost relay)
|
|
||||||
send("sh -c true\r")
|
|
||||||
time.sleep(0.05)
|
|
||||||
RESPOND = False
|
|
||||||
pump(1.0)
|
|
||||||
|
|
||||||
print("phase B: after external command with query replies DROPPED:")
|
|
||||||
for ch, ms in measure_echo("fghij"):
|
|
||||||
print(f" {ch}: {ms:.0f} ms" if ms else f" {ch}: NO ECHO in 3s")
|
|
||||||
send("\x15")
|
|
||||||
pump(0.3)
|
|
||||||
|
|
||||||
# Does it persist across further commands, with responses restored?
|
|
||||||
RESPOND = True
|
|
||||||
send("sh -c true\r")
|
|
||||||
pump(1.0)
|
|
||||||
print("phase C: responses restored, after another external command:")
|
|
||||||
for ch, ms in measure_echo("klmno"):
|
|
||||||
print(f" {ch}: {ms:.0f} ms" if ms else f" {ch}: NO ECHO in 3s")
|
|
||||||
|
|
||||||
send("\x15")
|
|
||||||
pump(0.2)
|
|
||||||
send("exit\r")
|
|
||||||
pump(0.5)
|
|
||||||
try:
|
|
||||||
proc.wait(timeout=3)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
proc.kill()
|
|
||||||
@@ -15,8 +15,6 @@ in
|
|||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
||||||
home.packages = [ pkgs.jj-starship ];
|
|
||||||
|
|
||||||
programs.starship = {
|
programs.starship = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enableFishIntegration = true;
|
enableFishIntegration = true;
|
||||||
@@ -29,7 +27,9 @@ in
|
|||||||
"$git_branch"
|
"$git_branch"
|
||||||
"$git_commit"
|
"$git_commit"
|
||||||
"$git_status"
|
"$git_status"
|
||||||
"\${custom.jj}"
|
"$jj_bookmark"
|
||||||
|
"$jj_status"
|
||||||
|
"$jj_change"
|
||||||
"$hostname"
|
"$hostname"
|
||||||
"$cmd_duration"
|
"$cmd_duration"
|
||||||
"$character"
|
"$character"
|
||||||
@@ -42,21 +42,15 @@ in
|
|||||||
};
|
};
|
||||||
cmd_duration = {
|
cmd_duration = {
|
||||||
min_time = 5001;
|
min_time = 5001;
|
||||||
show_notifications = if pkgs.stdenv.hostPlatform.isLinux then false else true;
|
# Must stay off. On macOS the notification blocks the prompt for ~2s,
|
||||||
min_time_to_notify = 30000;
|
# and fish's $CMD_DURATION keeps its value until the next command
|
||||||
|
# actually runs. So after any long command (every TUI session),
|
||||||
|
# every prompt redraw -- Enter on an empty line, vi mode switches,
|
||||||
|
# repaints -- re-sent the notification and froze the shell for 2s.
|
||||||
|
# That was the "post-TUI typing lag" (see docs/CHANGELOG.md).
|
||||||
|
show_notifications = false;
|
||||||
format = "[$duration]($style) ";
|
format = "[$duration]($style) ";
|
||||||
};
|
};
|
||||||
custom = {
|
|
||||||
jj = {
|
|
||||||
when = "jj-starship detect";
|
|
||||||
shell = [
|
|
||||||
"jj-starship"
|
|
||||||
"--no-jj-prefix"
|
|
||||||
"--no-git-prefix"
|
|
||||||
];
|
|
||||||
format = "$output ";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
directory = {
|
directory = {
|
||||||
truncate_to_repo = true;
|
truncate_to_repo = true;
|
||||||
truncation_length = 100;
|
truncation_length = 100;
|
||||||
|
|||||||
@@ -10,31 +10,13 @@ let
|
|||||||
cfg = config.nmasur.presets.programs.zellij;
|
cfg = config.nmasur.presets.programs.zellij;
|
||||||
|
|
||||||
# fish latches feature flags from its startup ENVIRONMENT before config.fish
|
# fish latches feature flags from its startup ENVIRONMENT before config.fish
|
||||||
# runs, so the `set -gx fish_features no-query-term` in config.fish only
|
# runs, so `set -gx fish_features no-query-term` in config.fish only affects
|
||||||
# protects CHILD fish processes — which is why subshells/exec fish were always
|
# child fish processes. Spawning pane shells with the variable already
|
||||||
# immune to the post-TUI typing lag while zellij-spawned pane shells were not.
|
# exported keeps fish from sending terminal queries (OSC 11, CPR, DA1) that
|
||||||
# With query-term latched on, fish sends OSC 11 + CPR + DA1 queries after
|
# zellij may fail to relay; on fish 4.8.1 a single missed reply was shown to
|
||||||
# every command and waits for replies; if zellij fails to relay even one
|
# degrade that shell's reader for good.
|
||||||
# reply (a race during TUI teardown or heavy output), that fish process's
|
|
||||||
# reader is PERMANENTLY degraded — reproduced deterministically in a PTY
|
|
||||||
# harness on fish 4.8.1 (see docs/CHANGELOG.md 2026-08-29 and
|
|
||||||
# presets/programs/lag-triage/upstream_repro.py). Spawning fish with the
|
|
||||||
# variable already exported makes every pane shell immune.
|
|
||||||
# Flight recorder for the still-unsolved post-TUI typing lag: sampling the
|
|
||||||
# process CURES the lag (a wedged thread gets kicked loose), so the only way
|
|
||||||
# to observe it is a recorder that is already running before the lag starts.
|
|
||||||
# Armed by `touch ~/.local/state/lag-triage/RECORD`; new panes then log
|
|
||||||
# fish's reader/thread internals to ~/.local/state/lag-triage/flight/.
|
|
||||||
# Remove the RECORD file to disable (zero overhead when off).
|
|
||||||
fish-no-query-term = pkgs.writeShellScriptBin "fish-no-query-term" ''
|
fish-no-query-term = pkgs.writeShellScriptBin "fish-no-query-term" ''
|
||||||
export fish_features=no-query-term
|
export fish_features=no-query-term
|
||||||
dir="$HOME/.local/state/lag-triage"
|
|
||||||
if [ -e "$dir/RECORD" ]; then
|
|
||||||
mkdir -p "$dir/flight"
|
|
||||||
find "$dir/flight" -type f -mtime +3 -delete 2>/dev/null
|
|
||||||
export FISH_DEBUG='reader,term-support,proc-termowner,iothread,fd-monitor,topic-monitor'
|
|
||||||
export FISH_DEBUG_OUTPUT="$dir/flight/fish-$(date +%Y%m%d-%H%M%S)-$$.log"
|
|
||||||
fi
|
|
||||||
exec ${lib.getExe pkgs.fish} "$@"
|
exec ${lib.getExe pkgs.fish} "$@"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
@@ -178,6 +160,10 @@ in
|
|||||||
|
|
||||||
show_startup_tips = false;
|
show_startup_tips = false;
|
||||||
|
|
||||||
|
# For nested sessions, automatically descend into the session without
|
||||||
|
# fullscreening it.
|
||||||
|
nested_session_handling = "descend";
|
||||||
|
|
||||||
keybinds = {
|
keybinds = {
|
||||||
locked = lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable) {
|
locked = lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable) {
|
||||||
# For servers that I SSH into, use c-a-G to unlock
|
# For servers that I SSH into, use c-a-G to unlock
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ in
|
|||||||
git.enable = lib.mkDefault true;
|
git.enable = lib.mkDefault true;
|
||||||
helix.enable = lib.mkDefault true;
|
helix.enable = lib.mkDefault true;
|
||||||
jujutsu.enable = lib.mkDefault true;
|
jujutsu.enable = lib.mkDefault true;
|
||||||
lag-triage.enable = lib.mkDefault true;
|
|
||||||
lazygit.enable = lib.mkDefault true;
|
lazygit.enable = lib.mkDefault true;
|
||||||
# neovim.enable = lib.mkDefault true;
|
# neovim.enable = lib.mkDefault true;
|
||||||
nix-index.enable = lib.mkDefault true;
|
nix-index.enable = lib.mkDefault true;
|
||||||
|
|||||||
@@ -106,6 +106,12 @@ in
|
|||||||
lidarr = {
|
lidarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
};
|
};
|
||||||
|
shelfmark = {
|
||||||
|
enable = true;
|
||||||
|
environment = {
|
||||||
|
URL_BASE = "/shelfmark";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Allows shared group to read/write the sabnzbd directory
|
# Allows shared group to read/write the sabnzbd directory
|
||||||
@@ -161,6 +167,22 @@ in
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
match = [
|
||||||
|
{
|
||||||
|
host = [ hostnames.download ];
|
||||||
|
path = [ "/shelfmark*" ];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
handle = [
|
||||||
|
{
|
||||||
|
handler = "reverse_proxy";
|
||||||
|
upstreams = [
|
||||||
|
{ dial = "localhost:${builtins.toString config.services.shelfmark.environment.FLASK_PORT}"; }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
{
|
{
|
||||||
match = [
|
match = [
|
||||||
{
|
{
|
||||||
@@ -203,7 +225,7 @@ in
|
|||||||
upstreams = [
|
upstreams = [
|
||||||
{
|
{
|
||||||
# Bazarr only dynamically sets the port, not the host
|
# Bazarr only dynamically sets the port, not the host
|
||||||
dial = "localhost:${builtins.toString config.services.bazarr.listenPort}";
|
dial = "localhost:${builtins.toString config.services.bazarr.settings.general.port}";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -837,7 +837,7 @@ in
|
|||||||
{
|
{
|
||||||
matcher = {
|
matcher = {
|
||||||
id = "byName";
|
id = "byName";
|
||||||
options = "localhost:${builtins.toString config.services.bazarr.listenPort}";
|
options = "localhost:${builtins.toString config.services.bazarr.settings.general.port}";
|
||||||
};
|
};
|
||||||
properties = [
|
properties = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ in
|
|||||||
"postgresql.target"
|
"postgresql.target"
|
||||||
];
|
];
|
||||||
requires = [
|
requires = [
|
||||||
|
"postgresql.target"
|
||||||
"mathesar-secret.service"
|
"mathesar-secret.service"
|
||||||
"mathesar-postgres-secret.service"
|
"mathesar-postgres-secret.service"
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ in
|
|||||||
hostName = "localhost";
|
hostName = "localhost";
|
||||||
maxUploadSize = "50G";
|
maxUploadSize = "50G";
|
||||||
config = {
|
config = {
|
||||||
adminpassFile = config.secrets.nextcloud.dest;
|
|
||||||
dbtype = "pgsql"; # Enables postgresql
|
dbtype = "pgsql"; # Enables postgresql
|
||||||
};
|
};
|
||||||
settings = {
|
settings = {
|
||||||
@@ -209,8 +208,14 @@ in
|
|||||||
permissions = "0440";
|
permissions = "0440";
|
||||||
};
|
};
|
||||||
systemd.services.nextcloud-secret = {
|
systemd.services.nextcloud-secret = {
|
||||||
requiredBy = [ "nextcloud-setup.service" ];
|
requiredBy = [
|
||||||
before = [ "nextcloud-setup.service" ];
|
"nextcloud-setup.service"
|
||||||
|
"prometheus-nextcloud-exporter.service"
|
||||||
|
];
|
||||||
|
before = [
|
||||||
|
"nextcloud-setup.service"
|
||||||
|
"prometheus-nextcloud-exporter.service"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
secrets.nextcloud-oidc-secret = {
|
secrets.nextcloud-oidc-secret = {
|
||||||
@@ -249,9 +254,9 @@ in
|
|||||||
networking.hosts."127.0.0.1" = [ hostnames.content ];
|
networking.hosts."127.0.0.1" = [ hostnames.content ];
|
||||||
services.prometheus.exporters.nextcloud = {
|
services.prometheus.exporters.nextcloud = {
|
||||||
enable = true;
|
enable = true;
|
||||||
username = config.services.nextcloud.config.adminuser;
|
username = "root";
|
||||||
url = "https://${hostnames.content}";
|
url = "https://${hostnames.content}";
|
||||||
passwordFile = config.services.nextcloud.config.adminpassFile;
|
passwordFile = config.secrets.nextcloud.dest;
|
||||||
};
|
};
|
||||||
nmasur.presets.services.prometheus-exporters.scrapeTargets = [
|
nmasur.presets.services.prometheus-exporters.scrapeTargets = [
|
||||||
"127.0.0.1:${builtins.toString config.services.prometheus.exporters.nextcloud.port}"
|
"127.0.0.1:${builtins.toString config.services.prometheus.exporters.nextcloud.port}"
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ in
|
|||||||
after = [
|
after = [
|
||||||
"postgresql.target"
|
"postgresql.target"
|
||||||
];
|
];
|
||||||
|
requires = [
|
||||||
|
"postgresql.target"
|
||||||
|
];
|
||||||
# requires = [ "pgweb-secret.service" ];
|
# requires = [ "pgweb-secret.service" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
|
|||||||
@@ -45,8 +45,9 @@ in
|
|||||||
if ! systemctl is-active --quiet postgresql.service; then break; fi
|
if ! systemctl is-active --quiet postgresql.service; then break; fi
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
done
|
done
|
||||||
${config.services.postgresql.package}/bin/psql -d postgres -tAc 'ALTER DATABASE template1 REFRESH COLLATION VERSION;' || true
|
for db in $(${config.services.postgresql.package}/bin/psql -d postgres -tAc 'SELECT datname FROM pg_database WHERE datallowconn;'); do
|
||||||
${config.services.postgresql.package}/bin/psql -d postgres -tAc 'ALTER DATABASE postgres REFRESH COLLATION VERSION;' || true
|
${config.services.postgresql.package}/bin/psql -d postgres -tAc "ALTER DATABASE \"$db\" REFRESH COLLATION VERSION;" || true
|
||||||
|
done
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyA5V29j
|
||||||
|
dWc3Rmd2T3hHdkNlSE1nejVKdGY0UkUxM2U4eStQWEk5Z2pLOXlVCkExWWhzMzZZ
|
||||||
|
Yk5uWVY2anc3TTJTNkdVd0pRcmx6bGhkRWtHZlh6Yk93bGMKLT4gc3NoLWVkMjU1
|
||||||
|
MTkgWXlTVU1RIHhzallQU211VzduSkVHWVMzblRDcnY0L1VLMENoWi9RSkhaL0dS
|
||||||
|
a1Q2MjAKM1ZHcENURld0Ry9iUjVGRUd3RHA3Q29oQW8vYlN1SW1pNTdVOE5LSDRu
|
||||||
|
MAotPiBzc2gtZWQyNTUxOSBuanZYNUEgMi9IVU5rWXp3UHVZYWxFWE5mN0lHRlJY
|
||||||
|
V0dNSWh2YVRya1o5US9nSURSZwpXSHFhdTBOVExxSlhsYlRnWTR4Y1ZURWlCK2pG
|
||||||
|
OTdQdFNXUHoraWF5bW1VCi0+IHNzaC1lZDI1NTE5IENxSU9VQSBESmZuM25IYjNx
|
||||||
|
L3BDWmh6ZDFLdDVvMlY2Vzlra0t5WHZMbkUxOTlJMWlnCjZNcmdoZ2o1c1ZkSW02
|
||||||
|
QTR0WTRDcWFIYkZpRzdJMHJ4aWhJYjVFRU9aUDQKLT4gc3NoLWVkMjU1MTkgejFP
|
||||||
|
Y1p3IGVZUnZUM2p3R1ROZk92MVNscHRTMytKd3EvZE5JT0orYmE3U1h0SERyV28K
|
||||||
|
SXNIa0FMTHJ6dE5lVW85Q0N2N2F5QU1EU3crc1diTmM0SXVkQ0xlY3lpYwotLS0g
|
||||||
|
Y1ZxeE8wSkVpRllTT29QZTQ3L25ReGZ6WEMzSnZvSGJQaUNNV2J4ZzNzYwp3Ll3z
|
||||||
|
WxwIP3+BEVkWO52W77uZVfS9it6ecqvcFmEQK0PCzrKsTkm5Xlw3mAp42kXeIg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
inherit (config.nmasur.settings) hostnames;
|
||||||
|
cfg = config.nmasur.presets.services.valheim;
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
options.nmasur.presets.services.valheim = {
|
||||||
|
enable = lib.mkEnableOption "Valheim dedicated server";
|
||||||
|
|
||||||
|
serverName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "Midgard";
|
||||||
|
description = "Server name in server browser.";
|
||||||
|
};
|
||||||
|
|
||||||
|
worldName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "Midgard";
|
||||||
|
description = "World save name.";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 2456;
|
||||||
|
description = "Server port.";
|
||||||
|
};
|
||||||
|
|
||||||
|
crossplay = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable cross-platform play.";
|
||||||
|
};
|
||||||
|
|
||||||
|
openFirewall = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Open UDP ports in firewall.";
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
default = ./valheim-password.age;
|
||||||
|
description = "Age-encrypted secret file containing the server password.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
||||||
|
allowUnfreePackages = [
|
||||||
|
"valheim-server"
|
||||||
|
"steamworks-sdk-redist"
|
||||||
|
];
|
||||||
|
|
||||||
|
secrets.valheim-password = {
|
||||||
|
source = cfg.passwordFile;
|
||||||
|
dest = "${config.secretsDirectory}/valheim-password";
|
||||||
|
owner = "valheim";
|
||||||
|
group = "valheim";
|
||||||
|
permissions = "0400";
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.valheim-password-secret = {
|
||||||
|
requiredBy = [ "valheim.service" ];
|
||||||
|
before = [ "valheim.service" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
services.valheim = {
|
||||||
|
enable = true;
|
||||||
|
serverName = cfg.serverName;
|
||||||
|
worldName = cfg.worldName;
|
||||||
|
port = cfg.port;
|
||||||
|
crossplay = cfg.crossplay;
|
||||||
|
openFirewall = cfg.openFirewall;
|
||||||
|
passwordFile = config.secrets.valheim-password.dest;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Configure Cloudflare DNS (no proxy for UDP game traffic)
|
||||||
|
nmasur.presets.services.cloudflare.noProxyDomains = [ hostnames.valheim ];
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -61,6 +61,7 @@ in
|
|||||||
stalwart.enable = lib.mkDefault true;
|
stalwart.enable = lib.mkDefault true;
|
||||||
thelounge.enable = lib.mkDefault true;
|
thelounge.enable = lib.mkDefault true;
|
||||||
uptime-kuma.enable = lib.mkDefault true;
|
uptime-kuma.enable = lib.mkDefault true;
|
||||||
|
valheim.enable = lib.mkDefault false;
|
||||||
vaultwarden.enable = lib.mkDefault true;
|
vaultwarden.enable = lib.mkDefault true;
|
||||||
victoriametrics.enable = lib.mkDefault true;
|
victoriametrics.enable = lib.mkDefault true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services.valheim;
|
||||||
|
|
||||||
|
startScript = pkgs.writeShellScriptBin "valheim-server-start" ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONFIG_DIR="${cfg.dataDir}/.config/unity3d/IronGate/Valheim"
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
|
||||||
|
${lib.optionalString (cfg.adminList != [ ]) ''
|
||||||
|
cat << 'EOF' > "$CONFIG_DIR/adminlist.txt"
|
||||||
|
// List of Steam IDs for adminlist ONE per line
|
||||||
|
${lib.concatStringsSep "\n" cfg.adminList}
|
||||||
|
EOF
|
||||||
|
''}
|
||||||
|
|
||||||
|
${lib.optionalString (cfg.permittedList != [ ]) ''
|
||||||
|
cat << 'EOF' > "$CONFIG_DIR/permittedlist.txt"
|
||||||
|
// List of Steam IDs for permittedlist ONE per line
|
||||||
|
${lib.concatStringsSep "\n" cfg.permittedList}
|
||||||
|
EOF
|
||||||
|
''}
|
||||||
|
|
||||||
|
${lib.optionalString (cfg.bannedList != [ ]) ''
|
||||||
|
cat << 'EOF' > "$CONFIG_DIR/bannedlist.txt"
|
||||||
|
// List of Steam IDs for bannedlist ONE per line
|
||||||
|
${lib.concatStringsSep "\n" cfg.bannedList}
|
||||||
|
EOF
|
||||||
|
''}
|
||||||
|
|
||||||
|
${
|
||||||
|
if cfg.passwordFile != null then
|
||||||
|
''
|
||||||
|
if [ ! -f "${cfg.passwordFile}" ]; then
|
||||||
|
echo "Valheim password file '${cfg.passwordFile}' not found!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
PASS="$(tr -d '\r\n' < '${cfg.passwordFile}')"
|
||||||
|
''
|
||||||
|
else
|
||||||
|
''
|
||||||
|
PASS="${cfg.password}"
|
||||||
|
''
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ''${#PASS} -lt 5 ]; then
|
||||||
|
echo "Valheim server password must be at least 5 characters long!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ARGS=(
|
||||||
|
"-name" "${cfg.serverName}"
|
||||||
|
"-port" "${toString cfg.port}"
|
||||||
|
"-world" "${cfg.worldName}"
|
||||||
|
"-password" "$PASS"
|
||||||
|
"-public" "${if cfg.public then "1" else "0"}"
|
||||||
|
)
|
||||||
|
|
||||||
|
${lib.optionalString cfg.crossplay ''ARGS+=("-crossplay")''}
|
||||||
|
${lib.optionalString (cfg.saveDir != null) ''ARGS+=("-savedir" "${cfg.saveDir}")''}
|
||||||
|
${lib.concatMapStringsSep "\n" (arg: ''ARGS+=("${arg}")'') cfg.extraArgs}
|
||||||
|
|
||||||
|
exec ${cfg.package}/bin/valheim-server "''${ARGS[@]}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
|
||||||
|
options.services.valheim = {
|
||||||
|
enable = lib.mkEnableOption "Valheim dedicated server";
|
||||||
|
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = lib.types.package;
|
||||||
|
default = pkgs.valheim-server;
|
||||||
|
defaultText = lib.literalExpression "pkgs.valheim-server";
|
||||||
|
description = "The valheim-server package to use.";
|
||||||
|
};
|
||||||
|
|
||||||
|
serverName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "Valheim Server";
|
||||||
|
example = "Some cozy server";
|
||||||
|
description = "The name listed in the server browser.";
|
||||||
|
};
|
||||||
|
|
||||||
|
worldName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "Dedicated";
|
||||||
|
example = "Midgard";
|
||||||
|
description = ''
|
||||||
|
The name of the world file to use, without the extension.
|
||||||
|
If the world does not exist, the server will generate a world from a random seed.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 2456;
|
||||||
|
description = ''
|
||||||
|
The port on which to listen for incoming connections.
|
||||||
|
Ports (port + 1) and (port + 2) are used for Steam queries and routing.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
crossplay = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Whether to enable cross-platform play via PlayFab backend.";
|
||||||
|
};
|
||||||
|
|
||||||
|
public = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = true;
|
||||||
|
description = "Whether to list the server in the public server browser.";
|
||||||
|
};
|
||||||
|
|
||||||
|
openFirewall = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Whether to open UDP ports in the firewall.";
|
||||||
|
};
|
||||||
|
|
||||||
|
password = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
The server password in plain text.
|
||||||
|
Prefer `passwordFile` to avoid storing passwords in the world-readable Nix store.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordFile = lib.mkOption {
|
||||||
|
type = lib.types.nullOr (lib.types.either lib.types.str lib.types.path);
|
||||||
|
default = null;
|
||||||
|
description = "Path to file containing the server password.";
|
||||||
|
};
|
||||||
|
|
||||||
|
dataDir = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
default = "/var/lib/valheim";
|
||||||
|
description = "Directory for valheim server data and state.";
|
||||||
|
};
|
||||||
|
|
||||||
|
saveDir = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.path;
|
||||||
|
default = null;
|
||||||
|
description = "Directory where world saves are placed (-savedir). Defaults to Unity config dir in dataDir.";
|
||||||
|
};
|
||||||
|
|
||||||
|
adminList = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
example = [ "72057602627862526" ];
|
||||||
|
description = "List of Steam IDs to add to adminlist.txt.";
|
||||||
|
};
|
||||||
|
|
||||||
|
permittedList = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
example = [ "72057602627862526" ];
|
||||||
|
description = "List of Steam IDs to add to permittedlist.txt.";
|
||||||
|
};
|
||||||
|
|
||||||
|
bannedList = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
example = [ "72057602627862526" ];
|
||||||
|
description = "List of Steam IDs to add to bannedlist.txt.";
|
||||||
|
};
|
||||||
|
|
||||||
|
bepinexMods = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.package;
|
||||||
|
default = [ ];
|
||||||
|
description = "BepInEx mods to install.";
|
||||||
|
};
|
||||||
|
|
||||||
|
bepinexConfigs = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.path;
|
||||||
|
default = [ ];
|
||||||
|
description = "Config files for BepInEx mods.";
|
||||||
|
};
|
||||||
|
|
||||||
|
extraArgs = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = "Extra command line arguments to pass to valheim-server.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
||||||
|
users.users.valheim = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "valheim";
|
||||||
|
home = cfg.dataDir;
|
||||||
|
createHome = true;
|
||||||
|
};
|
||||||
|
users.groups.valheim = { };
|
||||||
|
|
||||||
|
systemd.services.valheim = {
|
||||||
|
description = "Valheim dedicated server";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
after = [ "network.target" ];
|
||||||
|
path = with pkgs; [ coreutils ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "exec";
|
||||||
|
User = "valheim";
|
||||||
|
Group = "valheim";
|
||||||
|
StateDirectory = "valheim";
|
||||||
|
WorkingDirectory = cfg.dataDir;
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "10s";
|
||||||
|
ExecStart = "${startScript}/bin/valheim-server-start";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall = lib.mkIf cfg.openFirewall {
|
||||||
|
allowedUDPPorts = [
|
||||||
|
cfg.port
|
||||||
|
(cfg.port + 1)
|
||||||
|
(cfg.port + 2)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.serverName != "";
|
||||||
|
message = "services.valheim.serverName must not be empty.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.worldName != "";
|
||||||
|
message = "services.valheim.worldName must not be empty.";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = (cfg.password != null && cfg.password != "") || cfg.passwordFile != null;
|
||||||
|
message = "services.valheim requires either services.valheim.password or services.valheim.passwordFile to be set.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
Reference in new issue
Block a user