Kieran's opinionated (and probably slightly dumb) nix config

Compare changes

Choose any two refs to compare.

-1
README.md
···
├── machines
│ ├── atalanta # my macOS M4 machine
│ ├── ember # my dell r210 server (in my basement)
-
│ ├── john # shared server for cedarville
│ ├── moonlark # my framework 13 <dead>
│ ├── nest # shared tilde server through hc
│ ├── prattle # oracle cloud x86_64 server
+28 -28
flake.lock
···
]
},
"locked": {
-
"lastModified": 1764627417,
-
"narHash": "sha256-D6xc3Rl8Ab6wucJWdvjNsGYGSxNjQHzRc2EZ6eeQ6l4=",
+
"lastModified": 1765326679,
+
"narHash": "sha256-fTLX9kDwLr9Y0rH/nG+h1XG5UU+jBcy0PFYn5eneRX8=",
"owner": "nix-community",
"repo": "disko",
-
"rev": "5a88a6eceb8fd732b983e72b732f6f4b8269bef3",
+
"rev": "d64e5cdca35b5fad7c504f615357a7afe6d9c49e",
"type": "github"
},
"original": {
···
]
},
"locked": {
-
"lastModified": 1765170903,
-
"narHash": "sha256-O8VTGey1xxiRW+Fpb+Ps9zU7ShmxUA1a7cMTcENCVNg=",
+
"lastModified": 1765384171,
+
"narHash": "sha256-FuFtkJrW1Z7u+3lhzPRau69E0CNjADku1mLQQflUORo=",
"owner": "nix-community",
"repo": "home-manager",
-
"rev": "20561be440a11ec57a89715480717baf19fe6343",
+
"rev": "44777152652bc9eacf8876976fa72cc77ca8b9d8",
"type": "github"
},
"original": {
···
"nixpkgs": "nixpkgs_3"
},
"locked": {
-
"lastModified": 1765159287,
-
"narHash": "sha256-C+dVEekU31QPaPShMaUbs3LqOVVqzq0b4gKC1jX8Mlk=",
+
"lastModified": 1765418662,
+
"narHash": "sha256-8SSYagIUn+m9CKUYddq3DN1xkh04KCO0itB/LMgEgpc=",
"owner": "nix-community",
"repo": "nix-vscode-extensions",
-
"rev": "dccd0cc3693bff67e4856b5a22445223aabc4d4b",
+
"rev": "0f6679daa3f5bc2b09827b67f49caf0ac8e3a4c8",
"type": "github"
},
"original": {
···
},
"nixos-facter-modules": {
"locked": {
-
"lastModified": 1764252389,
-
"narHash": "sha256-3bbuneTKZBkYXlm0bE36kUjiDsasoIC1GWBw/UEJ9T4=",
+
"lastModified": 1765442039,
+
"narHash": "sha256-k3lYQ+A1F7aTz8HnlU++bd9t/x/NP2A4v9+x6opcVg0=",
"owner": "numtide",
"repo": "nixos-facter-modules",
-
"rev": "5ea68886d95218646d11d3551a476d458df00778",
+
"rev": "9dd775ee92de63f14edd021d59416e18ac2c00f1",
"type": "github"
},
"original": {
···
},
"nixpkgs-unstable": {
"locked": {
-
"lastModified": 1764950072,
-
"narHash": "sha256-BmPWzogsG2GsXZtlT+MTcAWeDK5hkbGRZTeZNW42fwA=",
+
"lastModified": 1765186076,
+
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
"owner": "nixos",
"repo": "nixpkgs",
-
"rev": "f61125a668a320878494449750330ca58b78c557",
+
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
"type": "github"
},
"original": {
···
},
"nixpkgs_4": {
"locked": {
-
"lastModified": 1764983851,
-
"narHash": "sha256-y7RPKl/jJ/KAP/VKLMghMgXTlvNIJMHKskl8/Uuar7o=",
+
"lastModified": 1765311797,
+
"narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=",
"owner": "nixos",
"repo": "nixpkgs",
-
"rev": "d9bc5c7dceb30d8d6fafa10aeb6aa8a48c218454",
+
"rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b",
"type": "github"
},
"original": {
···
},
"locked": {
-
"lastModified": 1765213466,
-
"narHash": "sha256-JdQa7m3a/oWun8TGJ+jamAdxn820RFjqDLNnl4d8a+0=",
+
"lastModified": 1765470296,
+
"narHash": "sha256-bURojPUn8jloR046JNZf6qrYNmEPfFEoDaLTKoP9pg4=",
"owner": "nix-community",
"repo": "NUR",
-
"rev": "0c5cabc4f46e5ce7e45827c22b21173a887acff2",
+
"rev": "441a70568483c0c48b338cca2030e3d9c7aef3ba",
"type": "github"
},
"original": {
···
"sqlite-lib-src": "sqlite-lib-src"
},
"locked": {
-
"lastModified": 1765171220,
-
"narHash": "sha256-K+Cs6k0nQYRwW+RwlKCZabLBOVel84C2wPEZjYOH6JA=",
+
"lastModified": 1765368304,
+
"narHash": "sha256-Q3JC5+FYtsKJU70WIhGhsAYWzu0CvUmmbdYhcFe46Pg=",
"ref": "refs/heads/master",
-
"rev": "ca8217e99806280fa77316b46b0b243647ed491c",
-
"revCount": 1722,
+
"rev": "a53d124ea4746109c1933f7adc72f0bde1309890",
+
"revCount": 1731,
"type": "git",
"url": "https://tangled.org/tangled.org/core"
},
···
"zig2nix": "zig2nix"
},
"locked": {
-
"lastModified": 1765255474,
-
"narHash": "sha256-Bs/wb2EIDe1QHsmHHQ34L0se4eANdGsQpxAsc+gDCrU=",
+
"lastModified": 1765397837,
+
"narHash": "sha256-nMlS9SA8MLJHJ0X/zEg3eG18mLw5vvZpZBbTbVcGFTI=",
"owner": "neurosnap",
"repo": "zmx",
-
"rev": "b5b525c333f086798e319b1a27f2bc0119ebca5c",
+
"rev": "a22dba538a31480ed450b389f397e15880a1c53a",
"type": "github"
},
"original": {
+3
flake.nix
···
hash = "sha256-7mkrPl2CQSfc1lRjl1ilwxdYcK5iRU//QGKmdCicK30=";
};
});
+
+
zmx-binary = prev.callPackage ./packages/zmx.nix { };
})
];
};
···
home-manager.darwinModules.home-manager
agenix.darwinModules.default
unstable-overlays
+
nur.modules.darwin.default
./machines/atalanta
];
};
+8 -3
machines/atalanta/home/default.nix
···
(inputs.import-tree ../../../modules/home)
];
-
nixpkgs.enable = true;
-
home = {
username = "kierank";
homeDirectory = "/Users/kierank";
···
zmx = {
enable = true;
-
hosts = [ "t.*" "p.*" "e.*" ];
+
hosts = [ "t.*" "p.*" "e.*" "j.*" ];
};
hosts = {
···
hostname = "192.168.0.94"; # ember
};
+
"j.*" = {
+
hostname = "john.cedarville.edu";
+
user = "klukas";
+
};
+
# Regular hosts
john = {
+
hostname = "john.cedarville.edu";
user = "klukas";
+
zmx = true;
};
bandit = {
+1
machines/atalanta/home-manager.nix
···
];
home-manager = {
+
useGlobalPkgs = true;
extraSpecialArgs = {
inherit inputs outputs;
};
+1
machines/ember/default.nix
···
{
imports = [
(inputs.import-tree ../../modules/home)
+
../../modules/home/system/nixpkgs.nix.disabled
];
nixpkgs.enable = true;
-35
machines/john/default.nix
···
-
{
-
inputs,
-
pkgs,
-
...
-
}:
-
{
-
imports = [
-
(inputs.import-tree ../../modules/home)
-
];
-
-
nixpkgs.enable = true;
-
-
home = {
-
username = "klukas";
-
homeDirectory = "/home/students/2029/klukas";
-
-
packages = with pkgs; [ ];
-
};
-
-
atelier = {
-
shell.enable = true;
-
};
-
-
# Enable home-manager
-
programs.home-manager.enable = true;
-
-
# keep hm in .local/state since we are using nix-portable
-
xdg.enable = true;
-
-
# Nicely reload system units when changing configs
-
systemd.user.startServices = "sd-switch";
-
-
# https://nixos.wiki/wiki/FAQ/When_do_I_update_stateVersion
-
home.stateVersion = "23.05";
-
}
-2
machines/moonlark/home/default.nix
···
(inputs.import-tree ../../../modules/home)
];
-
nixpkgs.enable = true;
-
home = {
username = "kierank";
homeDirectory = "/home/kierank";
+1
machines/moonlark/home-manager.nix
···
];
home-manager = {
+
useGlobalPkgs = true;
extraSpecialArgs = {
inherit inputs outputs;
};
+1
machines/nest/default.nix
···
{
imports = [
(inputs.import-tree ../../modules/home)
+
../../modules/home/system/nixpkgs.nix.disabled
];
nixpkgs.enable = true;
-2
machines/prattle/home/default.nix
···
(inputs.import-tree ../../../modules/home)
];
-
nixpkgs.enable = true;
-
home = {
username = "kierank";
homeDirectory = "/home/kierank";
+1
machines/prattle/home-manager.nix
···
];
home-manager = {
+
useGlobalPkgs = true;
extraSpecialArgs = {
inherit inputs outputs;
};
+1
machines/tacyon/default.nix
···
{
imports = [
(inputs.import-tree ../../modules/home)
+
../../modules/home/system/nixpkgs.nix.disabled
];
nixpkgs.enable = true;
-2
machines/terebithia/home/default.nix
···
(inputs.import-tree ../../../modules/home)
];
-
nixpkgs.enable = true;
-
home = {
username = "kierank";
homeDirectory = "/home/kierank";
+1
machines/terebithia/home-manager.nix
···
];
home-manager = {
+
useGlobalPkgs = true;
extraSpecialArgs = {
inherit inputs outputs;
};
+28 -4
modules/home/apps/bore/bore.1.md
···
# SYNOPSIS
-
**bore** [*SUBDOMAIN*] [*PORT*] [**--label** *LABEL*] [**--save**]
+
**bore** [*SUBDOMAIN*] [*PORT*] [**--protocol** *PROTOCOL*] [**--label** *LABEL*] [**--save**]
**bore** **--list** | **-l**
···
# DESCRIPTION
-
**bore** is a tunneling service that uses frp (fast reverse proxy) to expose local services to the internet via bore.dunkirk.sh. It provides a simple CLI for creating and managing HTTP tunnels with optional labels and persistent configuration.
+
**bore** is a tunneling service that uses frp (fast reverse proxy) to expose local services to the internet via bore.dunkirk.sh. It provides a simple CLI for creating and managing HTTP, TCP, and UDP tunnels with optional labels and persistent configuration.
# OPTIONS
···
**-s**, **--saved**
: List all saved tunnel configurations from bore.toml in the current directory.
+
+
**-p**, **--protocol** *PROTOCOL*
+
: Specify the protocol to use for the tunnel: **http** (default), **tcp**, or **udp**.
**--label** *LABEL*
: Assign a label/tag to the tunnel for organization and identification.
···
[api]
port = 3000
+
protocol = "http"
label = "dev"
+
+
[database]
+
port = 5432
+
protocol = "tcp"
+
label = "postgres"
+
+
[game-server]
+
port = 27015
+
protocol = "udp"
+
label = "game"
```
When running **bore** without arguments in a directory with bore.toml, you'll be prompted to choose between creating a new tunnel or using a saved configuration.
# EXAMPLES
-
Create a simple tunnel:
+
Create a simple HTTP tunnel:
```
$ bore myapp 8000
```
-
Create a tunnel with a label:
+
Create an HTTP tunnel with a label:
```
$ bore api 3000 --label dev
+
```
+
+
Create a TCP tunnel for a database:
+
```
+
$ bore database 5432 --protocol tcp --label postgres
+
```
+
+
Create a UDP tunnel for a game server:
+
```
+
$ bore game-server 27015 --protocol udp --label game
```
Save a tunnel configuration:
+7 -1
modules/home/apps/bore/completions/bore.bash
···
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
-
opts="--list --saved --label --save -l -s"
+
opts="--list --saved --protocol --label --save -l -s -p"
# Complete flags
if [[ ${cur} == -* ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+
return 0
+
fi
+
+
# Complete protocol values after --protocol or -p
+
if [[ ${prev} == "--protocol" ]] || [[ ${prev} == "-p" ]]; then
+
COMPREPLY=( $(compgen -W "http tcp udp" -- ${cur}) )
return 0
fi
+1
modules/home/apps/bore/completions/bore.fish
···
# Complete flags
complete -c bore -s l -l list -d 'List active tunnels'
complete -c bore -s s -l saved -d 'List saved tunnels from bore.toml'
+
complete -c bore -s p -l protocol -d 'Specify protocol' -xa 'http tcp udp'
complete -c bore -l label -d 'Assign a label to the tunnel' -r
complete -c bore -l save -d 'Save tunnel configuration to bore.toml'
+2
modules/home/apps/bore/completions/bore.zsh
···
'-l[List active tunnels]' \
'--saved[List saved tunnels from bore.toml]' \
'-s[List saved tunnels from bore.toml]' \
+
'--protocol[Specify protocol]:protocol:(http tcp udp)' \
+
'-p[Specify protocol]:protocol:(http tcp udp)' \
'--label[Assign a label to the tunnel]:label:' \
'--save[Save tunnel configuration to bore.toml]' \
&& return 0
+195 -38
modules/home/apps/bore/default.nix
···
fi
# Filter only online tunnels with valid conf
-
echo "$tunnels" | ${pkgs.jq}/bin/jq -r '.proxies[] | select(.status == "online" and .conf != null) | "\(.name) → https://\(.conf.subdomain).${cfg.domain}"' | while read -r line; do
+
echo "$tunnels" | ${pkgs.jq}/bin/jq -r '.proxies[] | select(.status == "online" and .conf != null) | if .type == "http" then "\(.name) → https://\(.conf.subdomain).${cfg.domain} [http]" elif .type == "tcp" then "\(.name) → tcp://\(.conf.remotePort) → localhost:\(.conf.localPort) [tcp]" elif .type == "udp" then "\(.name) → udp://\(.conf.remotePort) → localhost:\(.conf.localPort) [udp]" else "\(.name) [\(.type)]" end' | while read -r line; do
${pkgs.gum}/bin/gum style --foreground 35 "✓ $line"
done
exit 0
···
current_tunnel="''${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then
port="''${BASH_REMATCH[1]}"
+
elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
+
protocol="''${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
label="''${BASH_REMATCH[1]}"
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$label]"
+
proto_display="''${protocol:-http}"
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display] [$label]"
label=""
+
protocol=""
elif [[ -z "$line" ]] && [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port"
+
proto_display="''${protocol:-http}"
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display]"
current_tunnel=""
port=""
+
protocol=""
fi
done < "$CONFIG_FILE"
# Handle last entry if file doesn't end with blank line
if [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then
+
proto_display="''${protocol:-http}"
if [[ -n "$label" ]]; then
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$label]"
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display] [$label]"
else
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port"
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display]"
fi
fi
exit 0
fi
-
# Get subdomain
+
# Get tunnel name/subdomain
if [ -n "$1" ]; then
-
subdomain="$1"
+
tunnel_name="$1"
else
# Check if we have a bore.toml in current directory
if [ -f "$CONFIG_FILE" ]; then
···
if [ "$choice" = "Use saved tunnel" ]; then
# Extract tunnel names from TOML
saved_names=$(${pkgs.gnugrep}/bin/grep '^\[' "$CONFIG_FILE" | ${pkgs.gnused}/bin/sed 's/^\[\(.*\)\]$/\1/')
-
subdomain=$(echo "$saved_names" | ${pkgs.gum}/bin/gum choose)
+
tunnel_name=$(echo "$saved_names" | ${pkgs.gum}/bin/gum choose)
-
if [ -z "$subdomain" ]; then
+
if [ -z "$tunnel_name" ]; then
${pkgs.gum}/bin/gum style --foreground 196 "No tunnel selected"
exit 1
fi
···
in_section=false
while IFS= read -r line; do
if [[ "$line" =~ ^\[([^]]+)\] ]]; then
-
if [[ "''${BASH_REMATCH[1]}" = "$subdomain" ]]; then
+
if [[ "''${BASH_REMATCH[1]}" = "$tunnel_name" ]]; then
in_section=true
else
in_section=false
···
elif [[ "$in_section" = true ]]; then
if [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then
port="''${BASH_REMATCH[1]}"
+
elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
+
protocol="''${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
label="''${BASH_REMATCH[1]}"
fi
fi
done < "$CONFIG_FILE"
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ Loaded from bore.toml: $subdomain → localhost:$port''${label:+ [$label]}"
+
proto_display="''${protocol:-http}"
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ Loaded from bore.toml: $tunnel_name → localhost:$port [$proto_display]''${label:+ [$label]}"
else
-
# New tunnel
-
subdomain=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
-
if [ -z "$subdomain" ]; then
-
${pkgs.gum}/bin/gum style --foreground 196 "No subdomain provided"
+
# New tunnel - prompt for protocol first to determine what to ask for
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
+
if [ -z "$protocol" ]; then
+
protocol="http"
+
fi
+
+
if [ "$protocol" = "http" ]; then
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
+
else
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
+
fi
+
+
if [ -z "$tunnel_name" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
exit 1
fi
fi
else
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
echo
-
subdomain=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
-
if [ -z "$subdomain" ]; then
-
${pkgs.gum}/bin/gum style --foreground 196 "No subdomain provided"
+
# Prompt for protocol first
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
+
if [ -z "$protocol" ]; then
+
protocol="http"
+
fi
+
+
if [ "$protocol" = "http" ]; then
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
+
else
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
+
fi
+
+
if [ -z "$tunnel_name" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
exit 1
fi
fi
else
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
echo
-
subdomain=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
-
if [ -z "$subdomain" ]; then
-
${pkgs.gum}/bin/gum style --foreground 196 "No subdomain provided"
+
# Prompt for protocol first
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
+
if [ -z "$protocol" ]; then
+
protocol="http"
+
fi
+
+
if [ "$protocol" = "http" ]; then
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
+
else
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
+
fi
+
+
if [ -z "$tunnel_name" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
exit 1
fi
fi
fi
-
# Validate subdomain
-
if ! echo "$subdomain" | ${pkgs.gnugrep}/bin/grep -qE '^[a-z0-9-]+$'; then
-
${pkgs.gum}/bin/gum style --foreground 196 "Invalid subdomain (use only lowercase letters, numbers, and hyphens)"
-
exit 1
+
# Validate tunnel name (only for http subdomains)
+
if [ "$protocol" = "http" ]; then
+
if ! echo "$tunnel_name" | ${pkgs.gnugrep}/bin/grep -qE '^[a-z0-9-]+$'; then
+
${pkgs.gum}/bin/gum style --foreground 196 "Invalid subdomain (use only lowercase letters, numbers, and hyphens)"
+
exit 1
+
fi
fi
# Get port (skip if loaded from saved config)
···
exit 1
fi
-
# Get optional label and save flag (skip if loaded from saved config)
+
# Get optional protocol, label and save flag (skip if loaded from saved config)
save_config=false
if [ -z "$label" ]; then
shift 2 2>/dev/null || true
while [[ $# -gt 0 ]]; do
case "$1" in
+
--protocol|-p)
+
protocol="$2"
+
shift 2
+
;;
--label|-l)
label="$2"
shift 2
···
;;
esac
done
+
+
# Prompt for protocol if not provided via flag and not loaded from saved config and not already set
+
if [ -z "$protocol" ]; then
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
+
if [ -z "$protocol" ]; then
+
protocol="http"
+
fi
+
fi
+
+
# Prompt for label if not provided via flag and not loaded from saved config
+
if [ -z "$label" ]; then
+
# Allow multiple labels selection
+
labels=$(${pkgs.gum}/bin/gum choose --no-limit --header "Labels (select multiple):" "dev" "prod" "custom")
+
+
if [ -n "$labels" ]; then
+
# Check if custom was selected
+
if echo "$labels" | ${pkgs.gnugrep}/bin/grep -q "custom"; then
+
custom_label=$(${pkgs.gum}/bin/gum input --placeholder "my-label" --prompt "Custom label: ")
+
if [ -z "$custom_label" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No custom label provided"
+
exit 1
+
fi
+
# Replace 'custom' with the actual custom label
+
labels=$(echo "$labels" | ${pkgs.gnused}/bin/sed "s/custom/$custom_label/")
+
fi
+
# Join labels with comma
+
label=$(echo "$labels" | ${pkgs.coreutils}/bin/tr '\n' ',' | ${pkgs.gnused}/bin/sed 's/,$//')
+
fi
+
fi
+
fi
+
+
# Default protocol to http if still not set
+
if [ -z "$protocol" ]; then
+
protocol="http"
fi
# Check if local port is accessible
···
# Save configuration if requested
if [ "$save_config" = true ]; then
# Check if tunnel already exists in TOML
-
if [ -f "$CONFIG_FILE" ] && ${pkgs.gnugrep}/bin/grep -q "^\[$subdomain\]" "$CONFIG_FILE"; then
+
if [ -f "$CONFIG_FILE" ] && ${pkgs.gnugrep}/bin/grep -q "^\[$tunnel_name\]" "$CONFIG_FILE"; then
# Update existing entry
-
${pkgs.gnused}/bin/sed -i "/^\[$subdomain\]/,/^\[/{
+
${pkgs.gnused}/bin/sed -i "/^\[$tunnel_name\]/,/^\[/{
s/^port[[:space:]]*=.*/port = $port/
+
s/^protocol[[:space:]]*=.*/protocol = \"$protocol\"/
''${label:+s/^label[[:space:]]*=.*/label = \"$label\"/}
}" "$CONFIG_FILE"
else
# Append new entry
{
echo ""
-
echo "[$subdomain]"
+
echo "[$tunnel_name]"
echo "port = $port"
+
if [ "$protocol" != "http" ]; then
+
echo "protocol = \"$protocol\""
+
fi
if [ -n "$label" ]; then
echo "label = \"$label\""
fi
···
config_file=$(${pkgs.coreutils}/bin/mktemp)
trap "${pkgs.coreutils}/bin/rm -f $config_file" EXIT
-
# Encode label into proxy name if provided (format: subdomain[label])
-
proxy_name="$subdomain"
+
# Encode label into proxy name if provided (format: tunnel_name[label1,label2])
+
proxy_name="$tunnel_name"
if [ -n "$label" ]; then
-
proxy_name="''${subdomain}[''${label}]"
+
proxy_name="''${tunnel_name}[''${label}]"
fi
-
${pkgs.coreutils}/bin/cat > $config_file <<EOF
+
# Build proxy configuration based on protocol
+
if [ "$protocol" = "http" ]; then
+
${pkgs.coreutils}/bin/cat > $config_file <<EOF
serverAddr = "${cfg.serverAddr}"
serverPort = ${toString cfg.serverPort}
···
type = "http"
localIP = "127.0.0.1"
localPort = $port
-
subdomain = "$subdomain"
+
subdomain = "$tunnel_name"
EOF
+
elif [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then
+
# For TCP/UDP, enable admin API to query allocated port
+
# Use Python to find a free port (cross-platform and guaranteed to work)
+
admin_port=$(${pkgs.python3}/bin/python3 -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()')
+
+
${pkgs.coreutils}/bin/cat > $config_file <<EOF
+
serverAddr = "${cfg.serverAddr}"
+
serverPort = ${toString cfg.serverPort}
+
+
auth.method = "token"
+
auth.tokenSource.type = "file"
+
auth.tokenSource.file.path = "${cfg.authTokenFile}"
+
+
webServer.addr = "127.0.0.1"
+
webServer.port = $admin_port
+
+
[[proxies]]
+
name = "$proxy_name"
+
type = "$protocol"
+
localIP = "127.0.0.1"
+
localPort = $port
+
remotePort = 0
+
EOF
+
else
+
${pkgs.gum}/bin/gum style --foreground 196 "Invalid protocol: $protocol (must be http, tcp, or udp)"
+
exit 1
+
fi
# Start tunnel
-
public_url="https://$subdomain.${cfg.domain}"
echo
${pkgs.gum}/bin/gum style --foreground 35 "✓ Tunnel configured"
-
${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
-
${pkgs.gum}/bin/gum style --foreground 117 " Public: $public_url"
+
${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
+
if [ "$protocol" = "http" ]; then
+
public_url="https://$tunnel_name.${cfg.domain}"
+
${pkgs.gum}/bin/gum style --foreground 117 " Public: $public_url"
+
else
+
${pkgs.gum}/bin/gum style --foreground 117 " Protocol: $protocol"
+
${pkgs.gum}/bin/gum style --foreground 214 " Waiting for server to allocate port..."
+
fi
echo
${pkgs.gum}/bin/gum style --foreground 214 "Connecting to ${cfg.serverAddr}:${toString cfg.serverPort}..."
+
echo
-
exec ${pkgs.frp}/bin/frpc -c $config_file
+
# For TCP/UDP, capture output to parse allocated port
+
if [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then
+
# Start frpc in background and capture its PID
+
${pkgs.frp}/bin/frpc -c $config_file 2>&1 | while IFS= read -r line; do
+
echo "$line"
+
+
# Look for successful proxy start
+
if echo "$line" | ${pkgs.gnugrep}/bin/grep -q "start proxy success"; then
+
# Wait a moment for the proxy to fully initialize
+
sleep 1
+
+
# Query the frpc admin API for proxy status
+
proxy_status=$(${pkgs.curl}/bin/curl -s http://127.0.0.1:$admin_port/api/status 2>/dev/null || echo "{}")
+
+
# Try to extract remote port from JSON response
+
# Format: "remote_addr":"bore.dunkirk.sh:20097"
+
remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".tcp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null)
+
if [ -z "$remote_addr" ] || [ "$remote_addr" = "null" ]; then
+
remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".udp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null)
+
fi
+
+
# Extract just the port number
+
remote_port=$(echo "$remote_addr" | ${pkgs.gnugrep}/bin/grep -oP ':\K[0-9]+$')
+
+
if [ -n "$remote_port" ] && [ "$remote_port" != "null" ]; then
+
echo
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ Tunnel established"
+
${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
+
${pkgs.gum}/bin/gum style --foreground 117 " Remote: ${cfg.serverAddr}:$remote_port"
+
${pkgs.gum}/bin/gum style --foreground 117 " Type: $protocol"
+
echo
+
fi
+
fi
+
done
+
else
+
exec ${pkgs.frp}/bin/frpc -c $config_file
+
fi
'';
bore = pkgs.stdenv.mkDerivation {
+6 -11
modules/home/apps/ssh.nix
···
};
config = mkIf cfg.enable {
-
# On macOS (darwin), zmx must be installed manually due to build issues
-
# Download from: https://zmx.sh/a/zmx-0.0.2-macos-aarch64.tar.gz
-
# Extract and place in PATH (e.g., ~/.local/bin/)
-
-
# On Linux, use the zmx flake
+
# zmx provides pre-built binaries that we download instead of building from source
+
# This avoids the zig2nix dependency which causes issues in CI
home.packages =
-
(optionals (cfg.zmx.enable && !pkgs.stdenv.isDarwin) [
-
inputs.zmx.packages.${pkgs.stdenv.hostPlatform.system}.default
-
])
-
++ (optionals cfg.zmx.enable [
+
(optionals cfg.zmx.enable [
+
pkgs.zmx-binary
pkgs.autossh
]);
···
extraOptions = hostCfg.extraOptions // (
if hostCfg.zmx then
{
-
RemoteCommand = "zmx attach %n";
+
RemoteCommand = "export PATH=$HOME/.nix-profile/bin:$PATH; zmx attach %n";
RequestTTY = "yes";
ControlPath = "~/.ssh/cm-%r@%h:%p";
ControlMaster = "auto";
···
port = mkIf (patternHost.port or null != null) patternHost.port;
user = mkIf (patternHost.user or null != null) patternHost.user;
extraOptions = {
-
RemoteCommand = "zmx attach %k";
+
RemoteCommand = "export PATH=$HOME/.nix-profile/bin:$PATH; zmx attach %k";
RequestTTY = "yes";
ControlPath = "~/.ssh/cm-%r@%h:%p";
ControlMaster = "auto";
-85
modules/home/apps/vscode.nix
···
-
{
-
lib,
-
pkgs,
-
config,
-
inputs,
-
...
-
}:
-
{
-
options.atelier.apps.vscode.enable = lib.mkEnableOption "Enable VSCode config";
-
config = lib.mkIf config.atelier.apps.vscode.enable {
-
nixpkgs.overlays = [
-
inputs.nix-vscode-extensions.overlays.default
-
inputs.catppuccin-vsc.overlays.default
-
];
-
programs.vscode = {
-
enable = true;
-
package = pkgs.unstable.vscode;
-
profiles.default = {
-
extensions = with pkgs.vscode-marketplace; [
-
ms-vscode.live-server
-
formulahendry.auto-rename-tag
-
edwinkofler.vscode-assorted-languages
-
golang.go
-
eamodio.gitlens
-
yzhang.markdown-all-in-one
-
github.vscode-github-actions
-
yoavbls.pretty-ts-errors
-
esbenp.prettier-vscode
-
ms-vscode.vscode-serial-monitor
-
prisma.prisma
-
ms-azuretools.vscode-docker
-
astro-build.astro-vscode
-
github.copilot
-
github.copilot-chat
-
dotjoshjohnson.xml
-
mikestead.dotenv
-
bradlc.vscode-tailwindcss
-
mechatroner.rainbow-csv
-
wakatime.vscode-wakatime
-
paulober.pico-w-go
-
ms-python.python
-
karunamurti.tera
-
biomejs.biome
-
bschulte.love
-
yinfei.luahelper
-
tamasfe.even-better-toml
-
fill-labs.dependi
-
rust-lang.rust-analyzer
-
dustypomerleau.rust-syntax
-
catppuccin.catppuccin-vsc
-
inputs.frc-nix.packages.${pkgs.stdenv.hostPlatform.system}.vscode-wpilib
-
];
-
userSettings = {
-
"editor.semanticHighlighting.enabled" = true;
-
"terminal.integrated.minimumContrastRatio" = 1;
-
"window.titleBarStyle" = "custom";
-
"gopls" = {
-
"ui.semanticTokens" = true;
-
};
-
"workbench.colorTheme" = "Catppuccin Macchiato";
-
"workbench.iconTheme" = "catppuccin-macchiato";
-
"catppuccin.accentColor" = lib.mkForce "blue";
-
"editor.fontFamily" = "'FiraCode Nerd Font', 'monospace', monospace";
-
"git.autofetch" = true;
-
"git.confirmSync" = false;
-
"github.copilot.editor.enableAutoCompletions" = false;
-
"editor.formatOnSave" = true;
-
"editor.defaultFormatter" = "biomejs.biome";
-
"[go]" = {
-
"editor.defaultFormatter" = "golang.go";
-
};
-
"[yaml]" = {
-
"editor.defaultFormatter" = "esbenp.prettier-vscode";
-
};
-
"[lua]" = {
-
"editor.defaultFormatter" = "yinfei.luahelper";
-
};
-
"[html]" = {
-
"editor.defaultFormatter" = "esbenp.prettier-vscode";
-
};
-
};
-
};
-
};
-
};
-
}
-26
modules/home/system/nixpkgs.nix
···
-
{
-
lib,
-
pkgs,
-
config,
-
inputs,
-
...
-
}:
-
{
-
options.nixpkgs.enable = lib.mkEnableOption "Enable custom nixpkgs overlays/config";
-
config = lib.mkIf config.nixpkgs.enable {
-
nixpkgs = {
-
overlays = [
-
(final: prev: {
-
unstable = import inputs.nixpkgs-unstable {
-
inherit (pkgs.stdenv.hostPlatform) system;
-
config.allowUnfree = true;
-
};
-
})
-
];
-
config = {
-
allowUnfree = true;
-
allowUnfreePredicate = _: true;
-
};
-
};
-
};
-
}
+26
modules/home/system/nixpkgs.nix.disabled
···
+
{
+
lib,
+
pkgs,
+
config,
+
inputs,
+
...
+
}:
+
{
+
options.nixpkgs.enable = lib.mkEnableOption "Enable custom nixpkgs overlays/config";
+
config = lib.mkIf config.nixpkgs.enable {
+
nixpkgs = {
+
overlays = [
+
(final: prev: {
+
unstable = import inputs.nixpkgs-unstable {
+
inherit (pkgs.stdenv.hostPlatform) system;
+
config.allowUnfree = true;
+
};
+
})
+
];
+
config = {
+
allowUnfree = true;
+
allowUnfreePredicate = _: true;
+
};
+
};
+
};
+
}
+48
modules/nixos/services/bore/README.md
···
+
# Bore
+
+
![screenshot](https://hc-cdn.hel1.your-objectstorage.com/s/v3/7652f29dacb8f76d_screenshot_2025-12-09_at_16.57.47.png)
+
+
Bore is a lightweight wrapper around `frp` which provides a dashboard and a nice `gum` based cli. It supports HTTP, TCP, and UDP tunneling. If you would like to run this in your own nix flake then simplify vendor this folder and `./modules/home/bore` and import the folders into the appropriate home manager and nixos configurations.
+
+
## Client Configuration
+
+
```nix
+
atelier = {
+
bore = {
+
enable = true;
+
authTokenFile = osConfig.age.secrets.bore.path
+
};
+
}
+
```
+
+
and be sure to have a definition for your agenix secret in the osConfig as well:
+
+
```nix
+
age = {
+
identityPaths = [
+
"path/to/ssh/key"
+
];
+
secrets = {
+
bore = {
+
file = ./path/to/bore.age;
+
owner = "username";
+
};
+
};
+
}
+
```
+
+
## Server Configuration
+
+
For TCP and UDP tunneling support, configure the server with allowed port ranges:
+
+
```nix
+
atelier.services.frps = {
+
enable = true;
+
domain = "bore.dunkirk.sh";
+
authTokenFile = config.age.secrets.bore.path;
+
allowedTCPPorts = [ 20000 20001 20002 20003 20004 ];
+
allowedUDPPorts = [ 20000 20001 20002 20003 20004 ];
+
};
+
```
+
+
The secret file is just a oneline file with the key in it. If you do end up deploying this feel free to email me and let me know! I would love to hear about your setup!
+23 -7
modules/nixos/services/bore/bore.nix
···
description = "Port for HTTP virtual host traffic";
};
+
allowedTCPPorts = lib.mkOption {
+
type = lib.types.listOf lib.types.port;
+
default = lib.lists.range 20000 20099;
+
example = [ 20000 20001 20002 20003 20004 ];
+
description = "TCP port range to allow for TCP tunnels (default: 20000-20099)";
+
};
+
+
allowedUDPPorts = lib.mkOption {
+
type = lib.types.listOf lib.types.port;
+
default = lib.lists.range 20000 20099;
+
example = [ 20000 20001 20002 20003 20004 ];
+
description = "UDP port range to allow for UDP tunnels (default: 20000-20099)";
+
};
+
authToken = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
···
}
];
-
# Open firewall port for frp control connection
-
networking.firewall.allowedTCPPorts = [ cfg.bindPort ];
+
# Open firewall ports for frp control connection and TCP/UDP tunnels
+
networking.firewall.allowedTCPPorts = [ cfg.bindPort ] ++ cfg.allowedTCPPorts;
+
networking.firewall.allowedUDPPorts = cfg.allowedUDPPorts;
# frp server service
systemd.services.frps =
···
# Subdomain support for *.${cfg.domain}
subDomainHost = "${cfg.domain}"
+
+
# Allow port ranges for TCP/UDP tunnels
+
# Format: [[{"start": 20000, "end": 20099}]]
+
allowPorts = [
+
{ start = 20000, end = 20099 }
+
]
# Custom 404 page
custom404Page = "${./404.html}"
···
}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
-
}
-
-
# Proxy /metrics to frps dashboard
-
handle /metrics {
-
reverse_proxy localhost:7400
}
# Proxy /api/* to frps dashboard
+11
modules/nixos/services/bore/bore.toml.example
···
[api]
port = 3000
+
protocol = "http"
label = "dev"
[frontend]
port = 5173
label = "local"
+
+
[database]
+
port = 5432
+
protocol = "tcp"
+
label = "postgres"
+
+
[game-server]
+
port = 27015
+
protocol = "udp"
+
label = "game"
+67 -12
modules/nixos/services/bore/dashboard.html
···
.tunnel-label {
display: inline-block;
padding: 0.125rem 0.5rem;
-
background: rgba(251, 146, 60, 0.2);
-
color: #fb923c;
-
border: 1px solid #fb923c;
font-size: 0.7rem;
font-weight: 500;
border-radius: 0;
+
margin-left: 0.25rem;
+
border: 1px solid;
}
.tunnel-url {
···
const MAX_FAIL_COUNT = 3;
let lastProxiesState = null;
+
// Predefined color palette for labels
+
const labelColors = [
+
{ color: '#a78bfa', bg: 'rgba(167, 139, 250, 0.2)' }, // purple
+
{ color: '#f472b6', bg: 'rgba(244, 114, 182, 0.2)' }, // pink
+
{ color: '#facc15', bg: 'rgba(250, 204, 21, 0.2)' }, // yellow
+
{ color: '#60a5fa', bg: 'rgba(96, 165, 250, 0.2)' }, // blue
+
{ color: '#f87171', bg: 'rgba(248, 113, 113, 0.2)' }, // red
+
{ color: '#38bdf8', bg: 'rgba(56, 189, 248, 0.2)' }, // sky
+
{ color: '#c084fc', bg: 'rgba(192, 132, 252, 0.2)' }, // violet
+
{ color: '#fb7185', bg: 'rgba(251, 113, 133, 0.2)' }, // rose
+
];
+
+
// Hash string to index
+
function stringToColorIndex(str) {
+
let hash = 0;
+
for (let i = 0; i < str.length; i++) {
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
+
}
+
return Math.abs(hash) % labelColors.length;
+
}
+
+
// Get label color and styles
+
function getLabelStyle(label) {
+
const trimmedLabel = label.trim();
+
if (trimmedLabel === 'prod') {
+
return {
+
color: '#22c55e',
+
bgColor: 'rgba(34, 197, 94, 0.2)',
+
borderColor: '#22c55e'
+
};
+
}
+
+
if (trimmedLabel === 'dev') {
+
return {
+
color: '#fb923c',
+
bgColor: 'rgba(251, 146, 60, 0.2)',
+
borderColor: '#fb923c'
+
};
+
}
+
+
const colorIndex = stringToColorIndex(trimmedLabel);
+
const colorScheme = labelColors[colorIndex];
+
return {
+
color: colorScheme.color,
+
bgColor: colorScheme.bg,
+
borderColor: colorScheme.color
+
};
+
}
+
async function fetchStats() {
try {
// Fetch server info
···
const subdomain = proxy.conf?.subdomain || 'unknown';
const url = `https://${subdomain}.bore.dunkirk.sh`;
-
// Parse label from proxy name (format: subdomain[label])
+
// Parse labels from proxy name (format: subdomain[label1,label2])
const labelMatch = proxy.name.match(/\[([^\]]+)\]$/);
-
const label = labelMatch ? labelMatch[1] : null;
-
const displayName = label ? proxy.name.replace(/\[[^\]]+\]$/, '') : proxy.name;
+
const labels = labelMatch ? labelMatch[1].split(',') : [];
+
const displayName = labels.length > 0 ? proxy.name.replace(/\[[^\]]+\]$/, '') : proxy.name;
+
+
const labelHtml = labels.map(label => {
+
const trimmedLabel = label.trim();
+
const style = getLabelStyle(trimmedLabel);
+
return `<span class="tunnel-label" style="color: ${style.color}; background: ${style.bgColor}; border-color: ${style.borderColor};">${trimmedLabel}</span>`;
+
}).join('');
return `
<div class="tunnel" data-tunnel="${proxy.name}">
<div class="tunnel-info">
<div class="tunnel-name">
${displayName || 'unnamed'}
-
${label ? `<span class="tunnel-label">${label}</span>` : ''}
+
${labelHtml}
</div>
<div class="tunnel-url">
<a href="${url}" target="_blank">${url}</a>
···
html += '<div class="offline-tunnels">';
html += '<div style="color: #8b949e; font-size: 0.85rem; margin-bottom: 0.75rem;">recently disconnected</div>';
html += offlineTunnels.map(proxy => {
-
// Parse label from proxy name (format: subdomain[label])
+
// Parse labels from proxy name (format: subdomain[label1,label2])
const labelMatch = proxy.name.match(/\[([^\]]+)\]$/);
-
const label = labelMatch ? labelMatch[1] : null;
-
const displayName = label ? proxy.name.replace(/\[[^\]]+\]$/, '') : proxy.name;
+
const labels = labelMatch ? labelMatch[1].split(',').map(l => l.trim()) : [];
+
const displayName = labels.length > 0 ? proxy.name.replace(/\[[^\]]+\]$/, '') : proxy.name;
+
const labelStr = labels.length > 0 ? ` [${labels.join(', ')}]` : '';
if (!proxy.conf) {
return `
<div class="offline-tunnel" data-tunnel="${proxy.name}">
-
<span class="offline-tunnel-name">${displayName || 'unnamed'}${label ? ` [${label}]` : ''}</span>
+
<span class="offline-tunnel-name">${displayName || 'unnamed'}${labelStr}</span>
<span class="offline-tunnel-stats">in: <span data-traffic-in="${proxy.name}">0 B</span> • out: <span data-traffic-out="${proxy.name}">0 B</span></span>
</div>
`;
···
const url = `https://${subdomain}.bore.dunkirk.sh`;
return `
<div class="offline-tunnel" data-tunnel="${proxy.name}">
-
<span class="offline-tunnel-name">${displayName || 'unnamed'}${label ? ` [${label}]` : ''} → ${url}</span>
+
<span class="offline-tunnel-name">${displayName || 'unnamed'}${labelStr} → ${url}</span>
<span class="offline-tunnel-stats">in: <span data-traffic-in="${proxy.name}">0 B</span> • out: <span data-traffic-out="${proxy.name}">0 B</span></span>
</div>
`;
+48
packages/zmx.nix
···
+
{ pkgs, lib, stdenv, fetchurl, autoPatchelfHook }:
+
+
stdenv.mkDerivation rec {
+
pname = "zmx";
+
version = "0.1.0";
+
+
src = fetchurl {
+
url = if stdenv.isLinux then
+
(if stdenv.isAarch64 then
+
"https://zmx.sh/a/zmx-${version}-linux-aarch64.tar.gz"
+
else
+
"https://zmx.sh/a/zmx-${version}-linux-x86_64.tar.gz")
+
else if stdenv.isDarwin then
+
(if stdenv.isAarch64 then
+
"https://zmx.sh/a/zmx-${version}-macos-aarch64.tar.gz"
+
else
+
"https://zmx.sh/a/zmx-${version}-macos-x86_64.tar.gz")
+
else throw "Unsupported platform";
+
+
hash = if stdenv.isLinux && stdenv.isAarch64 then
+
"sha256-sv83lR4DLJE+gsMtqCk6VCFdo5n4lhI0P1loxAf0iOg="
+
else if stdenv.isLinux then
+
"sha256-c+wCUcm7DEO55wXuHq0aP0Kn908jj1FM5Z+JQJnKE0M="
+
else if stdenv.isDarwin && stdenv.isAarch64 then
+
"sha256-dM6MFikdbpN+n8BK6fLbzyJfi88xetCWL9H5VfGB07o="
+
else
+
"sha256-B52NC8NEjVPDNSG11qPb0uRNExB66bllnK7ivXMJbHk=";
+
};
+
+
nativeBuildInputs = lib.optionals stdenv.isLinux [ autoPatchelfHook ];
+
+
sourceRoot = ".";
+
+
installPhase = ''
+
runHook preInstall
+
mkdir -p $out/bin
+
cp zmx $out/bin/
+
chmod +x $out/bin/zmx
+
runHook postInstall
+
'';
+
+
meta = with lib; {
+
description = "Session persistence for terminal processes";
+
homepage = "https://zmx.sh";
+
license = licenses.mit;
+
platforms = platforms.unix;
+
};
+
}