commit cc4c6820dc83140defba79c4055b68fb5e720585 Author: 28allday Date: Sun May 10 09:44:20 2026 +0100 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//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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f08251 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Editor / OS +*.swp +.DS_Store +.idea/ +.vscode/ + +# Local script backups +*.bak +*.bak.* + +# Local debug logs (used during development) +/tmp/okm-widths.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5acd172 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Gavin Nugent + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/OKM.sh b/OKM.sh new file mode 100755 index 0000000..0648ebb --- /dev/null +++ b/OKM.sh @@ -0,0 +1,1380 @@ +#!/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//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" </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 "|" + 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 "$@" diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f1c251 --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# OKM — Omarchy Kernel Manager + +A friendly TUI for installing, switching, and removing Linux kernels on +[Omarchy](https://omarchy.org/). One keybind opens a floating window +where you can drop in CachyOS, linux-zen, hardened, and friends — and +remove them safely later, without ever clobbering your stock kernel or +the one you're currently booted into. + +``` + ╔══════════════════════════════════════════════════════════════════╗ + ║ ║ + ║ OKM - OMARCHY KERNEL MANAGER ║ + ║ ║ + ║ Shall we tweak the kernel? ║ + ║ ║ + ╚══════════════════════════════════════════════════════════════════╝ + + Choose: + > View current kernel status │ Show current kernel info + Install or manage kernels │ Add linux-zen, linux-cachyos, etc + Revert to stock / clean up │ Remove extra kernels + Exit │ Quit the Kernel Manager +``` + +## Features + +- **One-keybind launch.** `Super+Shift+K` opens OKM in a floating + window via Omarchy's stock `TUI.float` rule — no custom windowrules + to maintain. +- **14 known kernels out of the box** — covering the four official + Arch kernels (`linux`, `linux-lts`, `linux-hardened`, `linux-zen`) + and ten CachyOS variants (default, BORE, EEVDF, BMQ, LTS, hardened, + server, RT-BORE, deckify, RC). +- **CachyOS repo bootstrap.** First time you pick a CachyOS kernel, + OKM signs the keyring, fetches the latest `cachyos-keyring` / + `cachyos-mirrorlist` versions live (no stale URLs), and adds the + repo to `/etc/pacman.conf` — running a full `pacman -Syu` first to + avoid partial-upgrade trouble. +- **AUR support.** If you've added a custom kernel to the + `KERNEL_SOURCE` table with `source=aur`, OKM dispatches to `yay` or + `paru` automatically. +- **Bootloader-aware.** After install or removal it runs + `mkinitcpio -P` and updates whichever bootloader you have: + GRUB, systemd-boot (with a `bootctl list` summary), Limine, or + rEFInd. Misses none of the common Arch setups. +- **Safe removal.** Two kernels are *always* protected: + 1. Your **stock kernel** — recorded the first time OKM runs. + 2. The **currently running kernel** — pacman would refuse anyway, + and a successful removal mid-session means a broken next boot. + Both appear in the removal screen with a clear reason, so you're + never staring at a confusingly empty list. +- **Adapts to terminal size.** Boxes auto-shrink for narrow + terminals; the title polls `tput cols` until the floating window + has settled (foot resizes ~40 ms after spawn) so the header never + renders at the wrong width. + +## Requirements + +- **Omarchy** (Arch Linux + Hyprland). The script bails out if it + can't find `~/.config/hypr` or `xdg-terminal-exec`. +- `gum` (`pacman -S gum`) +- `curl` +- `pacman` (you're on Arch — you have it) +- For CachyOS kernels: an internet connection on first install +- For AUR kernels: `yay` or `paru` + +## Install + +```bash +# Clone, then run (the script self-installs to /usr/local/bin/okm +# and adds a Super+Shift+K binding to ~/.config/hypr/bindings.conf). +git clone https://github.com/28allday/omarchy-kernel-manager.git +cd omarchy-kernel-manager +./OKM.sh +``` + +Pick **Install OKM to system** when prompted. After that you can +launch it any time with **Super+Shift+K** or by running `okm`. + +## Usage + +### Install a kernel + +`Super+Shift+K` → **Install or manage kernels** → pick from the list. +OKM tells you whether each kernel comes from pacman, CachyOS, or AUR, +sets up the repo if needed, installs `` and `-headers`, +and refreshes the bootloader. Reboot and select the new kernel from +your boot menu. + +### Switch back to stock + +`Super+Shift+K` → **View current kernel status** to confirm what +you're on, reboot via the boot menu into your stock kernel, then +launch OKM again to remove the others (see below). + +### Remove a kernel + +`Super+Shift+K` → **Revert to stock / clean up**. You'll see every +installed kernel except stock + currently running. Multi-select with +**Space**, confirm with **Enter**, OKM strips the package(s) plus +matching `-headers`, then refreshes the bootloader. + +If the kernel you want to remove *is* the running one, OKM tells you +to reboot into another kernel first — it won't pull the rug out. + +## How it identifies the running kernel + +OKM uses Arch's canonical mechanism: every kernel package writes a +`pkgbase` file at `/usr/lib/modules//pkgbase` +containing its package name. Reading that file is the only reliable +way to map `uname -r` back to a package — walking `/boot/vmlinuz-*` +just finds *some* installed kernel, not necessarily the one you're +booted into. + +## Files OKM touches + +| Path | What it does | +|---|---| +| `/usr/local/bin/okm` | The installed script | +| `~/.config/hypr/bindings.conf` | Adds a single `bindd` line in a marked block — the floating-window rule comes from Omarchy's stock `TUI.float` regex, no custom windowrules written | +| `~/.local/share/okm/kernel-state` | Records your stock kernel so it can never be accidentally removed | +| `/etc/pacman.conf` | First time you install a CachyOS kernel, adds the `[cachyos]` section + Include line | + +To uninstall: `sudo rm /usr/local/bin/okm` and remove the +`# OKM bindings - added by OKM installer` block from +`~/.config/hypr/bindings.conf`. + +## Repository + +Mirrored on: + +- **GitHub:** https://github.com/28allday/omarchy-kernel-manager +- **Forgejo:** https://git.no-signal.uk/nosignal/omarchy-kernel-manager + +## License + +MIT — see [LICENSE](LICENSE).