TechHeart’s Custom Omarchy Linux - 2026!
⬇️ Download the config and sound files HERE!
This guide shows you how to recreate our custom Omarchy Linux without overwriting upstream Omarchy scripts. The key idea is: wrap + patch at runtime, and keep your custom logic in your own files.
Im going to explain what to create and where to put it — and inside this blog post you’ll see code blocks for each script/config. You can copy/paste them, tweak a few variables (NAS IP, mount points, VPN service name, etc.), and you’re done.
What you’ll end up with:
Update-proof Omarchy menu patcher (adds a Custom submenu at runtime)
VPN control (PIA + PiVPN) + optional Waybar modules
NAS smart mount (LAN vs remote)
Safe Customization Status screen (won’t hang even if NFS is dead)
Screensaver mode toggles (stock vs long hypridle)
Windows startup/shutdown sound themes + sound-on-power actions
GParted fix for Wayland (pkexec env)
Amiga fonts + Alacritty font selection
Smart Fastfetch MOTD (logo only when it fits)
A few QoL Hyprland binds (cheatsheet launcher + emoji picker)
Optional PIA autoconnect systemd user service
Before you start: dependencies + assumptions
You’re on Omarchy / Hyprland / Wayland.
Recommended installs (names may vary by distro repo, but Arch/Omarchy users know the drill):
jq (required for the “smart MOTD” that checks workspace windows)
fastfetch
mpv (or ffplay / pw-play / paplay for audio playback)
gparted + polkit (for pkexec)
waybar (optional, only if you want the status modules)
emote (optional emoji picker)
Important: Every file below that is a script must be executable.
You can chmod +x each script as you create it, but I’ll also give you one
“do it all at once” command near the end.
VPN assumptions:
PIA CLI exists at /opt/piavpn/bin/piactl (or you’ll change the path)
PiVPN uses an OpenVPN client systemd unit (example: openvpn-client@pivpn)
NAS assumptions:
You mount NFS exports into /mnt/…
You know your NAS IP and export layout
📕 Part 1 — The “Update-Proof” Omarchy Menu Wrapper (the core concept)
Why this matters
Omarchy updates can overwrite upstream scripts. If you directly edit Omarchy’s menu, your changes can get nuked.
So instead, we do:
Don’t change Omarchy’s keybind (SUPER+ALT+SPACE still runs omarchy-menu)
Create a wrapper that “wins” in PATH
Wrapper runs a patcher script that:
reads upstream menu
patches a temp copy in /tmp
executes patched menu
falls back to stock if patch fails
Step 1: Add PATH precedence (so your wrapper wins)
Open/create:
~/.config/uwsm/env
Add (or ensure) PATH is ordered like this:
export OMARCHY_PATH=$HOME/.local/share/omarchy export PATH=$HOME/.local/bin:$OMARCHY_PATH/bin:$PATH
In this post, I’ll show the full file:
~/.config/uwsm/env:
# ~/.config/uwsm/env
#
# This file is sourced by UWSM (your session/environment manager) and sets
# environment variables used by Hyprland/Wayland apps.
#
# What this does (for your Omarchy customizations):
# - Ensures Omarchy’s binaries are available in PATH for menu helpers like:
# omarchy-menu, omarchy-launch-*, omarchy-cmd-*
# - Ensures ~/.local/bin is also in PATH (where your wrapper scripts live)
#
# Other settings:
# - Sets cursor theme + size
# - Sources UWSM defaults (terminal/editor)
# - Optionally activates `mise` if Omarchy’s present-command wrapper says it exists
# Changes require a relaunch of Hyprland to take effect.
# Ensure Omarchy bins are in the path
export OMARCHY_PATH=$HOME/.local/share/omarchy
export PATH=$HOME/.local/bin:$OMARCHY_PATH/bin:$PATH
export XCURSOR_THEME=Bibata-Modern-Ice
export XCURSOR_SIZE=24
# Set default terminal and editor
source ~/.config/uwsm/default
# Activate mise if present on the system
omarchy-cmd-present mise && eval "$(mise activate bash)"
✅ After changing this file: relaunch Hyprland/UWSM so PATH updates apply.
Step 2: Create the omarchy-menu wrapper
Create:
~/.local/bin/omarchy-menu
This file should simply exec your patcher:
~/.local/bin/omarchy-menu:
#!/usr/bin/env bash
# ~/.local/bin/omarchy-menu
#
# What this does:
# - This is a tiny wrapper that ensures when you run `omarchy-menu`,
# it launches *your* customized Omarchy menu (the patched one),
# instead of the stock/upstream menu.
#
# Why it matters:
# - Your Waybar config (and likely keybinds) call `omarchy-menu`.
# - Without this wrapper in PATH, users would keep launching the stock menu.
exec "$HOME/.config/omarchy/bin/omarchy-menu-nas.sh" "$@"
Make it executable:
chmod +x ~/.local/bin/omarchy-menu
Step 3: Create the patcher script that injects your submenu
Create:
~/.config/omarchy/bin/omarchy-menu-nas.sh
This is the heart of the setup — it patches upstream omarchy-menu at runtime.
~/.config/omarchy/bin/omarchy-menu-nas.sh:
#!/usr/bin/env bash
# omarchy-menu-nas.sh
#
# What this does (high level):
# - Reads the upstream Omarchy menu script (the one shipped by Omarchy)
# - Patches it *in memory* to:
# 1) Add a new "Custom" entry to the main menu
# 2) Add routing so selecting Custom opens a new custom submenu
# 3) Inject show_custom_menu() (NAS mounts, VPN toggles, cheatsheets, etc.)
# 4) Wrap shutdown/reboot calls so your shutdown-sound wrapper runs first
# - Writes the patched result to a temp file and executes it
#
# Why this approach is nice:
# - You do NOT modify Omarchy’s upstream script permanently
# - If Omarchy updates the menu, you can re-run this and re-patch on the fly
#
# SECURITY / PUBLISH NOTES:
# - This script contains *no passwords, tokens, or IP addresses*.
# - It DOES reveal some local paths (PIA path, Nautilus, your $HOME layout).
# That’s generally fine to publish. If you want it more generic, you *can*
# mention in your blog that those paths may vary per system.
set -euo pipefail
# Upstream Omarchy menu script we will patch.
UPSTREAM="$HOME/.local/share/omarchy/bin/omarchy-menu"
# Temporary file where we write the patched copy of the upstream menu.
PATCHED="$(mktemp /tmp/omarchy-menu.patched.XXXXXX)"
# Desktop notification helper (silent fallback if notify-send isn't installed).
notify() {
command -v notify-send >/dev/null 2>&1 && notify-send "Omarchy Custom Menu" "$1" || true
}
# Hard fail if upstream menu cannot be read.
if [[ ! -r "$UPSTREAM" ]]; then
notify "Can't read upstream menu: $UPSTREAM"
exit 1
fi
# Patch the upstream menu using an embedded Python script.
# Arguments passed to Python:
# $1 = upstream menu path
# $2 = output path (PATCHED)
python3 - <<'PY' "$UPSTREAM" "$PATCHED"
import sys, re
src_path, out_path = sys.argv[1], sys.argv[2]
s = open(src_path, "r", encoding="utf-8").read()
# The label inserted into the Omarchy main menu.
# Note: requires Nerd Font support for the icon glyph to render nicely.
CUSTOM_LABEL = " Custom"
# Wrapper script used to play shutdown sound before power actions.
ACTION = '"$HOME/.config/omarchy/bin/action-with-shutdown-sound.sh"'
# Menu action that lets you pick startup/shutdown sounds.
SOUND_PICKER = '"$HOME/.config/omarchy/bin/change-startup-shutdown-sounds.sh"'
# -----------------------------
# 1) Add Custom to main menu list (just above System)
# -----------------------------
# This regex finds the options text passed to the upstream "menu" call inside
# show_main_menu(). We then splice in CUSTOM_LABEL before "System".
main_menu_pat = r'(show_main_menu\(\)\s*\{\s*\n\s*go_to_menu "\$\(\s*menu "Go" "\s*)([^"]+)("\s*\)\s*"\s*\n\s*\}\s*)'
m = re.search(main_menu_pat, s, re.S)
# If upstream changed and the regex can't find what we expect:
# - write the original unmodified script out
# - exit cleanly (your wrapper will still run, but without custom menu)
if not m:
open(out_path, "w", encoding="utf-8").write(s)
sys.exit(0)
options = m.group(2)
# Only insert if Custom is not already present.
if ("Custom" not in options) and (CUSTOM_LABEL not in options):
items = options.split("\\n")
# Defensive cleanup: remove any existing Custom lines to avoid duplicates.
items = [x for x in items if ("Custom" not in x and x != CUSTOM_LABEL)]
# Find the first "System" entry and insert Custom just above it.
sys_idx = None
for i, it in enumerate(items):
if "System" in it:
sys_idx = i
break
# If "System" wasn't found, append Custom at the end.
if sys_idx is None:
items.append(CUSTOM_LABEL)
else:
items.insert(sys_idx, CUSTOM_LABEL)
options = "\\n".join(items)
# Splice the updated options back into the upstream script text.
s = s[:m.start(2)] + options + s[m.end(2):]
# -----------------------------
# 2) Route in go_to_menu()
# -----------------------------
# Upstream routes menu selections like:
# *trigger*) show_trigger_menu ;;
# We add:
# *custom*) show_custom_menu ;;
if "show_custom_menu" not in s:
if "*trigger*) show_trigger_menu ;;" in s:
s = s.replace(
"*trigger*) show_trigger_menu ;;",
"*trigger*) show_trigger_menu ;;\n *custom*) show_custom_menu ;;"
)
# -----------------------------
# 3) Inject show_custom_menu()
# -----------------------------
# We locate "show_trigger_menu() {" and insert our custom menu function right
# after that function ends (after the next "}\n\n").
if "show_custom_menu()" not in s:
inject_after = "show_trigger_menu() {"
idx = s.find(inject_after)
if idx != -1:
insert_point = s.find("}\n\n", idx)
if insert_point != -1:
# This is the actual custom menu definition inserted into upstream.
# It depends on upstream helper functions:
# - menu
# - present_terminal
# - show_main_menu
custom_fn = r'''
show_custom_menu() {
case $(menu "Custom" "🗄 Mount NAS (smart)\n🧹 Unmount NAS\n📁 Open /mnt (choose mount)\n──────── VPN ────────\n🛡 PIA Connect\n🚫 PIA Disconnect\n🔒 PiVPN Connect\n🔓 PiVPN Disconnect\n────── Cheatsheets ──────\n📄 Show Binds (TXT)\n🖼 Show Binds (PDF)\n────── Screensaver ──────\n Screensaver Stock Mode\n Screensaver Long Mode\n────── Sounds ──────\n🎵 Change Startup/Shutdown Sounds\n──────── Status ────────\nℹ Customization Status") in
*"Mount NAS"*)
present_terminal "$HOME/.config/omarchy/bin/nas-mount-smart.sh"
;;
*"Unmount NAS"*)
present_terminal "$HOME/.config/omarchy/bin/nas-unmount-all.sh"
;;
*"Open /mnt"*)
# Open file browser to /mnt (where mounts typically live).
setsid nautilus /mnt >/dev/null 2>&1 &
disown
;;
*"PIA Connect"*)
# Connect PIA VPN via piactl and show state, then wait for a keypress.
present_terminal "bash -lc '/opt/piavpn/bin/piactl connect || true; /opt/piavpn/bin/piactl get connectionstate || true; echo; read -n 1 -r -s -p \"Press any key…\"'"
;;
*"PIA Disconnect"*)
present_terminal "bash -lc '/opt/piavpn/bin/piactl disconnect || true; /opt/piavpn/bin/piactl get connectionstate || true; echo; read -n 1 -r -s -p \"Press any key…\"'"
;;
*"PiVPN Connect"*)
# Your own local scripts (outside Omarchy).
present_terminal "$HOME/.local/bin/pivpn-connect.sh"
;;
*"PiVPN Disconnect"*)
present_terminal "$HOME/.local/bin/pivpn-disconnect.sh"
;;
*"Show Binds (TXT)"*)
present_terminal "$HOME/.config/omarchy/bin/show-cheatsheet.sh --all"
;;
*"Show Binds (PDF)"*)
# PDF version runs in background (no terminal needed).
bash -lc "$HOME/.config/omarchy/bin/show-cheatsheet-pdf.sh --all" >/dev/null 2>&1 &
;;
*"Screensaver Stock Mode"*)
present_terminal "$HOME/.config/omarchy/bin/screensaver-stock-mode.sh"
;;
*"Screensaver Long Mode"*)
present_terminal "$HOME/.config/omarchy/bin/screensaver-long-mode.sh"
;;
*"Change Startup/Shutdown Sounds"*)
# Launch sound picker script in a terminal.
present_terminal ''' + SOUND_PICKER + r'''
;;
*"Customization Status"*)
present_terminal "$HOME/.config/omarchy/bin/vpn-nas-status.sh"
;;
*"────"*|*"────────"*)
# If user selects a divider line, just re-open the custom menu.
show_custom_menu
;;
*)
# Anything else returns to the main menu.
show_main_menu
;;
esac
}
'''
# Insert our function into upstream script text.
s = s[:insert_point+3] + custom_fn + s[insert_point+3:]
# -----------------------------
# 4) Wrap Omarchy System menu shutdown/reboot tokens
# -----------------------------
# Omarchy uses tokens like "omarchy-cmd-shutdown" (not always literal commands).
# We replace those tokens so they go through your ACTION wrapper first.
def wrap_token(token: str, replacement: str):
global s
# Negative lookbehind tries to avoid double-wrapping.
pat = rf'(?<!{re.escape("action-with-shutdown-sound.sh")} )\b{re.escape(token)}\b'
s = re.sub(pat, replacement, s)
wrap_token("omarchy-cmd-shutdown", f"{ACTION} shutdown")
wrap_token("omarchy-cmd-reboot", f"{ACTION} reboot")
# -----------------------------
# 5) Safety net: wrap any systemctl poweroff/reboot/halt
# -----------------------------
# If upstream ever directly calls systemctl poweroff/reboot/halt,
# we wrap it too: ACTION -- systemctl ...
def wrap_cmd(pattern, repl):
global s
s = re.sub(pattern, repl, s, flags=re.M)
wrap_cmd(
rf'(^|[;&\(\)\n]\s*)(?!{re.escape(ACTION)}\s+--\s+)((?:/usr/bin/)?systemctl\b[^\n;&\)]*\b(?:poweroff|reboot|halt)\b[^\n;&\)]*)',
rf'\1{ACTION} -- \2'
)
# Marker to confirm patch success (used by the bash wrapper below).
if "OMARCHY_CUSTOM_PATCH_MARKER" not in s and "show_custom_menu()" in s:
s += "\n# OMARCHY_CUSTOM_PATCH_MARKER\n"
# Write patched script out.
open(out_path, "w", encoding="utf-8").write(s)
PY
# Ensure patched menu script is executable and then run it.
chmod +x "$PATCHED"
# Notify user whether our marker exists (basic success check).
if grep -q "OMARCHY_CUSTOM_PATCH_MARKER" "$PATCHED"; then
notify "Custom menu patch applied ✅"
else
notify "Patch didn’t apply (upstream changed?) — running stock menu."
fi
# Execute patched menu, passing through any args
exec bash "$PATCHED" "$@"
Make it executable:
chmod +x ~/.config/omarchy/bin/omarchy-menu-nas.sh
How to test:
Hit SUPER + ALT + SPACE
Confirm you see your Custom submenu
If upstream changes later and patching fails: it should fall back safely and notify you.
📗Part 2 — VPN Setup (PIA + PiVPN) — do this before NAS
Why here?
Because your NAS smart mount may rely on PiVPN when you’re remote, and your dotfiles backup workflow may temporarily disconnect PIA.
Step 1: PiVPN connect/disconnect scripts
Create:
~/.local/bin/pivpn-connect.sh
~/.local/bin/pivpn-disconnect.sh
Paste them:
~/.local/bin/pivpn-connect.sh:
#!/usr/bin/env bash
# pivpn-connect.sh
#
# What this does:
# - Connects your PiVPN OpenVPN client via systemd:
# openvpn-client@pivpn
# - Coordinates with PIA VPN (if installed) to avoid conflicts:
# - If PIA is Connected/Connecting, it disconnects PIA first
# - It writes a small state flag so the disconnect script can later restore PIA
#
# Key behavior:
# - Writes /tmp/pivpn-pia-was-connected when it *actually* had to disconnect PIA
# - If PiVPN is already active, it exits cleanly
set -euo pipefail
# OpenVPN systemd unit for PiVPN
SERVICE="openvpn-client@pivpn"
# PIA CLI path (used to disconnect PIA before bringing PiVPN up)
PIACTL="/opt/piavpn/bin/piactl"
# State flag file used to remember whether PIA was connected before PiVPN connect
STATE_FILE="/tmp/pivpn-pia-was-connected"
log() {
# Only print if stdout is a terminal
if [[ -t 1 ]]; then
echo "$@"
fi
}
log "[PiVPN] Connecting…"
# 1) Handle PIA state
# If PIA is connected (or connecting), disconnect it first and remember that fact.
if [[ -x "$PIACTL" ]]; then
state="$("$PIACTL" get connectionstate 2>/dev/null || true)"
if [[ "$state" == "Connected" || "$state" == "Connecting" ]]; then
log "[PiVPN] PIA is $state – disconnecting it first…"
echo "1" > "$STATE_FILE" # <-- real file write, no log()
"$PIACTL" disconnect || true
sleep 2
else
rm -f "$STATE_FILE"
fi
else
# If piactl isn't installed/executable, ensure we don't leave stale state behind
rm -f "$STATE_FILE"
fi
# 2) If PiVPN already active, bail
if systemctl is-active --quiet "$SERVICE"; then
log "[PiVPN] $SERVICE is already active."
exit 0
fi
# 3) Start via systemd (polkit will handle auth if needed)
log "[PiVPN] Starting $SERVICE via systemd…"
if ! systemctl start "$SERVICE"; then
log "[PiVPN] ERROR: failed to start $SERVICE."
exit 1
fi
log "[PiVPN] Started."
~/.local/bin/pivpn-disconnect.sh:
#!/usr/bin/env bash
# pivpn-disconnect.sh
#
# What this does:
# - Disconnects your PiVPN OpenVPN client via systemd:
# openvpn-client@pivpn
# - Optionally restores PIA VPN if it was connected before you enabled PiVPN:
# - pivpn-connect.sh writes /tmp/pivpn-pia-was-connected when it had to
# disconnect PIA to bring PiVPN up
# - If that state file exists here, this script reconnects PIA after
# stopping PiVPN, then removes the state file
set -euo pipefail
# OpenVPN systemd unit for PiVPN
SERVICE="openvpn-client@pivpn"
# PIA CLI path (used to restore PIA after PiVPN is stopped)
PIACTL="/opt/piavpn/bin/piactl"
# State flag file created by pivpn-connect.sh if it disconnected PIA
STATE_FILE="/tmp/pivpn-pia-was-connected"
log() {
# Only print if stdout is a terminal
if [[ -t 1 ]]; then
echo "$@"
fi
}
log "[PiVPN] Disconnecting…"
# 1) Stop PiVPN via systemd (polkit will handle auth if needed)
if systemctl is-active --quiet "$SERVICE"; then
if ! systemctl stop "$SERVICE"; then
log "[PiVPN] ERROR: failed to stop $SERVICE."
exit 1
fi
else
log "[PiVPN] $SERVICE is not active."
fi
log "[PiVPN] Disconnected."
# 2) If PIA was previously connected, reconnect it now
if [[ -x "$PIACTL" && -f "$STATE_FILE" ]]; then
log "[PiVPN] PIA was connected before PiVPN. Reconnecting PIA…"
"$PIACTL" connect || true
rm -f "$STATE_FILE"
fi
Make executable:
chmod +x ~/.local/bin/pivpn-connect.sh ~/.local/bin/pivpn-disconnect.sh
Step 2: Optional PIA autoconnect user service
Create:
~/.config/systemd/user/pia-autoconnect.service
~/.config/systemd/user/pia-autoconnect.service:
[Unit]
Description=PIA headless autoconnect
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/opt/piavpn/bin/piactl background enable
ExecStart=/opt/piavpn/bin/piactl set protocol wireguard
ExecStart=/opt/piavpn/bin/piactl set region us-seattle
ExecStart=/opt/piavpn/bin/piactl set requestportforward false
ExecStart=/opt/piavpn/bin/piactl connect
RemainAfterExit=yes
[Install]
WantedBy=default.target
Enable:
systemctl –user daemon-reload systemctl –user enable –now pia-autoconnect.service systemctl –user status pia-autoconnect.service
📘Part 3 — NAS Smart Mount (LAN vs Remote)
Create:
~/.config/omarchy/bin/nas-mount-smart.sh
~/.config/omarchy/bin/nas-mount-smart.sh:
#!/usr/bin/env bash
# nas-mount-smart.sh
#
# What this does:
# - Attempts to mount multiple NFS shares from a NAS to local /mnt mountpoints
# - Tries the "local/LAN path" first
# - If mounts fail, it falls back to a PiVPN connection and retries
set -euo pipefail
echo "=== nas-mount-smart: $(date) ==="
# PRIVATE: NAS IP on your LAN (obfuscated for publishing)
NAS_IP="NAS_IP_HERE"
# PRIVATE: NAS export base path (obfuscated for publishing)
EXPORT_BASE="/mnt/NAS_EXPORT_BASE"
# Mounts to create
# PRIVATE: share names + mountpoints obfuscated for publishing
MOUNTS=(
"share_1:/mnt/mount_point_1"
"share_2:/mnt/mount_point_2"
"share_3:/mnt/mount_point_3"
"share_4:/mnt/mount_point_4"
)
# Scripts you already have
PIVPN_CONNECT="$HOME/.local/bin/pivpn-connect.sh"
# ---- helpers -------------------------------------------------------------
log(){ echo "[nas] $*"; }
have(){ command -v "$1" >/dev/null 2>&1; }
is_mounted() {
# SAFE mount check (won’t hang on dead NFS)
local target="$1"
awk -v t="$target" '$5==t {found=1} END{exit(found?0:1)}' /proc/self/mountinfo
}
nas_port_open() {
# Don’t rely only on ping; test NFS port quickly.
# 2049 is NFSv4. This doesn’t “touch” the mountpoints.
if have nc; then
nc -z -w1 "$NAS_IP" 2049 >/dev/null 2>&1
else
timeout 1 bash -lc "cat < /dev/null > /dev/tcp/$NAS_IP/2049" >/dev/null 2>&1
fi
}
nas_reachable() {
# Use ping OR NFS port. Ping might be blocked; port check is better.
ping -c 1 -W 1 "$NAS_IP" >/dev/null 2>&1 || nas_port_open
}
warm_sudo() {
log "Warming sudo (may prompt once)..."
sudo -v
}
mount_one() {
local share="$1" target="$2"
mkdir -p "$target"
if is_mounted "$target"; then
log "Already mounted: $target"
return 0
fi
local src="${NAS_IP}:${EXPORT_BASE}/${share}"
log "Mounting: $src -> $target"
# IMPORTANT: timeout so we never hang forever if routing is broken
# - Use a longer timeout if your network is slow.
if timeout 8 sudo mount "$src" "$target"; then
return 0
else
log "Mount failed (or timed out) for $target"
return 1
fi
}
mount_all() {
local ok=0 total=0
for item in "${MOUNTS[@]}"; do
total=$((total+1))
share="${item%%:*}"
target="${item##*:}"
if mount_one "$share" "$target"; then
ok=$((ok+1))
fi
done
log "Mounted $ok / $total mount(s)"
[[ "$ok" -eq "$total" ]]
}
wait_for_nas() {
local tries=20
local i
for i in $(seq 1 "$tries"); do
if nas_reachable; then
log "NAS is reachable ✅"
return 0
fi
log "Waiting for NAS ($NAS_IP) to become reachable... ($i/$tries)"
sleep 1
done
return 1
}
# ---- main ---------------------------------------------------------------
# 1) Try LOCAL mount path FIRST — no VPN changes
if nas_reachable; then
log "NAS reachable (local path) -> attempting mount..."
else
log "NAS not reachable right now -> still attempting a quick mount with timeouts..."
fi
warm_sudo
if mount_all; then
notify-send "NAS" "Mounted ✅"
exit 0
fi
# 2) If local mount failed, fall back to PiVPN path
log "Local mount failed -> falling back to PiVPN path..."
log "Connecting PiVPN..."
"$PIVPN_CONNECT" || true
log "Waiting for openvpn-client@pivpn to be active..."
# give systemd a moment even if connect script returns quickly
for _ in $(seq 1 15); do
systemctl is-active --quiet openvpn-client@pivpn && break
sleep 1
done
if ! systemctl is-active --quiet openvpn-client@pivpn; then
log "ERROR: PiVPN did not become active."
notify-send "NAS" "PiVPN failed ❌"
exit 1
fi
if ! wait_for_nas; then
log "ERROR: NAS still not reachable over PiVPN (routes/NAS IP/VPN config)"
notify-send "NAS" "NAS unreachable over PiVPN ❌"
exit 1
fi
log "Attempting mount after PiVPN is ready..."
if mount_all; then
notify-send "NAS" "Mounted (PiVPN) ✅"
exit 0
fi
notify-send "NAS" "Mount failed ❌"
exit 1
Create:
~/.config/omarchy/bin/nas-unmount-all.sh
~/.config/omarchy/bin/nas-unmount-all.sh:
#!/bin/bash
# nas-unmount-all.sh
#
# What this does:
# - Unmounts a list of NAS mountpoints (if they are currently mounted)
# - Uses mountpoint(1) to check safely before unmounting
# - Uses sudo for umount (will prompt if sudo isn't warmed)
#
# Publish note:
# - Mount paths are obfuscated for privacy. Replace with your real mountpoints.
set -euo pipefail
# PRIVATE: mount paths obfuscated for publishing
MOUNTS=(/mnt/mount_point_1 /mnt/mount_point_2 /mnt/mount_point_3 /mnt/mount_point_4)
for m in "${MOUNTS[@]}"; do
# If it's a mountpoint, unmount it; otherwise do nothing.
# "|| true" prevents a failure from killing the entire script.
mountpoint -q "$m" && sudo umount "$m" || true
done
notify-send "NAS" "Unmount done ✅"
Make executable:
chmod +x ~/.config/omarchy/bin/nas-mount-smart.sh ~/.config/omarchy/bin/nas-unmount-all.sh
You will likely customize:
NAS IP
export paths
mountpoints list
📕Part 3.5 — Dotfiles Backup to NAS (rsync + tar.gz, VPN-aware)
This is my “save my whole setup” button.
It backs up all of ~/.config to my NAS in two ways:
- Rsync mirror (fast, incremental, easy to browse)
- Timestamped tar.gz archive (one-file snapshot you can store forever)
It’s also VPN-aware:
- Tries to mount the NAS locally first
- If it’s not reachable, it brings up PiVPN and retries
- If PIA is currently connected, it can temporarily disconnect it so LAN/NAS routes work
- When it’s done, it unmounts NAS mounts and restores PIA (if it was on originally)
You MUST customize these before running it
Open the script and edit:
NAS_MOUNT(the “main” NAS mount you check for)NAS_DOTFILES_DIR(where backups should live on the NAS)NAS_MOUNTS(all mountpoints you want auto-unmounted at the end)MOUNT_SCRIPT(your NFS mounting helper — replace with your own if needed)PIVPN_SERVICEandPIVPN_CONNECT(your PiVPN unit/script names)PIACTLpath (if yours isn’t/opt/piavpn/bin/piactl)rsync --excludelist (optional)
Create the script
Create:
~/dotfiles_backup.sh
~/dotfiles_backup.sh:
#!/bin/bash
# dotfiles_backup.sh
#
# What this does (high level):
# - Backs up your ~/.config directory to a NAS location using rsync
# - Also creates a timestamped tar.gz archive of ~/.config on the NAS
# - Tries a "local mount" first; if not mounted, it can bring up PiVPN and retry
# - Coordinates with PIA VPN so routing doesn’t block NAS access:
# - If PIA was connected at the start, it disconnects it before mounting/backup
# - After backup + unmount, it restores PIA if it was originally connected
#
# What it depends on:
# - Your NAS mount script (MOUNT_SCRIPT) that mounts all required NFS shares
# - PiVPN connect script (pivpn-connect.sh) + systemd OpenVPN unit
# - Optional: PIA CLI (piactl) for VPN coordination
# - rsync, tar, mountpoint, sudo, systemctl
#
# Publish note / privacy:
# - PRIVATE: NAS paths, mountpoints, and backup directory names are obfuscated.
# - The exclude list (BraveSoftware/, Element/, mise/) is not sensitive.
# - No passwords or tokens are stored here.
set -euo pipefail
echo "=== dotfiles-backup: starting ==="
# --- Paths / services ---
# PRIVATE: NAS mountpoint (obfuscated for publishing)
NAS_MOUNT="/mnt/NAS_MOUNT_HERE"
# PRIVATE: backup directory on the NAS (obfuscated for publishing)
NAS_DOTFILES_DIR="$NAS_MOUNT/backups/NAS_DOTFILES_BACKUP_DIR"
# Source directory to back up
SRC_CONFIG="$HOME/.config"
# All NAS mounts your mount_nfs.sh creates
# PRIVATE: mountpoints obfuscated for publishing
NAS_MOUNTS=(
"/mnt/MOUNT_POINT_1"
"/mnt/MOUNT_POINT_2"
"/mnt/MOUNT_POINT_3"
"/mnt/MOUNT_POINT_4"
)
# PRIVATE: mount script path/name (obfuscated for publishing)
MOUNT_SCRIPT="$HOME/MOUNT_SCRIPT_HERE.sh"
PIVPN_SERVICE="openvpn-client@pivpn"
PIVPN_CONNECT="$HOME/.local/bin/pivpn-connect.sh"
PIACTL="/opt/piavpn/bin/piactl"
# Remember original PIA state so we can restore it at the end
INITIAL_PIA_STATE="Unknown"
if [[ -x "$PIACTL" ]]; then
INITIAL_PIA_STATE="$("$PIACTL" get connectionstate 2>/dev/null || echo "Unknown")"
echo "[dotfiles-backup] Initial PIA state: $INITIAL_PIA_STATE"
else
echo "[dotfiles-backup] PIA client not found at $PIACTL – skipping PIA handling."
fi
# --- Helper functions ---
try_mount() {
echo "[dotfiles-backup] Attempting to mount NAS via $MOUNT_SCRIPT..."
if [[ -x "$MOUNT_SCRIPT" ]]; then
"$MOUNT_SCRIPT"
else
echo "[dotfiles-backup] ERROR: $MOUNT_SCRIPT not found or not executable." >&2
return 1
fi
}
ensure_pivpn() {
if systemctl is-active --quiet "$PIVPN_SERVICE"; then
echo "[dotfiles-backup] PiVPN is already active (service $PIVPN_SERVICE)."
return 0
fi
echo "[dotfiles-backup] PiVPN is not active. Trying to bring it up with $PIVPN_CONNECT..."
if [[ -x "$PIVPN_CONNECT" ]]; then
# This may pop your polkit auth window (fingerprint/password), which is expected.
"$PIVPN_CONNECT" >/dev/null 2>&1 || {
echo "[dotfiles-backup] WARNING: pivpn-connect.sh returned a non-zero status." >&2
return 1
}
echo "[dotfiles-backup] PiVPN connect script was invoked."
else
echo "[dotfiles-backup] ERROR: $PIVPN_CONNECT not found or not executable; cannot use PiVPN." >&2
return 1
fi
}
ensure_pia_disconnected() {
# If PIA tools aren't present, nothing to do
if [[ ! -x "$PIACTL" ]]; then
return 0
fi
# Only disconnect if it *was* connected initially
if [[ "$INITIAL_PIA_STATE" == "Connected" || "$INITIAL_PIA_STATE" == "Connecting" ]]; then
echo "[dotfiles-backup] PIA was $INITIAL_PIA_STATE at start – disconnecting it so NAS is reachable..."
"$PIACTL" disconnect || true
sleep 3
else
echo "[dotfiles-backup] PIA was not connected at start – leaving it alone."
fi
}
cleanup_nas_mounts() {
echo "[dotfiles-backup] Cleaning up NAS mounts before finishing..."
for m in "${NAS_MOUNTS[@]}"; do
if mountpoint -q "$m"; then
echo " - Unmounting $m ..."
# This may trigger a polkit auth prompt if sudo is needed
sudo umount "$m" || echo " ! Failed to unmount $m (you may need to check it manually)."
fi
done
echo "[dotfiles-backup] NAS mount cleanup done."
}
restore_pia_if_needed() {
if [[ ! -x "$PIACTL" ]]; then
echo "[dotfiles-backup] PIA client not installed – nothing to restore."
return
fi
if [[ "$INITIAL_PIA_STATE" == "Connected" || "$INITIAL_PIA_STATE" == "Connecting" ]]; then
echo "[dotfiles-backup] Restoring PIA to its original state (connecting)..."
"$PIACTL" connect || true
else
echo "[dotfiles-backup] PIA was not connected originally – not reconnecting."
fi
}
fail_and_exit() {
local msg="$1"
echo "[dotfiles-backup] ERROR: $msg" >&2
echo "=== dotfiles-backup: FAILED ==="
# Try to clean up mounts if they exist
cleanup_nas_mounts
# Restore PIA if it was originally on
restore_pia_if_needed
exit 1
}
# --- 0) Handle PIA first so NAS is reachable ---
echo "[dotfiles-backup] Checking PIA VPN state..."
ensure_pia_disconnected
# --- 1) Check if NAS mount is available / mount it ---
echo "[dotfiles-backup] Checking if NAS mount ($NAS_MOUNT) is available..."
# Try local mount first (no VPN)
if ! mountpoint -q "$NAS_MOUNT"; then
echo "[dotfiles-backup] $NAS_MOUNT is not mounted."
echo "[dotfiles-backup] First, trying a normal local mount (no VPN)..."
try_mount || echo "[dotfiles-backup] Local mount attempt failed or script returned non-zero."
fi
# If still not mounted, try via PiVPN
if ! mountpoint -q "$NAS_MOUNT"; then
echo "[dotfiles-backup] NAS still not mounted. Assuming we might be remote."
echo "[dotfiles-backup] Trying to bring up PiVPN tunnel and mount over it..."
if ensure_pivpn; then
echo "[dotfiles-backup] PiVPN should now be active. Retrying NAS mount..."
try_mount || echo "[dotfiles-backup] Mount attempt over PiVPN returned non-zero."
else
fail_and_exit "Could not start PiVPN; cannot reach NAS remotely."
fi
fi
# Final check
if ! mountpoint -q "$NAS_MOUNT"; then
fail_and_exit "$NAS_MOUNT is still not mounted after local + PiVPN attempts. Is the NAS offline or PiVPN unreachable?"
fi
echo "[dotfiles-backup] NAS is mounted at $NAS_MOUNT ✅"
# --- 1.5) Verify NAS backup directory exists ---
echo "[dotfiles-backup] Verifying that $NAS_DOTFILES_DIR exists on the NAS..."
if [[ ! -d "$NAS_DOTFILES_DIR" ]]; then
fail_and_exit "Expected NAS backup directory $NAS_DOTFILES_DIR does not exist. Create it and try again."
fi
echo "[dotfiles-backup] Found existing NAS backup directory: $NAS_DOTFILES_DIR ✅"
# --- 2) Rsync dotfiles to NAS ---
DEST_CONFIG="$NAS_DOTFILES_DIR/.config"
mkdir -p "$DEST_CONFIG"
echo "[dotfiles-backup] Preparing destination: $DEST_CONFIG"
echo "[dotfiles-backup] Rsyncing ~/.config -> $DEST_CONFIG"
echo "[dotfiles-backup] Excluding: BraveSoftware/, Element/, mise/"
rsync -aAHv \
--exclude='BraveSoftware/' \
--exclude='Element/' \
--exclude='mise/' \
"$SRC_CONFIG/" \
"$DEST_CONFIG/"
echo "[dotfiles-backup] Rsync complete ✅"
# --- 3) Create tarball on NAS ---
TAR_PATH="$NAS_DOTFILES_DIR/dotfiles-config-$(date +%Y%m%d-%H%M%S).tar.gz"
echo "[dotfiles-backup] Creating tarball at:"
echo " $TAR_PATH"
echo "[dotfiles-backup] (Excluding BraveSoftware, Element, mise)"
tar czf "$TAR_PATH" \
--exclude='.config/BraveSoftware' \
--exclude='.config/Element' \
--exclude='.config/mise' \
-C "$HOME" .config
echo "[dotfiles-backup] Tarball created ✅"
# --- 4) Unmount NAS mounts before PIA comes back ---
cleanup_nas_mounts
# --- 5) Restore PIA if needed ---
restore_pia_if_needed
echo "[dotfiles-backup] All done."
echo "[dotfiles-backup] Directory backup: $DEST_CONFIG"
echo "[dotfiles-backup] Archive backup: $TAR_PATH"
echo "=== dotfiles-backup: finished successfully ==="
Make it executable:
chmod +x ~/dotfiles_backup.sh
Run it whenever you want to backup your dotfiles/config - this script uses my NAS mountpoints, and will connect to my homelab’s PiVPN if I’m remote, but you could modify it any way(s) you need.
📙Part 4 — Cheatsheets generated from your REAL Hyprland binds
Create:
~/.config/omarchy/bin/hypr-cheatgen.py
~/.config/omarchy/bin/hypr-cheatgen.py:
#!/usr/bin/env python3
# hypr-cheatgen.py
#
# What this does:
# - Pulls your current Hyprland keybinds via:
# hyprctl binds -j
# - Converts Hyprland's bind JSON into a readable cheat sheet:
# - ASCII (aligned, fits ~80 columns by default)
# - Markdown table
# - Optionally renders the ASCII cheat sheet to a landscape PDF (via ReportLab)
#
# Why this is useful:
# - Your Hyprland config can be complex; this generates a "living" cheatsheet
# from the binds Hypr is actually using right now.
#
# Dependencies / assumptions:
# - Requires `hyprctl` in PATH and Hyprland running
# - For PDF output: requires ReportLab (python-reportlab package on Arch)
import argparse
import json
import subprocess
import textwrap
from collections import defaultdict
# Hyprland modmask bits (from your output):
# 64=SUPER, 8=ALT, 4=CTRL, 1=SHIFT
MOD_BITS = [
(64, "SUPER"),
(8, "ALT"),
(4, "CTRL"),
(1, "SHIFT"),
]
# Optional: common X11 keycode mapping (works on typical US layouts)
# If your hyprctl JSON includes "keycode", this makes numbers readable.
X11_KEYCODE_MAP = {
10: "1", 11: "2", 12: "3", 13: "4", 14: "5", 15: "6", 16: "7", 17: "8", 18: "9", 19: "0",
24: "Q", 25: "W", 26: "E", 27: "R", 28: "T", 29: "Y", 30: "U", 31: "I", 32: "O", 33: "P",
38: "A", 39: "S", 40: "D", 41: "F", 42: "G", 43: "H", 44: "J", 45: "K", 46: "L",
52: "Z", 53: "X", 54: "C", 55: "V", 56: "B", 57: "N", 58: "M",
65: "SPACE", 36: "RETURN", 22: "BACKSPACE", 9: "ESCAPE", 23: "TAB",
107: "PRINT", 119: "DELETE",
# Add more if you want
}
def run(cmd: list[str]) -> str:
# Run a command and return stdout as text
return subprocess.check_output(cmd, text=True)
def decode_modmask(modmask) -> str:
"""
Hypr returns either:
- 'mod' as a string (sometimes)
- or 'modmask' as a number (what you're seeing)
We decode numeric modmasks into "SUPER + SHIFT + ..." style strings.
"""
try:
m = int(modmask)
except Exception:
return str(modmask).strip()
names = [name for bit, name in MOD_BITS if (m & bit)]
return " + ".join(names)
def key_from_bind(b: dict) -> str:
"""
Hypr versions differ; try a few fields.
Prefer printable key names, fall back to keycode/code when needed.
"""
k = b.get("key")
if k and str(k).strip():
return str(k).strip()
# Some builds include keycode
kc = b.get("keycode")
if kc is not None:
try:
kc_i = int(kc)
return X11_KEYCODE_MAP.get(kc_i, f"code:{kc_i}")
except Exception:
pass
# Fallback: sometimes "code" exists
code = b.get("code")
if code is not None:
return f"code:{code}"
return "" # truly unknown
def friendly_action(b: dict) -> str:
# Dispatcher + arg combined into a single friendly string
disp = (b.get("dispatcher") or "").strip()
arg = (b.get("arg") or "").strip()
return f"{disp} {arg}".strip()
def combo_string(b: dict) -> str:
# Build a human-readable key combo string like:
# SUPER + SHIFT + Q
mods = b.get("mod")
if not mods:
mods = b.get("modmask", "")
mods_s = decode_modmask(mods) if mods != "" else ""
key = key_from_bind(b)
if mods_s and key:
return f"{mods_s} + {key}"
if mods_s and not key:
return mods_s
return key
def to_ascii_sections(binds: list[dict], width: int = 80) -> str:
"""
Produce a strict, aligned, <=width ASCII cheat sheet:
[SECTION]
KEYS... (fixed column) | ACTION... (wrapped)
"""
groups = defaultdict(list)
for b in binds:
disp = (b.get("dispatcher") or "unknown").strip()
groups[disp].append((combo_string(b), friendly_action(b)))
out = []
for disp in sorted(groups.keys()):
out.append(f"[{disp.upper()}]")
rows = sorted(groups[disp], key=lambda x: (x[0], x[1]))
# Tuned for 80 columns:
# - key_w: fixed width for the keys column
# - act_w: remaining width for action wrapping
key_w = 26
act_w = max(10, width - (key_w + 3)) # " | "
for keys, action in rows:
keys = (keys or "").strip()
action = (action or "").strip()
keys_lines = textwrap.wrap(keys, width=key_w) or [""]
act_lines = textwrap.wrap(action, width=act_w) or [""]
n = max(len(keys_lines), len(act_lines))
keys_lines += [""] * (n - len(keys_lines))
act_lines += [""] * (n - len(act_lines))
for i in range(n):
out.append(f"{keys_lines[i]:<{key_w}} | {act_lines[i]}")
out.append("") # blank line between sections
return "\n".join(out).rstrip() + "\n"
def to_markdown(binds: list[dict]) -> str:
# Markdown output groups binds by dispatcher and renders a table per section.
groups = defaultdict(list)
for b in binds:
disp = (b.get("dispatcher") or "unknown").strip()
groups[disp].append((combo_string(b), friendly_action(b)))
md = ["# Hyprland Keybinds Cheat Sheet\n"]
for disp in sorted(groups.keys()):
md.append(f"## {disp}\n")
md.append("| Keys | Action |")
md.append("|---|---|")
for keys, action in sorted(groups[disp], key=lambda x: (x[0], x[1])):
md.append(f"| {keys} | {action} |")
md.append("")
return "\n".join(md)
def write_pdf_ascii(content: str, pdf_path: str, *, font_size: int = 10, line_h: int = 11):
"""
Render ASCII content to a landscape PDF without LaTeX (ReportLab).
Larger font looks nicer, but may push content onto more pages. That's OK.
"""
try:
import reportlab # noqa: F401
except ModuleNotFoundError:
raise SystemExit("PDF generation needs ReportLab. Install: sudo pacman -S python-reportlab")
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter, landscape
from reportlab.lib.units import inch
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
# Monospace font for perfect alignment
font = "Courier"
try:
pdfmetrics.registerFont(TTFont("DejaVuMono", "/usr/share/fonts/TTF/DejaVuSansMono.ttf"))
font = "DejaVuMono"
except Exception:
# Courier fallback is fine
pass
pagesize = landscape(letter)
c = canvas.Canvas(pdf_path, pagesize=pagesize)
w, h = pagesize
left = 0.5 * inch
right = 0.5 * inch
top = h - 0.5 * inch
bottom = 0.5 * inch
# If you want even larger text, bump font_size to 11 and line_h to 12.
c.setFont(font, font_size)
y = top
for line in content.splitlines():
if y < bottom:
c.showPage()
c.setFont(font, font_size)
y = top
# Avoid drawing outside page width (ReportLab won't wrap automatically)
c.drawString(left, y, line)
y -= line_h
c.save()
def main():
# CLI flags:
# --format ascii|md
# --width 80 (for ascii formatting)
# --out <file> (otherwise prints to stdout)
# --pdf <path> (ASCII only; uses reportlab)
ap = argparse.ArgumentParser()
ap.add_argument("--format", choices=["ascii", "md"], default="ascii")
ap.add_argument("--width", type=int, default=80)
ap.add_argument("--out", default=None, help="Write to a file instead of stdout")
ap.add_argument("--pdf", default=None, help="Also generate a PDF to this path (ASCII only; uses reportlab)")
ap.add_argument("--pdf-font-size", type=int, default=10, help="PDF font size (default 10)")
ap.add_argument("--pdf-line-height", type=int, default=11, help="PDF line height (default 11)")
args = ap.parse_args()
# Pull binds from hyprctl as JSON
binds = json.loads(run(["hyprctl", "binds", "-j"]))
# Render desired output format
if args.format == "ascii":
content = to_ascii_sections(binds, width=args.width)
else:
content = to_markdown(binds)
# Write to a file or stdout
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
f.write(content)
else:
print(content, end="")
# Optional PDF generation (best with ASCII output)
if args.pdf:
if args.format != "ascii":
raise SystemExit("--pdf is intended for --format ascii (so alignment stays perfect).")
write_pdf_ascii(
content,
args.pdf,
font_size=args.pdf_font_size,
line_h=args.pdf_line_height,
)
if __name__ == "__main__":
main()
Create:
~/.config/omarchy/bin/show-cheatsheet.sh
~/.config/omarchy/bin/show-cheatsheet.sh:
#!/bin/bash
# show-cheatsheet.sh
#
# What this does:
# - Ensures your Hyprland keybind cheat sheet files exist in:
# ~/.config/omarchy/cheats/
# - Regenerates the ASCII cheat sheet (fast) every time this runs
# - If called with --all, also generates:
# - Markdown version
# - PDF version (bigger font for readability)
# - Opens the ASCII cheat sheet in Alacritty using `less -SR`:
# - -S: don't wrap long lines (keeps columns aligned)
# - -R: allow raw control characters (color/formatting if ever used)
#
# Dependencies / assumptions:
# - hypr-cheatgen.py exists and is executable
# - alacritty exists at /usr/bin/alacritty
# - hyprctl is available (hypr-cheatgen.py calls it)
set -euo pipefail
# Ensure Hypr can find your scripts even if it doesn't inherit your shell PATH
# (common with graphical launchers / compositor environments).
export PATH="$HOME/.config/omarchy/bin:$HOME/.local/bin:/usr/local/bin:/usr/bin:$PATH"
# Output directory + files
CHEAT_DIR="$HOME/.config/omarchy/cheats"
TXT="$CHEAT_DIR/hypr-binds.txt"
MD="$CHEAT_DIR/hypr-binds.md"
PDF="$CHEAT_DIR/hypr-binds.pdf"
# Cheat sheet generator (the Python script you wrote)
CHEATGEN="$HOME/.config/omarchy/bin/hypr-cheatgen.py"
# Terminal used to display the cheat sheet
TERM_BIN="/usr/bin/alacritty"
# Ensure output directory exists
mkdir -p "$CHEAT_DIR"
# --- sanity checks with visible feedback ---------------------------------
# Ensure generator exists and is executable
if [[ ! -x "$CHEATGEN" ]]; then
notify-send "Cheat sheet" "hypr-cheatgen.py not executable: $CHEATGEN"
exit 1
fi
# Ensure terminal exists
if [[ ! -x "$TERM_BIN" ]]; then
notify-send "Cheat sheet" "Alacritty not found at $TERM_BIN"
exit 1
fi
# --- generate cheat sheets -----------------------------------------------
# Always regenerate TXT (fast)
"$CHEATGEN" --format ascii --width 80 --out "$TXT"
# Optional: regenerate MD + PDF when asked
if [[ "${1:-}" == "--all" ]]; then
"$CHEATGEN" --format md --out "$MD"
"$CHEATGEN" --format ascii --width 80 --out "$TXT" \
--pdf "$PDF" --pdf-font-size 15 --pdf-line-height 17
fi
# --- display --------------------------------------------------------------
# Open in terminal with less (no wrap, keep alignment)
# exec "$TERM_BIN" --title "Hypr Cheat Sheet" -e bash -lc "less -SR '$TXT'"
exec "$TERM_BIN" --class CheatSheet -e bash -lc "less -SR '$TXT'"
Create:
~/.config/omarchy/bin/show-cheatsheet-pdf.sh
~/.config/omarchy/bin/show-cheatsheet-pdf.sh:
#!/bin/bash
# show-cheatsheet-pdf.sh
#
# What this does:
# - Regenerates your Hyprland keybind cheat sheet as:
# - ASCII text (for alignment/debugging)
# - PDF (for easy reading / fullscreen viewing)
# - Then opens the PDF in a viewer:
# - Prefers zathura if installed
# - Falls back to evince
# - Otherwise uses xdg-open (system default)
#
# Notes:
# - You mentioned making the PDF viewer fullscreen via window rules
# (e.g., Hyprland window rules for Zathura/Evince).
#
# Dependencies / assumptions:
# - hypr-cheatgen.py exists and can run (calls hyprctl)
# - ReportLab must be installed for PDF generation
set -euo pipefail
# Ensure Hypr can find your scripts even if it doesn't inherit your shell PATH
export PATH="$HOME/.config/omarchy/bin:$HOME/.local/bin:/usr/local/bin:/usr/bin:$PATH"
# Output directory + files
CHEAT_DIR="$HOME/.config/omarchy/cheats"
TXT="$CHEAT_DIR/hypr-binds.txt"
PDF="$CHEAT_DIR/hypr-binds.pdf"
# Cheat sheet generator
CHEATGEN="$HOME/.config/omarchy/bin/hypr-cheatgen.py"
# Ensure output directory exists
mkdir -p "$CHEAT_DIR"
# Regenerate (always current)
"$CHEATGEN" --format ascii --width 80 --out "$TXT" \
--pdf "$PDF" --pdf-font-size 15 --pdf-line-height 17
# Open viewer (we'll make it fullscreen via window rules)
if command -v zathura >/dev/null 2>&1; then
exec zathura "$PDF"
elif command -v evince >/dev/null 2>&1; then
exec evince "$PDF"
else
exec xdg-open "$PDF"
fi
Make executable:
chmod +x ~/.config/omarchy/bin/hypr-cheatgen.py
~/.config/omarchy/bin/show-cheatsheet.sh
~/.config/omarchy/bin/show-cheatsheet-pdf.sh
It generates files into:
~/.config/omarchy/cheats/
Create the ~/.config/omarchy/cheats directory.
📕Part 5 — “Customization Status” screen (safe even if NFS is dead)
Create:
~/.config/omarchy/bin/vpn-nas-status.sh
~/.config/omarchy/bin/vpn-nas-status.sh:
#!/usr/bin/env bash
# vpn-nas-status.sh
#
# What this does:
# - Shows a quick "Customization Status" dashboard in the terminal:
# - Current sound theme + whether startup/shutdown MP3 files exist
# - Current screensaver mode (stock vs long)
# - PIA VPN connection state (via piactl)
# - PiVPN service active/inactive (systemd)
# - NAS reachability (ping)
# - Whether key mountpoints are mounted (using /proc/self/mountinfo — safe even if NFS is dead)
#
# Publish note:
# - NAS IP and mountpoint paths are obfuscated for privacy.
set -euo pipefail
# PIA's CLI (path may vary per system; not a secret)
PIACTL="/opt/piavpn/bin/piactl"
# PiVPN systemd service unit name (not a secret)
PIVPN_SERVICE="openvpn-client@pivpn"
# PRIVATE: NAS IP on your LAN (obfuscated for publishing)
NAS_IP="NAS_IP_HERE"
# PRIVATE: mount paths obfuscated for publishing
MOUNTS=(
"/mnt/mount_point_1"
"/mnt/mount_point_2"
"/mnt/mount_point_3"
"/mnt/mount_point_4"
)
# Omarchy customization state + sound assets
SOUND_DIR="$HOME/.config/omarchy/sounds"
STATE_DIR="$HOME/.config/omarchy/state"
MODE_FILE="$STATE_DIR/screensaver_mode"
CURRENT_SOUND_FILE="$STATE_DIR/current_sound"
STARTUP_MP3="$SOUND_DIR/startup.mp3"
SHUTDOWN_MP3="$SOUND_DIR/shutdown.mp3"
# Allow Ctrl+C to exit cleanly without an ugly stack of errors
trap 'echo; echo "[status] interrupted"; exit 0' INT TERM
is_mounted_mountinfo() {
# Check mounted status by reading /proc/self/mountinfo
# (safe even if a network mount is unresponsive)
local target="$1"
awk -v t="$target" '$5==t {found=1} END{exit(found?0:1)}' /proc/self/mountinfo
}
mounted_source_mountinfo() {
# If mounted, extract filesystem type + source from /proc/self/mountinfo
# Output example: "nfs4 10.0.0.118:/mnt/pool/share"
local target="$1"
awk -v t="$target" '
$5==t {
for (i=1; i<=NF; i++) if ($i=="-") {dash=i; break}
fstype=$(dash+1); source=$(dash+2);
printf("%s %s", fstype, source);
exit 0
}
END { exit 1 }
' /proc/self/mountinfo
}
current_sound() {
# Stored by your sound picker script (if set)
if [[ -f "$CURRENT_SOUND_FILE" ]]; then
head -n 1 "$CURRENT_SOUND_FILE" | tr -d '\r'
else
echo "Unknown (not set)"
fi
}
screensaver_mode() {
# Stored by your screensaver mode scripts (if set)
if [[ -f "$MODE_FILE" ]]; then
m="$(head -n 1 "$MODE_FILE" | tr -d '\r')"
case "$m" in
stock) echo "Stock" ;;
long) echo "Long" ;;
*) echo "Unknown ($m)" ;;
esac
else
echo "Unknown (not set)"
fi
}
echo "=== CUSTOMIZATION STATUS ==="
echo
echo "Sounds:"
echo " Theme: $(current_sound)"
echo " Startup Sound: $([[ -f "$STARTUP_MP3" ]] && echo "present ✅" || echo "missing ❌")"
echo " Shutdown Sound: $([[ -f "$SHUTDOWN_MP3" ]] && echo "present ✅" || echo "missing ❌")"
echo
echo "Screensaver:"
echo " Mode: $(screensaver_mode)"
echo
echo "PIA:"
if [[ -x "$PIACTL" ]]; then
# Print PIA connection state (ignore failures)
"$PIACTL" get connectionstate 2>/dev/null || true
else
echo "piactl not found at $PIACTL"
fi
echo
echo "PiVPN ($PIVPN_SERVICE):"
systemctl is-active --quiet "$PIVPN_SERVICE" && echo "active ✅" || echo "inactive ❌"
echo
echo "NAS reachability ($NAS_IP):"
if ping -c 1 -W 1 "$NAS_IP" >/dev/null 2>&1; then
echo "reachable ✅"
else
echo "not reachable ❌"
fi
echo
echo "Mountpoints (from /proc/self/mountinfo — safe even if NFS is dead):"
for m in "${MOUNTS[@]}"; do
if is_mounted_mountinfo "$m"; then
src="$(mounted_source_mountinfo "$m" || true)"
[[ -n "$src" ]] && echo "✅ $m ($src)" || echo "✅ $m"
else
echo "❌ $m"
fi
done
echo
read -n 1 -r -s -p "Press any key to close…"
echo
This script reads mount state via:
/proc/self/mountinfo
…so it won’t hang if NFS is down.
It also reads marker files:
~/.config/omarchy/state/screensaver_mode
~/.config/omarchy/state/current_sound
Create the directory:
mkdir -p ~/.config/omarchy/state
(Optional) seed defaults:
echo “stock” > ~/.config/omarchy/state/screensaver_mode echo “Windows XP” > ~/.config/omarchy/state/current_sound
📙Part 6 — Screensaver mode toggles (hypridle)
Create:
~/.config/omarchy/bin/screensaver-stock-mode.sh
~/.config/omarchy/bin/screensaver-long-mode.sh
Paste:
~/.config/omarchy/bin/screensaver-stock-mode.sh:
#!/usr/bin/env bash
# screensaver-stock-mode.sh
#
# What this does:
# - Switches Hypridle to your "stock" screensaver/idle configuration by:
# 1) Copying hypridle-stock.conf -> hypridle.conf
# 2) Restarting hypridle (systemd user service if present; otherwise manual restart)
# 3) Writing the selected mode ("stock") to Omarchy state:
# ~/.config/omarchy/state/screensaver_mode
#
# Dependencies / assumptions:
# - Assumes Hyprland config lives in ~/.config/hypr
# - Assumes hypridle-stock.conf exists
# - Uses `rg` (ripgrep) to detect whether hypridle.service exists as a user unit
set -euo pipefail
CFG_DIR="$HOME/.config/hypr"
SRC="$CFG_DIR/hypridle-stock.conf"
DST="$CFG_DIR/hypridle.conf"
STATE_DIR="$HOME/.config/omarchy/state"
MODE_FILE="$STATE_DIR/screensaver_mode"
echo "=== Screensaver: STOCK mode ==="
echo "[1/3] Source: $SRC"
echo "[1/3] Destination: $DST"
echo
# Ensure the source config exists before attempting to copy
if [[ ! -f "$SRC" ]]; then
echo "ERROR: missing $SRC"
command -v notify-send >/dev/null 2>&1 && notify-send "Screensaver" "Missing: hypridle-stock.conf" || true
exit 1
fi
# Copy stock config into place as the active hypridle.conf
cp -f "$SRC" "$DST"
echo "[2/3] Copied stock config -> hypridle.conf"
# Restart hypridle:
# - Prefer systemd user service if present
# - Otherwise kill + relaunch manually
if systemctl --user list-unit-files 2>/dev/null | rg -q '^hypridle\.service'; then
systemctl --user restart hypridle.service
echo "[3/3] Restarted hypridle.service (user)"
else
pkill -x hypridle 2>/dev/null || true
nohup hypridle -c "$DST" >/dev/null 2>&1 &
disown || true
echo "[3/3] Restarted hypridle (manual)"
fi
# Record selected mode so your status script can display it
mkdir -p "$STATE_DIR"
echo "stock" > "$MODE_FILE"
# Notify user
command -v notify-send >/dev/null 2>&1 && notify-send "Screensaver" "Stock mode enabled ✅" || true
echo
read -n 1 -r -s -p "Press any key to close…"
echo
~/.config/omarchy/bin/screensaver-long-mode.sh:
#!/usr/bin/env bash
# screensaver-long-mode.sh
#
# What this does:
# - Switches Hypridle to your "long" screensaver/idle configuration by:
# 1) Copying hypridle-long.conf -> hypridle.conf
# 2) Restarting hypridle (systemd user service if present; otherwise manual restart)
# 3) Writing the selected mode ("long") to Omarchy state:
# ~/.config/omarchy/state/screensaver_mode
#
# Dependencies / assumptions:
# - Assumes Hyprland config lives in ~/.config/hypr
# - Assumes hypridle-long.conf exists
# - Uses `rg` (ripgrep) to detect whether hypridle.service exists as a user unit
set -euo pipefail
CFG_DIR="$HOME/.config/hypr"
SRC="$CFG_DIR/hypridle-long.conf"
DST="$CFG_DIR/hypridle.conf"
STATE_DIR="$HOME/.config/omarchy/state"
MODE_FILE="$STATE_DIR/screensaver_mode"
echo "=== Screensaver: LONG mode ==="
echo "[1/3] Source: $SRC"
echo "[1/3] Destination: $DST"
echo
# Ensure the source config exists before attempting to copy
if [[ ! -f "$SRC" ]]; then
echo "ERROR: missing $SRC"
command -v notify-send >/dev/null 2>&1 && notify-send "Screensaver" "Missing: hypridle-long.conf" || true
exit 1
fi
# Copy long config into place as the active hypridle.conf
cp -f "$SRC" "$DST"
echo "[2/3] Copied long config -> hypridle.conf"
# Restart hypridle:
# - Prefer systemd user service if present
# - Otherwise kill + relaunch manually
if systemctl --user list-unit-files 2>/dev/null | rg -q '^hypridle\.service'; then
systemctl --user restart hypridle.service
echo "[3/3] Restarted hypridle.service (user)"
else
pkill -x hypridle 2>/dev/null || true
nohup hypridle -c "$DST" >/dev/null 2>&1 &
disown || true
echo "[3/3] Restarted hypridle (manual)"
fi
# Record selected mode so your status script can display it
mkdir -p "$STATE_DIR"
echo "long" > "$MODE_FILE"
# Notify user
command -v notify-send >/dev/null 2>&1 && notify-send "Screensaver" "Long mode enabled ✅" || true
echo
read -n 1 -r -s -p "Press any key to close…"
echo
These scripts assume you have two hypridle configs. You need “stock” and “long” versions.
~/.config/hypr/hypridle-stock.conf:
# ~/.config/hypr/hypridle-stock.conf
#
# STOCK screensaver/lock timing profile for hypridle.
# This file is used by:
# ~/.config/omarchy/bin/screensaver-stock-mode.sh
# which copies it to:
# ~/.config/hypr/hypridle.conf
# and restarts hypridle.
#
# What this profile does (high level):
# - Locks the session on sleep
# - Starts a screensaver relatively quickly
# - Locks the screen after a short idle period
# - Turns the display off shortly after locking
# - Restores DPMS + brightness state on resume
#
# Publish note:
# - No passwords, IPs, tokens, or hostnames are present here.
# - It references Omarchy helper commands (omarchy-lock-screen / omarchy-launch-screensaver).
general {
lock_cmd = omarchy-lock-screen # lock screen and 1password
before_sleep_cmd = loginctl lock-session # lock before suspend.
after_sleep_cmd = hyprctl dispatch dpms on # to avoid having to press a key twice to turn on the display.
inhibit_sleep = 3 # wait until screen is locked
}
listener {
timeout = 150 # 2.5min
on-timeout = pidof hyprlock || omarchy-launch-screensaver # start screensaver (if we haven't locked already)
}
listener {
timeout = 300 # 5min
on-timeout = loginctl lock-session # lock screen when timeout has passed
}
listener {
timeout = 330 # 5.5min
on-timeout = hyprctl dispatch dpms off # screen off when timeout has passed
on-resume = hyprctl dispatch dpms on && brightnessctl -r # screen on when activity is detected
}
~/.config/hypr/hypridle-long.conf:
# ~/.config/hypr/hypridle-long.conf
#
# LONG screensaver/lock timing profile for hypridle.
# This file is used by:
# ~/.config/omarchy/bin/screensaver-long-mode.sh
# which copies it to:
# ~/.config/hypr/hypridle.conf
# and restarts hypridle.
#
# What this profile does (high level):
# - Same behavior as the stock profile, but with longer idle timeouts
# - Screensaver starts later
# - Lock happens later
# - Display power-off happens later
#
# Publish note:
# - No passwords, IPs, tokens, or hostnames are present here.
# - It references Omarchy helper commands (omarchy-lock-screen / omarchy-launch-screensaver).
general {
lock_cmd = omarchy-lock-screen # lock screen and 1password
before_sleep_cmd = loginctl lock-session # lock before suspend.
after_sleep_cmd = hyprctl dispatch dpms on # to avoid having to press a key twice to turn on the display.
inhibit_sleep = 3 # wait until screen is locked
}
listener {
timeout = 600 # 10min (600)
on-timeout = pidof hyprlock || omarchy-launch-screensaver # start screensaver (if we haven't locked already)
}
listener {
timeout = 900 # 15min (900)
on-timeout = loginctl lock-session # lock screen when timeout has passed
}
listener {
timeout = 990 # 16.5min (990)
on-timeout = hyprctl dispatch dpms off # screen off when timeout has passed
on-resume = hyprctl dispatch dpms on && brightnessctl -r # screen on when activity is detected
}
📘Part 7 — Windows Startup + Shutdown Sounds
Step 1: Sound directory
Create:
mkdir -p ~/.config/omarchy/sounds
Step 2: Put sound files in place
You’ll need theme MP3s in:
~/.config/omarchy/sounds/
Example filenames:
winxpstartup.mp3, winxpshutdown.mp3
win2000startup.mp3, win2000shutdown.mp3
winvistastartup.mp3, winvistashutdown.mp3
win11startup.mp3, win11shutdown.mp3
I’ll provide these to readers as a ZIP download. Extract into ~/.config/omarchy/sounds/.
Active sounds will be copied to:
startup.mp3
shutdown.mp3
Step 3: Theme picker + shutdown action
Create:
~/.config/omarchy/bin/change-startup-shutdown-sounds.sh
~/.config/omarchy/bin/change-startup-shutdown-sounds.sh:
#!/usr/bin/env bash
# change-startup-shutdown-sounds.sh
#
# What this does:
# - Lets you pick a "sound theme" (Windows 2000 / XP / Vista / 11) from a dmenu-style selector
# - Copies the selected theme’s startup/shutdown MP3 files into:
# - ~/.config/omarchy/sounds/startup.mp3
# - ~/.config/omarchy/sounds/shutdown.mp3
# - Writes the selected theme name into:
# - ~/.config/omarchy/state/current_sound
# - Shows a desktop notification + prints a summary in the terminal
#
# Dependencies / assumptions:
# - Requires `omarchy-launch-walker` (Omarchy's dmenu/launcher wrapper)
# - Assumes the MP3 files already exist in $SOUND_DIR
set -euo pipefail
# Where the sound files live
SOUND_DIR="$HOME/.config/omarchy/sounds"
# Where we store the selected theme name
STATE_DIR="$HOME/.config/omarchy/state"
CURRENT_FILE="$STATE_DIR/current_sound"
# Small helper for desktop notifications (silent fallback if notify-send isn't installed)
notify() {
command -v notify-send >/dev/null 2>&1 && notify-send "Omarchy Sounds" "$1" || true
}
# Ensure folders exist
mkdir -p "$SOUND_DIR" "$STATE_DIR"
# Menu options shown to the user
options=$'Windows 2000\nWindows XP\nWindows Vista\nWindows 11'
# Launch the picker (dmenu-style). If the user cancels, exit cleanly.
choice="$(printf '%s\n' "$options" | omarchy-launch-walker --dmenu --width 360 --minheight 1 --maxheight 300 -p "Sound theme…" 2>/dev/null || true)"
[[ -z "${choice:-}" || "$choice" == "CNCLD" ]] && exit 0
# Map theme choice -> the corresponding MP3 files in SOUND_DIR
case "$choice" in
"Windows 2000")
startup_src="$SOUND_DIR/win2000startup.mp3"
shutdown_src="$SOUND_DIR/win2000shutdown.mp3"
;;
"Windows XP")
startup_src="$SOUND_DIR/winxpstartup.mp3"
shutdown_src="$SOUND_DIR/winxpshutdown.mp3"
;;
"Windows Vista")
startup_src="$SOUND_DIR/winvistastartup.mp3"
shutdown_src="$SOUND_DIR/winvistashutdown.mp3"
;;
"Windows 11")
startup_src="$SOUND_DIR/win11startup.mp3"
shutdown_src="$SOUND_DIR/win11shutdown.mp3"
;;
*)
notify "Unknown selection: $choice"
exit 1
;;
esac
# Validate the startup sound exists
if [[ ! -f "$startup_src" ]]; then
notify "Missing file: $(basename "$startup_src")"
echo "Missing: $startup_src" >&2
exit 1
fi
# Validate the shutdown sound exists
if [[ ! -f "$shutdown_src" ]]; then
notify "Missing file: $(basename "$shutdown_src")"
echo "Missing: $shutdown_src" >&2
exit 1
fi
# Copy the selected theme into the "active" sound filenames used by the other scripts
cp -f "$startup_src" "$SOUND_DIR/startup.mp3"
cp -f "$shutdown_src" "$SOUND_DIR/shutdown.mp3"
# Record which theme is active
echo "$choice" > "$CURRENT_FILE"
# Notify + print a quick summary for the user
notify "Sound theme set to: $choice ✅"
echo "Sound theme set to: $choice ✅"
echo
echo "startup.mp3 <- $(basename "$startup_src")"
echo "shutdown.mp3 <- $(basename "$shutdown_src")"
echo
read -n 1 -r -s -p "Press any key to close…"
echo
Create:
~/.config/omarchy/bin/action-with-shutdown-sound.sh
~/.config/omarchy/bin/action-with-shutdown-sound.sh:
#!/usr/bin/env bash
# action-with-shutdown-sound.sh
#
# What this does:
# - Ensures a default sound "theme" exists in ~/.config/omarchy/state/
# - Ensures shutdown.mp3 exists (seeds it from a fallback sound if needed)
# - Plays the shutdown sound *blocking* (so it completes before powering off)
# - Then performs the requested system action:
# - shutdown -> systemctl poweroff
# - reboot -> systemctl reboot
# - halt -> systemctl halt
# Or:
# -- <command...> -> play sound, then exec the command
set -euo pipefail
# Where your sounds live
SOUND_DIR="$HOME/.config/omarchy/sounds"
# Where your customization state lives (stores "current_sound" label)
STATE_DIR="$HOME/.config/omarchy/state"
CURRENT_FILE="$STATE_DIR/current_sound"
# Preferred "active" shutdown sound (selected by your menu/sound picker)
SOUND_SHUTDOWN="$SOUND_DIR/shutdown.mp3"
# Fallback sound (used if user hasn't selected anything yet)
FALLBACK_SHUTDOWN="$SOUND_DIR/winxpshutdown.mp3"
ensure_defaults() {
# Ensure required directories exist
mkdir -p "$SOUND_DIR" "$STATE_DIR"
# If no theme has been selected yet, seed a default label
if [[ ! -f "$CURRENT_FILE" ]]; then
echo "Windows XP" > "$CURRENT_FILE"
fi
# If shutdown.mp3 doesn't exist yet, seed it from fallback (if available)
if [[ ! -f "$SOUND_SHUTDOWN" && -f "$FALLBACK_SHUTDOWN" ]]; then
cp -f "$FALLBACK_SHUTDOWN" "$SOUND_SHUTDOWN"
fi
}
play_sound() {
local f="$1"
# Blocking play (important): we want the sound to finish before shutdown.
# Try several players in order, using the first one found.
if command -v mpv >/dev/null 2>&1; then
mpv --no-video --really-quiet --keep-open=no "$f" >/dev/null 2>&1 || true
elif command -v ffplay >/dev/null 2>&1; then
ffplay -nodisp -autoexit -loglevel quiet "$f" >/dev/null 2>&1 || true
elif command -v pw-play >/dev/null 2>&1; then
pw-play "$f" >/dev/null 2>&1 || true
elif command -v paplay >/dev/null 2>&1; then
paplay "$f" >/dev/null 2>&1 || true
fi
}
# Ensure default files/dirs are present before we proceed
ensure_defaults
# Choose which shutdown sound to play:
# - Prefer shutdown.mp3 if present
# - Otherwise fall back to the theme fallback sound
# - Otherwise play nothing
SOUND_TO_PLAY=""
if [[ -f "$SOUND_SHUTDOWN" ]]; then
SOUND_TO_PLAY="$SOUND_SHUTDOWN"
elif [[ -f "$FALLBACK_SHUTDOWN" ]]; then
SOUND_TO_PLAY="$FALLBACK_SHUTDOWN"
fi
case "${1-}" in
shutdown)
[[ -n "$SOUND_TO_PLAY" ]] && play_sound "$SOUND_TO_PLAY"
exec systemctl poweroff
;;
reboot)
[[ -n "$SOUND_TO_PLAY" ]] && play_sound "$SOUND_TO_PLAY"
exec systemctl reboot
;;
halt)
[[ -n "$SOUND_TO_PLAY" ]] && play_sound "$SOUND_TO_PLAY"
exec systemctl halt
;;
--)
# Generic wrapper mode:
# action-with-shutdown-sound.sh -- <command ...>
# Plays the sound, then execs whatever command you pass.
shift
[[ -n "$SOUND_TO_PLAY" ]] && play_sound "$SOUND_TO_PLAY"
exec "$@"
;;
*)
echo "Usage: $0 {shutdown|reboot|halt|-- <command...>}" >&2
exit 2
;;
esac
Create (optional startup helper):
~/.config/omarchy/bin/hypr-startup-sound.sh
~/.config/omarchy/bin/hypr-startup-sound.sh:
#!/usr/bin/env bash
# hypr-startup-sound.sh
#
# What this does:
# - Ensures your Omarchy sound/state folders exist
# - Ensures a default "current sound theme" label exists (if not set yet)
# - Ensures startup.mp3 exists (seeds it from a fallback sound if needed)
# - Plays the startup sound (best effort) using the first available player:
# mpv -> ffplay -> pw-play -> paplay
set -euo pipefail
# Where your sounds live
SOUND_DIR="$HOME/.config/omarchy/sounds"
# Where your customization state lives (stores "current_sound" label)
STATE_DIR="$HOME/.config/omarchy/state"
CURRENT_FILE="$STATE_DIR/current_sound"
# Preferred "active" startup sound (selected by your menu/sound picker)
SOUND_STARTUP="$SOUND_DIR/startup.mp3"
# Fallback sound (used if user hasn't selected anything yet)
FALLBACK_STARTUP="$SOUND_DIR/winxpstartup.mp3"
ensure_defaults() {
# Ensure required directories exist
mkdir -p "$SOUND_DIR" "$STATE_DIR"
# If no theme has been selected yet, seed a default label
if [[ ! -f "$CURRENT_FILE" ]]; then
echo "Windows XP" > "$CURRENT_FILE"
fi
# If startup.mp3 doesn't exist yet, seed it from fallback (if available)
if [[ ! -f "$SOUND_STARTUP" && -f "$FALLBACK_STARTUP" ]]; then
cp -f "$FALLBACK_STARTUP" "$SOUND_STARTUP"
fi
}
play_sound() {
local f="$1"
# Blocking play: the sound will play fully before the script exits.
# Try several players in order, using the first one found.
if command -v mpv >/dev/null 2>&1; then
mpv --no-video --really-quiet --keep-open=no "$f" >/dev/null 2>&1 || true
elif command -v ffplay >/dev/null 2>&1; then
ffplay -nodisp -autoexit -loglevel quiet "$f" >/dev/null 2>&1 || true
elif command -v pw-play >/dev/null 2>&1; then
pw-play "$f" >/dev/null 2>&1 || true
elif command -v paplay >/dev/null 2>&1; then
paplay "$f" >/dev/null 2>&1 || true
fi
}
# Ensure default files/dirs are present before we proceed
ensure_defaults
# Prefer startup.mp3; fall back to theme fallback sound; otherwise do nothing
if [[ -f "$SOUND_STARTUP" ]]; then
play_sound "$SOUND_STARTUP"
elif [[ -f "$FALLBACK_STARTUP" ]]; then
play_sound "$FALLBACK_STARTUP"
fi
Edit your main ~/.config/hypr/hyprland.conf with the following exec-once for that startup sound:
~/.config/hypr/hyprland.conf SNIPPET:
# Add these lines to hyprland.conf:
# Windows XP StartUp Sound
exec-once = $HOME/.config/omarchy/bin/hypr-startup-sound.sh
📗Part 8 — GParted fix (Wayland + pkexec)
Create:
~/.config/omarchy/bin/gparted-fixed.sh
~/.config/omarchy/bin/gparted-fixed.sh:
#!/usr/bin/env bash
# gparted-fixed.sh
#
# What this does:
# - Launches GParted as root via pkexec (PolicyKit)
# - Explicitly passes through the key environment variables that GUI apps often
# need under Wayland/Hyprland, so GParted reliably opens instead of failing
# with "cannot open display" / missing runtime dir issues.
#
# Why this exists:
# - Some GUI apps launched with pkexec don't inherit the right environment.
# - Setting XDG_RUNTIME_DIR / WAYLAND_DISPLAY / DISPLAY / XAUTHORITY makes the
# privileged GUI session connect properly to your current desktop session.
set -euo pipefail
# Current user numeric ID (used to build /run/user/<uid>)
USER_ID="$(id -u)"
# Run GParted as root with a sane GUI environment
exec pkexec env \
XDG_RUNTIME_DIR="/run/user/$USER_ID" \
WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-1}" \
DISPLAY="${DISPLAY:-:0}" \
XAUTHORITY="${XAUTHORITY:-$HOME/.Xauthority}" \
/usr/bin/gparted
Now you have two options:
Option A (recommended): update the GParted desktop launcher
Create/edit:
~/.local/share/applications/gparted.desktop
~/.local/share/applications/gparted.desktop:
[Desktop Entry]
Name=GParted
X-GNOME-FullName=GParted Partition Editor
Comment=Create, reorganize, and delete partitions
Exec=~/.config/omarchy/bin/gparted-fixed.sh %f
Icon=gparted
Terminal=false
Type=Application
Categories=GNOME;System;Filesystem;
Keywords=Partition;
StartupNotify=true
Option B: run the script manually ~/.config/omarchy/bin/gparted-fixed.sh
📕Part 9 — Fastfetch MOTD “Smart Mode” (logo only when it fits)
Create:
~/.config/omarchy/bin/terminal-motd.sh
~/.config/omarchy/bin/terminal-motd.sh:
#!/usr/bin/env bash
# terminal-motd.sh
#
# What this does:
# - Prints a nice "MOTD" / system info panel using fastfetch when you open a terminal
# - Only prints when:
# - stdout is a real TTY (interactive terminal)
# - it hasn't already been printed in the current terminal session
# - fastfetch exists
# - Adapts output based on terminal width:
# - Narrow terminal (<110 cols): compact, no logo, custom structure (includes localip)
# - Wide terminal: chooses logo size based on how many Alacritty windows are open
# on the current Hyprland workspace
#
# Dependencies / assumptions:
# - fastfetch installed
# - For workspace/window-aware behavior: hyprctl + jq installed and Hyprland running
set -euo pipefail
# If sourced, we can "return"; if executed, we must "exit".
_is_sourced() { [[ "${BASH_SOURCE[0]}" != "${0}" ]]; }
_die() { local rc="${1:-0}"; shift || true; _is_sourced && return "$rc" || exit "$rc"; }
# Only run when stdout is a TTY (real terminal)
[[ -t 1 ]] || _die 0
# Avoid re-printing inside the same terminal session
if [[ -n "${OMARCHY_MOTD_SHOWN:-}" ]]; then
_die 0
fi
export OMARCHY_MOTD_SHOWN=1
# If fastfetch isn't available, do nothing (silent)
command -v fastfetch >/dev/null 2>&1 || _die 0
# Terminal width (columns). Default to 120 if tput fails.
cols="$(tput cols 2>/dev/null || echo 120)"
# Always go compact if the terminal is narrow
if (( cols < 110 )); then
fastfetch --logo none \
--structure "title:separator:os:kernel:uptime:packages:shell:terminal:wm:cpu:memory:disk:break:localip" \
2>/dev/null || true
_die 0
fi
# If Hyprland JSON tools aren't available, just do "no logo"
if ! command -v hyprctl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
fastfetch --logo none 2>/dev/null || true
_die 0
fi
# Give Hypr a split second to register the new client
sleep 0.05
# Get current workspace id
ws_id="$(hyprctl activeworkspace -j 2>/dev/null | jq -r '.id // empty' || true)"
if [[ -z "${ws_id:-}" ]]; then
fastfetch --logo none 2>/dev/null || true
_die 0
fi
# Count Alacritty windows on this workspace.
# NOTE: Hyprland "class" is usually "Alacritty" but sometimes it's "org.alacritty.Alacritty".
alacritty_count="$(
hyprctl clients -j 2>/dev/null | jq --argjson ws "$ws_id" '
[ .[]
| select(.workspace.id == $ws)
| select((.class // "" | ascii_downcase) | test("alacritty"))
] | length
' 2>/dev/null || echo 2
)"
# If this is the first/only Alacritty window on the workspace, show small logo.
# If multiple terminals are open, disable logo to keep output compact.
if [[ "${alacritty_count:-2}" -le 1 ]]; then
fastfetch --logo small 2>/dev/null || true
else
fastfetch --logo none 2>/dev/null || true
fi
_die 0
Now in your ~/.bashrc, you add a small block (do not blindly replace your whole bashrc).
Add to ~/.bashrc (append near the bottom)
In this post I’ll show the exact block to add:
~/.bashrc:
#Add these to end of .bashrc:
export PATH="$HOME/.config/omarchy/bin:$PATH"
export BAT_THEME="Catppuccin Mocha"
[[ $- == *i* ]] && ~/.config/omarchy/bin/terminal-motd.sh
Notes:
Runs only for interactive shells
Sets a Bat theme (optional)
Uses fastfetch with logic based on terminal width + number of Alacritty windows on current workspace
📗Part 10 — Hyprland config: add binds (do not replace whole file)
You’re adding binds, not replacing all of bindings.conf.
Open:
~/.config/hypr/bindings.conf
Add these lines (example includes cheatsheet + emote):
~/.config/hypr/bindings.conf:
# --- Omarchy customizations: add these binds to ~/.config/hypr/bindings.conf ---
#
# What these do:
# - SUPER+SHIFT+K → Generate ALL cheat sheets (TXT + MD + PDF) and open the TXT view
# - SUPER+ALT+K → Generate + open the PDF cheat sheet (fullscreen via your window rules)
# - SUPER+ALT+E → Launch the emoji picker (`emote`)
# Custom cheatsheet (generate + show everything)
bind = SUPER SHIFT, K, exec, $HOME/.config/omarchy/bin/show-cheatsheet.sh --all
# Custom cheatsheet (open PDF viewer)
bind = SUPER ALT, K, exec, $HOME/.config/omarchy/bin/show-cheatsheet-pdf.sh
# Emoji picker
bind = SUPER ALT, E, exec, emote
Then reload Hyprland.
📘Part 11 — Waybar VPN modules (optional)
If you want the Waybar indicators/toggles, create these files:
~/.config/waybar/pia-status.sh
~/.config/waybar/pia-toggle.sh
~/.config/waybar/pia-pick-region.sh
~/.config/waybar/pivpn-status.sh
~/.config/waybar/pivpn-toggle.sh
Paste them:
~/.config/waybar/pia-status.sh:
#!/usr/bin/env bash
# pia-status.sh
#
# What this does:
# - Outputs JSON for a Waybar custom module showing PIA VPN status:
# - "text": a toggle icon (on/off)
# - "class": active/inactive (for CSS styling)
# - "tooltip": multi-line details (state, region, public IP, VPN IP)
#
# Waybar usage:
# - This script is typically called by a Waybar "custom" module with `exec`
# - Waybar reads the JSON and displays the text + tooltip
#
# Dependencies / assumptions:
# - PIA is installed and `piactl` exists at /opt/piavpn/bin/piactl
# - Optional: `jq` (used for safer JSON escaping; script has a fallback escaper)
set -euo pipefail
export LC_ALL=C LANG=C
PIACTL="/opt/piavpn/bin/piactl"
# Helper that never hard-fails: returns empty/partial output if piactl errors.
safe_get() { "$PIACTL" get "$1" 2>/dev/null | tr -d '\r' || true; }
# Query PIA state fields
state="$(safe_get connectionstate)"
region="$(safe_get region)"
pubip="$(safe_get pubip)"
vpnip="$(safe_get vpnip)"
# Pick icon + CSS class based on connection state
if [[ "$state" == "Connected" ]]; then
text=""; cls="active"
else
text=""; cls="inactive"
fi
# Build tooltip with real newlines (no markup)
tip="PIA VPN: ${state:-Unknown}
Region: ${region:-auto}
Public IP: ${pubip:-unknown}
VPN IP: ${vpnip:-unknown}"
if command -v jq >/dev/null 2>&1; then
# Use jq to JSON-escape fields so newlines render correctly
printf '{"text":%s,"class":%s,"tooltip":%s}\n' \
"$(printf %s "$text" | jq -Rs .)" \
"$(printf %s "$cls" | jq -Rs .)" \
"$(printf %s "$tip" | jq -Rs .)"
else
# Fallback escaper if jq isn't available
esc() {
local s=${1//\\/\\\\}
s=${s//\"/\\\"}
s=${s//$'\n'/\\n}
printf '%s' "$s"
}
printf '{"text":"%s","class":"%s","tooltip":"%s"}\n' \
"$(esc "$text")" "$(esc "$cls")" "$(esc "$tip")"
fi
~/.config/waybar/pia-toggle.sh:
#!/usr/bin/env bash
# pia-toggle.sh
#
# What this does:
# - Simple toggle for Private Internet Access (PIA) using piactl:
# - If PIA is Connected or Connecting -> disconnect
# - Otherwise -> connect
# - Enables `piactl background` so the daemon can run headless (no GUI needed)
#
# Typical usage:
# - Called from a Waybar button (custom module "on-click") or a keybind
#
# Dependencies / assumptions:
# - PIA is installed and `piactl` exists at /opt/piavpn/bin/piactl
PIACTL="/opt/piavpn/bin/piactl"
# Make sure daemon can run headless
$PIACTL background enable >/dev/null 2>&1
# Read current connection state
state="$($PIACTL get connectionstate 2>/dev/null)"
if [[ "$state" == "Connected" || "$state" == "Connecting" ]]; then
# If already up (or coming up), disconnect
$PIACTL disconnect
else
# Otherwise connect
$PIACTL connect
fi
~/.config/waybar/pia-pick-region.sh:
#!/usr/bin/env bash
# pia-pick-region.sh
#
# What this does:
# - Fetches available Private Internet Access (PIA) regions using piactl
# - Lets you pick a region using a simple UI picker:
# - rofi (preferred)
# - wofi
# - zenity (GUI fallback)
# - Sets the selected region via:
# piactl set region "<choice>"
# - Sends a desktop notification confirming the selection
#
# Dependencies / assumptions:
# - PIA is installed and `piactl` exists at /opt/piavpn/bin/piactl
# - You are logged in to PIA (otherwise regions may be empty)
# - One of rofi/wofi/zenity is installed for selection UI
PIACTL="/opt/piavpn/bin/piactl"
# Get regions from piactl, then normalize to one region per line
regions="$($PIACTL get regions 2>/dev/null | tr ' ' '\n' | sed '/^$/d')"
[ -z "$regions" ] && notify-send "PIA" "No regions yet. Try: piactl login" && exit 1
# Pick with rofi/wofi, fallback to zenity
if command -v rofi >/dev/null; then
choice="$(echo "$regions" | rofi -dmenu -p 'PIA region')"
elif command -v wofi >/dev/null; then
choice="$(echo "$regions" | wofi --dmenu -p 'PIA region')"
elif command -v zenity >/dev/null; then
choice="$(echo "$regions" | zenity --list --column=Region)"
else
notify-send "PIA" "Install rofi/wofi/zenity to pick regions."
exit 1
fi
# If user cancelled, exit cleanly
[ -z "$choice" ] && exit 0
# Apply selected region and notify
$PIACTL set region "$choice" && notify-send "PIA" "Region set: $choice"
~/.config/waybar/pivpn-status.sh:
#!/usr/bin/env bash
# pivpn-status.sh
#
# What this does:
# - Outputs JSON for a Waybar custom module showing PiVPN status:
# - "text": a toggle icon (on/off)
# - "class": active/inactive (for CSS styling)
# - "tooltip": multi-line details (state, tun interface, VPN IP)
#
# How it detects status:
# - Checks whether the systemd service `openvpn-client@pivpn` is active
# - Attempts to find a tun interface (tun0, tun1, etc.) and read its IPv4 address
#
# Dependencies / assumptions:
# - systemd
# - iproute2 (`ip`)
# - Optional: `jq` (used for safer JSON escaping; script has a fallback escaper)
set -euo pipefail
export LC_ALL=C LANG=C
SERVICE="openvpn-client@pivpn"
# Figure out if the PiVPN systemd service is active
if systemctl is-active --quiet "$SERVICE"; then
state="Connected"
text=""
cls="active"
else
state="Disconnected"
text=""
cls="inactive"
fi
# Try to detect tun interface + VPN IP
tun_if="$(ip -o link show | awk -F': ' '/tun[0-9]+/ {print $2; exit}' 2>/dev/null || true)"
vpn_ip=""
if [[ -n "${tun_if:-}" ]]; then
vpn_ip="$(ip -o -4 addr show "$tun_if" 2>/dev/null | awk '{print $4}' | cut -d/ -f1 || true)"
fi
# Build tooltip with real newlines (no markup)
tip="PiVPN: ${state:-Unknown}
Interface: ${tun_if:-none}
VPN IP: ${vpn_ip:-unknown}"
if command -v jq >/dev/null 2>&1; then
# Use jq to JSON-escape fields so newlines render correctly
printf '{"text":%s,"class":%s,"tooltip":%s}\n' \
"$(printf %s "$text" | jq -Rs .)" \
"$(printf %s "$cls" | jq -Rs .)" \
"$(printf %s "$tip" | jq -Rs .)"
else
# Fallback escaper if jq isn't available
esc() {
local s=${1//\\/\\\\}
s=${s//\"/\\\"}
s=${s//$'\n'/\\n}
printf '%s' "$s"
}
printf '{"text":"%s","class":"%s","tooltip":"%s"}\n' \
"$(esc "$text")" "$(esc "$cls")" "$(esc "$tip")"
fi
~/.config/waybar/pivpn-toggle.sh:
#!/usr/bin/env bash
# pivpn-toggle.sh
#
# What this does:
# - Toggles your PiVPN connection using the systemd OpenVPN client unit:
# openvpn-client@pivpn
# - If the service is active:
# - runs your pivpn-disconnect.sh helper (silently)
# - If the service is inactive:
# - runs your pivpn-connect.sh helper (silently)
#
# Why helper scripts are used:
# - Your connect/disconnect scripts can handle extra logic like:
# - coordinating with PIA (disconnecting/restoring)
# - routes, DNS, cleanup, notifications, etc.
#
# Dependencies / assumptions:
# - systemd + an OpenVPN client unit named openvpn-client@pivpn
# - These helper scripts exist:
# ~/.local/bin/pivpn-connect.sh
# ~/.local/bin/pivpn-disconnect.sh
set -euo pipefail
SERVICE="openvpn-client@pivpn"
if systemctl is-active --quiet "$SERVICE"; then
# PiVPN is ON → disconnect (and maybe restore PIA)
~/.local/bin/pivpn-disconnect.sh >/dev/null 2>&1
else
# PiVPN is OFF → connect (and maybe drop PIA)
~/.local/bin/pivpn-connect.sh >/dev/null 2>&1
fi
Then edit your Waybar config to include modules. Since every Waybar config differs, I’m going to provide:
an example module snippet to paste into your config.jsonc
optional CSS
~/.config/waybar/config.jsonc SNIPPET:
* Find the "modules-right": [...] array and add these two entries wherever you
want the icons to appear:
// --- Custom VPN buttons (add these entries to modules-right) ---
"custom/pia",
"custom/pivpn"
* Paste this anywhere at the top level of the JSON (most people put it near
other custom/* blocks):
// --- Custom VPN button: PIA ---
// Requires these scripts:
// ~/.config/waybar/pia-status.sh
// ~/.config/waybar/pia-toggle.sh
// ~/.config/waybar/pia-pick-region.sh (right-click)
"custom/pia": {
"format": "{}",
"return-type": "json",
"interval": 3,
"exec": "$HOME/.config/waybar/pia-status.sh",
"on-click": "$HOME/.config/waybar/pia-toggle.sh",
"on-click-right": "$HOME/.config/waybar/pia-pick-region.sh"
},
// --- Custom VPN button: PiVPN ---
// Requires these scripts:
// ~/.config/waybar/pivpn-status.sh
// ~/.config/waybar/pivpn-toggle.sh
//
// Note: pivpn-toggle.sh calls your helper scripts:
// ~/.local/bin/pivpn-connect.sh
// ~/.local/bin/pivpn-disconnect.sh
"custom/pivpn": {
"format": "{}",
"exec": "$HOME/.config/waybar/pivpn-status.sh",
"interval": 5,
"return-type": "json",
"on-click": "$HOME/.config/waybar/pivpn-toggle.sh"
}
~/.config/waybar/style.css SNIPPET:
/* ---------------------------
PIA + PiVPN buttons (Waybar)
---------------------------
These IDs match the Waybar custom modules:
"custom/pia" -> #custom-pia
"custom/pivpn" -> #custom-pivpn
The scripts output JSON with:
"class": "active" or "inactive"
...so we can style connected vs disconnected states.
*/
/* Base styling for both VPN icons */
#custom-pia,
#custom-pivpn {
margin: 0 6px;
font-size: 14px;
}
/* Extra tiny gap between the two VPN icons */
#custom-pivpn {
margin-left: 7px;
}
/* PIA status styling */
#custom-pia.active {
color: #a6e3a1;
}
#custom-pia.inactive {
opacity: 0.7;
}
/* PiVPN status styling */
#custom-pivpn.active {
color: #a6e3a1;
}
#custom-pivpn.inactive {
opacity: 0.7;
}
Restart Waybar.
📙Part 12 — Fonts (Amiga vibes)
Copy your preferred .ttf fonts into:
~/.local/share/fonts/
Then:
fc-cache -fv
In this post, I’ll show the fonts I used (and/or provide a ZIP):
~/.local/share/fonts file list:
MicroKnight_v1.0.ttf omarchy.ttf Topaz_a500_v1.0.ttf
MicroKnightPlus_v1.0.ttf P0T-NOoDLE_v1.0.ttf TopazPlus_a1200_v1.0.ttf
"mO'sOul_v1.0.ttf" Topaz_a1200_v1.0.ttf TopazPlus_a500_v1.0.ttf
Set your terminal font in Alacritty:
~/.config/alacritty/alacritty.toml:
general.import = [ "~/.config/omarchy/current/theme/alacritty.toml" ]
[env]
TERM = "xterm-256color"
[font]
normal = { family = "TopazPlus a600a1200a4000", style = "Regular" }
bold = { family = "TopazPlus a600a1200a4000", style = "Bold" }
italic = { family = "TopazPlus a600a1200a4000", style = "Italic" }
size = 12
[window]
padding.x = 14
padding.y = 14
decorations = "None"
[keyboard]
bindings = [
{ key = "F11", action = "ToggleFullscreen" },
{ key = "Insert", mods = "Shift", action = "Paste" },
{ key = "Insert", mods = "Control", action = "Copy" }
]
⬇️ Download the config and sound files HERE!
Let’s go TechHearters!!