Port desktop integration to Omarchy 4 (Quickshell/Lua)

The backup engine was unaffected, but every piece of desktop integration
was written against Omarchy 3 and had gone silently dead on Omarchy 4.

- Keybinding: Hyprland 0.56 uses the Lua config provider, so ~/.config/hypr/
  *.conf is never read. Detect the flavour and write bindings.lua /
  autostart.lua (o.window, o.bind, o.launch_on_start) or the old .conf
  syntax, and strip our stale .conf block on upgraded machines.
- Match the window by app-id TUI.redpill rather than the title "RED PILL":
  an unspaced id survives a Lua string, a Hyprland exec and a .desktop Exec
  without quoting bugs.
- Live-apply via hyprctl reload on Lua; `hyprctl keyword` is rejected by the
  non-legacy parser.
- Terminal: resolve through xdg-terminal-exec instead of hardcoding
  alacritty, which is no longer an Omarchy dependency.
- Theme: record the active theme in backup.log (and a .theme sidecar for
  local tarballs). Omarchy 4 keeps it in ~/.local/state/omarchy, which the
  default excludes skip, so restores were silently losing the theme.
- Gate the Walker blocks on Walker actually being installed.
- Uninstaller cleans both the .lua and .conf layouts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
28allday 2026-07-27 22:18:55 +01:00
parent 59ad45e4bf
commit 1d7d3ee596
3 changed files with 355 additions and 162 deletions

View file

@ -18,15 +18,17 @@ A unified backup and restore system for [Omarchy](https://omarchy.com). Backs up
- **Backup verification**: checksum manifests (xxh128 or sha256) with integrity checking - **Backup verification**: checksum manifests (xxh128 or sha256) with integrity checking
- **New machine restore**: standalone `restore.sh` written to the backup drive - works without Red Pill installed - **New machine restore**: standalone `restore.sh` written to the backup drive - works without Red Pill installed
- **Username migration**: automatically fixes paths when restoring to a different username/home directory - **Username migration**: automatically fixes paths when restoring to a different username/home directory
- **Theme restoration**: reapplies Omarchy theme after restore - **Theme restoration**: records the active Omarchy theme in the snapshot and reapplies it after restore
- **Login notifications**: reminds you if no backup in 7+ days - **Login notifications**: reminds you if no backup in 7+ days
- **Walker integration**: searchable from the Omarchy launcher - **Launcher integration**: searchable from the Omarchy launcher (`Super+Space`)
- **Hyprland keybind**: `Super+Alt+B` opens a floating TUI window - **Hyprland keybind**: `Super+Alt+B` opens a floating TUI window
- **Omarchy 3 and 4**: detects the Lua config (Omarchy 4 / Quickshell) or the older `.conf` layout and writes whichever the machine actually reads
## Requirements ## Requirements
- **OS**: [Omarchy](https://omarchy.com) (Arch Linux) - **OS**: [Omarchy](https://omarchy.com) 3 or 4 (Arch Linux)
- **Dependencies**: `rsync`, `libnewt` (whiptail), `zstd`, `alacritty`, `desktop-file-utils` (all ship with Omarchy) - **Dependencies**: `rsync`, `libnewt` (whiptail), `zstd`, `desktop-file-utils` (all ship with Omarchy)
- **Terminal**: whichever one you use — the TUI is launched through `xdg-terminal-exec`, falling back to ghostty/kitty/alacritty/foot
- **Backup destination**: any external drive (ext4, NTFS, btrfs, exFAT, etc.) - **Backup destination**: any external drive (ext4, NTFS, btrfs, exFAT, etc.)
## Quick Start ## Quick Start
@ -114,9 +116,11 @@ Shows the Matrix quote, then restores from the latest snapshot on your external
|------|---------| |------|---------|
| `~/.local/share/applications/redpill.desktop` | User application menu entry | | `~/.local/share/applications/redpill.desktop` | User application menu entry |
| `/usr/share/applications/redpill.desktop` | System application menu entry | | `/usr/share/applications/redpill.desktop` | System application menu entry |
| `~/.config/walker/config.toml` | Walker launcher custom command entry | | `~/.config/hypr/bindings.lua` | Hyprland keybind (`Super+Alt+B`) + window rules — Omarchy 4 |
| `~/.config/hypr/bindings.conf` | Hyprland keybind (`Super+Alt+B`) + window rules | | `~/.config/hypr/autostart.lua` | Login notification autostart — Omarchy 4 |
| `~/.config/hypr/autostart.conf` | Login notification autostart | | `~/.config/hypr/bindings.conf` | Keybind + window rules — Omarchy 3 |
| `~/.config/hypr/autostart.conf` | Login notification autostart — Omarchy 3 |
| `~/.config/walker/config.toml` | Walker custom command entry — Omarchy 3 only |
### Systemd Service ### Systemd Service
@ -228,9 +232,9 @@ bash Redpill_uninstall.sh
The uninstaller removes: The uninstaller removes:
- All binaries from `/usr/local/bin/` - All binaries from `/usr/local/bin/`
- Desktop entries (user + system) - Desktop entries (user + system)
- Hyprland keybinding and window rules - Hyprland keybinding and window rules (`.lua` and `.conf` layouts)
- Hyprland autostart entry - Hyprland autostart entry
- Walker custom command entry - Walker custom command entry (Omarchy 3)
- Systemd auto-backup service - Systemd auto-backup service
- Optionally: config and state directories - Optionally: config and state directories

View file

@ -97,37 +97,55 @@ fi
update-desktop-database "$HOME/.local/share/applications" >/dev/null 2>&1 || true update-desktop-database "$HOME/.local/share/applications" >/dev/null 2>&1 || true
sudo update-desktop-database /usr/share/applications >/dev/null 2>&1 || true sudo update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
# 4) Remove Hyprland keybinding block from bindings.conf # 4) Remove Hyprland keybinding block.
BINDINGS_FILE="$HOME/.config/hypr/bindings.conf" # Omarchy 4 keeps bindings in bindings.lua, Omarchy 3 in bindings.conf — an
if [[ -f "$BINDINGS_FILE" ]] && grep -q '# Red Pill Backup TUI' "$BINDINGS_FILE"; then # upgraded machine can have both, so clean whichever carries our marker.
# Remove the block between "# Red Pill Backup TUI" and "# End Red Pill Backup TUI" (inclusive) _removed_bind=0
sed -i '/# Red Pill Backup TUI/,/# End Red Pill Backup TUI/d' "$BINDINGS_FILE" _removed_lua=0
for BINDINGS_FILE in "$HOME/.config/hypr/bindings.lua" "$HOME/.config/hypr/bindings.conf"; do
[[ -f "$BINDINGS_FILE" ]] || continue
grep -q 'Red Pill Backup TUI' "$BINDINGS_FILE" || continue
[[ "$BINDINGS_FILE" == *.lua ]] && _removed_lua=1
# Remove the block between the start and end markers (inclusive), in either
# comment syntax
sed -i '/^[-#]* *Red Pill Backup TUI/,/^[-#]* *End Red Pill Backup TUI/d' "$BINDINGS_FILE"
# Clean up any resulting double blank lines # Clean up any resulting double blank lines
sed -i '/^$/N;/^\n$/d' "$BINDINGS_FILE" sed -i '/^$/N;/^\n$/d' "$BINDINGS_FILE"
_ok "Removed Red Pill keybinding and window rules from $BINDINGS_FILE" _ok "Removed Red Pill keybinding and window rules from $BINDINGS_FILE"
_removed_bind=1
done
# Remove live Hyprland bindings if running if (( _removed_bind )); then
# Remove live Hyprland bindings if running. The Lua parser rejects
# `hyprctl keyword`, so reload the config there instead.
if command -v hyprctl >/dev/null 2>&1 && hyprctl monitors -j &>/dev/null; then if command -v hyprctl >/dev/null 2>&1 && hyprctl monitors -j &>/dev/null; then
if (( _removed_lua )); then
hyprctl reload >/dev/null 2>&1 || true
else
hyprctl keyword unbind "SUPER ALT, B" >/dev/null 2>&1 || true hyprctl keyword unbind "SUPER ALT, B" >/dev/null 2>&1 || true
fi
_ok "Removed live Hyprland keybinding" _ok "Removed live Hyprland keybinding"
fi fi
else else
_skip "Hyprland keybinding in bindings.conf" _skip "Hyprland keybinding (not found in bindings.lua or bindings.conf)"
fi fi
# 5) Remove redpill-notify from Hyprland autostart # 5) Remove redpill-notify from Hyprland autostart (both flavours)
AUTOSTART_FILE="$HOME/.config/hypr/autostart.conf" _removed_autostart=0
if [[ -f "$AUTOSTART_FILE" ]] && grep -q 'redpill-notify' "$AUTOSTART_FILE"; then for AUTOSTART_FILE in "$HOME/.config/hypr/autostart.lua" "$HOME/.config/hypr/autostart.conf"; do
sed -i '/# Red Pill backup reminder on login/d' "$AUTOSTART_FILE" [[ -f "$AUTOSTART_FILE" ]] || continue
sed -i '/exec-once = redpill-notify/d' "$AUTOSTART_FILE" grep -q 'redpill-notify' "$AUTOSTART_FILE" || continue
sed -i '/Red Pill backup reminder on login/d' "$AUTOSTART_FILE"
sed -i '/End Red Pill backup reminder/d' "$AUTOSTART_FILE"
sed -i '/redpill-notify/d' "$AUTOSTART_FILE"
# Clean up any resulting double blank lines # Clean up any resulting double blank lines
sed -i '/^$/N;/^\n$/d' "$AUTOSTART_FILE" sed -i '/^$/N;/^\n$/d' "$AUTOSTART_FILE"
_ok "Removed redpill-notify from $AUTOSTART_FILE" _ok "Removed redpill-notify from $AUTOSTART_FILE"
else _removed_autostart=1
_skip "redpill-notify in autostart.conf" done
fi (( _removed_autostart )) || _skip "redpill-notify in Hyprland autostart"
# 6) Remove Walker custom command entry for RED PILL # 6) Remove Walker custom command entry for RED PILL (Omarchy 3 only)
WCONF="$HOME/.config/walker/config.toml" WCONF="$HOME/.config/walker/config.toml"
if [[ -f "$WCONF" ]] && grep -q 'label = "RED PILL"' "$WCONF"; then if [[ -f "$WCONF" ]] && grep -q 'label = "RED PILL"' "$WCONF"; then
# Remove the [[builtins.custom_commands.entries]] block for RED PILL # Remove the [[builtins.custom_commands.entries]] block for RED PILL
@ -159,8 +177,8 @@ else
_skip "RED PILL in Walker config" _skip "RED PILL in Walker config"
fi fi
# 7) Kill walker to refresh # 7) Kill walker to refresh (Omarchy 3 only — Omarchy 4 has no Walker)
pkill -x walker >/dev/null 2>&1 || true command -v walker >/dev/null 2>&1 && pkill -x walker >/dev/null 2>&1 || true
# 8) Ask about config and state directories # 8) Ask about config and state directories
echo echo

View file

@ -5,8 +5,10 @@ set -euo pipefail
# Never edits icons or your System menu. # Never edits icons or your System menu.
# 0) Deps — check only, these all ship with omarchy # 0) Deps — check only, these all ship with omarchy
# NB: no terminal is listed here. Omarchy 4 lets the user pick their terminal
# (xdg-terminal-exec resolves it), so pulling in a specific one would be wrong.
missing=() missing=()
for pkg in rsync libnewt zstd alacritty desktop-file-utils; do for pkg in rsync libnewt zstd desktop-file-utils; do
pacman -Q "$pkg" &>/dev/null || missing+=("$pkg") pacman -Q "$pkg" &>/dev/null || missing+=("$pkg")
done done
if (( ${#missing[@]} > 0 )); then if (( ${#missing[@]} > 0 )); then
@ -96,6 +98,47 @@ timestamp(){ date +"%Y-%m-%d_%H-%M-%S"; }
hosttag(){ hostnamectl --static 2>/dev/null || hostname; } hosttag(){ hostnamectl --static 2>/dev/null || hostname; }
choose_comp(){ have zstd && echo zstd || echo gzip; } choose_comp(){ have zstd && echo zstd || echo gzip; }
# ── Omarchy theme name ──
# Omarchy 4 moved current-theme state to ~/.local/state/omarchy (which is
# excluded from backups), Omarchy 3 kept it in ~/.config/omarchy.
current_theme_name(){
local f
for f in "$HOME/.local/state/omarchy/current/theme.name" \
"$HOME/.config/omarchy/current/theme.name"; do
if [[ -f "$f" ]]; then
head -n1 "$f" 2>/dev/null | tr -d '[:space:]'
return 0
fi
done
return 1
}
# Theme recorded in a snapshot's backup.log, falling back to the Omarchy 3
# location inside the just-restored $HOME (older backups have no Theme: line).
snapshot_theme_name(){
local log="${1:-}" t=""
if [[ -f "$log" ]]; then
t="$(grep -oP '^Theme:\s+\K.*' "$log" 2>/dev/null | head -n1)" || t=""
fi
if [[ -z "$t" && -f "$HOME/.config/omarchy/current/theme.name" ]]; then
t="$(head -n1 "$HOME/.config/omarchy/current/theme.name" 2>/dev/null)" || t=""
fi
printf '%s' "$(printf '%s' "$t" | tr -d '[:space:]')"
}
# Reapply a theme to the running session. Silent no-op if we don't know one.
reapply_theme(){
local t="${1:-}"
[[ -n "$t" ]] || return 0
have omarchy-theme-set || return 0
_info "Reapplying theme" "$t"
if omarchy-theme-set "$t" >/dev/null 2>&1; then
_ok "Theme '$t' applied to running session"
else
_warn "Could not apply theme '$t' — is it installed on this machine?"
fi
}
# ── Prompt to reboot after restore ── # ── Prompt to reboot after restore ──
prompt_reboot(){ prompt_reboot(){
echo echo
@ -573,6 +616,9 @@ mk_backup(){
echo "Hostname: $(hosttag)" echo "Hostname: $(hosttag)"
echo "User: $USER" echo "User: $USER"
echo "Home: $HOME" echo "Home: $HOME"
# Recorded here because Omarchy 4 keeps the current theme in
# ~/.local/state/omarchy, which the default excludes skip.
echo "Theme: $(current_theme_name 2>/dev/null || echo unknown)"
echo echo
} > "$logfile" } > "$logfile"
@ -805,14 +851,29 @@ if [[ -n "$OLD_HOME" && "$OLD_HOME" != "$HOME" ]]; then
echo " ${_green}${_reset} Paths updated for new home directory" echo " ${_green}${_reset} Paths updated for new home directory"
fi fi
# Reapply omarchy theme if available # Reapply omarchy theme if available.
THEME_NAME_FILE="$HOME/.config/omarchy/current/theme.name" # Omarchy 4 keeps the current theme in ~/.local/state/omarchy (not backed up),
if command -v omarchy-theme-set >/dev/null 2>&1 && [[ -f "$THEME_NAME_FILE" ]]; then # so the snapshot's backup.log is the source of truth; fall back to the
RESTORED_THEME="$(cat "$THEME_NAME_FILE")" # Omarchy 3 path for snapshots taken before that line existed.
if [[ -n "$RESTORED_THEME" ]]; then RESTORED_THEME=""
if [[ -f "$BACKUP_LOG" ]]; then
RESTORED_THEME="$(grep -oP '^Theme:\s+\K.*' "$BACKUP_LOG" 2>/dev/null | head -n1)" || RESTORED_THEME=""
fi
if [[ -z "$RESTORED_THEME" || "$RESTORED_THEME" == "unknown" ]]; then
if [[ -f "$HOME/.config/omarchy/current/theme.name" ]]; then
RESTORED_THEME="$(head -n1 "$HOME/.config/omarchy/current/theme.name" 2>/dev/null)" || RESTORED_THEME=""
else
RESTORED_THEME=""
fi
fi
RESTORED_THEME="$(printf '%s' "$RESTORED_THEME" | tr -d '[:space:]')"
if command -v omarchy-theme-set >/dev/null 2>&1 && [[ -n "$RESTORED_THEME" ]]; then
echo " Applying theme: $RESTORED_THEME" echo " Applying theme: $RESTORED_THEME"
omarchy-theme-set "$RESTORED_THEME" >/dev/null 2>&1 || true if omarchy-theme-set "$RESTORED_THEME" >/dev/null 2>&1; then
echo " ${_green}${_reset} Theme applied" echo " ${_green}${_reset} Theme applied"
else
echo " ${_yellow}${_reset} Could not apply theme '$RESTORED_THEME' — is it installed?"
fi fi
fi fi
@ -894,6 +955,11 @@ config_backup(){
tar -czpf "$out" "${rels[@]}" tar -czpf "$out" "${rels[@]}"
fi fi
popd >/dev/null popd >/dev/null
# Sidecar with the active theme — the tarball itself can't carry it on
# Omarchy 4, where theme state lives outside ~/.config.
current_theme_name > "${out}.theme" 2>/dev/null || rm -f "${out}.theme"
_ok "Config backup written: $out" _ok "Config backup written: $out"
} }
@ -1299,18 +1365,7 @@ restore_snapshot(){
fix_restored_paths "$source/backup.log" fix_restored_paths "$source/backup.log"
# Reapply omarchy theme so the restored theme takes effect in the running session # Reapply omarchy theme so the restored theme takes effect in the running session
if have omarchy-theme-set; then reapply_theme "$(snapshot_theme_name "$source/backup.log")"
local theme_name_file="$HOME/.config/omarchy/current/theme.name"
if [[ -f "$theme_name_file" ]]; then
local restored_theme
restored_theme="$(cat "$theme_name_file")"
if [[ -n "$restored_theme" ]]; then
_info "Reapplying theme" "$restored_theme"
omarchy-theme-set "$restored_theme" >/dev/null 2>&1 || true
_ok "Theme '$restored_theme' applied to running session"
fi
fi
fi
prompt_reboot prompt_reboot
} }
@ -1346,19 +1401,14 @@ restore_local(){
rm -f "$tmp_log" rm -f "$tmp_log"
fi fi
# Reapply omarchy theme so the restored theme takes effect in the running session # Reapply omarchy theme — prefer the sidecar written alongside the tarball,
if have omarchy-theme-set; then # else whatever the Omarchy 3 config tree carried
local theme_name_file="$HOME/.config/omarchy/current/theme.name" local local_theme=""
if [[ -f "$theme_name_file" ]]; then if [[ -f "${a}.theme" ]]; then
local restored_theme local_theme="$(head -n1 "${a}.theme" 2>/dev/null | tr -d '[:space:]')" || local_theme=""
restored_theme="$(cat "$theme_name_file")"
if [[ -n "$restored_theme" ]]; then
_info "Reapplying theme" "$restored_theme"
omarchy-theme-set "$restored_theme" >/dev/null 2>&1 || true
_ok "Theme '$restored_theme' applied to running session"
fi
fi
fi fi
[[ -n "$local_theme" ]] || local_theme="$(snapshot_theme_name "")"
reapply_theme "$local_theme"
prompt_reboot prompt_reboot
} }
@ -1721,48 +1771,82 @@ if ! sudo chmod +x /usr/local/bin/bluepill; then
exit 1 exit 1
fi fi
# 3) Applications entry — write to BOTH system & user dirs # 3) Work out how to open the TUI in a terminal window.
# Omarchy 4 resolves the user's chosen terminal through xdg-terminal-exec, so
# prefer that and only fall back to probing individual terminals.
#
# The window is identified by app-id/class rather than title: the same string
# has to survive a Lua string, a Hyprland exec (which goes through /bin/sh) and
# a .desktop Exec key, and an unspaced app-id needs no quoting in any of them.
# It must contain a dot — ghostty rejects app-ids that don't look like an ID.
REDPILL_APPID="TUI.redpill"
REDPILL_LAUNCH=""
if command -v xdg-terminal-exec >/dev/null 2>&1; then
REDPILL_LAUNCH="xdg-terminal-exec --app-id=$REDPILL_APPID -e redpill"
elif command -v ghostty >/dev/null 2>&1; then
REDPILL_LAUNCH="ghostty --class=$REDPILL_APPID -e redpill"
elif command -v kitty >/dev/null 2>&1; then
REDPILL_LAUNCH="kitty --class=$REDPILL_APPID redpill"
elif command -v alacritty >/dev/null 2>&1; then
REDPILL_LAUNCH="alacritty --class $REDPILL_APPID -e redpill"
elif command -v foot >/dev/null 2>&1; then
REDPILL_LAUNCH="foot --app-id=$REDPILL_APPID redpill"
else
echo "Warning: no supported terminal found — run 'redpill' from a shell instead" >&2
fi
# With no terminal available, fall back to a Terminal=true desktop entry so the
# launcher still has something to run.
if [[ -n "$REDPILL_LAUNCH" ]]; then
REDPILL_EXEC="$REDPILL_LAUNCH"
REDPILL_TERMINAL_FIELD="false"
else
REDPILL_EXEC="redpill"
REDPILL_TERMINAL_FIELD="true"
fi
# 4) Applications entry — write to BOTH system & user dirs
USER_DESK="$HOME/.local/share/applications/redpill.desktop" USER_DESK="$HOME/.local/share/applications/redpill.desktop"
SYS_DESK="/usr/share/applications/redpill.desktop" SYS_DESK="/usr/share/applications/redpill.desktop"
mkdir -p "$(dirname "$USER_DESK")" write_desktop_entry(){
cat > "$USER_DESK" <<'DESK' cat <<DESK
[Desktop Entry] [Desktop Entry]
Name=RED PILL Name=RED PILL
Comment=Unified backup & restore for Hyprland configs and user data Comment=Unified backup & restore for Hyprland configs and user data
Exec=alacritty --title "RED PILL" -e redpill Exec=${REDPILL_EXEC}
Terminal=false Terminal=${REDPILL_TERMINAL_FIELD}
Type=Application Type=Application
Categories=System;Utility; Categories=System;Utility;
NoDisplay=false NoDisplay=false
StartupNotify=false StartupNotify=false
Keywords=hyprland;backup;restore;configs;redpill;bluepill;rsync; Keywords=hyprland;backup;restore;configs;redpill;bluepill;rsync;
DESK DESK
}
if ! sudo tee "$SYS_DESK" >/dev/null <<'DESK'; then mkdir -p "$(dirname "$USER_DESK")"
[Desktop Entry] write_desktop_entry > "$USER_DESK"
Name=RED PILL
Comment=Unified backup & restore for Hyprland configs and user data if ! write_desktop_entry | sudo tee "$SYS_DESK" >/dev/null; then
Exec=alacritty --title "RED PILL" -e redpill
Terminal=false
Type=Application
Categories=System;Utility;
NoDisplay=false
StartupNotify=false
Keywords=hyprland;backup;restore;configs;redpill;bluepill;rsync;
DESK
echo "Warning: Failed to create system desktop entry" >&2 echo "Warning: Failed to create system desktop entry" >&2
fi fi
update-desktop-database "$HOME/.local/share/applications" >/dev/null 2>&1 || true update-desktop-database "$HOME/.local/share/applications" >/dev/null 2>&1 || true
sudo update-desktop-database /usr/share/applications >/dev/null 2>&1 || true sudo update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
# 4) Walker: unhide Applications (sed-only) + add custom command safety net # 5) Walker: unhide Applications (sed-only) + add custom command safety net.
# Omarchy 4 dropped Walker for the Quickshell menu, which reads .desktop files
# directly — so this whole block only applies where Walker is actually present.
WCONF="$HOME/.config/walker/config.toml" WCONF="$HOME/.config/walker/config.toml"
mkdir -p "$(dirname "$WCONF")"
if [ ! -f "$WCONF" ]; then if ! command -v walker >/dev/null 2>&1; then
cat >"$WCONF" <<'TOML' echo "Walker not installed — skipping launcher config (Omarchy 4 indexes .desktop entries directly)"
else
mkdir -p "$(dirname "$WCONF")"
if [ ! -f "$WCONF" ]; then
cat >"$WCONF" <<TOML
[builtins.applications] [builtins.applications]
launch_prefix = "uwsm app -- " launch_prefix = "uwsm app -- "
hidden = false hidden = false
@ -1772,11 +1856,11 @@ hidden = false
[[builtins.custom_commands.entries]] [[builtins.custom_commands.entries]]
label = "RED PILL" label = "RED PILL"
command = "alacritty --title 'RED PILL' -e redpill" command = "${REDPILL_EXEC}"
keywords = ["redpill","backup","restore","hyprland"] keywords = ["redpill","backup","restore","hyprland"]
icon = "" icon = ""
TOML TOML
else else
if grep -q '^\[builtins\.applications\]' "$WCONF"; then if grep -q '^\[builtins\.applications\]' "$WCONF"; then
sed -i '/^\[builtins\.applications\]/,/^\[/{s/^\s*hidden\s*=.*/hidden = false/}' "$WCONF" sed -i '/^\[builtins\.applications\]/,/^\[/{s/^\s*hidden\s*=.*/hidden = false/}' "$WCONF"
if ! sed -n '/^\[builtins\.applications\]/,/^\[/{/^\s*hidden\s*=/p}' "$WCONF" | grep -q .; then if ! sed -n '/^\[builtins\.applications\]/,/^\[/{/^\s*hidden\s*=/p}' "$WCONF" | grep -q .; then
@ -1802,62 +1886,117 @@ hidden = false
TOML TOML
fi fi
if ! grep -q 'label = "RED PILL"' "$WCONF"; then if ! grep -q 'label = "RED PILL"' "$WCONF"; then
cat >>"$WCONF" <<'TOML' cat >>"$WCONF" <<TOML
[[builtins.custom_commands.entries]] [[builtins.custom_commands.entries]]
label = "RED PILL" label = "RED PILL"
command = "alacritty --title 'RED PILL' -e redpill" command = "${REDPILL_EXEC}"
keywords = ["redpill","backup","restore","hyprland"] keywords = ["redpill","backup","restore","hyprland"]
icon = "" icon = ""
TOML TOML
fi fi
fi
fi fi
# 5) Hyprland keybinding: Super+Alt+B → floating RED PILL window # 6) Hyprland keybinding: Super+Alt+B → floating RED PILL window
BINDINGS_FILE="$HOME/.config/hypr/bindings.conf" #
# Omarchy 4 / Hyprland 0.56 use the Lua config provider: ~/.config/hypr/*.conf
# is never read, so the binding has to go into bindings.lua. Omarchy 3 boxes
# still use bindings.conf. Detect which, and write the matching syntax.
BINDINGS_LUA="$HOME/.config/hypr/bindings.lua"
BINDINGS_CONF="$HOME/.config/hypr/bindings.conf"
# Detect preferred terminal with --title support hypr_config_flavour(){
REDPILL_TERM="" if [[ -f "$BINDINGS_LUA" ]]; then
if command -v ghostty >/dev/null 2>&1; then echo lua
REDPILL_TERM="ghostty --title='RED PILL' -e" elif [[ -f "$BINDINGS_CONF" ]]; then
elif command -v kitty >/dev/null 2>&1; then echo conf
REDPILL_TERM="kitty --title='RED PILL' -e" elif hyprctl systeminfo 2>/dev/null | grep -q 'configProvider:[[:space:]]*lua'; then
elif command -v alacritty >/dev/null 2>&1; then # Lua session that has no personal bindings file yet — create one
REDPILL_TERM="alacritty --title='RED PILL' -e" echo lua
elif command -v foot >/dev/null 2>&1; then else
REDPILL_TERM="foot --title='RED PILL' --" echo none
fi
}
HYPR_FLAVOUR="$(hypr_config_flavour)"
# The keybinding needs a terminal; the autostart entry further down doesn't.
BIND_FLAVOUR="$HYPR_FLAVOUR"
if [[ -z "$REDPILL_LAUNCH" ]]; then
echo "Skipping Hyprland keybinding — no terminal to launch the TUI in" >&2
BIND_FLAVOUR="none"
fi fi
if [[ -n "$REDPILL_TERM" && -f "$BINDINGS_FILE" ]]; then # Machines upgraded from Omarchy 3 keep an inert bindings.conf. Strip any block
if ! grep -q "# Red Pill Backup TUI" "$BINDINGS_FILE"; then # we left there so the binding isn't defined in two places.
echo "Adding keybinding (Super+Alt+B) to $BINDINGS_FILE" if [[ "$HYPR_FLAVOUR" == "lua" && -f "$BINDINGS_CONF" ]] && \
grep -q '# Red Pill Backup TUI' "$BINDINGS_CONF"; then
sed -i '/# Red Pill Backup TUI/,/# End Red Pill Backup TUI/d' "$BINDINGS_CONF"
sed -i '/^$/N;/^\n$/d' "$BINDINGS_CONF"
echo "Removed stale Red Pill block from $BINDINGS_CONF (Hyprland no longer reads it)"
fi
case "$BIND_FLAVOUR" in
lua)
mkdir -p "$(dirname "$BINDINGS_LUA")"
[[ -f "$BINDINGS_LUA" ]] || : > "$BINDINGS_LUA"
if ! grep -q -- "-- Red Pill Backup TUI" "$BINDINGS_LUA"; then
echo "Adding keybinding (Super+Alt+B) to $BINDINGS_LUA"
# One property per o.window() call, matching Omarchy's own style
{
echo ""
echo "-- Red Pill Backup TUI"
echo "o.window({ class = \"^${REDPILL_APPID}\$\" }, { float = true })"
echo "o.window({ class = \"^${REDPILL_APPID}\$\" }, { center = true })"
echo "o.window({ class = \"^${REDPILL_APPID}\$\" }, { size = { 800, 600 } })"
echo "o.window({ class = \"^${REDPILL_APPID}\$\" }, { pin = true })"
echo "o.bind(\"SUPER + ALT + B\", \"Red Pill Backup\", \"${REDPILL_LAUNCH}\")"
echo "-- End Red Pill Backup TUI"
} >> "$BINDINGS_LUA"
fi
;;
conf)
if ! grep -q "# Red Pill Backup TUI" "$BINDINGS_CONF"; then
echo "Adding keybinding (Super+Alt+B) to $BINDINGS_CONF"
{ {
echo "" echo ""
echo "# Red Pill Backup TUI" echo "# Red Pill Backup TUI"
echo "windowrule = match:title RED PILL, float on" echo "windowrule = match:class ^${REDPILL_APPID}\$, float on"
echo "windowrule = match:title RED PILL, size 800 600" echo "windowrule = match:class ^${REDPILL_APPID}\$, size 800 600"
echo "windowrule = match:title RED PILL, center on" echo "windowrule = match:class ^${REDPILL_APPID}\$, center on"
echo "windowrule = match:title RED PILL, pin on" echo "windowrule = match:class ^${REDPILL_APPID}\$, pin on"
echo "bindd = SUPER ALT, B, Red Pill Backup, exec, $REDPILL_TERM redpill" echo "bindd = SUPER ALT, B, Red Pill Backup, exec, $REDPILL_LAUNCH"
echo "# End Red Pill Backup TUI" echo "# End Red Pill Backup TUI"
} >> "$BINDINGS_FILE" } >> "$BINDINGS_CONF"
fi fi
;;
*)
echo "No Hyprland bindings file found — skipping Super+Alt+B keybinding" >&2
;;
esac
# Apply dynamically so no Hyprland reload needed # Apply to the running session so the binding works without a logout.
if command -v hyprctl >/dev/null 2>&1 && hyprctl monitors -j &>/dev/null; then if [[ "$BIND_FLAVOUR" != "none" ]] && command -v hyprctl >/dev/null 2>&1 && hyprctl monitors -j &>/dev/null; then
if [[ "$BIND_FLAVOUR" == "lua" ]]; then
# The Lua parser rejects `hyprctl keyword` ("can't work with non-legacy
# parsers"), so re-read the config instead. Hyprland usually autoreloads on
# write anyway; this just makes it deterministic.
hyprctl reload >/dev/null 2>&1 || true
else
hyprctl keyword unbind "SUPER ALT, B" >/dev/null 2>&1 || true hyprctl keyword unbind "SUPER ALT, B" >/dev/null 2>&1 || true
hyprctl keyword windowrule "match:title RED PILL, float on" >/dev/null 2>&1 || true hyprctl keyword windowrule "match:class ^${REDPILL_APPID}\$, float on" >/dev/null 2>&1 || true
hyprctl keyword windowrule "match:title RED PILL, size 800 600" >/dev/null 2>&1 || true hyprctl keyword windowrule "match:class ^${REDPILL_APPID}\$, size 800 600" >/dev/null 2>&1 || true
hyprctl keyword windowrule "match:title RED PILL, center on" >/dev/null 2>&1 || true hyprctl keyword windowrule "match:class ^${REDPILL_APPID}\$, center on" >/dev/null 2>&1 || true
hyprctl keyword windowrule "match:title RED PILL, pin on" >/dev/null 2>&1 || true hyprctl keyword windowrule "match:class ^${REDPILL_APPID}\$, pin on" >/dev/null 2>&1 || true
hyprctl keyword bindd "SUPER ALT, B, Red Pill Backup, exec, $REDPILL_TERM redpill" >/dev/null 2>&1 || true hyprctl keyword bindd "SUPER ALT, B, Red Pill Backup, exec, $REDPILL_LAUNCH" >/dev/null 2>&1 || true
fi fi
fi fi
# 6) Config directory (no custom theming - uses system defaults) # 7) Config directory (no custom theming - uses system defaults)
mkdir -p "$HOME/.config/redpill" mkdir -p "$HOME/.config/redpill"
# 7) Create RECOVERY.txt guide # 8) Create RECOVERY.txt guide
cat > "$HOME/.config/redpill/RECOVERY.txt" <<'RECOVERY' cat > "$HOME/.config/redpill/RECOVERY.txt" <<'RECOVERY'
RED PILL - Recovery Guide RED PILL - Recovery Guide
============================== ==============================
@ -1939,7 +2078,7 @@ SNAPSHOT STRUCTURE (on external drive):
You can browse any snapshot directly in a file manager. You can browse any snapshot directly in a file manager.
RECOVERY RECOVERY
# 8) Auto-backup on shutdown — systemd service + wrapper script # 9) Auto-backup on shutdown — systemd service + wrapper script
# Wrapper: checks preconditions silently, runs full backup if ready # Wrapper: checks preconditions silently, runs full backup if ready
sudo tee /usr/local/bin/redpill-autobackup >/dev/null <<'AUTOBACKUP' sudo tee /usr/local/bin/redpill-autobackup >/dev/null <<'AUTOBACKUP'
#!/usr/bin/env bash #!/usr/bin/env bash
@ -2041,12 +2180,14 @@ EOF
sudo systemctl daemon-reload sudo systemctl daemon-reload
# Don't enable here — user opts in via TUI menu option 11 # Don't enable here — user opts in via TUI menu option 11
# 9) Login notification checker — reminds user to plug in backup drive # 10) Login notification checker — reminds user to plug in backup drive
sudo tee /usr/local/bin/redpill-notify >/dev/null <<'NOTIFY' sudo tee /usr/local/bin/redpill-notify >/dev/null <<'NOTIFY'
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# Wait for notification daemon (mako) to be ready # Wait for the notification daemon to claim the bus name.
# (mako on Omarchy 3, omarchy-shell/Quickshell on Omarchy 4 — either way it's
# org.freedesktop.Notifications, and either way it isn't up instantly.)
sleep 5 sleep 5
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/redpill" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/redpill"
@ -2077,24 +2218,54 @@ fi
NOTIFY NOTIFY
sudo chmod +x /usr/local/bin/redpill-notify sudo chmod +x /usr/local/bin/redpill-notify
# Add redpill-notify to Hyprland autostart (once only) # Add redpill-notify to Hyprland autostart (once only).
AUTOSTART_FILE="$HOME/.config/hypr/autostart.conf" # Same .conf-vs-.lua split as the keybinding above.
if [[ -f "$AUTOSTART_FILE" ]]; then AUTOSTART_LUA="$HOME/.config/hypr/autostart.lua"
if ! grep -q 'redpill-notify' "$AUTOSTART_FILE"; then AUTOSTART_CONF="$HOME/.config/hypr/autostart.conf"
echo "Adding redpill-notify to $AUTOSTART_FILE"
# Same cleanup as the bindings: drop our line from the now-inert .conf
if [[ "$HYPR_FLAVOUR" == "lua" && -f "$AUTOSTART_CONF" ]] && \
grep -q 'redpill-notify' "$AUTOSTART_CONF"; then
sed -i '/# Red Pill backup reminder on login/d;/redpill-notify/d' "$AUTOSTART_CONF"
sed -i '/^$/N;/^\n$/d' "$AUTOSTART_CONF"
echo "Removed stale redpill-notify line from $AUTOSTART_CONF (Hyprland no longer reads it)"
fi
case "$HYPR_FLAVOUR" in
lua)
mkdir -p "$(dirname "$AUTOSTART_LUA")"
[[ -f "$AUTOSTART_LUA" ]] || : > "$AUTOSTART_LUA"
if ! grep -q 'redpill-notify' "$AUTOSTART_LUA"; then
echo "Adding redpill-notify to $AUTOSTART_LUA"
{
echo ""
echo "-- Red Pill backup reminder on login"
echo 'o.launch_on_start("redpill-notify")'
echo "-- End Red Pill backup reminder"
} >> "$AUTOSTART_LUA"
fi
;;
conf)
if [[ -f "$AUTOSTART_CONF" ]] && ! grep -q 'redpill-notify' "$AUTOSTART_CONF"; then
echo "Adding redpill-notify to $AUTOSTART_CONF"
{ {
echo "" echo ""
echo "# Red Pill backup reminder on login" echo "# Red Pill backup reminder on login"
echo "exec-once = redpill-notify" echo "exec-once = redpill-notify"
} >> "$AUTOSTART_FILE" } >> "$AUTOSTART_CONF"
fi fi
fi ;;
*)
echo "No Hyprland autostart file found — login reminders won't run" >&2
;;
esac
# 10) Reindex and restart Walker so the new entry is searchable immediately # 11) Reindex launcher entries so RED PILL is searchable immediately
update-desktop-database "$HOME/.local/share/applications" >/dev/null 2>&1 || true update-desktop-database "$HOME/.local/share/applications" >/dev/null 2>&1 || true
sudo update-desktop-database /usr/share/applications >/dev/null 2>&1 || true sudo update-desktop-database /usr/share/applications >/dev/null 2>&1 || true
# Kill walker more safely - only exact process name match # Omarchy 3: nudge Walker to re-read its index. Omarchy 4's menu watches
pkill -x walker >/dev/null 2>&1 || true # .desktop dirs itself, so there's nothing to restart there.
command -v walker >/dev/null 2>&1 && pkill -x walker >/dev/null 2>&1 || true
echo "Installed:" echo "Installed:"
echo " - redpill (TUI) at /usr/local/bin/redpill" echo " - redpill (TUI) at /usr/local/bin/redpill"