$$ \usepackage{amssymb} \newcommand{\N}{\mathbb{N}} \newcommand{\C}{\mathbb{C}} \newcommand{\R}{\mathbb{R}} \newcommand{\Z}{\mathbb{Z}} \newcommand{\ZZ}{\ooalign{Z\cr\hidewidth\kern0.1em\raisebox{-0.5ex}{Z}\hidewidth\cr}} \newcommand{\colim}{\text{colim}} \newcommand{\weaktopo}{\tau_\text{weak}} \newcommand{\strongtopo}{\tau_\text{strong}} \newcommand{\normtopo}{\tau_\text{norm}} \newcommand{\green}[1]{\textcolor{ForestGreen}{#1}} \newcommand{\red}[1]{\textcolor{red}{#1}} \newcommand{\blue}[1]{\textcolor{blue}{#1}} \newcommand{\orange}[1]{\textcolor{orange}{#1}} \newcommand{\tr}{\text{tr}} \newcommand{\id}{\text{id}} \newcommand{\im}{\text{im}\>} \newcommand{\res}{\text{res}} \newcommand{\TopTwo}{\underline{\text{Top}^{(2)}}} \newcommand{\CW}[1]{\underline{#1\text{-CW}}} \newcommand{\ZZ}{% \ooalign{Z\cr\hidewidth\raisebox{-0.5ex}{Z}\hidewidth\cr}% } % specific for this document \newcommand{\cellOne}{\textcolor{green}{1}} \newcommand{\cellTwo}{\textcolor{red}{2}} \newcommand{\cellThree}{\textcolor{brown}{3}} \newcommand{\cellFour}{\textcolor{YellowOrange}{4}} $$

Applying topological data analysis to 4 years of NixOS usage

NixOS
Linux
Haskell
Git
DevOps
Reproducibility
English
Author

Luca Leon Happel

Published

March 5, 2026

Abstract

A few weeks ago all my laptops stopped working. Just weeks before my masters thesis deadline! NixOS helped my in this situation a lot, as it allowed me to clone the state of my old machine onto a different temporary laptop, continuing my work and finishing my masters just in time. This led me to analyze my NixOS usage over time

What is NixOS

NixOS is a Linux distribution built entirely around the Nix package manager. Unlike traditional distros where system state accumulates through years of apt install and config edits, NixOS describes the entire system (packages, services, users, kernel parameters, dotfiles) in a single declarative configuration file checked into version control. From that one file, any machine can be reproduced exactly, bit for bit.

There are a lot of caveats in the above paragraph though: One can still accumulate state by saving files or installing programs outside of the nix-managed system. For the sake of simplicity, we will assume a setup similar to impermanence managed with Manager Manual though, where these problems are mitigated (more or less?).

Why I Chose NixOS

Note

You may skip this section if you are only interested in the git analysis and visualisation part.

My Linux journey started with Ubuntu back in 2010. It worked, but package management was a constant pita. Ubuntu frequently broke things trying to install software that wasn’t in the official repos.

So I switched to using Manjaro with i3wm in 2014 (I still have videos of using it back then). Getting i3-gaps running on Ubuntu before that was was an even worse experience: PPAs, manual running make and make installed, …. Lol, good luck reproducing any of that later on. You’d litterally forget what you have done 5 minutes before, so why even bother trying to update your system afterwards or something?

By early 2022 I had enough. Juggling multiple projects with different GHC versions, all stored all around my system in the most myserious locations possible. cabal and stack handle dependency for some projects, other times I just use a bare GHC for Haskell. Everything was stiched together with glue and some toothpicks. Tbh, it was a miracle that anything worked at all. HLS was mostly non-working if I remember correctly. I still remember that some cabal files were needed to get HLS working even in non-cabal projects? Idk, but it was just bad. So I switched to NixOS after solving many of these problems with the Nix package manager.

The hardware for this experiment was my trusty old ThinkPad X230T: A machine I’d assume is more likely to survive a nuclear apocalypse than a cockroach. And more importantly, it was capable of taking the beating I gave it constantly:

My ThinkPad X230T

My ThinkPad X230T

My ThinkPad X230T

My ThinkPad X230T

My ThinkPad X230T

My ThinkPad X230T

My ThinkPad X230T

My ThinkPad X230T

Starting February 28th, 2022, I had NixOS running on my ThinkPad and promptly spent more of my time with system administration than any reasonable person would. I dove into the more arcane corners of UEFI and systemd, patched and compiled custom kernels, and at one point wrote my own kernel module just to get the touchscreen working. I also, for reasons that still aren’t entirely clear to me, built a monadic X11 window manager and status bar in Haskell. None of this was strictly necessary, but it was very educational, and I enjoyed every bit of it.

Let’s look at what four years of that actually looks like in version control:

Analyzing My NixOS Configuration

This is the commit graph of my NixOS configuration together with a file hirarchy of all files ever checked into the repository:

Show the code
import os
import itertools
import functools
from datetime import datetime
import git
import plotly.graph_objects as go

REPO_URL = "https://github.com/quoteme/nixos.git"
REPO_DIR = "/tmp/quoteme-nixos"
repo : git.Repo = (
    git.Repo(REPO_DIR)
    if os.path.exists(REPO_DIR)
    else git.Repo.clone_from(REPO_URL, REPO_DIR, bare=True)
)
repo.remotes.origin.fetch("+refs/heads/*:refs/heads/*")
commits = list(repo.iter_commits('--all'))
# summary of the total number of commits
print(f"Total commits: {len(commits)}")
print(f"First commit: {datetime.fromtimestamp(commits[-1].committed_date)}")
print(f"Last commit: {datetime.fromtimestamp(commits[0].committed_date)}")
print(f"Avg. time between commits: {(commits[0].committed_date - commits[-1].committed_date) / len(commits) / 3600:.2f} hours")
Total commits: 749
First commit: 2022-02-28 23:33:24
Last commit: 2026-06-11 07:45:18
Avg. time between commits: 50.09 hours
Show the code
import json
import plotly.io as pio
from datetime import datetime
from pathlib import PurePosixPath
from typing import Optional
from IPython.display import HTML

# Type aliases
Sha        = str
LaneMap    = dict[Sha, int]
LaneState  = tuple[LaneMap, int]
Coord      = Optional[float]          # None used as segment separator in edge lists
EdgePoint  = tuple[Coord, Coord]
NodeId     = str                       # unique path string, e.g. "home/luca/foo.nix"
FilePath   = str

def assign_lane(acc: LaneState, c: git.Commit) -> LaneState:
    lanes, n = acc
    my_lane: int  = lanes.get(c.hexsha, n)
    base: LaneMap = {**lanes, c.hexsha: my_lane}
    def fold_parent(acc2: LaneState, ip: tuple[int, git.Commit]) -> LaneState:
        m, k = acc2; i, p = ip
        return (m, k) if p.hexsha in m \
            else ({**m, p.hexsha: my_lane if i == 0 else k}, k + int(i != 0))
    return functools.reduce(fold_parent, enumerate(c.parents),
                            (base, n + int(c.hexsha not in lanes)))

lane_map: LaneMap
lane_map, _ = functools.reduce(assign_lane, reversed(commits), ({}, 0))

def build_commit_branches(refs: list[git.Reference]) -> dict[Sha, list[str]]:
    def fold(d: dict[Sha, list[str]], ref: git.Reference) -> dict[Sha, list[str]]:
        return {**d, ref.commit.hexsha: d.get(ref.commit.hexsha, []) + [ref.name]}
    return functools.reduce(fold, refs, {})

commit_branches: dict[Sha, list[str]] = build_commit_branches(list(repo.refs))

def hover_label(c: git.Commit) -> str:
    body:     str       = c.message.split("\n", 1)[1].strip() if "\n" in c.message else ""
    branches: list[str] = commit_branches.get(c.hexsha, [])
    files:    list[str] = list(c.stats.files.keys())
    return "".join([
        f"<b>{c.summary}</b><br>",
        f"<span style='color:#888'>{c.hexsha[:7]} · {c.committed_datetime.strftime('%Y-%m-%d %H:%M:%S %Z')}</span>",
        (f"<br><i>🌿 {', '.join(branches)}</i>"                               if branches        else ""),
        (f"<br>{body[:300].replace(chr(10), '<br>')}"                         if body            else ""),
        (f"<br><span style='color:#aaa'>{'<br>'.join(files[:20])}</span>"     if files           else ""),
        (f"<br><span style='color:#aaa'>…+{len(files)-20} more</span>"        if len(files) > 20 else ""),
    ])

# ── Commit scatter ───────────────────────────────────────────────────────────
hash_pos: dict[Sha, tuple[datetime, int]] = {
    c.hexsha: (c.committed_datetime, lane_map.get(c.hexsha, 0)) for c in commits
}
edge_segs: list[EdgePoint] = [
    pt
    for c in commits for p in c.parents if p.hexsha in hash_pos
    for (cx, cy), (px, py) in [(hash_pos[c.hexsha], hash_pos[p.hexsha])]
    for pt in [(cx, cy), (px, cy), (px, py), (None, None)]
]
edge_x: list[Coord]
edge_y: list[Coord]
edge_x, edge_y = map(list, zip(*edge_segs)) if edge_segs else ([], [])

dates:  list[datetime] = [c.committed_datetime      for c in commits]
lanes:  list[int]      = [lane_map.get(c.hexsha, 0) for c in commits]
hover:  list[str]      = [hover_label(c)            for c in commits]
shas:   list[str]      = [c.hexsha                  for c in commits]

fig_commits = go.Figure([
    go.Scatter(x=edge_x, y=edge_y, mode="lines",
               line=dict(width=1.5), hoverinfo="none", showlegend=False),
    go.Scatter(x=dates, y=lanes, mode="markers",
               marker=dict(size=9, color=lanes, colorscale="Turbo", line=dict(width=0)),
               text=hover, hovertemplate="%{text}<extra></extra>",
               customdata=shas, showlegend=False),
]).update_layout(
    title=dict(text="Git history · quoteme/nixos", font=dict(size=16)),
    xaxis=dict(title="Date", showgrid=True), yaxis=dict(visible=False),
    height=520, hovermode="closest",
    plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)",
    margin=dict(l=20, r=20, t=50, b=40),
)

# ── File tree ────────────────────────────────────────────────────────────────
all_files: list[FilePath] = sorted({f for c in commits for f in c.stats.files})

def path_nodes(path: FilePath) -> list[tuple[NodeId, NodeId]]:
    """Return (parent_id, child_id) pairs for every prefix of `path`, root first."""
    parts: tuple[str, ...] = PurePosixPath(path).parts
    return [("", ".")] + [
        ("/".join(parts[:i]) or ".", "/".join(parts[:i+1]))
        for i in range(len(parts))
    ]

seen_edges: set[tuple[NodeId, NodeId]] = set()
raw_edges:  list[tuple[NodeId, NodeId]] = []
all_nodes:  list[NodeId] = ["."]
for path in all_files:
    for par, child in path_nodes(path):
        if child not in all_nodes: all_nodes.append(child)
        if par and (par, child) not in seen_edges:
            seen_edges.add((par, child)); raw_edges.append((par, child))

children_of: dict[NodeId, list[NodeId]] = functools.reduce(
    lambda d, e: {**d, e[0]: d.get(e[0], []) + [e[1]]},
    raw_edges, {}
)

# Reingold–Tilford x-placement (mutates x_pos via a single-element counter cell)
x_pos:   dict[NodeId, float] = {}
_ctr:    list[int]            = [0]   # mutable cell; avoids nonlocal in a nested def

def assign_x(node: NodeId) -> None:
    kids: list[NodeId] = children_of.get(node, [])
    if not kids:
        x_pos[node] = float(_ctr[0]); _ctr[0] += 1
    else:
        for k in kids: assign_x(k)
        x_pos[node] = sum(x_pos[k] for k in kids) / len(kids)

depth: dict[NodeId, int] = {}

def assign_depth(node: NodeId, d: int = 0) -> None:
    depth[node] = d
    for k in children_of.get(node, []): assign_depth(k, d + 1)

assign_x(".")
assign_depth(".")

ACTIVE: str = "#4C78A8"
MUTED:  str = "#dedede"

node_x:      list[float] = [x_pos.get(n, 0.0)    for n in all_nodes]
node_y:      list[float] = [float(-depth.get(n, 0)) for n in all_nodes]
node_labels: list[str]   = [PurePosixPath(n).name or "." for n in all_nodes]
node_colors: list[str]   = [ACTIVE] * len(all_nodes)

edge_nx: list[Coord] = [pt for p, c in raw_edges for pt in [x_pos.get(p, 0.0), x_pos.get(c, 0.0), None]]
edge_ny: list[Coord] = [pt for p, c in raw_edges for pt in [-float(depth.get(p, 0)), -float(depth.get(c, 0)), None]]

fig_tree = go.Figure([
    go.Scatter(x=edge_nx, y=edge_ny, mode="lines",
               line=dict(width=0.6, color="#ccc"), hoverinfo="none", showlegend=False),
    go.Scatter(x=node_x, y=node_y, mode="markers+text",
               marker=dict(size=6, color=node_colors),
               text=node_labels, textposition="top center",
               textfont=dict(size=8), customdata=all_nodes,
               hovertemplate="<b>%{customdata}</b><extra></extra>",
               showlegend=False),
]).update_layout(
    title=dict(text="File tree · quoteme/nixos", font=dict(size=16)),
    xaxis=dict(visible=False), yaxis=dict(visible=False),
    height=700, hovermode="closest",
    plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)",
    margin=dict(l=10, r=10, t=50, b=10),
)

commit_files: list[list[FilePath]] = [list(c.stats.files.keys()) for c in commits]

# Precompute the actual Turbo hex colours for every commit so JS can restore
# them without having to know anything about Plotly's internal colorscale maths.
import matplotlib.cm as cm
_max_lane = max(lanes) if max(lanes) > 0 else 1
commit_colors: list[str] = [
    "#{:02x}{:02x}{:02x}".format(*[int(v * 255) for v in cm.turbo(l / _max_lane)[:3]])
    for l in lanes
]

h1_html: str = pio.to_html(fig_commits, full_html=False, include_plotlyjs="cdn",  div_id="git-graph")
h2_html: str = pio.to_html(fig_tree,    full_html=False, include_plotlyjs=False, div_id="file-tree")

js: str = f"""
<style>
  #git-graph, #file-tree {{ max-width: 80%; margin-left: auto; margin-right: auto; }}
</style>
<div style="display:flex;gap:2rem;align-items:center;margin:0.5rem 0 0.25rem;font-size:0.85rem;">
  <label style="display:flex;align-items:center;gap:0.5rem;">
    Active opacity
    <input id="opacity-active" type="range" min="0" max="100" value="100" style="width:110px">
    <span id="opacity-active-val">100%</span>
  </label>
  <label style="display:flex;align-items:center;gap:0.5rem;">
    Muted opacity
    <input id="opacity-muted" type="range" min="0" max="100" value="3" style="width:110px">
    <span id="opacity-muted-val">3%</span>
  </label>
</div>
<script>
(function poll() {{
  const gitEl  = document.getElementById("git-graph");
  const treeEl = document.getElementById("file-tree");
  if (!gitEl  || !gitEl.on ||
      !treeEl || !treeEl.on) {{ setTimeout(poll, 50); return; }}

  const commitFiles  = {json.dumps(commit_files)};
  const nodeIds      = {json.dumps(all_nodes)};
  const commitColors = {json.dumps(commit_colors)};
  const ACTIVE = "{ACTIVE}", MUTED = "{MUTED}", RED = "#e45756";
  const allTreeActive = nodeIds.map(() => ACTIVE);
  const noLines       = commitColors.map(() => 0);

  const sliderActive  = document.getElementById("opacity-active");
  const sliderMuted   = document.getElementById("opacity-muted");
  const labelActive   = document.getElementById("opacity-active-val");
  const labelMuted    = document.getElementById("opacity-muted-val");

  const getActive = () => parseInt(sliderActive.value) / 100;
  const getMuted  = () => parseInt(sliderMuted.value)  / 100;

  sliderActive.addEventListener("input", () => {{
    labelActive.textContent = sliderActive.value + "%";
    if (currentNodeId !== null) applyFileTreeHover(currentNodeId);
  }});
  sliderMuted.addEventListener("input", () => {{
    labelMuted.textContent = sliderMuted.value + "%";
    if (currentNodeId !== null) applyFileTreeHover(currentNodeId);
  }});

  // ── commit graph hover → hovered commit + touched files turn red ──────────
  let lastHoveredSha = null;
  gitEl.on("plotly_hover", ev => {{
    const pt = ev.points[0];
    if (pt.curveNumber !== 1) return;
    gitEl.style.cursor = "pointer";
    lastHoveredSha = pt.customdata;
    const idx      = pt.pointIndex;
    const modified = commitFiles[idx] || [];

    // hovered dot → red; all others keep their colour
    const dotColors = commitColors.map((c, i) => i === idx ? RED : c);
    Plotly.restyle("git-graph", {{
      "marker.color":      [dotColors],
      "marker.opacity":    [commitColors.map(() => 1)],
      "marker.line.width": [noLines],
    }}, [1]);

    // touched file nodes → red; others → MUTED
    const s = new Set(modified);
    const treeColors = nodeIds.map(id =>
      id === "." || s.has(id) || modified.some(f => f.startsWith(id + "/"))
        ? RED : MUTED
    );
    Plotly.restyle("file-tree", {{"marker.color": [treeColors]}}, [1]);
  }});

  gitEl.on("plotly_unhover", () => {{
    gitEl.style.cursor = "";
    Plotly.restyle("git-graph", {{
      "marker.color":      [commitColors],
      "marker.opacity":    [commitColors.map(() => getActive())],
      "marker.line.width": [noLines],
    }}, [1]);
    Plotly.restyle("file-tree", {{"marker.color": [allTreeActive]}}, [1]);
  }});

  // ── commit double-click → open GitHub commit page ──────────────────────────
  gitEl.on("plotly_doubleclick", ev => {{
    // plotly_doubleclick fires without point data; use the last hovered point
    if (lastHoveredSha) {{
      window.open("https://github.com/quoteme/nixos/commit/" + lastHoveredSha, "_blank");
    }}
  }});

  // ── file-tree hover → hovered node + matching commits turn red ────────────
  let currentNodeId = null;

  function applyFileTreeHover(nodeId) {{
    const opMuted = getMuted();
    const hits = commitFiles.map(files =>
      nodeId === "." || files.some(f => f === nodeId || f.startsWith(nodeId + "/"))
    );

    // hit commits → red + full opacity; miss → original colour + muted opacity
    const colors  = hits.map((hit, i) => hit ? RED : commitColors[i]);
    const opacity = hits.map(hit => hit ? 1.0 : opMuted);
    Plotly.restyle("git-graph", {{
      "marker.color":      [colors],
      "marker.opacity":    [opacity],
      "marker.line.width": [noLines],
    }}, [1]);

    // hovered node + its ancestors + its descendants → red; others → MUTED
    const treeColors = nodeIds.map(id =>
      id === nodeId
        || id === "."
        || nodeId.startsWith(id + "/")
        || id.startsWith(nodeId + "/")
          ? RED : MUTED
    );
    Plotly.restyle("file-tree", {{"marker.color": [treeColors]}}, [1]);
  }}

  treeEl.on("plotly_hover", ev => {{
    const pt = ev.points[0];
    if (pt.curveNumber !== 1) return;
    currentNodeId = pt.customdata;
    applyFileTreeHover(currentNodeId);
  }});

  treeEl.on("plotly_unhover", () => {{
    currentNodeId = null;
    Plotly.restyle("git-graph", {{
      "marker.color":      [commitColors],
      "marker.opacity":    [commitColors.map(() => getActive())],
      "marker.line.width": [noLines],
    }}, [1]);
    Plotly.restyle("file-tree", {{"marker.color": [allTreeActive]}}, [1]);
  }});
}})();
</script>"""

HTML(h1_html + h2_html + js)

Hover over any file in the tree to see when it was touched in the commit graph, or hover a commit to see which files it changed. Hovering ./xmonad for example shows a continuous stretch of work that ends on February 4th, 2023, when I removed Xmonad from my config.

Note that modules/hardware/laptops/ shows fewer commits than you’d expect. Hardware config stabilizes fast; window manager and shell stuff lives elsewhere.

Statistical Analysis of Commit History

Linear fit on the cumulative commit count, to get a baseline commit rate:

Show the code
from scipy.stats import linregress
import numpy as np
ts_sorted = np.array(sorted(c.committed_date for c in commits), dtype=float)
t0        = ts_sorted[0]
span_days = (ts_sorted[-1] - t0) / 86400
daily_counts = np.zeros(int(span_days) + 1, dtype=float)
for t in ts_sorted:
    day_idx = int((t - t0) / 86400)
    daily_counts[day_idx] += 1
cumulative_counts = np.cumsum(daily_counts)
days = np.arange(len(cumulative_counts))
slope, intercept, r_value, p_value, std_err = linregress(days,
                                                        cumulative_counts)
print(f"Estimated commits/day: {slope:.2f}")
print(f"R² value: {r_value**2:.4f}")
Estimated commits/day: 0.43
R² value: 0.9526
Show the code
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 5))
plt.plot(days, cumulative_counts, label="Cumulative commits", color="#4C78A8", lw=2)
plt.plot(days, slope * days + intercept, label=f"Fitted line (slope = {slope:.2f} commits/day)", color="#F58518", lw=2, ls="--")
plt.title("Cumulative commit count over time", fontsize=14)
plt.xlabel("Days since first commit")
plt.ylabel("Cumulative commits")
plt.legend()
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("cumulative_commits.png", dpi=150, bbox_inches="tight", transparent
=True)
plt.show()

Topological Data Analysis of the time series of commits

Does the commit activity have any periodic structure? Weekly rhythms, bursts followed by quiet stretches? TDA can answer this without picking a model: embed the time series via a sliding-window (Takens-style) embedding, then compute its persistent homology with ripser. Loops in \(H_1\) mean periodic structure; persistence (death minus birth) means how strong the cycle is.

Show the code
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from ripser import ripser
from persim import plot_diagrams
from sklearn.preprocessing import MinMaxScaler

# ── 1. Build an oscillatory scalar time series ────────────────────────────────
# The previous attempt embedded the raw (monotonically-increasing) commit
# timestamps directly.  A monotone signal unrolls as a *line* in R^w, with no
# loops, no H1.  For Takens embedding to reveal periodic structure the
# underlying signal must itself oscillate.  We therefore count commits per day
# and use that as the signal: it rises and falls with weekly/monthly rhythm.

ts_sorted = np.array(sorted(c.committed_date for c in commits), dtype=float)
t0        = ts_sorted[0]
span_days = int((ts_sorted[-1] - t0) / 86400) + 1

# Daily commit counts (index = integer day offset from first commit)
daily_counts = np.zeros(span_days, dtype=float)
for t in ts_sorted:
    day_idx = int((t - t0) / 86400)
    daily_counts[day_idx] += 1

# Optional: smooth with a 3-day rolling average to reduce single-day spikes
kernel = np.ones(3) / 3
signal = np.convolve(daily_counts, kernel, mode="same")

print(f"Signal length : {len(signal)} days")
print(f"Non-zero days : {np.count_nonzero(daily_counts)}")
print(f"Max commits/day: {int(daily_counts.max())}")
Signal length : 1564 days
Non-zero days : 287
Max commits/day: 18
/Users/luca/Documents/happel/quoteme.github.io/.venv/lib/python3.13/site-packages/persim/landscapes/visuals.py:310: SyntaxWarning: invalid escape sequence '\l'
  ax.plot(ls[:, 0], ls[:, 1], label=f"$\lambda_{{{depth}}}$", alpha=alpha)
/Users/luca/Documents/happel/quoteme.github.io/.venv/lib/python3.13/site-packages/persim/landscapes/visuals.py:469: SyntaxWarning: invalid escape sequence '\l'
  ax.plot(domain, l, label=f"$\lambda_{{{depth}}}$", alpha=alpha)
Show the code
# ── 2. Sliding-window (Takens) embedding ─────────────────────────────────────
# Window w = 14 captures ≈ two weeks so that a 7-day cycle can close into a
# loop inside the embedding space (the loop needs at least w > period points).
# Stride = 1 keeps every window; reduce if the cloud is too large.
w      = 14
stride = 1

signal_norm = MinMaxScaler().fit_transform(signal.reshape(-1, 1)).ravel()
cloud = np.array([
    signal_norm[i : i + w]
    for i in range(0, len(signal_norm) - w + 1, stride)
])
print(f"Point cloud shape : {cloud.shape}  ({cloud.shape[0]} points in ℝ^{cloud.shape[1]})")
Point cloud shape : (1551, 14)  (1551 points in ℝ^14)
Show the code
# ── 3. Persistent homology via Ripser ─────────────────────────────────────────
result   = ripser(cloud, maxdim=1)
diagrams = result["dgms"]   # diagrams[0] = H0, diagrams[1] = H1

h0, h1 = diagrams
h0_finite = h0[np.isfinite(h0[:, 1])]
print(f"H0 finite features : {len(h0_finite)}")
print(f"H1 features        : {len(h1)}")
if len(h1):
    persistence = h1[:, 1] - h1[:, 0]
    top5 = np.sort(persistence)[::-1][:5]
    print(f"Top-5 H1 persistence values: {np.round(top5, 4)}")
H0 finite features : 1065
H1 features        : 704
Top-5 H1 persistence values: [0.3583 0.1708 0.1268 0.1217 0.1152]
Show the code
# ── 4. Visualise ─────────────────────────────────────────────────────────────
matplotlib.rcParams.update({
    "figure.facecolor"   : "none",
    "axes.facecolor"     : "none",
    "savefig.transparent": True,
})

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Persistence diagram
plot_diagrams(diagrams, ax=axes[0], show=False)
axes[0].set_title("Persistence diagram", fontsize=13)
axes[0].set_xlabel("Birth")
axes[0].set_ylabel("Death")

# H1 barcode (sorted by persistence)
ax = axes[1]
if len(h1):
    order = np.argsort(h1[:, 1] - h1[:, 0])[::-1]
    for rank, idx in enumerate(order):
        birth, death = h1[idx]
        ax.plot([birth, death], [rank, rank], lw=2, color="#4C78A8")
    ax.set_title("H₁ barcode (1-cycles / loops)", fontsize=13)
    ax.set_xlabel("Filtration value")
    ax.set_ylabel("Feature index (sorted by persistence)")
    ax.invert_yaxis()
else:
    ax.text(0.5, 0.5, "No H₁ features detected",
            ha="center", va="center", transform=ax.transAxes, fontsize=12)
    ax.set_title("H₁ barcode", fontsize=13)

plt.suptitle(
    f"TDA of daily commit-count signal  ·  sliding-window embedding  (w={w}, stride={stride})",
    fontsize=11, y=1.01,
)
plt.tight_layout()
plt.savefig("tda_commit_times.png", dpi=150, bbox_inches="tight", transparent=True)
plt.show()

In the persistence diagram, points far from the diagonal are the significant ones. The barcode shows the same thing as horizontal bars.

Note

Why not embed the raw timestamps? They’re monotonic, so a sliding window just traces a line in \(\mathbb{R}^w\) with trivial \(H_1\). The signal has to oscillate. So we use the daily commit count instead, with window \(w = 14 > 7\) so a weekly cycle can actually close into a loop.

Show the code
plt.figure(figsize=(10, 3))
plt.plot(signal, color="#4C78A8", lw=1.5)
plt.title("Daily commit count (3-day rolling average)", fontsize=11)
plt.xlabel("Days since first commit")
plt.ylabel("Commits per day")
plt.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("daily_commit_count.png", dpi=150, bbox_inches="tight", transparent=True)
plt.show()

Evaluation

Kinda obvious I work on my NixOS config every other day, but nice to see it confirmed. Testament to how addictive NixOS is, I guess. You probably catch Nix-fever harder if you’re into reproducible stuff and hacking, idk.

The TDA tells me my work shows up as energy spikes every 1-2 days that smoothen out over time. There are some “obsessive” periods where features get pushed rapidly, but the converse never really happens, I don’t “shut down” for many days in a row. If you like the work you do, you do not need rest, it already is the rest you need.

So: \(H_0\) confirms no long stretches of inactivity. \(H_1\) shows real loops, meaning the daily commit count oscillates on a short period (roughly weekly). Organic work, like a passion project should be.

Privacy and Security Considerations

Publishing my config openly exposes my hardware inventory, software stack, and commit cadence. Why do I not care? Because no secrets are managed by NixOS or enter the repo, no credentials are hardcoded, and the hardware-identifying stuff is either innocuous or already public.

Still, even a “clean” repo leaks via commit timing, file names, and diff content. Let’s see what someone could actually extract.

Exploit Analysis

I scan for sensitive keywords in commit messages, file paths, and historical diffs (including lines later deleted):

Show the code
import re
from IPython.display import HTML

SENSITIVE = [
    "api", "key", "password", "passwd", "secret", "token", "credential",
    "private", "auth", "oauth", "ssh", "gpg", "pgp", "cert", "tls", "ssl",
    "vulnerability", "exploit", "cve", "backdoor", "leak",
]
PAT = re.compile(r"(?<!\w)(" + "|".join(SENSITIVE) + r")(?!\w)", re.IGNORECASE)

# ── 1. Commit messages ────────────────────────────────────────────────────────
msg_hits: list[tuple[str, str, list[str]]] = []
for c in commits:
    found = PAT.findall(c.message)
    if found:
        msg_hits.append((c.hexsha[:7], c.message.splitlines()[0][:80], sorted(set(w.lower() for w in found))))

# ── 2. File paths ─────────────────────────────────────────────────────────────
path_hits: list[tuple[str, list[str]]] = []
for path in all_files:
    found = PAT.findall(path)
    if found:
        path_hits.append((path, sorted(set(w.lower() for w in found))))

# ── 3. Commit diffs ───────────────────────────────────────────────────────────
CONTEXT = 60  # characters of context around each match

def diff_snippets(diff_text: str) -> list[str]:
    snippets = []
    for m in PAT.finditer(diff_text):
        start = max(0, m.start() - CONTEXT)
        end   = min(len(diff_text), m.end() + CONTEXT)
        snippet = diff_text[start:end].replace("\n", " ")
        snippets.append(f"…{snippet}…")
    return snippets[:3]  # at most 3 snippets per commit

diff_hits: list[tuple[str, str, list[str]]] = []
for c in commits:
    if not c.parents:
        continue
    try:
        diff_text = repo.git.diff(c.parents[0].hexsha, c.hexsha, unified=0)
    except Exception:
        continue
    if PAT.search(diff_text):
        snippets = diff_snippets(diff_text)
        diff_hits.append((c.hexsha[:7], c.committed_datetime.strftime("%Y-%m-%d"), snippets))

# ── Render results ────────────────────────────────────────────────────────────
def kw(words):
    return " ".join(f'<code style="color:#c0392b">{w}</code>' for w in words)

parts = []

parts.append(f"<h4>1. Commit messages: {len(msg_hits)} hit(s)</h4>")
if msg_hits:
    rows = "".join(
        f"<tr><td><code>{sha}</code></td><td>{msg}</td><td>{kw(words)}</td></tr>"
        for sha, msg, words in msg_hits
    )
    parts.append(f'<table style="font-size:0.82rem;width:100%;border-collapse:collapse">'
                 f'<thead><tr><th>SHA</th><th>Message</th><th>Keywords</th></tr></thead>'
                 f'<tbody>{rows}</tbody></table>')
else:
    parts.append("<p>✓ No sensitive keywords found in commit messages.</p>")

parts.append(f"<h4>2. File paths: {len(path_hits)} hit(s)</h4>")
if path_hits:
    rows = "".join(
        f"<tr><td><code>{path}</code></td><td>{kw(words)}</td></tr>"
        for path, words in path_hits
    )
    parts.append(f'<table style="font-size:0.82rem;width:100%;border-collapse:collapse">'
                 f'<thead><tr><th>Path</th><th>Keywords</th></tr></thead>'
                 f'<tbody>{rows}</tbody></table>')
else:
    parts.append("<p>✓ No sensitive keywords found in file paths.</p>")

parts.append(f"<h4>3. Commit diffs: {len(diff_hits)} hit(s)</h4>")
if diff_hits:
    rows = "".join(
        f"<tr><td><code>{sha}</code></td><td>{date}</td>"
        f"<td style='font-size:0.78rem'>" + "<br>".join(
            f'<span style="background:#fff3cd;padding:1px 3px;border-radius:2px">{s}</span>'
            for s in snips
        ) + "</td></tr>"
        for sha, date, snips in diff_hits
    )
    parts.append(f'<table style="font-size:0.82rem;width:100%;border-collapse:collapse">'
                 f'<thead><tr><th>SHA</th><th>Date</th><th>Context snippets</th></tr></thead>'
                 f'<tbody>{rows}</tbody></table>')
else:
    parts.append("<p>✓ No sensitive keywords found in diffs.</p>")

HTML("\n".join(parts))

1. Commit messages: 8 hit(s)

SHA Message Keywords
fdc446e xremap for windows key key
acc3dbf fix: capslock key key
b04664f add: backslash using FN key key
9ba2171 make workspace-preview open on deafult asus-rog key pressed key
f6c171a add preview key key
33a6fd6 make keyboard only use compose key when pressing rctrl and altgr key
35c871e added key descriptions key
b1cc0b8 added new github auth auth

2. File paths: 1 hit(s)

Path Keywords
modules/security/gpg.nix gpg

3. Commit diffs: 80 hit(s)

SHA Date Context snippets
39e146a 2026-04-30 …nome-keyring-daemon --start --daemonize --components=pkcs11,ssh,secrets) - export SSH_AUTH_SOCK - '';…
e5c941b 2026-04-23 …val $(gnome-keyring-daemon --start --daemonize --components=ssh,secrets) + eval $(gnome-keyring-daemon --start --dae…
…nome-keyring-daemon --start --daemonize --components=pkcs11,ssh,secrets) diff --git a/modules/security/gpg.nix b/modules/se…
…omponents=pkcs11,ssh,secrets) diff --git a/modules/security/gpg.nix b/modules/security/gpg.nix index 0806a68..deaef28 10064…
1eedbba 2026-04-20 …ake.nix @@ -234,0 +235,2 @@ + modules.security.gpg.enable = true; + modules.security.gpg.enableSS…
…security.gpg.enable = true; + modules.security.gpg.enableSSHSupport = false; diff --git a/modules/security/gpg…
…gpg.enableSSHSupport = false; diff --git a/modules/security/gpg.nix b/modules/security/gpg.nix new file mode 100644 index 0…
64d30df 2026-04-19 … "$HOME/.var/app/com.bitwarden.desktop/data/.bitwarden-ssh-agent.sock"; + SSH_AUTH_SOCK = "$HOME/.var/app/com.bitwa…
…OCK = "$HOME/.var/app/com.bitwarden.desktop/data/.bitwarden-ssh-agent.sock"; @@ -37,4 +42,2 @@ - montigrafana = - "…
… montigrafana = - "xdg-open http://localhost:3000 && ssh -L 3000:localhost:3000 [email protected]"; - montikuma =…
1ca6ff2 2026-04-02 …d todo with text to specified page via direct PluginService API + function addTodoWithText(text, pageId) { + if (!text …
…d todo with text to specified page via direct PluginService API - function addTodoWithText(text, pageId) { - if (…
…lation is allowed - // when calling another plugin's API. This is NOT internal IPC (forbidden). - // We're in…
3e70361 2026-03-20 …nished tasks, quit anyway?\n(Open task manager with default key 'w')" +quit_origin = "center" +quit_offset = [0, 0, 50, 15]…
ea3a575 2026-03-15 …y.duration-desc") + model: [ + { "key": "15", "name": "15s" }, + { "key": "30", "n…
… { "key": "15", "name": "15s" }, + { "key": "30", "name": "30s" }, + { "key": "60", "n…
… { "key": "30", "name": "30s" }, + { "key": "60", "name": "60s" }, + { "key": "120", "…
95056c0 2026-03-15 …e$" + open-floating true +} + +// Example: block out two password managers from screen capture. +// (This example rule is com…
…nsist of modifiers separated by + signs, followed by an XKB key name + // in the end. To find an XKB name for a particul…
…me + // in the end. To find an XKB name for a particular key, you may use a program + // like wev. + // + // "M…
a0f8920 2026-03-15 …e$" + open-floating true +} + +// Example: block out two password managers from screen capture. +// (This example rule is com…
…nsist of modifiers separated by + signs, followed by an XKB key name + // in the end. To find an XKB name for a particul…
…me + // in the end. To find an XKB name for a particular key, you may use a program + // like wev. + // + // "M…
c81ba8c 2026-03-07 … New component with cursor-following context menu +- Direct API access via `PluginService.getPluginAPI("todo")` +- No IPC o…
… nl, pl, pt, ru, sv, tr, uk-UA, zh-CN, zh-TW) +- Consistent key naming: `clipper.component.key` + +**New Translation Keys A…
… zh-CN, zh-TW) +- Consistent key naming: `clipper.component.key` + +**New Translation Keys Added:** +```json +{ + "clipper…
845577e 2026-02-28 …- # Define a user account. Don't forget to set a password with ‘passwd’. - # TODO: set passwort using hash…
…Define a user account. Don't forget to set a password with ‘passwd’. - # TODO: set passwort using hashed password @…
…th ‘passwd’. - # TODO: set passwort using hashed password @@ -226,4 +188,0 @@ - # List packages installed …
4be74ad 2026-02-25 …, $menu @@ -245 +244,0 @@ $mainMod = SUPER # Sets "Windows" key as main modifier -bind = $mainMod, X, exec, $terminal @@ -2…
d813613 2026-02-24 …: 405, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/determinate/3.16.3…
…: 377, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/hercules-ci/flake-parts/0.1.377%2Brev…
… 1026, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/cachix/git-hooks.nix/0.1.1026%2Brev-8…
948af7f 2026-02-24 …: 405, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/determinate/3.16.3…
…: 377, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/hercules-ci/flake-parts/0.1.377%2Brev…
… 1026, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/cachix/git-hooks.nix/0.1.1026%2Brev-8…
6a9b900 2026-02-24 …bleGnomeKeyring = true; - security.pam.services.greetd-password.enableGnomeKeyring = true; - services.logind.settings.…
…ms.dconf.enable = true; + security.pam.services.greetd-password.enableGnomeKeyring = true; + security.pam.services.gre…
d699902 2026-02-23 …name = "Better Vim bindings"; - remap = { - # slash key "/" - "KEY_CONNECT" = "SHIFT-7"; - # backslash ke…
…ey "/" - "KEY_CONNECT" = "SHIFT-7"; - # backslash key "\" - "KEY_FINANCE" = "RIGHTALT-MINUS"; - # Open …
…KEY_FINANCE" = "RIGHTALT-MINUS"; - # Open square brace key "[" - "KEY_SPORT" = "RIGHTALT-8"; - # Closed squa…
179e2b2 2026-02-23 …e: "Papirus-Dark"; + terminal: "kitty"; + ssh-command: "{terminal} -e \"{ssh-client} {host} [-p {port…
…al: "kitty"; + ssh-command: "{terminal} -e \"{ssh-client} {host} [-p {port}]\""; + drun-display-format: "{…
309e5f9 2026-02-23 …e: "Papirus-Dark"; + terminal: "kitty"; + ssh-command: "{terminal} -e \"{ssh-client} {host} [-p {port…
…al: "kitty"; + ssh-command: "{terminal} -e \"{ssh-client} {host} [-p {port}]\""; + drun-display-format: "{…
5ba43e7 2026-02-19 … "$HOME/.var/app/com.bitwarden.desktop/data/.bitwarden-ssh-agent.sock"; + };…
d5b6be3 2026-02-18 … "$HOME/.var/app/com.bitwarden.desktop/data/.bitwarden-ssh-agent.sock"; + };…
93336f8 2025-12-16 …bleGnomeKeyring = true; + security.pam.services.greetd-password.enableGnomeKeyring = true; @@ -75 +76 @@ in { - servic…
…val $(gnome-keyring-daemon --start --daemonize --components=ssh,secrets) + export SSH_AUTH_SOCK + '';…
d9c06d8 2025-11-03 …-asexuality -asexually -ash -ash-blonde -ash-gray -ash-key -ash-pan -ashame -ashamed -ashamed(p) -ashamedly -ash…
… -ceromancy -ceroplastic -cerous -ceroxylon -cerrado -cert -certa -certain -certain(a) -certain(p) -certainly -c…
…e -creche -crecy -credat -crede -credence -credenda -credential -credentials -credenza -credibility -credible -credibl…
f3e4264 2025-10-04 …-update: + permissions: + contents: write + id-token: write + issues: write + pull-requests: write + …
…-update: + permissions: + contents: write + id-token: write + issues: write + pull-requests: write + …
816e1f1 2025-10-04 …-update: + permissions: + contents: write + id-token: write + issues: write + pull-requests: write + …
7d8b621 2025-10-04 …-update: + permissions: + contents: write + id-token: write + issues: write + pull-requests: write + …
454aa57 2025-08-31 …nd/Configuring/Keywords/ +$mainMod = SUPER # Sets "Windows" key as main modifier + +# Example binds, see https://wiki.hypr.…
…+asexuality +asexually +ash +ash-blonde +ash-gray +ash-key +ash-pan +ashame +ashamed +ashamed(p) +ashamedly +ash…
… +ceromancy +ceroplastic +cerous +ceroxylon +cerrado +cert +certa +certain +certain(a) +certain(p) +certainly +c…
7e192e5 2025-08-18 …+asexuality +asexually +ash +ash-blonde +ash-gray +ash-key +ash-pan +ashame +ashamed +ashamed(p) +ashamedly +ash…
… +ceromancy +ceroplastic +cerous +ceroxylon +cerrado +cert +certa +certain +certain(a) +certain(p) +certainly +c…
…e +creche +crecy +credat +crede +credence +credenda +credential +credentials +credenza +credibility +credible +credibl…
cc8917e 2025-08-18 …nd/Configuring/Keywords/ +$mainMod = SUPER # Sets "Windows" key as main modifier + +# Example binds, see https://wiki.hypr.…
2e09268 2025-06-02 …"gtk" - # ]; - # "org.freedesktop.impl.portal.Secret" = [ - # "gnome-keyring" - # ]; - # };…
be4dbf3 2025-03-05 … GDK_DEBUG = "gl-no-fractional"; - GDK_DISABLE = "gles-api,color-mgmt,vulkan"; - GSK_RENDERER = "opengl"; + …
…DK_DEBUG = "gl-no-fractional"; + # GDK_DISABLE = "gles-api,color-mgmt,vulkan"; + # GSK_RENDERER = "opengl";…
c91f32d 2025-03-05 … GDK_DEBUG = "gl-no-fractional"; + GDK_DISABLE = "gles-api,color-mgmt,vulkan"; + GSK_RENDERER = "opengl"; + };…
8c80cc7 2025-01-02 … montigrafana = + "xdg-open http://localhost:3000 && ssh -L 3000:localhost:3000 [email protected]"; + montikuma =…
… + montikuma = + "xdg-open http://localhost:3001 && ssh -L 3001:localhost:3001 [email protected]"; + montipostgr…
…01:localhost:3001 [email protected]"; + montipostgres = "ssh -L 5432:localhost:5432 [email protected]"; + montipromet…
307f8f2 2024-11-18 …remote.remote-containers - ms-vscode-remote.remote-ssh-edit - ms-vscode.remote-explorer - ms-vsc…
…azuretools.vscode-docker - ms-vscode-remote.remote-ssh - # .env - irongeek.vscode-env - …
…ode-dotnet-pack - visualstudioexptteam.intellicode-api-usage-examples - visualstudioexptteam.vscodeintell…
1b23b25 2024-11-13 …TODO this does not work :( - description = "Use escape key as Hyper key"; - languages = [ ]; - symbolsFile =…
…s not work :( - description = "Use escape key as Hyper key"; - languages = [ ]; - symbolsFile = pkgs.writeTe…
…rtial modifier_keys - xkb_symbols "hyper" { - key { [Hyper_R] }; - modifier_map Mod3 { , H…
d86fb6f 2024-11-07 …= { - modi = "combi"; - combi-modi = "drun,window,ssh"; - show-icons = true; - }; - }; @@ -208,0 +180 @@…
f4082c2 2024-10-30 … = "Better Vim bindings"; - remap = { - # slash key "/" - "KEY_CONNECT" = "SHIFT-7"; - # backslas…
…/" - "KEY_CONNECT" = "SHIFT-7"; - # backslash key "\" - "KEY_FINANCE" = "RIGHTALT-MINUS"; - # O…
…Y_FINANCE" = "RIGHTALT-MINUS"; - # Open square brace key "[" - "KEY_SPORT" = "RIGHTALT-8"; - # Close s…
435cae2 2024-09-17 … # Define a user account. Don't forget to set a password with ‘passwd’. - # TODO: set passwort using ha…
…Define a user account. Don't forget to set a password with ‘passwd’. - # TODO: set passwort using hashed password…
… ‘passwd’. - # TODO: set passwort using hashed password - users.users.root.initialHashedPassword = "";…
4804576 2024-09-14 …s = { - # Monti - montissh = "TERM=xterm-256color ssh [email protected]"; - montikuma = "xdg-open http://loc…
….de"; - montikuma = "xdg-open http://localhost:3001 && ssh -L 3001:localhost:3001 [email protected]"; - montiprom…
…- montiprometheus = "xdg-open http://localhost:9090 && ssh -L 9090:localhost:9090 [email protected]"; - montigraf…
b098a42 2024-09-12 …,4 @@ - montikuma = "xdg-open http://localhost:3001 && ssh -L 3001:localhost:3001 [email protected]"; - montipromethe…
…- montiprometheus = "xdg-open http://localhost:9090 && ssh -L 9090:localhost:9090 [email protected]"; - montigrafana …
…"; - montigrafana = "xdg-open http://localhost:3000 && ssh -L 3000:localhost:3000 [email protected]"; - # make montis…
7da1840 2024-09-12 …,4 @@ - montikuma = "xdg-open http://localhost:3001 && ssh -L 3001:localhost:3001 [email protected]"; - montipromethe…
…- montiprometheus = "xdg-open http://localhost:9090 && ssh -L 9090:localhost:9090 [email protected]"; - montigrafana …
…"; - montigrafana = "xdg-open http://localhost:3000 && ssh -L 3000:localhost:3000 [email protected]"; - # make montis…
4774cdc 2024-08-15 …me.nix @@ -39 +39 @@ - montissh = "TERM=xterm-256color ssh [email protected]"; + montissh = "TERM=xterm-256color ssh …
… ssh [email protected]"; + montissh = "TERM=xterm-256color ssh [email protected]";…
f459430 2024-07-31 … = "Better Vim bindings"; + remap = { + # slash key "/" + "KEY_CONNECT" = "SHIFT-7"; + # backslas…
…/" + "KEY_CONNECT" = "SHIFT-7"; + # backslash key "\" + "KEY_FINANCE" = "RIGHTALT-MINUS"; + # O…
…Y_FINANCE" = "RIGHTALT-MINUS"; + # Open square brace key "[" + "KEY_SPORT" = "RIGHTALT-8"; + # Close s…
68f6d4d 2024-07-30 … = "Better Vim bindings"; + remap = { + # slash key "/" + "KEY_CONNECT" = "SHIFT-7"; + # backslas…
…/" + "KEY_CONNECT" = "SHIFT-7"; + # backslash key "\" + "KEY_FINANCE" = "RIGHTALT-MINUS"; + # O…
…Y_FINANCE" = "RIGHTALT-MINUS"; + # Open square brace key "[" + "KEY_SPORT" = "RIGHTALT-8"; + # Close s…
adc159c 2024-07-01 …y" @@ -44,3 +49 @@ in - "dirhistory" - # "gpg-agent" - # "keychain" + "zoxide" @@ -48 +…
a9d5da8 2024-06-08 …t_application_mode is escape \x1b[?1l and was added to help ssh work better + reset_application_mode: true + } @@…
469aefc 2024-06-08 …t_application_mode is escape \x1b[?1l and was added to help ssh work better + reset_application_mode: true + } @@…
bd0456b 2024-03-01 …ame = Luca Leon Happel + # signingKey = "" + # [credential "https://github.com"] + # helper = + # helpe…
…elper = + # helper = !/run/current-system/sw/bin/gh auth git-credential + # [credential "https://gist.github.co…
…+ # helper = !/run/current-system/sw/bin/gh auth git-credential + # [credential "https://gist.github.com"] + # …
e8d0565 2024-03-01 …def montikuma [] { + xdg-open http://localhost:3001 + ssh -L 3001:localhost:3001 +} +def montiprometheus [] { + x…
…ntiprometheus [] { + xdg-open http://localhost:9090 + ssh -L 9090:localhost:9090 +} +def montigrafana [] { + xdg-…
… montigrafana [] { + xdg-open http://localhost:3000 + ssh -L 3000:localhost:3000 +} +def montipostgres [] { + ssh…
c7a7171 2024-02-25 …shell/config.nu +def montissh [] { + TERM=xterm-256color ssh [email protected] +} +# plugins +register ~/.cargo/bin/nu_plugi…
53ffbe1 2023-12-28 …"gtk" + # ]; + # "org.freedesktop.impl.portal.Secret" = [ + # "gnome-keyring" + # ]; + # };…
f845631 2023-10-31 …hell.enable = true; @@ -254,53 +254,0 @@ - # Password stuff - # seahorse.enable = true; - …
… # seahorse.enable = true; - # ssh.enableAskPassword = true; - - #kdeconnect.en…
… "dirhistory" - # "gpg-agent" - # "keychain" - …
ef07bc6 2023-10-23 …es port available on localhost:5432 + montipostgres = "ssh -L 5432:localhost:5432 [email protected]"; @@ -47 +48 @@ - …
8c39a9f 2023-10-17 … a/home.nix +++ b/home.nix @@ -39 +39 @@ - montissh = "ssh [email protected]"; + montissh = "TERM=xterm-256color ssh …
…"ssh [email protected]"; + montissh = "TERM=xterm-256color ssh [email protected]"; diff --git a/modules/desktop/kde.nix b/modu…
1b97388 2023-09-27 … # "lein" @@ -280 +280 @@ - "gpg-agent" + # "gpg-agent" @@ -309 +308,0 …
… "gpg-agent" + # "gpg-agent" @@ -309 +308,0 @@ - file-roller.enabl…
f9137f4 2023-08-21 …71Mng=", - "owner": "folke", - "repo": "which-key.nvim", - "rev": "87b1459b3e0be0340da2183fc4ec8a00b29…
…inal": { - "owner": "folke", - "repo": "which-key.nvim", - "type": "github" - } - }, - "plu…
0c5ce27 2023-07-30 …@ - seahorse.enable = true; - ssh.enableAskPassword = true; + # seahorse.enabl…
… # seahorse.enable = true; + # ssh.enableAskPassword = true; @@ -215 +215 @@ - …
c0ce971 2023-07-13 … not work :( - description = "Use escape key as Hyper key"; - languages = [ ]; - …
…- description = "Use escape key as Hyper key"; - languages = [ ]; - …
… xkb_symbols "hyper" { - key { [Hyper_R] }; - modifier_map Mo…
babe5cb 2023-07-06 … not work :( - description = "Use escape key as Hyper key"; - languages = [ ]; - …
…- description = "Use escape key as Hyper key"; - languages = [ ]; - …
… xkb_symbols "hyper" { - key { [Hyper_R] }; - modifier_map Mo…
2bcd1bb 2023-07-06 …e-containers - ms-vscode-remote.remote-ssh-edit - ms-vscode.remote-explorer - …
…scode-docker - ms-vscode-remote.remote-ssh - # .env - ironge…
…ack - visualstudioexptteam.intellicode-api-usage-examples - visualstudioexptteam.…
2f7e21a 2023-07-06 …remote.remote-containers + ms-vscode-remote.remote-ssh-edit + ms-vscode.remote-explorer + ms-vsc…
…azuretools.vscode-docker + ms-vscode-remote.remote-ssh + # .env + irongeek.vscode-env + …
…ode-dotnet-pack + visualstudioexptteam.intellicode-api-usage-examples + visualstudioexptteam.vscodeintell…
2751538 2023-06-28 … @@ + visualstudioexptteam.intellicode-api-usage-examples @@ -580 +582 @@ - visua…
ba6643f 2023-06-01 …e-containers + ms-vscode-remote.remote-ssh-edit + ms-vscode.remote-explorer + …
…scode-docker + ms-vscode-remote.remote-ssh + # Copilot + git…
7bc98f1 2023-05-16 …es not work :( - description = "Use escape key as Hyper key"; - languages = []; - …
…( - description = "Use escape key as Hyper key"; - languages = []; - sym…
… xkb_symbols "hyper" { - key { [Hyper_R] }; - modifier_map Mod3…
8ce583f 2023-03-12 …0,0 @@ - nwg-launchers @@ -535 +547 @@ - # Password stuff + # Password stuff @@ -587,0 +600,3 @@ + +…
…chers @@ -535 +547 @@ - # Password stuff + # Password stuff @@ -587,0 +600,3 @@ + + # xfce4-panel + …
ea730ec 2023-02-04 …g $ - -- {{{ Legend on how to use modifiers - -- Code | Key - -- M | super key - -- C | control - -- S |…
… how to use modifiers - -- Code | Key - -- M | super key - -- C | control - -- S | shift - -- M1 | alt…
…0 , xF86XK_ScreenSaver ), spawn "xdotool key super+s") - -- , ((0 , xF86XK_Launch1 …
bc5d368 2023-01-04 …ghtButton 1 = do - -- TODO: - -- send a key to toggle fullscreen (not maximize) on the window - …
… -- | isNthRightButton 1 = do + -- -- send a key to toggle fullscreen (not maximize) on the window + …
82c41bb 2022-12-02 …in__twilight_nvim", @@ -323 +306 @@ - "plugin__which-key.nvim": "plugin__which-key.nvim", + "plugin__which-ke…
…3 +306 @@ - "plugin__which-key.nvim": "plugin__which-key.nvim", + "plugin__which-key_nvim": "plugin__which-ke…
…27d2d2b3f97064e7686", @@ -1866 +1849 @@ - "plugin__which-key.nvim": { + "plugin__which-key_nvim": {…
a06fc5b 2022-10-30 …in:vimspector": "plugin:vimspector", - "plugin:which-key.nvim": "plugin:which-key.nvim", - "plugin:yuck.vim":…
…imspector", - "plugin:which-key.nvim": "plugin:which-key.nvim", - "plugin:yuck.vim": "plugin:yuck.vim", + …
…_vimspector": "plugin__vimspector", + "plugin__which-key.nvim": "plugin__which-key.nvim", + "plugin__yuck.vim…
3453724 2022-09-28 …ecoration a where + -- TODO: + -- send a key to toggle fullscreen (not maximize) on the window + …
5a84e46 2022-09-19 …hift-{y,x,c}, Move client to screen 1, 2, or 3 - -- [((m, key), screenWorkspace sc >>= flip whenJust (windows . f)) - -…
…Workspace sc >>= flip whenJust (windows . f)) - -- | (key, sc) <- zip [xK_y, xK_x, xK_c] [0..] - -- , (f, m) <-…
4fe54f8 2022-07-25 …,2 +283 @@ main = getDirectories >>= launch - -- key bindings - keys = myKeys, + …
6c4d6d4 2022-07-16 …0 , xF86XK_ScreenSaver ), spawn "xdotool key super+s") - , ((0 , xF86XK_Launch1 …
…0 , xF86XK_Launch1 ), spawn "xdotool key super+r") + -- , ((0 , xF86XK_ScreenSaver…
…0 , xF86XK_ScreenSaver ), spawn "xdotool key super+s") + -- , ((0 , xF86XK_Launch1 …
6ae2621 2022-07-12 … focusedBorderColor = myFocusedBorderColor, - -- key bindings - keys = myKeys, - m…
…focusedBorderColor = myFocusedBorderColor, + -- key bindings + keys = myKeys, + …
e4de490 2022-07-12 … focusedBorderColor = myFocusedBorderColor, - -- key bindings - keys = myKeys, - m…
…focusedBorderColor = myFocusedBorderColor, + -- key bindings + keys = myKeys, + …
d5bf94b 2022-07-05 …NixOS /!\ @@ -401,0 +393 @@ + @@ -408,0 +401,2 @@ + + # Password stuff @@ -410,0 +405 @@ + @@ -448,0 +444 @@ + @@ -450,0 +44…
b93465d 2022-06-23 …# TODO: This option somehow does not work??? - # ssh.enableAskPassword = true; + ssh.enableAskPasswor…
…? - # ssh.enableAskPassword = true; + ssh.enableAskPassword = true;…
38ff104 2022-06-23 …i-mode" + "dirhistory" + "gpg-agent" + "keychain" + "zs…
17045ac 2022-06-08 …+217 @@ myStartupHook = do - spawnOnce "onboard ; xdotool key 199 ; xdotool key 200" + -- spawnOnce "onboard ; xdotool…
…ook = do - spawnOnce "onboard ; xdotool key 199 ; xdotool key 200" + -- spawnOnce "onboard ; xdotool key 199 ; xdotool…
…199 ; xdotool key 200" + -- spawnOnce "onboard ; xdotool key 199 ; xdotool key 200" …
dcec2d0 2022-06-02 …@@ -195 +212 @@ - # TODO set passwort using hashed password + # TODO: set passwort using hashed password @@ -2…
…ashed password + # TODO: set passwort using hashed password @@ -217 +234 @@ - # TODO move this into another fi…
9fb85d5 2022-04-24 …per + ctrl + {m,x,y,z} + bspc node -g {marked,locked,sticky,private} + +# +# focus/swap +# + +# focus the node in the given dir…
…ugin:vim-vsnip": "plugin:vim-vsnip", - "plugin:which-key.nvim": "plugin:which-key.nvim" + "plugin:which-key.n…
…vim-vsnip", - "plugin:which-key.nvim": "plugin:which-key.nvim" + "plugin:which-key.nvim": "plugin:which-key.n…

The diff scan is the most thorough since it catches deleted code too. Use broad keyword matches like apik (prefix for apikey).

The one entry worth noting:

users.users.root.initialHashedPassword = ""

Empty initialHashedPassword is intentional. It disables password-based root login on first install, so I have to set credentials interactively the first time I boot.

Conclusion

NixOS is really addictive. It just makes a lot of fun to work with IT-infrastructure. You kinda become a system-administrator by accident, but there is a caveat:

The more you use it, the more it uses you

The more you use it, the more it uses you

The honest tradeoff is time. NixOS makes system-administration way to fun (although frustrating from time to time). I learned the hard way by probably spending many hundreds of hours just managing my system. Time spend, which I could have used for family and friends instead of learning IT-infrastructure, like kernel patches, etc.

Nontheless this was a nice experience and in the end I was left with a system near unbreakable to any outside influence (like in my case, where I lost all my laptops and NixOS got me up and running again in no time).