Files
Modus/install.sh
T
sankalp ab9b9df4e1 feat(install): rewrite install.sh with interactive mode selection
- 3 install modes: Full, Minimal, Custom (pick categories)
- Dry-run flag (-n) to preview packages without installing
- Progress bar with percentage during package install
- Better visual hierarchy with section headers and dividers
- Package categories: Core, Shell, Hyprland, UI, Hardware, Network, Audio, Build
- Failed package summary at end
- Cleaner banner and completion output
2026-07-18 15:12:21 +05:45

596 lines
19 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ███╗ ███╗ ██████╗ ██████╗ ██╗ ██╗███████╗
# ████╗ ████║██╔═══██╗██╔══██╗██║ ██║██╔════╝
# ██╔████╔██║██║ ██║██║ ██║██║ ██║███████╗
# ██║╚██╔╝██║██║ ██║██║ ██║██║ ██║╚════██║
# ██║ ╚═╝ ██║╚██████╔╝██████╔╝╚██████╔╝███████║
# ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
#
# A hackable shell for Hyprland
# Installation Script for Arch Linux
#
# Repository: https://github.com/S4NKALP/Modus
# License: GPLv3
set -e
set -u
set -o pipefail
# ── CLI flags ──────────────────────────────────────────────────────
AUTO_YES=false
DRY_RUN=false
MINIMAL=false
for arg in "$@"; do
case "$arg" in
-y|--yes) AUTO_YES=true ;;
-n|--dry-run) DRY_RUN=true ;;
-m|--minimal) MINIMAL=true ;;
-h|--help)
echo "Usage: install.sh [OPTIONS]"
echo ""
echo "Options:"
echo " -y, --yes Skip all prompts"
echo " -n, --dry-run Show what would be installed without installing"
echo " -m, --minimal Install only core packages"
echo " -h, --help Show this help"
exit 0
;;
esac
done
REPO_URL="https://github.com/S4NKALP/Modus.git"
INSTALL_DIR="$HOME/.config/Modus"
# ── Package definitions ────────────────────────────────────────────
CORE_PACKAGES=(
uv
fabric-cli-git
uwsm
)
SHELL_PACKAGES=(
cliphist
slurp
grim
swappy
wl-clipboard
wtype
libnotify
playerctl
matugen-bin
)
HYPRLAND_PACKAGES=(
hypridle
hyprsunset
hyprpicker
hyprshot
gtk-session-lock
)
UI_PACKAGES=(
awww
apple-fonts
webp-pixbuf-loader
cinnamon-desktop
libmediaart
)
HARDWARE_PACKAGES=(
acpi
brightnessctl
power-profiles-daemon
ddcutil
at-spi2-core
)
NETWORK_PACKAGES=(
networkmanager
network-manager-applet
blueman
)
AUDIO_PACKAGES=(
pipewire
libpulse
)
BUILD_PACKAGES=(
gcc
make
pkgconf
meson
ninja
wayland-protocols
gobject-introspection
gtk-layer-shell
librsvg
libqalculate
appmenu-gtk-module
libdbusmenu-gtk3
libdbusmenu-qt5
pciutils
wf-recorder
ffmpeg
)
ALL_PACKAGES=(
"${CORE_PACKAGES[@]}"
"${SHELL_PACKAGES[@]}"
"${HYPRLAND_PACKAGES[@]}"
"${UI_PACKAGES[@]}"
"${HARDWARE_PACKAGES[@]}"
"${NETWORK_PACKAGES[@]}"
"${AUDIO_PACKAGES[@]}"
"${BUILD_PACKAGES[@]}"
)
# ── Colors ─────────────────────────────────────────────────────────
if [ -t 1 ]; then
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
RED=$(tput setaf 1)
CYAN=$(tput setaf 6)
BLUE=$(tput setaf 4)
MAGENTA=$(tput setaf 5)
BOLD=$(tput bold)
DIM=$(tput dim)
RESET=$(tput sgr0)
else
GREEN="" YELLOW="" RED="" CYAN="" BLUE="" MAGENTA="" BOLD="" DIM="" RESET=""
fi
ARROW="→"
CHECK="✔"
CROSS="✖"
INFO="ℹ"
WARN="⚠"
BULLET="•"
# ── Helpers ────────────────────────────────────────────────────────
header() {
echo ""
echo -e " ${BOLD}${CYAN}╔══════════════════════════════════════════════╗${RESET}"
echo -e " ${BOLD}${CYAN}║${RESET} ${BOLD}Modus Installer${RESET} ${DIM}v2.0${RESET} ${BOLD}${CYAN}║${RESET}"
echo -e " ${BOLD}${CYAN}╚══════════════════════════════════════════════╝${RESET}"
echo ""
}
section() {
echo ""
echo -e " ${BOLD}${MAGENTA}── $1 ──${RESET}"
}
step() {
echo -e " ${CYAN}${ARROW}${RESET} $1"
}
success() {
echo -e " ${GREEN}${CHECK}${RESET} ${GREEN}$1${RESET}"
}
warn() {
echo -e " ${YELLOW}${WARN}${RESET} ${YELLOW}$1${RESET}"
}
error() {
echo -e " ${RED}${CROSS}${RESET} ${RED}$1${RESET}"
}
info() {
echo -e " ${BLUE}${INFO}${RESET} ${DIM}$1${RESET}"
}
divider() {
echo -e " ${DIM}──────────────────────────────────────────────${RESET}"
}
confirm() {
local prompt="$1"
local default="${2:-n}"
local yn
if [ "$AUTO_YES" = true ]; then
return 0
fi
if [ "$default" = "y" ]; then
read -rp " ${BOLD}${prompt} [Y/n]:${RESET} " yn
[[ ! "$yn" =~ ^[Nn]$ ]]
else
read -rp " ${BOLD}${prompt} [y/N]:${RESET} " yn
[[ "$yn" =~ ^[Yy]$ ]]
fi
}
spinner() {
local pid=$1
local message=$2
local spin=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r %b%s%b %s" "$CYAN" "${spin[i]}" "$RESET" "$message"
i=$(((i + 1) % 10))
sleep 0.1
done
printf "\r %b%s%b %s\n" "$GREEN" "${CHECK}" "$RESET" "$message"
}
print_packages() {
local -n pkgs=$1
local count=${#pkgs[@]}
echo -e " ${DIM}${BULLET}${RESET} ${BOLD}$2${RESET} ${DIM}(${count} packages)${RESET}"
for pkg in "${pkgs[@]}"; do
echo -e " ${DIM}${BULLET}${RESET} ${pkg}"
done
}
# ── Cleanup ────────────────────────────────────────────────────────
cleanup() {
if [ -n "${SUDO_KEEPER_PID:-}" ]; then
kill "$SUDO_KEEPER_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT INT TERM
# ── Banner ─────────────────────────────────────────────────────────
clear
header
echo -e " ${BOLD}A hackable shell for Hyprland${RESET}"
echo -e " ${DIM}https://github.com/S4NKALP/Modus${RESET}"
echo ""
# ── Pre-flight checks ─────────────────────────────────────────────
section "Pre-flight checks"
step "Checking operating system..."
if ! grep -qi "arch" /etc/os-release; then
error "This script requires Arch Linux or an Arch-based distribution"
exit 1
fi
success "Arch Linux detected"
step "Checking user permissions..."
if [ "$(id -u)" -eq 0 ]; then
error "Please run this script as a regular user, not as root"
exit 1
fi
success "Running as regular user"
step "Checking git..."
if ! command -v git &>/dev/null; then
error "git is not installed. Run: sudo pacman -S git"
exit 1
fi
success "git found"
# ── Install mode selection ────────────────────────────────────────
section "Install mode"
if [ "$MINIMAL" = true ]; then
PACKAGES=("${CORE_PACKAGES[@]}")
info "Minimal mode: ${#PACKAGES[@]} core packages only"
elif [ "$AUTO_YES" = true ]; then
PACKAGES=("${ALL_PACKAGES[@]}")
info "Full install: ${#PACKAGES[@]} packages"
else
echo -e " ${BOLD}Choose install mode:${RESET}"
echo ""
echo -e " ${GREEN}${ARROW}${RESET} ${BOLD}1) Full${RESET} ${DIM}— everything (${#ALL_PACKAGES[@]} packages)${RESET}"
echo -e " ${YELLOW}${ARROW}${RESET} ${BOLD}2) Minimal${RESET} ${DIM}— core only (${#CORE_PACKAGES[@]} packages)${RESET}"
echo -e " ${CYAN}${ARROW}${RESET} ${BOLD}3) Custom${RESET} ${DIM}— pick categories${RESET}"
echo ""
read -rp " ${BOLD}Select [1-3]:${RESET} " mode_choice
case "$mode_choice" in
2)
PACKAGES=("${CORE_PACKAGES[@]}")
;;
3)
PACKAGES=("${CORE_PACKAGES[@]}")
echo ""
for cat in "SHELL:Shell tools" "HYPRLAND:Hyprland extras" "UI:Desktop widgets" "HARDWARE:Hardware control" "NETWORK:Bluetooth & network" "AUDIO:Audio" "BUILD:Build tools"; do
local_name="${cat%%:*}"
local_label="${cat#*:}"
local_ref="${local_name}_PACKAGES[@]"
local_count="${#${!local_ref}}"
if confirm "${local_label} (${local_count} packages)?" "y"; then
PACKAGES+=("${!local_ref}")
fi
done
;;
*)
PACKAGES=("${ALL_PACKAGES[@]}")
;;
esac
fi
divider
echo -e " ${BOLD}Will install ${GREEN}${#PACKAGES[@]}${RESET}${BOLD} packages${RESET}"
divider
# ── Sudo ───────────────────────────────────────────────────────────
section "Permissions"
info "Some packages require root privileges"
if ! sudo -v 2>/dev/null; then
error "Sudo authentication failed"
exit 1
fi
success "Sudo authenticated"
while true; do
sudo -n true
sleep 60
kill -0 "$$" || exit
done 2>/dev/null &
SUDO_KEEPER_PID=$!
# ── Show package list ─────────────────────────────────────────────
section "Packages"
if [ "$DRY_RUN" = true ]; then
echo ""
printf " ${DIM}%s${RESET}\n" "${PACKAGES[@]}"
echo ""
info "Dry run — no packages will be installed"
exit 0
fi
if confirm "View full package list?"; then
echo ""
printf " ${DIM}${BULLET} %s${RESET}\n" "${PACKAGES[@]}"
echo ""
fi
if ! confirm "Proceed with installation?"; then
warn "Cancelled"
exit 0
fi
# ── AUR helper ─────────────────────────────────────────────────────
section "AUR helper"
AUR=""
if command -v paru &>/dev/null; then
AUR="paru"
success "Using paru"
elif command -v yay &>/dev/null; then
AUR="yay"
success "Using yay"
else
step "Installing yay-bin..."
tmpdir=$(mktemp -d)
(
git clone --quiet --depth=1 https://aur.archlinux.org/yay-bin.git "$tmpdir/yay-bin"
cd "$tmpdir/yay-bin"
makepkg -si --noconfirm >/dev/null 2>&1
) &
spinner $! "Building yay-bin"
wait $! || {
error "Failed to install yay-bin"
rm -rf "$tmpdir"
exit 1
}
rm -rf "$tmpdir"
AUR="yay"
success "yay-bin installed"
fi
# ── Repository ─────────────────────────────────────────────────────
section "Repository"
if [ -d "$INSTALL_DIR" ]; then
step "Pulling latest changes..."
git -C "$INSTALL_DIR" pull --quiet 2>/dev/null || true
success "Repository updated"
else
step "Cloning repository..."
git clone --quiet "$REPO_URL" "$INSTALL_DIR" 2>/dev/null || true
success "Repository cloned"
fi
info "${INSTALL_DIR}"
# ── Install packages ──────────────────────────────────────────────
section "Installing packages"
step "Syncing databases..."
$AUR -Syy --noconfirm >/dev/null 2>&1 || true
success "Databases synced"
installed=0
failed=0
failed_pkgs=()
total=${#PACKAGES[@]}
for pkg in "${PACKAGES[@]}"; do
if $AUR -S --needed --noconfirm "$pkg" >/dev/null 2>&1; then
installed=$((installed + 1))
else
failed=$((failed + 1))
failed_pkgs+=("$pkg")
fi
pct=$((installed * 100 / total))
printf "\r ${CYAN}${ARROW}${RESET} [%-50s] %3d%% (%d/%d)" "$(printf '#%.0s' $(seq 1 $((pct / 2))))" "$pct" "$installed" "$total"
done
echo ""
if [ $failed -eq 0 ]; then
success "All ${installed} packages installed"
else
warn "${failed} package(s) failed:"
for pkg in "${failed_pkgs[@]}"; do
echo -e " ${RED}${CROSS}${RESET} ${pkg}"
done
fi
# ── Build native modules ──────────────────────────────────────────
section "Building native modules"
# Global Menu shim
step "Compiling libmenu_button_shim.so..."
SHIM_SRC="$INSTALL_DIR/src/window/globalmenu/libmenu_button_shim.c"
SHIM_OUT="$INSTALL_DIR/src/window/globalmenu/libmenu_button_shim.so"
if [ -f "$SHIM_SRC" ]; then
if gcc -shared -fPIC -O2 -o "$SHIM_OUT" "$SHIM_SRC" "$(pkg-config --cflags --libs gtk+-3.0)" -ldl 2>/dev/null; then
success "libmenu_button_shim.so built"
else
warn "Failed to compile shim — global menu may not work"
fi
else
warn "Shim source not found — skipped"
fi
# App Capture
step "Compiling libappcapture.so..."
if [ -d "$INSTALL_DIR/src/window/switcher/app-capture" ]; then
if (
cd "$INSTALL_DIR/src/window/switcher/app-capture"
meson setup builddir --wipe >/dev/null 2>&1 || meson setup builddir >/dev/null 2>&1
meson compile -C builddir >/dev/null 2>&1
); then
success "libappcapture.so built"
else
warn "Failed to compile app-capture"
fi
else
warn "App-capture source not found — skipped"
fi
# ── Hyprland config ───────────────────────────────────────────────
section "Hyprland configuration"
HYPR_CONFIG="$HOME/.config/hypr/hyprland.lua"
MODUS_MODULE_LINE="dofile(\"$INSTALL_DIR/config/hypr/modus.lua\")"
if [ -f "$HYPR_CONFIG" ]; then
if grep -qF "$MODUS_MODULE_LINE" "$HYPR_CONFIG"; then
success "Modus module already in hyprland.lua"
else
step "Adding Modus module to hyprland.lua..."
{
echo ""
echo "-- Modus configuration"
echo "$MODUS_MODULE_LINE"
} >> "$HYPR_CONFIG"
success "Module added"
fi
else
warn "hyprland.lua not found at ${HYPR_CONFIG}"
info "Add manually: ${MODUS_MODULE_LINE}"
fi
step "Symlinking hypridle.conf..."
HYPR_DIR="$HOME/.config/hypr"
HYPRIDLE_CONF="$INSTALL_DIR/config/hypr/hypridle.conf"
HYPRIDLE_TARGET="$HYPR_DIR/hypridle.conf"
mkdir -p "$HYPR_DIR"
if [ -f "$HYPRIDLE_CONF" ]; then
ln -sf "$HYPRIDLE_CONF" "$HYPRIDLE_TARGET"
success "hypridle.conf symlinked"
else
warn "hypridle.conf not found — skipped"
fi
# ── Environment ───────────────────────────────────────────────────
section "Global environment"
APP_CONF_DIR="$HOME/.config/environment.d"
APP_CONF_FILE="$APP_CONF_DIR/appmenu.conf"
step "Writing environment.d config..."
mkdir -p "$APP_CONF_DIR"
cat > "$APP_CONF_FILE" << 'EOF'
GTK_MODULES=appmenu-gtk-module
UBUNTU_MENUPROXY=1
EOF
success "environment.d/appmenu.conf written"
PAM_FILE="$HOME/.pam_environment"
step "Updating pam_environment..."
for entry in "GTK_MODULES DEFAULT=appmenu-gtk-module" "UBUNTU_MENUPROXY DEFAULT=1"; do
var_name="${entry%% *}"
if ! grep -q "^${var_name} " "$PAM_FILE" 2>/dev/null; then
echo "$entry" >> "$PAM_FILE"
fi
done
success "pam_environment updated"
# ── Matugen ────────────────────────────────────────────────────────
section "Matugen"
MATUGEN_CONFIG="$HOME/.config/matugen/config.toml"
MODUS_BLOCK='[templates.modus]
input_path = "~/.config/Modus/config/matugen/templates/modus.css"
output_path = "~/.config/Modus/src/shared/styles/colors.css"
post_hook = "fabric-cli exec modus '\''app.set_css()'\'' &"'
HYPR_BLOCK='[templates.hyprland]
input_path = "~/.config/Modus/config/matugen/templates/hyprland-colors.lua"
output_path = "~/.config/Modus/config/matugen/colors.lua"'
if [ -f "$MATUGEN_CONFIG" ]; then
if grep -qF "[templates.modus]" "$MATUGEN_CONFIG"; then
success "Modus template already configured"
else
step "Adding Modus template..."
echo "" >> "$MATUGEN_CONFIG"
echo "$MODUS_BLOCK" >> "$MATUGEN_CONFIG"
success "Modus template added"
fi
if grep -qF "[templates.hyprland]" "$MATUGEN_CONFIG"; then
success "Hyprland template already configured"
else
step "Adding Hyprland template..."
echo "" >> "$MATUGEN_CONFIG"
echo "$HYPR_BLOCK" >> "$MATUGEN_CONFIG"
success "Hyprland template added"
fi
else
warn "matugen config not found at ${MATUGEN_CONFIG}"
info "Install matugen first, then re-run this script"
fi
# ── Launch ─────────────────────────────────────────────────────────
section "Launch"
step "Stopping existing instances..."
if killall modus 2>/dev/null; then
success "Stopped running instance"
sleep 1
else
info "No existing instance found"
fi
step "Starting Modus..."
if uwsm app -- uv run --project "$INSTALL_DIR" start >/dev/null 2>&1 & then
disown
sleep 2
if pgrep -x "modus" >/dev/null; then
success "Modus is running"
else
warn "Modus may not have started — check logs"
fi
else
error "Failed to start Modus"
exit 1
fi
# ── Summary ────────────────────────────────────────────────────────
echo ""
echo -e " ${GREEN}${BOLD}╔══════════════════════════════════════════╗${RESET}"
echo -e " ${GREEN}${BOLD}║${RESET} ${GREEN}${BOLD}║${RESET}"
echo -e " ${GREEN}${BOLD}║${RESET} ${GREEN}${BOLD}Installation complete!${RESET} ${GREEN}${BOLD}║${RESET}"
echo -e " ${GREEN}${BOLD}║${RESET} ${GREEN}${BOLD}║${RESET}"
echo -e " ${GREEN}${BOLD}╚══════════════════════════════════════════╝${RESET}"
echo ""
divider
info "Packages: ${GREEN}${installed}${RESET} installed${RED:+, ${failed} failed}"
info "Location: ${INSTALL_DIR}"
info "Config: ${INSTALL_DIR}/config/"
divider
echo ""