omarchy-kernel-manager/OKM.sh
28allday cc4c6820dc Initial release of OKM — Omarchy Kernel Manager
A TUI for installing, switching, and removing Linux kernels on Omarchy.
Launches in a floating window via Super+Shift+K.

Features:
- 14 known kernels (4 official Arch + 10 CachyOS variants)
- Auto bootstraps the CachyOS repo (live-fetched keyring/mirrorlist
  versions, runs full -Syu first to avoid partial-upgrade trouble)
- AUR support via yay/paru
- Bootloader-aware: mkinitcpio -P + GRUB / systemd-boot / Limine /
  rEFInd
- Identifies the running kernel via /usr/lib/modules/<rel>/pkgbase
  (the canonical Arch mechanism, not vmlinuz path-walking)
- Safe removal — protects both the stock kernel and the currently
  running kernel, with a clear UX explaining why each is locked
- Adapts to terminal width; polls tput cols on startup so the title
  doesn't render at the wrong width while foot resizes the window
2026-05-10 09:44:20 +01:00

1380 lines
49 KiB
Bash
Executable file

#!/bin/bash
set -Euo pipefail
# =============================================================================
# OKM - OMARCHY KERNEL MANAGER
# Manage and switch between different Linux kernels on Omarchy (Arch-based)
# =============================================================================
STATE_DIR="$HOME/.local/share/okm"
STATE_FILE="$STATE_DIR/kernel-state"
BINDINGS_CONFIG=""
# Adaptive box widths — set by compute_widths() based on terminal size.
# OKM_W = inner content width for `gum style --width`
# OKM_BOX_W = outer width including double border (1 col each side) +
# horizontal padding "1 2" (2 cols each side) = OKM_W + 6.
# Used by center_output as the target width to centre the box around.
# Defaults are the historical hardcoded values; compute_widths shrinks
# them if the terminal is too narrow, preventing the menu heading from
# wrapping on small floating windows.
OKM_W=70
OKM_BOX_W=76
compute_widths() {
local cols
cols=$(tput cols 2>/dev/null || echo 80)
# Cap inner width at 70; otherwise leave 6 cols for border/padding.
local w=$(( cols - 6 ))
(( w > 70 )) && w=70
(( w < 30 )) && w=30 # absolute minimum so we still render *something*
OKM_W=$w
OKM_BOX_W=$(( w + 6 ))
}
# Wait for the terminal's reported width to stop changing (foot/Hyprland
# resize the floating window in two steps after spawn). We poll every
# ~25 ms for up to 250 ms, returning as soon as we see the same width
# twice in a row.
poll_terminal_size() {
local prev curr
prev=$(tput cols 2>/dev/null || echo 80)
local i
for i in 1 2 3 4 5 6 7 8 9 10; do
sleep 0.025
curr=$(tput cols 2>/dev/null || echo 80)
if [[ "$curr" == "$prev" && $i -ge 2 ]]; then
return 0
fi
prev=$curr
done
}
# Kernel descriptions (for known kernels).
# CachyOS dropped the standalone `-lto` variants - the base packages
# (linux-cachyos, linux-cachyos-bore, etc.) are themselves Clang+ThinLTO
# builds, so a separate -lto package would now duplicate them.
declare -A KERNEL_DESCRIPTIONS=(
["linux"]="Stock Arch kernel - stable and well-tested"
["linux-lts"]="Long Term Support - stable for production"
["linux-hardened"]="Security-focused with hardening patches"
["linux-zen"]="Low latency for desktop/gaming"
["linux-cachyos"]="CachyOS default - 1000Hz, Clang+ThinLTO"
["linux-cachyos-bore"]="BORE scheduler for gaming (Clang+ThinLTO)"
["linux-cachyos-bmq"]="BMQ scheduler (Project C)"
["linux-cachyos-deckify"]="For handhelds (Steam Deck, etc.)"
["linux-cachyos-eevdf"]="Tweaked EEVDF for responsiveness"
["linux-cachyos-lts"]="LTS with BORE scheduler"
["linux-cachyos-hardened"]="Hardened with BORE scheduler"
["linux-cachyos-rc"]="Mainline RC with latest features"
["linux-cachyos-server"]="Tuned for server workloads (300Hz)"
["linux-cachyos-rt-bore"]="Real-time preemption with BORE"
)
# Kernel availability: pacman (official repos), cachyos, or aur
declare -A KERNEL_SOURCE=(
["linux"]="pacman"
["linux-lts"]="pacman"
["linux-hardened"]="pacman"
["linux-zen"]="pacman"
["linux-cachyos"]="cachyos"
["linux-cachyos-bore"]="cachyos"
["linux-cachyos-bmq"]="cachyos"
["linux-cachyos-deckify"]="cachyos"
["linux-cachyos-eevdf"]="cachyos"
["linux-cachyos-lts"]="cachyos"
["linux-cachyos-hardened"]="cachyos"
["linux-cachyos-rc"]="cachyos"
["linux-cachyos-server"]="cachyos"
["linux-cachyos-rt-bore"]="cachyos"
)
# Preferred display order for known kernels
KNOWN_KERNEL_ORDER=(
"linux"
"linux-lts"
"linux-hardened"
"linux-zen"
"linux-cachyos"
"linux-cachyos-bore"
"linux-cachyos-eevdf"
"linux-cachyos-bmq"
"linux-cachyos-lts"
"linux-cachyos-hardened"
"linux-cachyos-server"
"linux-cachyos-rt-bore"
"linux-cachyos-deckify"
"linux-cachyos-rc"
)
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
info() { echo "[*] $*"; }
err() { echo "[!] $*" >&2; }
die() {
local msg="$1"; local code="${2:-1}"
echo "FATAL: $msg" >&2
exit "$code"
}
# Center output in terminal (matching WOPR_muilti_mon.sh style)
center_output() {
local width=${1:-70}
local term_width=$(tput cols)
local padding=$(( (term_width - width) / 2 ))
[[ $padding -lt 0 ]] && padding=0
while IFS= read -r line; do
printf "%${padding}s%s\n" "" "$line"
done
}
check_package() {
pacman -Qi "$1" &>/dev/null
}
check_dependencies() {
command -v gum >/dev/null || die "gum is required. Install it with: sudo pacman -S gum"
command -v pacman >/dev/null || die "pacman is required (are you on Arch?)"
command -v curl >/dev/null || die "curl is required. Install it with: sudo pacman -S curl"
command -v xdg-terminal-exec >/dev/null || die "xdg-terminal-exec is required (ships with Omarchy). This script is Omarchy-only."
[[ -d "$HOME/.config/hypr" ]] || die "Hyprland config not found at ~/.config/hypr — Omarchy required."
}
check_aur_helper() {
if command -v yay >/dev/null 2>&1; then
echo "yay"
return 0
elif command -v paru >/dev/null 2>&1; then
echo "paru"
return 0
else
return 1
fi
}
# =============================================================================
# THEME DETECTION (matching WOPR_muilti_mon.sh)
# =============================================================================
detect_system_theme() {
local omarchy_theme_dir="$HOME/.config/omarchy/current/theme"
local ghostty_conf="$omarchy_theme_dir/ghostty.conf"
# Default colors (fallback)
local accent_color="6" # Cyan (ANSI color 6)
local border_color="7" # White/light gray
local cursor_color="6" # Cyan
# Read colors from Omarchy ghostty theme if available
if [[ -f "$ghostty_conf" ]]; then
# Extract accent color (use palette 6 or 2 as accent - typically cyan/green)
local palette_6=$(grep "^palette = 6=" "$ghostty_conf" 2>/dev/null | cut -d'=' -f3 | tr -d ' #')
local palette_2=$(grep "^palette = 2=" "$ghostty_conf" 2>/dev/null | cut -d'=' -f3 | tr -d ' #')
if [[ -n "$palette_6" ]]; then
# Convert hex to gum color (use the hex directly)
accent_color="#$palette_6"
cursor_color="#$palette_6"
elif [[ -n "$palette_2" ]]; then
accent_color="#$palette_2"
cursor_color="#$palette_2"
fi
# Get foreground color for borders
local fg_color=$(grep "^foreground = " "$ghostty_conf" 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
if [[ -n "$fg_color" ]]; then
border_color="$fg_color"
fi
fi
# Set gum environment variables to match theme
export GUM_CHOOSE_CURSOR_FOREGROUND="$cursor_color"
export GUM_CHOOSE_SELECTED_FOREGROUND="$accent_color"
export GUM_STYLE_BORDER_FOREGROUND="$border_color"
export GUM_SPIN_SPINNER_FOREGROUND="$accent_color"
}
detect_bindings_config() {
# Omarchy always ships ~/.config/hypr/bindings.conf
local primary="$HOME/.config/hypr/bindings.conf"
if [[ -f "$primary" ]]; then
BINDINGS_CONFIG="$primary"
return 0
fi
err "Expected Hyprland config at $primary but it was not found."
die "Is this an Omarchy system? Could not find ~/.config/hypr/bindings.conf"
}
# =============================================================================
# KERNEL DETECTION (IMPROVED - NO HARDCODED FALLBACK)
# =============================================================================
# Get all installed kernel packages from pacman
get_all_installed_kernel_packages() {
# Query pacman for all packages matching linux kernel patterns
# This catches: linux, linux-lts, linux-hardened, linux-zen, linux-cachyos-*, etc.
# Filter out -headers packages (they're not kernels)
pacman -Qq 2>/dev/null | grep -E '^linux(-lts|-hardened|-zen|-cachyos.*|-liquorix|-xanmod.*)?$' | grep -v -- '-headers$' || true
}
# Find which package owns the currently running kernel.
# Canonical Arch mechanism: every kernel package writes a `pkgbase` file
# at /usr/lib/modules/<kernelrelease>/pkgbase containing its package name.
# That's the *only* reliable way to map `uname -r` back to a package
# (walking /boot/vmlinuz-* just finds *some* installed kernel, not the
# one currently running).
find_running_kernel_package() {
local kernel_release
kernel_release=$(uname -r)
local pkgbase_file="/usr/lib/modules/$kernel_release/pkgbase"
if [[ -r "$pkgbase_file" ]]; then
# Trim whitespace
local pkg
pkg=$(tr -d '[:space:]' < "$pkgbase_file")
if [[ -n "$pkg" ]]; then
echo "$pkg"
return 0
fi
fi
# Fallback: ask pacman who owns the modules dir for this kernel release.
# This handles the unusual case of a missing/empty pkgbase file but a
# still-installed kernel package.
local modules_dir="/usr/lib/modules/$kernel_release"
if [[ -d "$modules_dir" ]]; then
local owner
owner=$(LC_ALL=C pacman -Qo "$modules_dir" 2>/dev/null | awk '{print $5}' || echo "")
if [[ -n "$owner" ]]; then
echo "$owner"
return 0
fi
fi
# If all else fails, return empty string (caller will handle)
echo ""
}
detect_current_kernel() {
# Authoritative path: read pkgbase from the running kernel's modules dir.
# This is the canonical Arch mechanism and unambiguous - prefer it over
# any pattern matching on uname -r (which can collide between e.g.
# `linux-cachyos-bore` and `linux-cachyos-bore-lto`).
local pkg
pkg=$(find_running_kernel_package)
if [[ -n "$pkg" ]]; then
echo "$pkg"
return 0
fi
local current_kernel_release
current_kernel_release=$(uname -r)
# Fallback: pattern-match on uname -r. Used only when /usr/lib/modules
# is missing or unreadable for the running kernel (very unusual).
# Check CachyOS variants (most specific first, check LTO variants too)
if [[ "$current_kernel_release" == *"cachyos"*"rt-bore"*"lto"* ]]; then
echo "linux-cachyos-rt-bore-lto"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"rt-bore"* ]]; then
echo "linux-cachyos-rt-bore"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"deckify"*"lto"* ]]; then
echo "linux-cachyos-deckify-lto"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"deckify"* ]]; then
echo "linux-cachyos-deckify"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"hardened"*"lto"* ]]; then
echo "linux-cachyos-hardened-lto"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"hardened"* ]]; then
echo "linux-cachyos-hardened"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"server"*"lto"* ]]; then
echo "linux-cachyos-server-lto"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"server"* ]]; then
echo "linux-cachyos-server"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"bore"*"lto"* ]]; then
echo "linux-cachyos-bore-lto"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"bore"* ]]; then
echo "linux-cachyos-bore"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"bmq"*"lto"* ]]; then
echo "linux-cachyos-bmq-lto"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"bmq"* ]]; then
echo "linux-cachyos-bmq"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"eevdf"*"lto"* ]]; then
echo "linux-cachyos-eevdf-lto"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"eevdf"* ]]; then
echo "linux-cachyos-eevdf"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"lts"*"lto"* ]]; then
echo "linux-cachyos-lts-lto"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"lts"* ]]; then
echo "linux-cachyos-lts"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"rc"*"lto"* ]]; then
echo "linux-cachyos-rc-lto"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"rc"* ]]; then
echo "linux-cachyos-rc"
return 0
elif [[ "$current_kernel_release" == *"cachyos"*"lto"* ]]; then
echo "linux-cachyos-lto"
return 0
elif [[ "$current_kernel_release" == *"cachyos"* ]]; then
echo "linux-cachyos"
return 0
# Check other kernel types
elif [[ "$current_kernel_release" == *"zen"* ]]; then
echo "linux-zen"
return 0
elif [[ "$current_kernel_release" == *"lts"* ]]; then
echo "linux-lts"
return 0
elif [[ "$current_kernel_release" == *"hardened"* ]]; then
echo "linux-hardened"
return 0
fi
# Last resort: return first installed kernel package. We've already tried
# find_running_kernel_package above, so don't call it again here.
local first_kernel
first_kernel=$(get_all_installed_kernel_packages | head -n1)
if [[ -n "$first_kernel" ]]; then
echo "$first_kernel"
return 0
fi
# Absolute last resort if no kernels detected (shouldn't happen)
echo "unknown"
}
get_installed_kernels() {
get_all_installed_kernel_packages
}
is_kernel_installed() {
check_package "$1"
}
get_kernel_description() {
local kernel="$1"
if [[ -n "${KERNEL_DESCRIPTIONS[$kernel]:-}" ]]; then
echo "${KERNEL_DESCRIPTIONS[$kernel]}"
else
echo "Custom or third-party kernel"
fi
}
get_kernel_source() {
local kernel="$1"
echo "${KERNEL_SOURCE[$kernel]:-unknown}"
}
# =============================================================================
# STATE MANAGEMENT
# =============================================================================
init_state() {
if [[ ! -f "$STATE_FILE" ]]; then
local current_kernel
current_kernel=$(detect_current_kernel)
# Verify the detected kernel is actually installed
if [[ "$current_kernel" == "unknown" ]] || ! check_package "$current_kernel"; then
err "Warning: Could not reliably detect current kernel package"
# Try to find ANY installed kernel
local any_kernel
any_kernel=$(get_all_installed_kernel_packages | head -n1)
if [[ -n "$any_kernel" ]]; then
current_kernel="$any_kernel"
info "Using '$current_kernel' as original kernel"
else
die "No kernel packages found on system - cannot initialize state"
fi
fi
mkdir -p "$STATE_DIR"
cat > "$STATE_FILE" <<STATE
ORIGINAL_KERNEL=$current_kernel
INITIALIZED_DATE=$(date +%Y-%m-%d)
STATE
info "Initialized kernel state - original kernel: $current_kernel"
fi
}
get_original_kernel() {
if [[ -f "$STATE_FILE" ]]; then
# shellcheck source=/dev/null
source "$STATE_FILE"
echo "${ORIGINAL_KERNEL:-}"
else
echo ""
fi
}
# =============================================================================
# CACHYOS REPOSITORY SETUP
# =============================================================================
# Fetch the latest package filename from CachyOS repository
fetch_latest_cachyos_package() {
local package_prefix="$1"
local repo_url="https://mirror.cachyos.org/repo/x86_64/cachyos/"
# Fetch the repo index and find the latest package
local latest_pkg
latest_pkg=$(curl -s "$repo_url" | grep -oP "${package_prefix}-[0-9]+-[0-9]+-any\.pkg\.tar\.zst" | sort -V | tail -n1)
if [[ -z "$latest_pkg" ]]; then
# Fallback: try harder with a more flexible pattern
latest_pkg=$(curl -s "$repo_url" | grep -oP "${package_prefix}[^\"]*\.pkg\.tar\.zst" | head -n1)
fi
echo "$latest_pkg"
}
setup_cachyos_repo() {
# Check if CachyOS repo already exists
if grep -q "\[cachyos\]" /etc/pacman.conf 2>/dev/null; then
info "CachyOS repository already configured"
return 0
fi
echo ""
echo "════════════════════════════════════════════════════════════════"
echo " ⚠ SYSTEM UPDATE REQUIRED"
echo "════════════════════════════════════════════════════════════════"
echo ""
echo " Adding a new repository requires a full system update to avoid"
echo " partial upgrade conflicts (Arch best practice)."
echo ""
echo " This will run: sudo pacman -Syu"
echo ""
echo "════════════════════════════════════════════════════════════════"
echo ""
read -p "Proceed with system upgrade before adding CachyOS repo? [Y/n]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
info "Upgrading system..."
sudo pacman -Syu || die "System upgrade failed"
else
err "WARNING: Skipping system upgrade - this may cause package conflicts"
read -p "Continue anyway? [y/N]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
die "CachyOS repository setup cancelled"
fi
fi
info "Setting up CachyOS repository..."
# Install cachyos-keyring and add repository
sudo pacman-key --recv-keys F3B607488DB35A47 --keyserver keyserver.ubuntu.com || die "Failed to receive CachyOS key"
sudo pacman-key --lsign-key F3B607488DB35A47 || die "Failed to sign CachyOS key"
# Fetch latest package versions dynamically. We refuse to fall back to a
# hardcoded version string — stale URLs 404 and pacman -U fails opaquely.
info "Fetching latest CachyOS package versions..."
local keyring_pkg
local mirrorlist_pkg
keyring_pkg=$(fetch_latest_cachyos_package "cachyos-keyring")
mirrorlist_pkg=$(fetch_latest_cachyos_package "cachyos-mirrorlist")
if [[ -z "$keyring_pkg" ]] || [[ -z "$mirrorlist_pkg" ]]; then
die "Could not discover current cachyos-keyring/mirrorlist versions from the CachyOS mirror. Check network connectivity to mirror.cachyos.org and try again."
fi
info "Found latest versions: $keyring_pkg, $mirrorlist_pkg"
# Download to a private tmpdir so /tmp doesn't accumulate stale .pkg files
local tmpdir
tmpdir=$(mktemp -d -t okm-cachyos.XXXXXX) || die "Failed to create temp dir"
# shellcheck disable=SC2064
trap "rm -rf '$tmpdir'" RETURN
curl -L --fail --output "$tmpdir/$keyring_pkg" "https://mirror.cachyos.org/repo/x86_64/cachyos/$keyring_pkg" || die "Failed to download cachyos-keyring"
curl -L --fail --output "$tmpdir/$mirrorlist_pkg" "https://mirror.cachyos.org/repo/x86_64/cachyos/$mirrorlist_pkg" || die "Failed to download cachyos-mirrorlist"
sudo pacman -U --noconfirm "$tmpdir/$keyring_pkg" "$tmpdir/$mirrorlist_pkg" || die "Failed to install CachyOS packages"
# Add CachyOS repository to pacman.conf
if ! grep -q "\[cachyos\]" /etc/pacman.conf; then
info "Adding CachyOS repository to pacman.conf..."
sudo tee -a /etc/pacman.conf > /dev/null <<'REPO'
# CachyOS repository - added by OKM
[cachyos]
Include = /etc/pacman.d/cachyos-mirrorlist
REPO
fi
# Update package database (safe now since we did -Syu above)
sudo pacman -Sy || die "Failed to update package database"
info "CachyOS repository configured successfully"
}
# =============================================================================
# BOOTLOADER MANAGEMENT
# =============================================================================
update_bootloader() {
info "Updating bootloader configuration..."
local handled=0
# mkinitcpio: regenerate initramfs/UKIs for all installed presets.
# This is what actually puts the new kernel image in place; pacman's
# mkinitcpio hook runs it on install/remove, but a manual -P after
# bootloader rewires won't hurt and surfaces preset errors.
if command -v mkinitcpio >/dev/null 2>&1; then
info "Regenerating initramfs (mkinitcpio -P)..."
sudo mkinitcpio -P || err "mkinitcpio -P reported errors"
handled=1
fi
# GRUB
if command -v grub-mkconfig >/dev/null 2>&1 && [[ -f /boot/grub/grub.cfg ]]; then
info "Detected GRUB - regenerating config..."
sudo grub-mkconfig -o /boot/grub/grub.cfg || err "GRUB update may have failed"
handled=1
fi
# systemd-boot: entries are written by kernel-install hooks (or by
# mkinitcpio's hook for UKI presets). Surface bootctl status so the
# user can see the new entries actually appeared.
if [[ -d /boot/loader ]] || [[ -d /efi/loader ]]; then
info "Detected systemd-boot - new entries should appear via kernel-install / mkinitcpio hooks."
if command -v bootctl >/dev/null 2>&1; then
sudo bootctl --no-pager list 2>/dev/null | head -40 || true
fi
handled=1
fi
# Limine - increasingly common on Arch
if command -v limine-update >/dev/null 2>&1; then
info "Detected Limine - running limine-update..."
sudo limine-update || err "limine-update reported errors"
handled=1
elif command -v limine-mkinitcpio-update >/dev/null 2>&1; then
info "Detected Limine (mkinitcpio variant) - updating..."
sudo limine-mkinitcpio-update || err "limine-mkinitcpio-update reported errors"
handled=1
fi
# rEFInd
if command -v refind-install >/dev/null 2>&1 && [[ -d /boot/EFI/refind ]]; then
info "Detected rEFInd - it auto-detects new kernels at boot; no action needed."
handled=1
fi
if [[ $handled -eq 0 ]]; then
err "Could not detect a bootloader - update your bootloader configuration manually before rebooting."
fi
}
# =============================================================================
# KERNEL INSTALLATION
# =============================================================================
install_kernel() {
local kernel="$1"
local original_kernel
original_kernel=$(get_original_kernel)
# Check if already installed
if is_kernel_installed "$kernel"; then
clear
echo ""
gum style --align center --width "$OKM_W" --border normal --padding "1 2" "Kernel Already Installed" | center_output "$OKM_BOX_W"
echo ""
gum style --align center --width "$OKM_W" "$kernel is already installed and available in your bootloader." | center_output "$OKM_W"
echo ""
gum style --align center --width "$OKM_W" --faint "To use it, reboot and select it from the boot menu." | center_output "$OKM_W"
echo ""
read -p "Press Enter to continue..."
return 0
fi
# Show installation confirmation
clear
echo ""
gum style --align center --width "$OKM_W" --border double --padding "1 2" $'Kernel Installation\n\n'"$kernel" | center_output "$OKM_BOX_W"
echo ""
local description
description=$(get_kernel_description "$kernel")
gum style --align center --width "$OKM_W" --bold "$description" | center_output "$OKM_W"
echo ""
gum style --align center --width "$OKM_W" "Your stock kernel ($original_kernel) will be kept as a safe fallback." | center_output "$OKM_W"
echo ""
gum style --align center --width "$OKM_W" --faint "After installation, reboot and select the new kernel from boot menu." | center_output "$OKM_W"
echo ""
# Calculate padding for confirm
local term_width=$(tput cols)
local confirm_width=40
local confirm_pad=$(( (term_width - confirm_width) / 2 ))
[[ $confirm_pad -lt 0 ]] && confirm_pad=0
local confirm_pad_str=$(printf "%${confirm_pad}s" "")
local choice
choice=$(gum choose --height 6 \
"${confirm_pad_str}Yes - Install $kernel" \
"${confirm_pad_str}No - Cancel") || return 0
if [[ "$choice" != *"Yes"* ]]; then
return 0
fi
# Determine how to install the kernel
local source
source=$(get_kernel_source "$kernel")
case "$source" in
"cachyos")
setup_cachyos_repo
;;
"aur")
# Check for AUR helper
local aur_helper
if ! aur_helper=$(check_aur_helper); then
clear
echo ""
gum style --align center --width "$OKM_W" --border double --padding "1 2" "⚠ AUR Helper Required" | center_output "$OKM_BOX_W"
echo ""
gum style --align center --width "$OKM_W" "$kernel is an AUR package and requires an AUR helper." | center_output "$OKM_W"
echo ""
gum style --align center --width "$OKM_W" "Please install 'yay' or 'paru' first:" | center_output "$OKM_W"
gum style --align center --width "$OKM_W" --faint " sudo pacman -S yay" | center_output "$OKM_W"
echo ""
read -p "Press Enter to continue..."
return 1
fi
;;
"unknown")
# Unknown source - try pacman first, may fail
err "Warning: Unknown package source for $kernel, attempting pacman install..."
;;
esac
# Install kernel and headers
clear
echo ""
info "Installing $kernel and ${kernel}-headers..."
echo ""
case "$source" in
"aur")
local aur_helper
if ! aur_helper=$(check_aur_helper); then
die "AUR helper (yay/paru) not found - cannot install AUR package $kernel"
fi
$aur_helper -S --needed --noconfirm "$kernel" "${kernel}-headers" || die "Failed to install $kernel"
;;
*)
sudo pacman -S --needed --noconfirm "$kernel" "${kernel}-headers" || die "Failed to install $kernel"
;;
esac
# Update bootloader
update_bootloader
# Show success message
clear
echo ""
gum style --align center --width "$OKM_W" --border double --padding "1 2" "✓ Installation Complete" | center_output "$OKM_BOX_W"
echo ""
gum style --align center --width "$OKM_W" "$kernel has been installed successfully!" | center_output "$OKM_W"
echo ""
gum style --align center --width "$OKM_W" --bold "IMPORTANT: Reboot to use the new kernel" | center_output "$OKM_W"
echo ""
gum style --align center --width "$OKM_W" --faint "Select it from your bootloader menu on next boot." | center_output "$OKM_W"
echo ""
read -p "Press Enter to continue..."
}
# =============================================================================
# KERNEL REMOVAL
# =============================================================================
remove_kernels_interactive() {
local original_kernel
original_kernel=$(get_original_kernel)
local running_kernel
running_kernel=$(detect_current_kernel)
# Walk all installed kernels and bucket them:
# removable_kernels = anything beyond stock, NOT currently running
# protected_kernels = stock and/or running (with reason)
# We surface the protected list in the UI so the user understands why
# a kernel they expected to see isn't selectable, instead of getting a
# silently-empty "No Extra Kernels Found" message.
local installed_kernels=()
local protected_kernels=() # parallel: each entry "<pkg>|<reason>"
while IFS= read -r kernel; do
if [[ "$kernel" == "$running_kernel" && "$kernel" == "$original_kernel" ]]; then
protected_kernels+=("$kernel|stock + currently running")
elif [[ "$kernel" == "$running_kernel" ]]; then
protected_kernels+=("$kernel|currently running - reboot into another kernel first")
elif [[ "$kernel" == "$original_kernel" ]]; then
protected_kernels+=("$kernel|stock kernel - protected fallback")
else
installed_kernels+=("$kernel")
fi
done < <(get_installed_kernels)
if [[ ${#installed_kernels[@]} -eq 0 ]]; then
clear
echo ""
gum style --align center --width "$OKM_W" --border normal --padding "1 2" "No Removable Kernels" | center_output "$OKM_BOX_W"
echo ""
gum style --align center --width "$OKM_W" "All installed kernels are currently protected:" | center_output "$OKM_W"
echo ""
for entry in "${protected_kernels[@]}"; do
local pkg="${entry%%|*}"
local reason="${entry#*|}"
gum style --align center --width "$OKM_W" "$pkg ($reason)" | center_output "$OKM_W"
done
echo ""
if [[ "$running_kernel" != "$original_kernel" ]]; then
gum style --align center --width "$OKM_W" --faint "Tip: to remove the running kernel, reboot into your stock kernel ($original_kernel) first via the bootloader, then come back here." | center_output "$OKM_W"
echo ""
fi
read -r -p "Press Enter to continue..."
return 0
fi
# Show removal menu
clear
echo ""
gum style --align center --width "$OKM_W" --border double --padding "1 2" "Remove Extra Kernels" | center_output "$OKM_BOX_W"
echo ""
gum style --align center --width "$OKM_W" "Select kernels to remove (Space to select, Enter to confirm):" | center_output "$OKM_W"
echo ""
gum style --align center --width "$OKM_W" --faint "Protected: stock ($original_kernel) and running ($running_kernel) kernels are not listed." | center_output "$OKM_W"
echo ""
# Build options for gum choose
local -a options=()
local term_width=$(tput cols)
local menu_width=$OKM_W
local menu_pad=$(( (term_width - menu_width) / 2 ))
[[ $menu_pad -lt 0 ]] && menu_pad=0
local pad_str=$(printf "%${menu_pad}s" "")
for kernel in "${installed_kernels[@]}"; do
local description
description=$(get_kernel_description "$kernel")
local formatted_option=$(printf "%-26s │ %s" "$kernel" "$description")
options+=("${pad_str}${formatted_option}")
done
# Use gum choose with --no-limit for multi-select
local selected
selected=$(gum choose --no-limit --height $((${#options[@]} + 4)) "${options[@]}") || return 0
if [[ -z "$selected" ]]; then
return 0
fi
# Extract kernel names from selections
local -a kernels_to_remove=()
while IFS= read -r line; do
# Remove padding and extract kernel name (before │)
local kernel_name=$(echo "$line" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*│.*//' | xargs)
kernels_to_remove+=("$kernel_name")
done <<< "$selected"
if [[ ${#kernels_to_remove[@]} -eq 0 ]]; then
return 0
fi
# Confirm removal
clear
echo ""
gum style --align center --width "$OKM_W" --border double --padding "1 2" "⚠ Confirm Kernel Removal" | center_output "$OKM_BOX_W"
echo ""
gum style --align center --width "$OKM_W" --bold "The following kernels will be removed:" | center_output "$OKM_W"
echo ""
for kernel in "${kernels_to_remove[@]}"; do
gum style --align center --width "$OKM_W" "$kernel" | center_output "$OKM_W"
done
echo ""
gum style --align center --width "$OKM_W" --faint "Your stock kernel ($original_kernel) will remain installed." | center_output "$OKM_W"
echo ""
local confirm_pad=$(( (term_width - 40) / 2 ))
[[ $confirm_pad -lt 0 ]] && confirm_pad=0
local confirm_pad_str=$(printf "%${confirm_pad}s" "")
local choice
choice=$(gum choose --height 6 \
"${confirm_pad_str}Yes - Remove selected kernels" \
"${confirm_pad_str}No - Cancel") || return 0
if [[ "$choice" != *"Yes"* ]]; then
return 0
fi
# Remove selected kernels
clear
echo ""
info "Removing selected kernels..."
echo ""
for kernel in "${kernels_to_remove[@]}"; do
# Build removal package list dynamically
local -a pkgs=("$kernel")
# Only add headers if they're actually installed
if check_package "${kernel}-headers"; then
pkgs+=("${kernel}-headers")
info "Removing $kernel and ${kernel}-headers..."
else
info "Removing $kernel (headers not installed)..."
fi
# Try -Rns first (removes dependencies). If that fails because of a
# dependency cycle on -headers, fall back to plain -R. We deliberately
# let stderr through so pacman's actual error reaches the user
# (running-kernel refusal, dep cycle, target-not-found, etc.).
if ! sudo pacman -Rns --noconfirm "${pkgs[@]}"; then
err "pacman -Rns failed for $kernel; retrying with -R..."
sudo pacman -R --noconfirm "${pkgs[@]}" || err "Failed to remove $kernel"
fi
done
# Update bootloader
update_bootloader
# Show success message
clear
echo ""
gum style --align center --width "$OKM_W" --border double --padding "1 2" "✓ Removal Complete" | center_output "$OKM_BOX_W"
echo ""
gum style --align center --width "$OKM_W" "Selected kernels have been removed successfully." | center_output "$OKM_W"
echo ""
gum style --align center --width "$OKM_W" "Stock kernel ($original_kernel) remains as your fallback." | center_output "$OKM_W"
echo ""
read -p "Press Enter to continue..."
}
# =============================================================================
# STATUS DISPLAY
# =============================================================================
show_kernel_status() {
local current_kernel
local original_kernel
local kernel_version
current_kernel=$(detect_current_kernel)
original_kernel=$(get_original_kernel)
kernel_version=$(uname -r)
clear
echo ""
gum style --align center --width "$OKM_W" --border double --padding "1 2" "Current Kernel Status" | center_output "$OKM_BOX_W"
echo ""
# Current running kernel
gum style --align center --width "$OKM_W" --bold "Currently Running:" | center_output "$OKM_W"
gum style --align center --width "$OKM_W" " Package: $current_kernel" | center_output "$OKM_W"
gum style --align center --width "$OKM_W" " Version: $kernel_version" | center_output "$OKM_W"
echo ""
# Original/stock kernel
gum style --align center --width "$OKM_W" --bold "Stock Kernel (Protected):" | center_output "$OKM_W"
gum style --align center --width "$OKM_W" " $original_kernel" | center_output "$OKM_W"
echo ""
# Installed kernels (DYNAMIC - shows ALL installed kernels, not just known ones)
gum style --align center --width "$OKM_W" --bold "Installed Kernels:" | center_output "$OKM_W"
local installed_kernels
installed_kernels=$(get_installed_kernels)
if [[ -z "$installed_kernels" ]]; then
gum style --align center --width "$OKM_W" --faint " (none detected)" | center_output "$OKM_W"
else
while IFS= read -r kernel; do
local marker=""
[[ "$kernel" == "$current_kernel" ]] && marker="← currently running"
[[ "$kernel" == "$original_kernel" ]] && marker="$marker [protected]"
gum style --align center --width "$OKM_W" "$kernel $marker" | center_output "$OKM_W"
done <<< "$installed_kernels"
fi
echo ""
read -p "Press Enter to continue..."
}
# =============================================================================
# KERNEL INSTALLATION MENU
# =============================================================================
show_install_menu() {
clear
echo ""
gum style --align center --width "$OKM_W" --border double --padding "1 2" "Install / Manage Kernels" | center_output "$OKM_BOX_W"
echo ""
gum style --align center --width "$OKM_W" "Select a kernel to install:" | center_output "$OKM_W"
echo ""
# Build menu options combining known kernels and installed unknowns
local -a display_kernels=()
local -a seen_kernels=()
# First add known kernels in preferred order (only if they're available)
for kernel in "${KNOWN_KERNEL_ORDER[@]}"; do
local source
source=$(get_kernel_source "$kernel")
# Skip if kernel source is unknown (not configured)
if [[ "$source" == "unknown" ]]; then
continue
fi
# Check if AUR packages should be shown
if [[ "$source" == "aur" ]]; then
# Only show AUR packages if AUR helper is available
if ! check_aur_helper >/dev/null 2>&1; then
continue
fi
fi
display_kernels+=("$kernel")
seen_kernels+=("$kernel")
done
# Then add any installed kernels not in the known list (e.g., custom Omarchy kernels)
while IFS= read -r kernel; do
local already_seen=false
for seen in "${seen_kernels[@]}"; do
if [[ "$kernel" == "$seen" ]]; then
already_seen=true
break
fi
done
if [[ "$already_seen" == false ]]; then
display_kernels+=("$kernel")
fi
done < <(get_installed_kernels)
local -a options=()
local term_width=$(tput cols)
local menu_width=$OKM_W
local menu_pad=$(( (term_width - menu_width) / 2 ))
[[ $menu_pad -lt 0 ]] && menu_pad=0
local pad_str=$(printf "%${menu_pad}s" "")
for kernel in "${display_kernels[@]}"; do
local status=""
if is_kernel_installed "$kernel"; then
status="[INSTALLED]"
fi
local description
description=$(get_kernel_description "$kernel")
local formatted_option=$(printf "%-26s │ %s %s" "$kernel" "$description" "$status")
options+=("${pad_str}${formatted_option}")
done
# Add back option. Arrow lives in the description column, not the
# label, so the label's leading char stays aligned with kernel names.
options+=("${pad_str}$(printf "%-26s │ %s" "Back to Main Menu" "← Return to menu")")
local choice
choice=$(gum choose --height $((${#options[@]} + 4)) "${options[@]}") || return 0
if [[ -z "$choice" ]]; then
return 0
fi
# Extract kernel name (before │)
local kernel_name=$(echo "$choice" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*│.*//' | xargs)
if [[ "$kernel_name" == "Back to Main Menu" ]]; then
return 0
fi
install_kernel "$kernel_name"
}
# =============================================================================
# MAIN MENU
# =============================================================================
show_main_menu() {
clear
echo ""
# W.O.P.R style title (matching WOPR_muilti_mon.sh)
gum style --align center --width "$OKM_W" --border double --padding "1 2" $'OKM - OMARCHY KERNEL MANAGER\n\nShall we tweak the kernel?' | center_output "$OKM_BOX_W"
echo ""
# Initialize padding for menu (matching WOPR_muilti_mon.sh)
local term_width=$(tput cols)
local menu_width=$OKM_W
local menu_pad=$(( (term_width - menu_width) / 2 ))
[[ $menu_pad -lt 0 ]] && menu_pad=0
local pad_str=$(printf "%${menu_pad}s" "")
# Main menu options (matching WOPR_muilti_mon.sh format with padded entries and │ separator)
local choice
choice=$(gum choose --height 12 \
"${pad_str}$(printf "%-32s │ %s" "View current kernel status" "Show current kernel info")" \
"${pad_str}$(printf "%-32s │ %s" "Install or manage kernels" "Add linux-zen, linux-cachyos, etc")" \
"${pad_str}$(printf "%-32s │ %s" "Revert to stock / clean up" "Remove extra kernels")" \
"${pad_str}$(printf "%-32s │ %s" "Exit" "Quit the Kernel Manager")") || return 1
# Extract the menu choice (before │)
local menu_choice=$(echo "$choice" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*│.*//' | xargs)
case "$menu_choice" in
"View current kernel status")
show_kernel_status
return 0
;;
"Install or manage kernels")
show_install_menu
return 0
;;
"Revert to stock / clean up")
remove_kernels_interactive
return 0
;;
"Exit")
return 1
;;
*)
return 0
;;
esac
}
# =============================================================================
# CLEANUP OLD INSTALLATIONS
# =============================================================================
cleanup_old_installation() {
local old_installed_path="/usr/local/bin/omarchy-kernel-manager.sh"
local new_installed_path="/usr/local/bin/okm"
local old_desktop_file="$HOME/.local/share/applications/omarchy-kernel-manager.desktop"
local new_desktop_file="$HOME/.local/share/applications/okm.desktop"
local bindings_conf="${BINDINGS_CONFIG:-$HOME/.config/hypr/bindings.conf}"
local hyprland_conf="$HOME/.config/hypr/hyprland.conf"
info "Cleaning up any previous installation..."
# Strip any previous OKM block from bindings.conf and (defensively) from
# hyprland.conf in case an earlier install version wrote there.
cleanup_hypr_rules "$bindings_conf"
if [[ "$hyprland_conf" != "$bindings_conf" ]] && grep -q "OKM\|Omarchy Kernel Manager" "$hyprland_conf" 2>/dev/null; then
cleanup_hypr_rules "$hyprland_conf"
fi
# Remove old desktop entries (both old and new names)
if [[ -f "$old_desktop_file" ]]; then
info "Removing old omarchy-kernel-manager desktop entry..."
rm -f "$old_desktop_file"
fi
if [[ -f "$new_desktop_file" ]]; then
info "Removing old OKM desktop entry..."
rm -f "$new_desktop_file"
fi
# Remove old scripts (requires sudo)
if [[ -f "$old_installed_path" ]]; then
info "Removing old omarchy-kernel-manager.sh script..."
sudo rm -f "$old_installed_path" || err "Failed to remove old script (continuing anyway)"
fi
if [[ -f "$new_installed_path" ]]; then
info "Removing old okm script..."
sudo rm -f "$new_installed_path" || err "Failed to remove old script (continuing anyway)"
fi
info "✓ Cleanup complete"
}
# Add Hyprland keybinding for OKM (Super+Shift+K).
# Omarchy-only: launch via xdg-terminal-exec with TUI.float app-id so
# Omarchy's stock `tag +floating-window, match:class TUI.float` rule
# (in ~/.local/share/omarchy/default/hypr/apps/system.conf) handles
# float/center/sizing. We deliberately inject NO windowrules.
configure_okm_binding() {
local hypr_config="${BINDINGS_CONFIG:-$HOME/.config/hypr/bindings.conf}"
[[ -f "$hypr_config" ]] || { err "Hyprland bindings config not found at $hypr_config"; return 1; }
# Avoid duplicate injection
if grep -q "# OKM bindings - added by OKM installer" "$hypr_config" 2>/dev/null; then
info "OKM keybinding already present in $hypr_config"
return 0
fi
# Detect bind style (Omarchy ships bindd for descriptive binds)
local bind_style="bindd"
if ! grep -q "^bindd[[:space:]]*=" "$hypr_config" 2>/dev/null; then
if grep -q "^bind[[:space:]]*=" "$hypr_config" 2>/dev/null; then
bind_style="bind"
fi
fi
local launch_cmd="xdg-terminal-exec --app-id=TUI.float -e /usr/local/bin/okm"
info "Adding OKM keybinding to $hypr_config (Super+Shift+K)"
{
echo ""
echo "# OKM bindings - added by OKM installer"
echo "# (TUI.float app-id triggers Omarchy's stock floating-window rule.)"
if [[ "$bind_style" == "bindd" ]]; then
echo "bindd = SUPER SHIFT, K, OKM, exec, $launch_cmd"
else
echo "bind = SUPER SHIFT, K, exec, $launch_cmd"
fi
echo "# End OKM bindings"
} >> "$hypr_config" || { err "Failed to append OKM bindings to $hypr_config"; return 1; }
hyprctl reload >/dev/null 2>&1 || info "Hyprland reload may have failed; relog if binds inactive."
}
# Remove any previously added OKM/Omarchy Kernel Manager Hyprland window rules
cleanup_hypr_rules() {
local hypr_config="$1"
[[ -f "$hypr_config" ]] || return 0
# Create backup before modifying (with timestamp to avoid conflicts)
local backup_file="${hypr_config}.bak.$(date +%s)"
cp "$hypr_config" "$backup_file" || {
err "Warning: Failed to create backup of $hypr_config"
return 1
}
# Remove the block between our markers (handles both old and new names)
sed -i '/# Omarchy Kernel Manager window rules/,/# End Omarchy Kernel Manager window rules/d' "$hypr_config" || {
err "Error modifying $hypr_config - restoring backup"
mv "$backup_file" "$hypr_config"
return 1
}
sed -i '/# OKM window rules/,/# End OKM window rules/d' "$hypr_config" || {
err "Error modifying $hypr_config - restoring backup"
mv "$backup_file" "$hypr_config"
return 1
}
# Remove any stray rules that mention "Omarchy Kernel Manager" or "OKM"
sed -i '/windowrulev2.*Omarchy Kernel Manager/d' "$hypr_config" || {
err "Error modifying $hypr_config - restoring backup"
mv "$backup_file" "$hypr_config"
return 1
}
sed -i '/windowrule.*Omarchy Kernel Manager/d' "$hypr_config" || {
err "Error modifying $hypr_config - restoring backup"
mv "$backup_file" "$hypr_config"
return 1
}
sed -i '/windowrulev2.*title.*OKM/d' "$hypr_config" || {
err "Error modifying $hypr_config - restoring backup"
mv "$backup_file" "$hypr_config"
return 1
}
sed -i '/windowrule.*OKM/d' "$hypr_config" || {
err "Error modifying $hypr_config - restoring backup"
mv "$backup_file" "$hypr_config"
return 1
}
# Remove OKM binding block if present (Super+Shift+K)
sed -i '/# OKM bindings - added by OKM installer/,/# End OKM bindings/d' "$hypr_config" || {
err "Error modifying $hypr_config - restoring backup"
mv "$backup_file" "$hypr_config"
return 1
}
# Verify the file is still valid (non-empty)
if [[ ! -s "$hypr_config" ]]; then
err "Error: $hypr_config became empty - restoring backup"
mv "$backup_file" "$hypr_config"
return 1
fi
# Keep only the most recent backup (delete older ones to avoid clutter)
find "$(dirname "$hypr_config")" -name "$(basename "$hypr_config").bak.*" -type f | sort -r | tail -n +6 | xargs rm -f 2>/dev/null || true
return 0
}
# =============================================================================
# MAIN EXECUTION
# =============================================================================
main() {
# Check dependencies
check_dependencies
# Foot inside Omarchy's TUI.float floating window starts at the default
# 80 cols and snaps to its real geometry ~40 ms later (we confirmed this
# via instrumentation: 80 → 86 → 115 over ~37 ms on an Acer Nitro). If we
# render the title box during that startup window it lands at the old
# left position while gum choose redraws on WINCH at the new centre,
# making the box look fragmented. A short settle pause lets tput cols
# report the final width before any output happens.
poll_terminal_size
# Compute adaptive UI widths for the current terminal, and refresh on resize.
compute_widths
trap compute_widths WINCH
# Locate Hyprland bindings.conf
detect_bindings_config
# Auto-install on first run if not already installed
local installed_path="/usr/local/bin/okm"
local old_installed_path="/usr/local/bin/omarchy-kernel-manager.sh"
local current_script
current_script=$(realpath "$0")
# Check if we're running from the installed location
if [[ "$current_script" != "$installed_path" ]]; then
# Running from a different location (Downloads, etc.) - offer to install/update
echo ""
echo "════════════════════════════════════════════════════════════════"
if [[ -f "$installed_path" ]] || [[ -f "$old_installed_path" ]]; then
echo " UPDATE OKM - OMARCHY KERNEL MANAGER"
echo "════════════════════════════════════════════════════════════════"
echo ""
echo " You're running a different version from your installed copy."
echo " This will update your installation."
else
echo " WELCOME TO OKM - OMARCHY KERNEL MANAGER"
echo "════════════════════════════════════════════════════════════════"
echo ""
echo " This appears to be your first time running OKM."
echo " It needs to be installed to your system."
fi
echo ""
echo " This will:"
echo " • Clean up any old installation"
echo " • Copy the script to /usr/local/bin/ (requires sudo)"
echo " • Add a Hyprland keybind (Super+Shift+K)"
echo ""
echo "════════════════════════════════════════════════════════════════"
echo ""
if [[ -f "$installed_path" ]] || [[ -f "$old_installed_path" ]]; then
read -p "Update OKM? [Y/n]: " -n 1 -r
else
read -p "Install OKM to system? [Y/n]: " -n 1 -r
fi
echo
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
# Install/Update to system
local is_update=false
if [[ -f "$installed_path" ]]; then
is_update=true
info "Updating installation..."
else
info "Installing to system..."
fi
echo ""
# Clean up any old installation first
cleanup_old_installation
echo ""
# Copy script with proper permissions in one step
sudo install -m 755 "$0" "$installed_path" || die "Failed to install script to $installed_path"
# Verify the installation
if [[ ! -x "$installed_path" ]]; then
die "Installation failed: $installed_path is not executable"
fi
info "✓ Script installed to $installed_path"
# Add Hyprland keybinding (Super+Shift+K)
configure_okm_binding || err "Failed to add OKM keybinding"
echo ""
if $is_update; then
info "✓ Update complete!"
else
info "✓ Installation complete!"
fi
echo ""
echo "════════════════════════════════════════════════════════════════"
echo " HOW TO USE"
echo "════════════════════════════════════════════════════════════════"
echo ""
echo " • Press Super+Shift+K to launch OKM in a floating window"
echo " • Or run: okm"
echo ""
echo "════════════════════════════════════════════════════════════════"
echo ""
# Don't auto-launch from the installer's terminal - OKM's TUI is sized
# for the floating TUI.float window and looks broken in any other
# terminal. The keybind is now wired up; let the user trigger it.
exit 0
else
info "Skipping installation. You can install later by running this script again."
echo ""
read -p "Continue running from current location? [Y/n]: " -n 1 -r
echo
echo
if [[ $REPLY =~ ^[Nn]$ ]]; then
exit 0
fi
fi
else
# Already running from installed location: ensure binding exists
# (cheap idempotent guard - returns immediately if marker is present)
configure_okm_binding || err "Failed to ensure OKM keybinding in Hyprland config"
fi
# Detect and apply system theme
detect_system_theme
# Initialize state on first run
init_state
# Main menu loop
while true; do
if ! show_main_menu; then
# User selected Exit - show goodbye message and close
clear
echo ""
gum style --align center --width "$OKM_W" --border normal --padding "1 2" $'OKM Closed\n\nClosing terminal...' | center_output "$OKM_BOX_W"
echo ""
sleep 1
# Force terminal close if launched from desktop
if [[ -n "${DESKTOP_STARTUP_ID:-}" ]] || [[ -n "${XDG_ACTIVATION_TOKEN:-}" ]]; then
# Launched from app launcher - kill the terminal window
kill -TERM $PPID 2>/dev/null || true
fi
exit 0
fi
done
}
main "$@"