From 54652d14cbcb8dce08c917ebf590f3d67731cad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lkecan=20Bozdo=C4=9Fan?= Date: Sat, 14 Mar 2026 12:01:15 +0000 Subject: [PATCH 01/11] hm.rmpc: add `ShowHelp` key binding --- nix/users/ilkecan/multimedia/audio/rmpc.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/users/ilkecan/multimedia/audio/rmpc.nix b/nix/users/ilkecan/multimedia/audio/rmpc.nix index 1820fbd..c1b09be 100644 --- a/nix/users/ilkecan/multimedia/audio/rmpc.nix +++ b/nix/users/ilkecan/multimedia/audio/rmpc.nix @@ -41,6 +41,7 @@ ), keybinds: ( global: { + "?": ShowHelp, ":": CommandMode, ",": VolumeDown, "s": Stop, From 8ed7f4d051f7c7b5f3c800304739319029569dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lkecan=20Bozdo=C4=9Fan?= Date: Sat, 14 Mar 2026 12:02:12 +0000 Subject: [PATCH 02/11] hm.claude-code: remove `starship-claude` ... in favor of `ccstatusline`. --- .../dotfiles/.local/bin/starship-claude | 348 ------------------ nix/users/ilkecan/llm/claude-code.nix | 1 - 2 files changed, 349 deletions(-) delete mode 100755 nix/users/ilkecan/dotfiles/.local/bin/starship-claude diff --git a/nix/users/ilkecan/dotfiles/.local/bin/starship-claude b/nix/users/ilkecan/dotfiles/.local/bin/starship-claude deleted file mode 100755 index 7b16205..0000000 --- a/nix/users/ilkecan/dotfiles/.local/bin/starship-claude +++ /dev/null @@ -1,348 +0,0 @@ -#!/usr/bin/env bash -# -# starship-claude - Use Starship for your Claude Code status line -# -# This script parses the claude code status line data to provide -# environment variables you can use in a standard Starship prompt. -# -# Formatted values (for display): -# - `CLAUDE_MODEL` - Short model name with icon (haiku/sonnet/opus) -# - `CLAUDE_MODEL_NERD` - Model with version (e.g., "󱚦 opus 4.5") -# - `CLAUDE_CONTEXT` - Context window usage percentage (left-padded, e.g., "15%") -# - `CLAUDE_COST` - Formatted session cost (e.g., "$0.71") -# - `CLAUDE_SUMMARY` - Session summary extracted from transcript -# - `CLAUDE_STAR` - Star character for prompt decoration -# -# Raw values (direct from JSON): -# - `CLAUDE_SESSION_ID` - Session UUID -# - `CLAUDE_TRANSCRIPT_PATH` - Path to session transcript JSONL -# - `CLAUDE_CWD` - Current working directory from session -# - `CLAUDE_WORKSPACE_CURRENT_DIR` - workspace.current_dir -# - `CLAUDE_WORKSPACE_PROJECT_DIR` - workspace.project_dir -# - `CLAUDE_VERSION` - Claude Code version -# - `CLAUDE_OUTPUT_STYLE` - Output style name -# - `CLAUDE_MODEL_ID` - Full model identifier (e.g., "claude-sonnet-4-5-20250929") -# - `CLAUDE_MODEL_DISPLAY_NAME` - Model display name (e.g., "Sonnet 4.5") -# - `CLAUDE_COST_RAW` - Raw cost in USD (number) -# - `CLAUDE_TOTAL_DURATION_MS` - Total session duration in ms -# - `CLAUDE_API_DURATION_MS` - Total API call duration in ms -# - `CLAUDE_LINES_ADDED` - Total lines added -# - `CLAUDE_LINES_REMOVED` - Total lines removed -# - `CLAUDE_TOTAL_INPUT_TOKENS` - Cumulative input tokens -# - `CLAUDE_TOTAL_OUTPUT_TOKENS` - Cumulative output tokens -# - `CLAUDE_CONTEXT_SIZE` - Context window size (e.g., 200000) -# - `CLAUDE_EXCEEDS_200K` - Whether session exceeds 200k tokens ("true"/"false") -# - `CLAUDE_INPUT_TOKENS` - Current usage input tokens -# - `CLAUDE_OUTPUT_TOKENS` - Current usage output tokens -# - `CLAUDE_CACHE_CREATION` - Cache creation input tokens -# - `CLAUDE_CACHE_READ` - Cache read input tokens -# -# Computed values: -# - `CLAUDE_CURRENT_TOKENS` - Sum of input + cache tokens (for context %) -# - `CLAUDE_PERCENT_RAW` - Raw percentage number (no padding) -# -# Also prints OSC 9;4 ConEmu terminal progress bar for context usage (optional) -# -# Repository: https://github.com/martinemde/starship-claude -# -# Usage: -# Add to ~/.claude/settings.json: -# { -# "statusLine": { -# "type": "command", -# "command": "~/.local/bin/starship-claude" -# } -# } -# -# Options can be added to the above command: -# --config PATH Use custom Starship config file location -# --path PATH Override the path context for starship prompt -# --no-progress Disable terminal context progress bar -# -# MIT License -# Copyright (c) 2026 Martin Emde -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# -set -o errexit -o nounset -o pipefail - -# Configuration: context window progress thresholds -# I use Dex Horthy's dumb zone at 40% context usage -PROGRESS_COMPACT=80 # Percentage at which Claude Code compacts context -PROGRESS_YELLOW=40 # Warning threshold for progress bar -PROGRESS_RED=60 # Error threshold for progress bar - -# Configuration: model display names -# NerdFont icons used below which may not render (esp not on GitHub) -HAIKU=" haiku" -SONNET="󰚩 sonnet" -OPUS="󱚦 opus" - -# The star can help visually differentiate from other prompts. -# FYI: Official UI guidelines indicate #D97757 as Claude Orange -export CLAUDE_STAR="" - -# Parse command line options -show_progress=1 -starship_config="" -starship_path="" -while [ $# -gt 0 ]; do - case "$1" in - --no-progress) - show_progress=0 - shift - ;; - --config) - if [ $# -lt 2 ]; then - echo "Error: --config requires a path argument" >&2 - exit 1 - fi - starship_config="$2" - shift 2 - ;; - --path) - if [ $# -lt 2 ]; then - echo "Error: --path requires a path argument" >&2 - exit 1 - fi - starship_path="$2" - shift 2 - ;; - *) - echo "Unknown option: $1" >&2 - exit 1 - ;; - esac -done - -payload="$(cat || true)" - -if command -v jq >/dev/null 2>&1 && [ -n "$payload" ]; then - # - # Extract all values from JSON in a SINGLE jq call for performance - # Output format: newline-separated values in a known order - # - jq_output="$(printf '%s' "$payload" | jq -r ' - [ - # Raw values (indexes 0-17) - (.transcript_path // ""), # 0 - (.cwd // ""), # 1 - (.workspace.current_dir // ""), # 2 - (.workspace.project_dir // ""), # 3 - (.version // ""), # 4 - (.output_style.name // ""), # 5 - (.model.id // ""), # 6 - (.model.display_name // ""), # 7 - (.cost.total_cost_usd // ""), # 8 - (.cost.total_duration_ms // ""), # 9 - (.cost.total_api_duration_ms // ""), # 10 - (.cost.total_lines_added // ""), # 11 - (.cost.total_lines_removed // ""), # 12 - (.context_window.total_input_tokens // ""), # 13 - (.context_window.total_output_tokens // ""), # 14 - (if .exceeds_200k_tokens == null then "" else .exceeds_200k_tokens | tostring end), # 15 - (.session_id // ""), # 16 - (.context_window.context_window_size // ""), # 17 - # Current usage tokens (indexes 18-21) - empty if current_usage is null - (if .context_window.current_usage == null then "" else (.context_window.current_usage.input_tokens // 0) end), # 18 - (if .context_window.current_usage == null then "" else (.context_window.current_usage.output_tokens // 0) end), # 19 - (if .context_window.current_usage == null then "" else (.context_window.current_usage.cache_creation_input_tokens // 0) end), # 20 - (if .context_window.current_usage == null then "" else (.context_window.current_usage.cache_read_input_tokens // 0) end), # 21 - # Computed values (indexes 22-23) - (.model.display_name // .model.id // ""), # 22 - raw_model - (.workspace.current_dir // .workspace.project_dir // .cwd // "") # 23 - dir - ] | .[] - ')" - - # Parse jq output into an array (one element per line) - # Initialize with defaults to handle partial/failed jq output - values=("" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "") - - # Only parse if jq produced output - if [ -n "$jq_output" ]; then - if command -v mapfile >/dev/null 2>&1; then - mapfile -t values <<<"$jq_output" - else - # Fallback for shells without mapfile (zsh, older bash) - i=0 - while IFS= read -r line; do - values[i]="$line" - i=$((i + 1)) - done <<<"$jq_output" - fi - fi - - # Export raw values (use :- to handle missing array indices) - export CLAUDE_TRANSCRIPT_PATH="${values[0]:-}" - export CLAUDE_CWD="${values[1]:-}" - export CLAUDE_WORKSPACE_CURRENT_DIR="${values[2]:-}" - export CLAUDE_WORKSPACE_PROJECT_DIR="${values[3]:-}" - export CLAUDE_VERSION="${values[4]:-}" - export CLAUDE_OUTPUT_STYLE="${values[5]:-}" - export CLAUDE_MODEL_ID="${values[6]:-}" - export CLAUDE_MODEL_DISPLAY_NAME="${values[7]:-}" - export CLAUDE_COST_RAW="${values[8]:-}" - export CLAUDE_TOTAL_DURATION_MS="${values[9]:-}" - export CLAUDE_API_DURATION_MS="${values[10]:-}" - export CLAUDE_LINES_ADDED="${values[11]:-}" - export CLAUDE_LINES_REMOVED="${values[12]:-}" - export CLAUDE_TOTAL_INPUT_TOKENS="${values[13]:-}" - export CLAUDE_TOTAL_OUTPUT_TOKENS="${values[14]:-}" - export CLAUDE_EXCEEDS_200K="${values[15]:-}" - export CLAUDE_SESSION_ID="${values[16]:-}" - export CLAUDE_CONTEXT_SIZE="${values[17]:-}" - export CLAUDE_INPUT_TOKENS="${values[18]:-}" - export CLAUDE_OUTPUT_TOKENS="${values[19]:-}" - export CLAUDE_CACHE_CREATION="${values[20]:-}" - export CLAUDE_CACHE_READ="${values[21]:-}" - - raw_model="${values[22]:-}" - dir="${values[23]:-}" - - # cd into workspace dir - if [ -n "$dir" ] && [ -d "$dir" ]; then - cd "$dir" - fi - - # - # Short model name: haiku / sonnet / opus (pure bash, no external commands) - # - if [ -n "$raw_model" ]; then - # Lowercase using bash parameter expansion (bash 4+) or tr fallback - if [[ "${BASH_VERSINFO[0]:-0}" -ge 4 ]]; then - lower_model="${raw_model,,}" - else - lower_model="$(printf '%s' "$raw_model" | tr '[:upper:]' '[:lower:]')" - fi - - case "$lower_model" in - *haiku*) short_model="$HAIKU" ;; - *sonnet*) short_model="$SONNET" ;; - *opus*) short_model="$OPUS" ;; - *) short_model="$raw_model" ;; - esac - - export CLAUDE_MODEL="$short_model" - export CLAUDE_MODEL_NERD="$short_model" - - # Extract version number using bash regex (no sed/grep) - if [[ "$raw_model" =~ ([0-9]+)\.([0-9]+) ]]; then - export CLAUDE_MODEL_NAME="$short_model ${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" - else - export CLAUDE_MODEL_NAME="$short_model" - fi - fi - - # - # Cost: format as $X.XX (only if we have a value) - # - raw_cost="${values[8]:-}" - if [ -n "$raw_cost" ]; then - export CLAUDE_COST="$(printf '$%.2f' "$raw_cost")" - fi - - # - # Session summary from transcript JSONL file - # - export CLAUDE_SUMMARY="" - transcript_path="${values[0]:-}" - if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then - summary="$(jq -rs '[.[] | select(.type == "summary")] | last | .summary // empty' "$transcript_path")" - if [ -n "$summary" ]; then - # Sanitize: replace newlines with spaces, collapse whitespace, trim - summary="${summary//$'\n'/ }" # newlines to spaces - summary="${summary// / }" # collapse double spaces (basic) - summary="${summary# }" # trim leading - summary="${summary% }" # trim trailing - export CLAUDE_SUMMARY="$summary" - fi - fi - - # - # Context window usage percentage - # - context_size="${values[17]:-}" - input_tokens="${values[18]:-}" - cache_creation="${values[20]:-}" - cache_read="${values[21]:-}" - - if [ -n "$context_size" ] && [ "$context_size" != "null" ] && [ -n "$input_tokens" ]; then - # Use 0 defaults for arithmetic when values exist but individual fields might be missing - current_tokens=$((${input_tokens:-0} + ${cache_creation:-0} + ${cache_read:-0})) - export CLAUDE_CURRENT_TOKENS="$current_tokens" - - if [ "$current_tokens" -gt 0 ]; then - percent_used=$((current_tokens * 100 / context_size)) - export CLAUDE_PERCENT_RAW="$percent_used" - export CLAUDE_CONTEXT="$(printf '%3s' "${percent_used}%")" - else - export CLAUDE_CONTEXT=" %" - fi - else - export CLAUDE_CONTEXT=" %" - fi -fi - -# Allow overriding starship command for testing -# Set STARSHIP_CMD to use a custom command (e.g., 'env' for testing) -starship_cmd="${STARSHIP_CMD:-starship}" - -# Determine Starship config path -# Priority: --config flag > default location -if [ -z "$starship_config" ]; then - starship_config="$HOME/.claude/starship.toml" -fi - -# Build starship prompt arguments -starship_args="prompt" -if [ -n "$starship_path" ]; then - starship_args="$starship_args --path $starship_path" -fi - -# Force non-zsh-style output so we don't get %{%} markers -STARSHIP_CONFIG="$starship_config" \ - STARSHIP_SHELL=sh \ - $starship_cmd $starship_args - -# Terminal progress bar (OSC 9;4) - sent AFTER starship to avoid render conflicts -# Can be disabled via --no-progress flag -if [ "$show_progress" = "1" ]; then - if [ -n "${percent_used:-}" ]; then - # Scale progress bar: 0-PROGRESS_COMPACT% context maps to 0-100% progress - # Claude compacts at PROGRESS_COMPACT%, so treat that as "full" - if [ "$percent_used" -ge "$PROGRESS_COMPACT" ]; then - progress_percent=100 - else - progress_percent=$((percent_used * 100 / PROGRESS_COMPACT)) - fi - - # OSC 9;4 progress bar: ESC ] 9 ; 4 ; ; BEL - # State based on actual context usage: - # 0-PROGRESS_YELLOW%: normal (state 1), PROGRESS_YELLOW-PROGRESS_RED%: warning (state 4), PROGRESS_RED%+: error (state 2) - if [ "$percent_used" -ge "$PROGRESS_RED" ]; then - progress_state=2 # Error - elif [ "$percent_used" -ge "$PROGRESS_YELLOW" ]; then - progress_state=4 # Warning - else - progress_state=1 # Normal - fi - printf '\033]9;4;%d;%d\a' "$progress_state" "$progress_percent" >/dev/tty 2>/dev/null || true - fi - # Don't clear progress bar when data is missing - just leave it alone -fi diff --git a/nix/users/ilkecan/llm/claude-code.nix b/nix/users/ilkecan/llm/claude-code.nix index bb2fb5f..63e8071 100644 --- a/nix/users/ilkecan/llm/claude-code.nix +++ b/nix/users/ilkecan/llm/claude-code.nix @@ -13,7 +13,6 @@ packages = with pkgs; [ llm-agents.ccstatusline # https://github.com/sirmalloc/ccstatusline llm-agents.ccusage # https://github.com/ryoppippi/ccusage - starship # https://github.com/starship/starship - https://github.com/martinemde/starship-claude ]; }; From 6b5005e2f17149e2173fc7dd595ba39235341901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lkecan=20Bozdo=C4=9Fan?= Date: Sat, 14 Mar 2026 12:01:02 +0000 Subject: [PATCH 03/11] flake: update inputs --- flake.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/flake.lock b/flake.lock index 1cd4393..84095d6 100644 --- a/flake.lock +++ b/flake.lock @@ -448,11 +448,11 @@ ] }, "locked": { - "lastModified": 1773412360, - "narHash": "sha256-d6FY3t40JRc3nNZUu40rhfcMrOlVQeXGjUvzG/l0G8M=", + "lastModified": 1773498847, + "narHash": "sha256-93jTY0A0uOJJRn3qPWbPUaE7J5l0MVhTwuyy2vZzBi8=", "owner": "numtide", "repo": "llm-agents.nix", - "rev": "a638ae24d87ea4bb42f57416c0b3c75d980e810d", + "rev": "de71dddfdd172ab4892ab0b56841e81c45ee8cc7", "type": "github" }, "original": { @@ -468,11 +468,11 @@ ] }, "locked": { - "lastModified": 1773427419, - "narHash": "sha256-WEyUhk7a6qBCTfYMV8BlThht2CyVaJ4B4tR+Cvp6Ks8=", + "lastModified": 1773501540, + "narHash": "sha256-Bz+4nN6+wrV2w4JLBlZEN4wl57E0RIs1VYI6R1kwyVg=", "owner": "natsukium", "repo": "mcp-servers-nix", - "rev": "d7fb706273373b7132174ad5fbcc9f52f0076882", + "rev": "1f4a6d4c5bc1349a0c95f8e4c8c819e6e10c3701", "type": "github" }, "original": { @@ -537,11 +537,11 @@ ] }, "locked": { - "lastModified": 1773433102, - "narHash": "sha256-0q2Uz4oNTX0+dIpN3zV2HLMHI8NOoRDwScatBS1v8ng=", + "lastModified": 1773501701, + "narHash": "sha256-+0LBAEm8F5h9Nm+hdS07aoS1W4oTtW6c8lltb66oOYQ=", "owner": "sodiboo", "repo": "niri-flake", - "rev": "20f866c7416799ebf5b88b07c9d32c6a440e825d", + "rev": "39ac039250a4a32bf8691405cac04864fc66a70d", "type": "github" }, "original": { @@ -689,11 +689,11 @@ ] }, "locked": { - "lastModified": 1773426551, - "narHash": "sha256-xkdf5jU1HDphAFy89WQsyStYdq0EjRsYCYsz5IrDEjo=", + "lastModified": 1773449015, + "narHash": "sha256-7Q4MsuktyGzRC5B+RuTITnZaDT5JqUq19P7wNFL+7ac=", "owner": "kaylorben", "repo": "nixcord", - "rev": "efe6a34e34d5ab0983d26ca290e5e0dba6e1abd1", + "rev": "3e0fc6c4baba8d1a63e11a6d8977a0f92c859801", "type": "github" }, "original": { @@ -762,11 +762,11 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1773282481, - "narHash": "sha256-oFe06TmOy8UUT1f7xMHqDpSYq2Fy1mkIsXZUvdnyfeY=", - "rev": "fe416aaedd397cacb33a610b33d60ff2b431b127", + "lastModified": 1773389992, + "narHash": "sha256-wLdaFm1T0uzQya3eG/5+LPbmyB92jE/AnMtVY6re818=", + "rev": "c06b4ae3d6599a672a6210b7021d699c351eebda", "type": "tarball", - "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre962285.fe416aaedd39/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre962836.c06b4ae3d659/nixexprs.tar.xz" }, "original": { "type": "tarball", @@ -783,11 +783,11 @@ ] }, "locked": { - "lastModified": 1773436970, - "narHash": "sha256-8W7J/EvtAvO4x8X2ebGE/hJns85fT6fHIdKrKf9/EII=", + "lastModified": 1773500884, + "narHash": "sha256-I8lZHLqVYmlq3bviRlVNR+LEy7GME76QT9Bl791j3XI=", "owner": "nix-community", "repo": "NUR", - "rev": "18e764719da8c51f8c69c5ea0f96c02e4e384d16", + "rev": "ab3ef50aa7a2c6ab5d6dd443c1dd9ec9df9bb40a", "type": "github" }, "original": { From 0a464d3c68b0eef943b6973dbdfe5206c7d04b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lkecan=20Bozdo=C4=9Fan?= Date: Sat, 14 Mar 2026 15:37:37 +0000 Subject: [PATCH 04/11] nixos.nix: upgrade to `2.34` --- .github/workflows/ci.yaml | 2 +- nix/hosts/mephistopheles/nix/default.nix | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0b5aa54..fecaa64 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,7 +20,7 @@ jobs: - uses: nixbuild/nix-quick-install-action@v34 with: nix_conf: | - experimental-features = ca-derivations flakes impure-derivations nix-command no-url-literals + experimental-features = ca-derivations flakes impure-derivations nix-command - run: nix run env: diff --git a/nix/hosts/mephistopheles/nix/default.nix b/nix/hosts/mephistopheles/nix/default.nix index f03c70a..0ca1630 100644 --- a/nix/hosts/mephistopheles/nix/default.nix +++ b/nix/hosts/mephistopheles/nix/default.nix @@ -36,7 +36,6 @@ in "flakes" "impure-derivations" "nix-command" - "no-url-literals" ]; trusted-users = [ userConfig.home.username ]; @@ -66,8 +65,10 @@ in http-connections = 128; # default: 25 keep-going = true; keep-outputs = true; + # lint-absolute-path-literals = "warn"; + # lint-short-path-literals = "warn"; + lint-url-literals = "fatal"; max-substitution-jobs = 128; # default: 16 - # warn-short-path-literals = true; }; gc = { From b6506395c161448ce6933e2850e7b1f2545e2d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lkecan=20Bozdo=C4=9Fan?= Date: Sat, 14 Mar 2026 16:45:42 +0000 Subject: [PATCH 05/11] nix/packages: create `patched/` directory --- nix/flake/per-system/apps/ci.nix | 2 +- nix/packages/default.nix | 4 ++-- nix/packages/{ => patched}/nix-fast-build.nix | 4 ++-- nix/users/ilkecan/nix/default.nix | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename nix/packages/{ => patched}/nix-fast-build.nix (72%) diff --git a/nix/flake/per-system/apps/ci.nix b/nix/flake/per-system/apps/ci.nix index 97763b8..953e3cf 100644 --- a/nix/flake/per-system/apps/ci.nix +++ b/nix/flake/per-system/apps/ci.nix @@ -5,7 +5,7 @@ pkgs.writeShellApplication { name = "ci"; - runtimeInputs = with pkgs; [ unstable.nix-fast-build ]; + runtimeInputs = with pkgs; [ patched.nix-fast-build ]; text = '' nix-fast-build --no-nom --skip-cached --cachix-cache ilkecan "$@" diff --git a/nix/packages/default.nix b/nix/packages/default.nix index 1378b9d..20fa42e 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -52,8 +52,8 @@ import inputs.nixpkgs { }) (_final: prev: { - unstable = prev.unstable // { - nix-fast-build = prev.unstable.callPackage ./nix-fast-build.nix { }; + patched = prev.patched // { + nix-fast-build = prev.patched.callPackage ./patched/nix-fast-build.nix { }; }; }) ]; diff --git a/nix/packages/nix-fast-build.nix b/nix/packages/patched/nix-fast-build.nix similarity index 72% rename from nix/packages/nix-fast-build.nix rename to nix/packages/patched/nix-fast-build.nix index 81abb4f..cec5e35 100644 --- a/nix/packages/nix-fast-build.nix +++ b/nix/packages/patched/nix-fast-build.nix @@ -13,8 +13,8 @@ nix-fast-build.overrideAttrs (prev: { patches = (prev.patches or [ ]) ++ [ (fetchpatch2 { name = "support-impure-derivations.patch"; - url = "https://patch-diff.githubusercontent.com/raw/Mic92/nix-fast-build/pull/301.patch"; - hash = "sha256-E2nffS/w3IRgr5r2VB/m5HgZkzDO4Ukn9f8n9gzkI8g="; + url = "https://github.com/Mic92/nix-fast-build/pull/301.diff?full_index=1"; + hash = "sha256-xVN5nOUIDTrCJ6jBeKp/MiYkHULYvj0xgBFqiQ34f+U="; }) ]; }) diff --git a/nix/users/ilkecan/nix/default.nix b/nix/users/ilkecan/nix/default.nix index 2d8548c..aaa3ad8 100644 --- a/nix/users/ilkecan/nix/default.nix +++ b/nix/users/ilkecan/nix/default.nix @@ -34,7 +34,7 @@ in nix-diff # https://github.com/Gabriella439/nix-diff nix-du # https://github.com/symphorien/nix-du unstable.nix-eval-jobs # https://github.com/nix-community/nix-eval-jobs - unstable.nix-fast-build # https://github.com/Mic92/nix-fast-build + patched.nix-fast-build # https://github.com/Mic92/nix-fast-build nix-melt # https://github.com/nix-community/nix-melt nix-output-monitor # https://github.com/maralorn/nix-output-monitor nix-tree # https://github.com/utdemir/nix-tree From 4e99d8d76a81903682bba4fe7e0b4464545069f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lkecan=20Bozdo=C4=9Fan?= Date: Sat, 14 Mar 2026 16:46:42 +0000 Subject: [PATCH 06/11] nix/packages: use `importTree` on `patched/` --- nix/flake/per-system/args/pkgs.nix | 3 ++- nix/packages/default.nix | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/nix/flake/per-system/args/pkgs.nix b/nix/flake/per-system/args/pkgs.nix index 5fa99be..aeb3c7c 100644 --- a/nix/flake/per-system/args/pkgs.nix +++ b/nix/flake/per-system/args/pkgs.nix @@ -1,5 +1,6 @@ { inputs, + lib, self, ... }: @@ -8,6 +9,6 @@ perSystem = { system, ... }: { - _module.args.pkgs = import "${self}/nix/packages" { inherit inputs system; }; + _module.args.pkgs = import "${self}/nix/packages" { inherit inputs lib system; }; }; } diff --git a/nix/packages/default.nix b/nix/packages/default.nix index 20fa42e..9856c8c 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -1,8 +1,18 @@ { inputs, + lib, system, }: +let + inherit (lib) + removeSuffix + ; + + inherit (lib.my) + importTree + ; +in import inputs.nixpkgs { localSystem = { inherit system; }; @@ -52,9 +62,14 @@ import inputs.nixpkgs { }) (_final: prev: { - patched = prev.patched // { - nix-fast-build = prev.patched.callPackage ./patched/nix-fast-build.nix { }; - }; + patched = + prev.patched + // importTree { + root = ./patched; + depth = 1; + importFn = x: prev.patched.callPackage x { }; + normalizeNameFn = removeSuffix ".nix"; + }; }) ]; } From 22b49da9e7c72af00bc03c9a2ead67bc0c9e9eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lkecan=20Bozdo=C4=9Fan?= Date: Sat, 14 Mar 2026 16:57:21 +0000 Subject: [PATCH 07/11] nix/packages: add `patched.automatic-timezoned` --- nix/packages/patched/automatic-timezoned.nix | 48 ++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 nix/packages/patched/automatic-timezoned.nix diff --git a/nix/packages/patched/automatic-timezoned.nix b/nix/packages/patched/automatic-timezoned.nix new file mode 100644 index 0000000..0a1468c --- /dev/null +++ b/nix/packages/patched/automatic-timezoned.nix @@ -0,0 +1,48 @@ +{ + applyPatches, + automatic-timezoned, + fetchpatch2, + lib, +}: + +let + inherit (lib) + mapAttrsToList + ; + + patches = { + "600" = "sha256-b6WJmnasro3f8GRE5XqFfLxKMuTgs1lxWSDH6CP7L7A="; + }; + + mkPatch = + number: hash: + fetchpatch2 { + name = "${number}.patch"; + url = "https://github.com/maxbrunet/automatic-timezoned/pull/${number}.diff?full_index=1"; + inherit hash; + }; +in +automatic-timezoned.overrideAttrs ( + final: prev: { + version = "${prev.version}-unstable-2026-03-11"; + dontVersionCheck = true; + + src = applyPatches { + name = final.pname; + src = prev.src.override { + tag = null; + rev = "2627ec2dcd85bbb640d40c735793e8ca3f99d18a"; + sha256 = "sha256-TH2I/cTSBDQzBvz4OCFxcbKf/aHqtFUsdJ/PQIvF0v0="; + }; + + patches = mapAttrsToList mkPatch patches; + }; + + cargoDeps = prev.cargoDeps.overrideAttrs (prev: { + vendorStaging = prev.vendorStaging.overrideAttrs { + inherit (final) src; + outputHash = "sha256-SKbZTwAcni0060gDypSclOAU0oP3txe8Jkkttyo0Y/Q="; + }; + }); + } +) From 4283c7639b8fdaad30942212ccf0de94903bd19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lkecan=20Bozdo=C4=9Fan?= Date: Sat, 14 Mar 2026 16:57:43 +0000 Subject: [PATCH 08/11] nix/modules/nixos: add `fix-automatic-timezoned` --- nix/modules/nixos/default.nix | 1 + nix/modules/nixos/fix-automatic-timezoned.nix | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 nix/modules/nixos/fix-automatic-timezoned.nix diff --git a/nix/modules/nixos/default.nix b/nix/modules/nixos/default.nix index 28455e6..8e9a28a 100644 --- a/nix/modules/nixos/default.nix +++ b/nix/modules/nixos/default.nix @@ -4,5 +4,6 @@ { imports = [ + ./fix-automatic-timezoned.nix ]; } diff --git a/nix/modules/nixos/fix-automatic-timezoned.nix b/nix/modules/nixos/fix-automatic-timezoned.nix new file mode 100644 index 0000000..429ffda --- /dev/null +++ b/nix/modules/nixos/fix-automatic-timezoned.nix @@ -0,0 +1,10 @@ +{ + pkgs, + ... +}: + +{ + + services.automatic-timezoned.package = pkgs.patched.automatic-timezoned; + systemd.services.automatic-timezoned.serviceConfig.Restart = "always"; +} From 85bbd1e20d9def8fc43c454e2e04d800efc9c549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lkecan=20Bozdo=C4=9Fan?= Date: Sun, 15 Mar 2026 01:55:46 +0300 Subject: [PATCH 09/11] nix/lib: add `flakeInputStorePath` --- nix/lib/my.nix | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/nix/lib/my.nix b/nix/lib/my.nix index b1075dd..72d58bb 100644 --- a/nix/lib/my.nix +++ b/nix/lib/my.nix @@ -5,7 +5,9 @@ let inherit (builtins) baseNameOf + convertHash readDir + hashString ; inherit (lib) @@ -25,6 +27,32 @@ let INFINITY = 1.0e308 * 2; in { + flakeInputStorePath = + { narHash, ... }: + let + narHashHex = convertHash { + hash = narHash; + toHashFormat = "base16"; + }; + + # The fingerprint Nix uses for source (content-addressed) paths + fingerprint = "source:sha256:${narHashHex}:/nix/store:source"; + + fullHashHex = hashString "sha256" fingerprint; + + # Nix truncates to 160 bits (20 bytes = 40 hex chars) + truncatedHex = substring 0 40 fullHashHex; + + # Trick: sha1 is also 160 bits, so convertHash will accept + # our 40-char hex string and produce the correct nix32 encoding + storeHash = convertHash { + hash = truncatedHex; + hashAlgo = "sha1"; + toHashFormat = "nix32"; + }; + in + "/nix/store/${storeHash}-source"; + flattenAttrs = sep: attrs: let From 48a2701d0a69c901cbaf5725e17519f727b76b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lkecan=20Bozdo=C4=9Fan?= Date: Sun, 15 Mar 2026 04:22:10 +0300 Subject: [PATCH 10/11] nix/inputs: replace outPath matching with lock based node resolution Previously, every non-patched flake input was unconditionally rebuilt via `mkFlake`, re-evaluating `flake.outputs` even when none of its transitive dependencies changed. flake.lock encodes the full resolved dependency graph as pure JSON. Reading it lets us compute `nodeDirtiness`, which nodes transitively depend on a patched input, without any input evaluation. Clean inputs are returned as is; only dirty ones go through `mkFlake`. This also replaces the `outPath` based `replacementMapping` with `nodeNameMapping` (lock node name -> canonical resolved input), removing the `outPath` collision constraint on additive patched inputs like `nixpkgs-patched`. --- AGENTS.md | 7 +-- nix/flake/cachix.nix | 4 +- nix/inputs.nix | 130 ++++++++++++++++++++++++++++++------------- nix/lib/my.nix | 5 ++ 4 files changed, 101 insertions(+), 45 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fddaac2..e189c98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,14 +72,13 @@ There are three nixpkgs inputs with distinct purposes: ### Input Patching -`nix/inputs/default.nix` applies upstream PRs to inputs and makes patched versions transparent by overwriting the originals under `inputs.*` (except `nixpkgs-patched`, which is always a separate additive input). Modules consume patched inputs the same way as stock ones. +`nix/inputs.nix` applies upstream PRs to inputs and makes patched versions transparent by overwriting the originals under `inputs.*` (except `nixpkgs-patched`, which is always a separate additive input). Modules consume patched inputs the same way as stock ones. -Transitive flake inputs are rewritten recursively: `replacementMapping` maps each top-level input's `outPath` to its resolved counterpart, so `follows` aliases like `nixpkgs-lib` or `nixpkgs-stable` are transparently replaced with the correct canonical node. +Transitive flake inputs are rewritten selectively using `flake.lock` node names. `nodeDirtiness` is a self-referential lazy map over all lock nodes: a node is dirty if it is directly patched or any of its lock-declared inputs resolves to a dirty node. Only dirty inputs are rebuilt via `mkFlake`; clean inputs are returned as-is, avoiding unnecessary `flake.outputs` re-evaluations. `nodeNameMapping` maps each top-level input's lock node name to its canonical resolved version, ensuring that `follows` aliases pointing to the same node share the same thunk. -Important invariants when editing `nix/inputs/default.nix`: +Important invariants when editing `nix/inputs.nix`: - **Top-level canonical nodes** — if a dependency is shared in multiple places, it should have a canonical representative at the top level and all repeats should follow that node. -- **Additive patched inputs must not be flake inputs** — `nixpkgs-patched` is excluded from `resolvedTopLevel` and `replacementMapping` because it is not declared in `flake.nix`'s inputs. This is by design: its pre-patch source `outPath` would collide with `nixpkgs-unstable` in `replacementMapping`. It enters the final result via the `patchedInputs // resolvedTopLevel` merge and is only consumed directly by self. - **`self` is special-cased** — the recursive rewrite applies to external inputs, but `self` itself is not rebuilt through that recursion. Only `self.inputs` is updated with the rewritten/exported input set to avoid recursive self-reimport. ### Custom pkgs Overlays diff --git a/nix/flake/cachix.nix b/nix/flake/cachix.nix index 6db99e2..f23e0f1 100644 --- a/nix/flake/cachix.nix +++ b/nix/flake/cachix.nix @@ -12,10 +12,8 @@ let ; inherit (lib.my) - storePathName + isPatchedInput ; - - isPatchedInput = x: storePathName x != "source"; in { flake.cachix = { diff --git a/nix/inputs.nix b/nix/inputs.nix index fd80d36..30416a8 100644 --- a/nix/inputs.nix +++ b/nix/inputs.nix @@ -4,11 +4,16 @@ }: let - inherit (builtins) - unsafeDiscardStringContext - ; - inherit (lib) + any + attrNames + attrValues + elem + filterAttrs + foldl' + importJSON + intersectAttrs + isString map mapAttrs mapAttrs' @@ -18,6 +23,8 @@ let inherit (lib.my) importTree + isFlake + isPatchedInput ; # NOTE: unfortunately there is no way to avoid hard-coded `system` yet @@ -83,62 +90,109 @@ let mkFlake { srcPath = src'; inherit sourceInfo; - inputs' = mapAttrs (_: resolveInput) src.inputs; + inputs' = mapAttrs ( + localName: resolveInput (resolveNodeName nodes.${topLevelNodeNames.${input}}.inputs.${localName}) + ) src.inputs; }; - inherit (inputs) self; - inputs' = removeAttrs inputs [ "self" ]; - - patchedInputs = importTree { + # Patch configurations from ./inputs/ that actually apply changes. importFn + # calls patchInput for every spec file; isPatchedInput filters out specs with + # no patches (where applyPatches returns the original src unchanged). + patchedInputs = filterAttrs (_: isPatchedInput) (importTree { root = ./inputs; depth = 1; importFn = x: patchInput (import x); normalizeNameFn = removeSuffix ".nix"; - }; + }); + + # Resolve a lock node reference to its canonical node name. + # String refs are already node names; array refs are follow-paths from root + # (e.g. ["nixpkgs"] resolves root→nixpkgs, ["bar","foo"] resolves root→bar→foo). + resolveNodeName = + inputSpec: + if isString inputSpec then + inputSpec + else + foldl' (nodeName: inputName: resolveNodeName nodes.${nodeName}.inputs.${inputName}) root inputSpec; + + lockFile = importJSON ../flake.lock; + inherit (lockFile) nodes root; - # Use the original realized source path as the replacement identity so - # aliases such as nixpkgs-lib and nixpkgs-stable collapse to one node. - inputKey = x: unsafeDiscardStringContext x.outPath; + rootInputs = nodes.${root}.inputs; + resolveTopLevelNodeName = name: resolveNodeName rootInputs.${name}; - # Rebuild a non-patched flake against resolved child inputs. - # Top-level canonical inputs are handled separately through replacementMapping. + inputs' = removeAttrs inputs [ "self" ]; + + # Node key for each top-level input (resolved via lock, not outPath). + topLevelNodeNames = mapAttrs (name: _: resolveTopLevelNodeName name) inputs'; + + # Shadowed inputs are those whose name also appears in inputs'. Additive + # patches (e.g. nixpkgs-patched) are excluded — they add a name not present + # in inputs', so they have no corresponding lock node to mark dirty. + shadowedInputNames = attrNames (intersectAttrs inputs' patchedInputs); + shadowedNodeNames = map resolveTopLevelNodeName shadowedInputNames; + + # Self-referential lazy map: a lock node is dirty if it is directly patched + # or any of its lock-declared inputs resolves to a dirty node. + # Terminates because flake.lock is a DAG. Pattern validated by Nix's own + # call-flake.nix (allNodes), adapted here with a nodeDirtiness check instead + # of unconditionally rebuilding everything. + nodeDirtiness = mapAttrs ( + name: node: + elem name shadowedNodeNames + || any (inputSpec: nodeDirtiness.${resolveNodeName inputSpec}) (attrValues (node.inputs or { })) + ) nodes; + + # Rebuild a dirty flake against resolved child inputs. Each child's lock node + # name is used to look up its canonical resolved version via resolveInput. resolveFlake = - input: - if input ? inputs then + nodeName: input: + if isFlake input then mkFlake { srcPath = input.outPath; sourceInfo = input.sourceInfo; - inputs' = mapAttrs (_: resolveInput) input.inputs; + inputs' = mapAttrs ( + localName: resolveInput (resolveNodeName nodes.${nodeName}.inputs.${localName}) + ) input.inputs; } else input; - # These are the canonical nodes for every raw top-level input. Any transitive - # dependency that is shared across the graph is expected to follow one of - # these exact top-level nodes. - resolvedTopLevel = mapAttrs (name: input: patchedInputs.${name} or (resolveFlake input)) inputs'; - - # Map each original top-level source identity to its canonical resolved node. - # This only covers raw top-level inputs: additive patched inputs such as - # nixpkgs-patched are intentionally excluded because their pre-patch source - # identity collides with another raw input (for example nixpkgs-unstable), - # and they are only meant to be consumed directly by self. - replacementMapping = mapAttrs' ( - name: input: nameValuePair (inputKey input) (resolvedTopLevel.${name}) + # Canonical resolved version of every raw top-level input: patched inputs use + # their patched version; dirty inputs are rebuilt via resolveFlake; clean + # inputs are returned as-is (no mkFlake, no flake.outputs re-evaluation). + resolvedTopLevel = mapAttrs ( + name: input: + patchedInputs.${name} or ( + let + nodeName = topLevelNodeNames.${name}; + in + if nodeDirtiness.${nodeName} then resolveFlake nodeName input else input + ) ) inputs'; - # Repository invariant: if a dependency is shared in multiple places, it - # should have a canonical representative at the top level and all repeats - # should follow that node. In that common case we reuse the canonical thunk - # through replacementMapping; the fallback only rebuilds non-canonical inputs - # that are not expected to be shared transitively. - resolveInput = input: replacementMapping.${inputKey input} or (resolveFlake input); + # Maps each top-level input's lock node name → its canonical resolved version. + # Transitive followers share the same lock node name, so they get the same thunk. + nodeNameMapping = mapAttrs' ( + name: _: nameValuePair topLevelNodeNames.${name} resolvedTopLevel.${name} + ) inputs'; - # to include additive patched inputs like `nixpkgs-patched` + # Resolve any input by its lock node name to its canonical version. + # Top-level canonical nodes are served from nodeNameMapping; transitive + # non-canonical nodes fall back to resolveFlake if dirty, or are returned + # as-is if clean. + resolveInput = + nodeName: input: + nodeNameMapping.${nodeName} + or (if nodeDirtiness.${nodeName} then resolveFlake nodeName input else input); + + # Merges additive patched inputs (e.g. nixpkgs-patched) with the resolved + # top-level inputs. Additive inputs are not present in inputs' so they would + # otherwise be dropped; this merge ensures they are exported alongside the rest. resolvedInputs = patchedInputs // resolvedTopLevel; in { - self = self // { + self = inputs.self // { inputs = resolvedInputs; }; } diff --git a/nix/lib/my.nix b/nix/lib/my.nix index 72d58bb..be0e1cb 100644 --- a/nix/lib/my.nix +++ b/nix/lib/my.nix @@ -22,6 +22,7 @@ let inherit (lib.my) importTree mkAbsolute + storePathName ; INFINITY = 1.0e308 * 2; @@ -103,6 +104,10 @@ in in if depth <= 0 then importFile root else importDir root; + isFlake = x: x._type or null == "flake"; + + isPatchedInput = x: storePathName x != "source"; + mkAbsolute = root: path: if isPath path then From 627dc8fd9ca130dce317dc5a853d1fa0c4948c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lkecan=20Bozdo=C4=9Fan?= Date: Sun, 15 Mar 2026 04:57:23 +0300 Subject: [PATCH 11/11] gh/workflows/ci: configure `nix.conf` more --- .github/workflows/ci.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fecaa64..b6ce63f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,6 +21,11 @@ jobs: with: nix_conf: | experimental-features = ca-derivations flakes impure-derivations nix-command + extra-substituters = https://ilkecan.cachix.org?priority=41 https://nix-community.cachix.org?priority=42 + extra-trusted-public-keys = ilkecan.cachix.org-1:hXb7Vo9EzaXiEb0elvG6Tt5TrP3zrcadyoX8c+lbeCY= nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= + http-connections = 128 + lint-url-literals = fatal + max-substitution-jobs = 128 - run: nix run env: