Files
dotevo/install.sh
edemilov 6183c7859b fix: persist zsh history on Kali look, skip TerminalLook module cleanly on missing prereqs
- write_zshrc_kali now sets HISTFILE/HISTSIZE/SAVEHIST and share_history/
  hist_ignore_dups so command history survives across sessions (previously
  unset, so history was in-memory only despite shipping
  zsh-history-substring-search and fzf history search).
- apply_terminal_look now guards install_terminal_prereqs and returns 0 with
  a skip message on failure, matching apply_hyprland's existing pattern, so
  an unrecognized package manager no longer aborts the whole script under
  set -euo pipefail and blocks a Hyprland module that would otherwise
  succeed.

Addresses findings 1 and 2 from the final whole-branch review.
2026-08-30 17:46:47 +03:00

502 lines
18 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# ${BASH_SOURCE[0]} is unset (not just empty) when this script arrives as a
# `bash -c "$(curl ...)"` command string rather than a real file -- guard it
# so `set -u` doesn't blow up. `dirname ""` -> "." (current dir), which
# ensure_repo_files() below correctly treats as "no local checkout, fetch."
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-}")" && pwd)"
CONFIG_HOME="${CONFIG_HOME:-${XDG_CONFIG_HOME:-$HOME/.config}}"
LOCAL_BIN="${LOCAL_BIN:-$HOME/.local/bin}"
DOTEVO_RAW_BASE="${DOTEVO_RAW_BASE:-https://git.edemilov.ru/edemilov/dotevo/raw/branch/master}"
OMZ_DIR="${OMZ_DIR:-$HOME/.oh-my-zsh}"
ZSH_CUSTOM_DIR="${ZSH_CUSTOM_DIR:-$HOME/.dotevo-zsh-custom}"
ASSETS_DIR="${ASSETS_DIR:-$HOME/.dotevo}"
check_arch() {
command -v pacman >/dev/null 2>&1
}
detect_pkg_manager() {
if command -v pacman >/dev/null 2>&1; then
echo pacman
elif command -v apt-get >/dev/null 2>&1; then
echo apt
elif command -v dnf >/dev/null 2>&1; then
echo dnf
elif command -v zypper >/dev/null 2>&1; then
echo zypper
else
echo unknown
fi
}
install_terminal_prereqs() {
local mgr
mgr=$(detect_pkg_manager)
case "$mgr" in
pacman) sudo pacman -S --needed --noconfirm zsh git curl fzf ;;
apt) sudo apt-get update && sudo apt-get install -y zsh git curl fzf ;;
dnf) sudo dnf install -y zsh git curl fzf ;;
zypper) sudo zypper install -y zsh git curl fzf ;;
*)
echo "dotevo: package manager not recognized -- install zsh git curl fzf manually" >&2
return 1
;;
esac
}
# When install.sh is curl-piped straight into bash (the normal way to run it),
# $BASH_SOURCE resolves to a transient fd path (e.g. /dev/fd/63) with no
# sibling files at all -- there's no local checkout of the rest of the repo.
# Detect that case and fetch the handful of files apply_hyprland() needs into a fresh
# temp dir instead, then repoint REPO_DIR there. Running from a real git
# checkout (packages.txt already sits next to this script) is left untouched.
ensure_repo_files() {
if [[ -f "$REPO_DIR/packages.txt" ]]; then
return 0
fi
local tmp_dir
tmp_dir="$(mktemp -d)"
mkdir -p "$tmp_dir/hypr" "$tmp_dir/local-bin" "$tmp_dir/terminal/p10k" "$tmp_dir/terminal/kali"
curl -fsSL "$DOTEVO_RAW_BASE/packages.txt" -o "$tmp_dir/packages.txt"
curl -fsSL "$DOTEVO_RAW_BASE/hypr/hyprland.conf" -o "$tmp_dir/hypr/hyprland.conf"
curl -fsSL "$DOTEVO_RAW_BASE/local-bin/capture-menu.sh" -o "$tmp_dir/local-bin/capture-menu.sh"
curl -fsSL "$DOTEVO_RAW_BASE/local-bin/screenshot.sh" -o "$tmp_dir/local-bin/screenshot.sh"
curl -fsSL "$DOTEVO_RAW_BASE/local-bin/toggle-recording.sh" -o "$tmp_dir/local-bin/toggle-recording.sh"
curl -fsSL "$DOTEVO_RAW_BASE/terminal/p10k/.p10k.zsh" -o "$tmp_dir/terminal/p10k/.p10k.zsh"
curl -fsSL "$DOTEVO_RAW_BASE/terminal/kali/prompt.zsh" -o "$tmp_dir/terminal/kali/prompt.zsh"
REPO_DIR="$tmp_dir"
}
slot_package() {
local slot="$1" choice="$2"
case "$slot:$choice" in
Terminal:Konsole) echo "konsole" ;;
Terminal:Terminator) echo "terminator" ;;
Terminal:Kitty) echo "kitty" ;;
Terminal:Alacritty) echo "alacritty" ;;
FileManager:Dolphin) echo "dolphin" ;;
FileManager:Nautilus) echo "nautilus" ;;
FileManager:Thunar) echo "thunar" ;;
Mail:Thunderbird) echo "thunderbird" ;;
Mail:KMail) echo "kmail" ;;
CloudSync:Nextcloud) echo "nextcloud-client" ;;
CloudSync:Syncthing) echo "syncthing" ;;
*) echo "dotevo: unknown slot/choice pair: $slot/$choice" >&2; return 1 ;;
esac
}
slot_binary() {
local slot="$1" choice="$2"
case "$slot:$choice" in
Terminal:Konsole) echo "konsole" ;;
Terminal:Terminator) echo "terminator" ;;
Terminal:Kitty) echo "kitty" ;;
Terminal:Alacritty) echo "alacritty" ;;
FileManager:Dolphin) echo "dolphin" ;;
FileManager:Nautilus) echo "nautilus" ;;
FileManager:Thunar) echo "thunar" ;;
Mail:Thunderbird) echo "thunderbird" ;;
Mail:KMail) echo "kmail" ;;
CloudSync:Nextcloud) echo "nextcloud --background" ;;
*) echo "dotevo: unknown slot/choice pair: $slot/$choice" >&2; return 1 ;;
esac
}
terminal_desktop_id() {
local choice="$1"
case "$choice" in
Konsole) echo "org.kde.konsole.desktop" ;;
Terminator) echo "terminator.desktop" ;;
Kitty) echo "kitty.desktop" ;;
Alacritty) echo "Alacritty.desktop" ;;
*) echo "dotevo: unknown terminal choice: $choice" >&2; return 1 ;;
esac
}
toggle_packages() {
local toggle="$1"
case "$toggle" in
Telegram) echo "telegram-desktop" ;;
Bitwarden) echo "bitwarden" ;;
ScreenCapture) echo "grim slurp satty wl-clipboard wf-recorder fuzzel libnotify" ;;
*) echo "dotevo: unknown toggle: $toggle" >&2; return 1 ;;
esac
}
run_picker() {
if [[ "${DOTEVO_NONINTERACTIVE:-0}" == "1" ]]; then
PICK_TERMINAL="${DOTEVO_TERMINAL:?DOTEVO_TERMINAL required in non-interactive mode}"
PICK_FILEMANAGER="${DOTEVO_FILEMANAGER:?DOTEVO_FILEMANAGER required in non-interactive mode}"
PICK_MAIL="${DOTEVO_MAIL:?DOTEVO_MAIL required in non-interactive mode}"
PICK_CLOUDSYNC="${DOTEVO_CLOUDSYNC:?DOTEVO_CLOUDSYNC required in non-interactive mode}"
read -ra PICK_TOGGLES <<< "${DOTEVO_TOGGLES:-}"
return 0
fi
PICK_TERMINAL=$(whiptail --title "dotevo" --radiolist \
"Choose your terminal emulator" 15 60 4 \
Konsole "" ON Terminator "" OFF Kitty "" OFF Alacritty "" OFF \
3>&1 1>&2 2>&3)
PICK_FILEMANAGER=$(whiptail --title "dotevo" --radiolist \
"Choose your file manager" 15 60 3 \
Dolphin "" ON Nautilus "" OFF Thunar "" OFF \
3>&1 1>&2 2>&3)
PICK_MAIL=$(whiptail --title "dotevo" --radiolist \
"Choose your mail client" 15 60 2 \
Thunderbird "" ON KMail "" OFF \
3>&1 1>&2 2>&3)
PICK_CLOUDSYNC=$(whiptail --title "dotevo" --radiolist \
"Choose your cloud sync client" 15 60 2 \
Nextcloud "" ON Syncthing "" OFF \
3>&1 1>&2 2>&3)
local toggle_raw
toggle_raw=$(whiptail --title "dotevo" --checklist \
"Choose optional apps (space to toggle, enter to confirm)" 15 70 3 \
Telegram "" OFF ScreenCapture "Screenshot/record via Print key" OFF Bitwarden "" OFF \
3>&1 1>&2 2>&3)
# whiptail --checklist returns space-separated quoted tokens, e.g. "Telegram" "Bitwarden"
read -ra PICK_TOGGLES <<< "$(echo "$toggle_raw" | tr -d '"')"
}
confirm_summary() {
local summary
summary="Terminal: $PICK_TERMINAL
File manager: $PICK_FILEMANAGER
Mail client: $PICK_MAIL
Cloud sync: $PICK_CLOUDSYNC
Telegram: $([[ " ${PICK_TOGGLES[*]} " == *" Telegram "* ]] && echo yes || echo no)
Screen capture: $([[ " ${PICK_TOGGLES[*]} " == *" ScreenCapture "* ]] && echo yes || echo no)
Bitwarden: $([[ " ${PICK_TOGGLES[*]} " == *" Bitwarden "* ]] && echo yes || echo no)"
if [[ "${DOTEVO_NONINTERACTIVE:-0}" == "1" ]]; then
echo "$summary"
return 0
fi
whiptail --title "dotevo -- confirm" --yesno "$summary
Install with these choices?" 18 60
}
build_extras_conf() {
echo "# Generated by dotevo -- rebuilt fresh on every install run, do not hand-edit"
if [[ " ${PICK_TOGGLES[*]} " == *" ScreenCapture "* ]]; then
echo "bind = , Print, exec, ~/.local/bin/capture-menu.sh"
fi
if [[ "$PICK_CLOUDSYNC" == "Nextcloud" ]]; then
echo "exec-once = $(slot_binary CloudSync Nextcloud)"
fi
if [[ " ${PICK_TOGGLES[*]} " == *" Telegram "* ]]; then
echo "exec-once = Telegram -startintray"
fi
if [[ " ${PICK_TOGGLES[*]} " == *" Bitwarden "* ]]; then
echo "exec-once = bitwarden-desktop --disable-gpu"
fi
}
render_hyprland_conf() {
local src_path="$1"
local terminal_bin fm_bin
terminal_bin=$(slot_binary Terminal "$PICK_TERMINAL")
fm_bin=$(slot_binary FileManager "$PICK_FILEMANAGER")
sed \
-e "s/^\$terminal = .*/\$terminal = ${terminal_bin}/" \
-e "s/^\$fileManager = .*/\$fileManager = ${fm_bin}/" \
"$src_path"
}
build_package_list() {
local pkgs=()
mapfile -t pkgs < "$REPO_DIR/packages.txt"
pkgs+=("$(slot_package Terminal "$PICK_TERMINAL")")
pkgs+=("$(slot_package FileManager "$PICK_FILEMANAGER")")
pkgs+=("$(slot_package Mail "$PICK_MAIL")")
pkgs+=("$(slot_package CloudSync "$PICK_CLOUDSYNC")")
local toggle pkg
for toggle in "${PICK_TOGGLES[@]}"; do
[[ -z "$toggle" ]] && continue
for pkg in $(toggle_packages "$toggle"); do
pkgs+=("$pkg")
done
done
printf '%s\n' "${pkgs[@]}" | awk '!seen[$0]++' | tr '\n' ' '
}
install_session_target() {
mkdir -p "$CONFIG_HOME/systemd/user"
cat > "$CONFIG_HOME/systemd/user/hyprland-session.target" <<'EOF'
[Unit]
Description=Hyprland Session Target
Requires=graphical-session.target
After=graphical-session.target
Wants=xdg-desktop-autostart.target
After=xdg-desktop-autostart.target
EOF
systemctl --user daemon-reload
systemctl --user add-wants hyprland-session.target dms.service
}
configure_dms_dock() {
if ! command -v dms >/dev/null 2>&1; then
echo "dotevo: dms not on PATH yet, skipping dock settings -- log into Hyprland once, then re-run dotevo (or run manually: dms ipc call settings set showDock true)"
return 0
fi
if ! dms ipc call settings get showDock >/dev/null 2>&1; then
echo "dotevo: DMS not running yet, skipping dock settings -- log into Hyprland once, then re-run dotevo (or run manually: dms ipc call settings set showDock true / dockAutoHide true / dockUseOverlayLayer true)"
return 0
fi
dms ipc call settings set showDock true
dms ipc call settings set dockAutoHide true
dms ipc call settings set dockUseOverlayLayer true
}
backup_if_exists() {
local target="$1"
[[ -e "$target" ]] || return 0
local backup_root
backup_root="$CONFIG_HOME/old2026_1/dotevo-backup-$(date +%Y%m%d-%H%M%S)"
local rel="${target#"$HOME"/}"
mkdir -p "$(dirname "$backup_root/$rel")"
mv "$target" "$backup_root/$rel"
}
clone_or_update_repo() {
local url="$1" dest="$2"
if [[ -d "$dest/.git" ]]; then
git -C "$dest" pull --ff-only
else
mkdir -p "$(dirname "$dest")"
git clone --depth 1 "$url" "$dest"
fi
}
run_component_picker() {
if [[ "${DOTEVO_NONINTERACTIVE:-0}" == "1" ]]; then
read -ra PICK_COMPONENTS <<< "${DOTEVO_COMPONENTS:?DOTEVO_COMPONENTS required in non-interactive mode}"
return 0
fi
local raw
while true; do
raw=$(whiptail --title "dotevo" --checklist \
"Choose what to set up (space to toggle, enter to confirm)" 15 60 2 \
Hyprland "Hyprland + DMS desktop rice" ON \
TerminalLook "zsh + prompt (Powerlevel10k or Kali)" OFF \
3>&1 1>&2 2>&3)
read -ra PICK_COMPONENTS <<< "$(echo "$raw" | tr -d '"')"
[[ "${#PICK_COMPONENTS[@]}" -gt 0 ]] && break
whiptail --title "dotevo" --msgbox "Pick at least one." 8 40
done
}
apply_hyprland() {
if ! check_arch; then
echo "dotevo: skipping Hyprland rice -- pacman not found"
return 0
fi
run_picker
if ! confirm_summary; then
echo "dotevo: Hyprland rice cancelled, nothing was changed."
return 0
fi
backup_if_exists "$CONFIG_HOME/hypr/hyprland.conf"
backup_if_exists "$CONFIG_HOME/hypr/extras.conf"
backup_if_exists "$CONFIG_HOME/xdg-terminals.list"
backup_if_exists "$LOCAL_BIN/capture-menu.sh"
backup_if_exists "$LOCAL_BIN/screenshot.sh"
backup_if_exists "$LOCAL_BIN/toggle-recording.sh"
# Sync + upgrade first. Without this, a local package database that's
# drifted from the mirrors can reference a package build that's already
# been rotated off every mirror (404 on install), and a bare `-Sy`
# without `-u` risks a partial upgrade -- unsupported on Arch and can
# break dependencies. `-Syu` is the only safe way to bring the database
# in sync before installing anything new.
sudo pacman -Syu --noconfirm
local pkg_list
pkg_list="$(build_package_list)"
# shellcheck disable=SC2086
sudo pacman -S --needed --noconfirm $pkg_list
mkdir -p "$CONFIG_HOME/hypr"
render_hyprland_conf "$REPO_DIR/hypr/hyprland.conf" > "$CONFIG_HOME/hypr/hyprland.conf"
build_extras_conf > "$CONFIG_HOME/hypr/extras.conf"
terminal_desktop_id "$PICK_TERMINAL" > "$CONFIG_HOME/xdg-terminals.list"
install_session_target
configure_dms_dock
if [[ " ${PICK_TOGGLES[*]} " == *" ScreenCapture "* ]]; then
mkdir -p "$LOCAL_BIN"
cp "$REPO_DIR/local-bin/capture-menu.sh" "$LOCAL_BIN/"
cp "$REPO_DIR/local-bin/screenshot.sh" "$LOCAL_BIN/"
cp "$REPO_DIR/local-bin/toggle-recording.sh" "$LOCAL_BIN/"
chmod +x "$LOCAL_BIN/capture-menu.sh" "$LOCAL_BIN/screenshot.sh" "$LOCAL_BIN/toggle-recording.sh"
fi
if [[ "$PICK_CLOUDSYNC" == "Syncthing" ]]; then
systemctl enable --now --user syncthing.service
fi
if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
hyprctl reload
else
echo "dotevo: log into Hyprland to see changes."
fi
}
run_terminal_look_picker() {
if [[ "${DOTEVO_NONINTERACTIVE:-0}" == "1" ]]; then
PICK_TERMINAL_LOOK="${DOTEVO_TERMINAL_LOOK:?DOTEVO_TERMINAL_LOOK required in non-interactive mode}"
return 0
fi
# shellcheck disable=SC2034
PICK_TERMINAL_LOOK=$(whiptail --title "dotevo" --radiolist \
"Choose your terminal look" 12 60 2 \
P10k "Powerlevel10k (your current setup)" ON \
Kali "Kali-style box prompt" OFF \
3>&1 1>&2 2>&3)
}
write_zshrc_p10k() {
local dest="$1"
cat > "$dest" <<'EOF'
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi
export ZSH="$HOME/.oh-my-zsh"
export ZSH_CUSTOM="$HOME/.dotevo-zsh-custom"
ZSH_THEME="powerlevel10k/powerlevel10k"
plugins=(git fzf zsh-autosuggestions zsh-syntax-highlighting zsh-history-substring-search)
source $ZSH/oh-my-zsh.sh
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
if command -v fzf >/dev/null 2>&1; then
eval "$(fzf --zsh)"
fi
EOF
}
apply_terminal_look_p10k() {
clone_or_update_repo "https://github.com/ohmyzsh/ohmyzsh.git" "$OMZ_DIR"
clone_or_update_repo "https://github.com/romkatv/powerlevel10k.git" "$ZSH_CUSTOM_DIR/themes/powerlevel10k"
clone_or_update_repo "https://github.com/zsh-users/zsh-autosuggestions.git" "$ZSH_CUSTOM_DIR/plugins/zsh-autosuggestions"
clone_or_update_repo "https://github.com/zsh-users/zsh-syntax-highlighting.git" "$ZSH_CUSTOM_DIR/plugins/zsh-syntax-highlighting"
clone_or_update_repo "https://github.com/zsh-users/zsh-history-substring-search.git" "$ZSH_CUSTOM_DIR/plugins/zsh-history-substring-search"
backup_if_exists "$HOME/.p10k.zsh"
cp "$REPO_DIR/terminal/p10k/.p10k.zsh" "$HOME/.p10k.zsh"
backup_if_exists "$HOME/.zshrc"
write_zshrc_p10k "$HOME/.zshrc"
}
write_zshrc_kali() {
local dest="$1"
cat > "$dest" <<'EOF'
HISTFILE=~/.zsh_history
HISTSIZE=10000
SAVEHIST=10000
setopt share_history hist_ignore_dups
source "$HOME/.dotevo/kali-prompt.zsh"
source "$HOME/.dotevo-zsh-custom/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh"
source "$HOME/.dotevo-zsh-custom/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh"
source "$HOME/.dotevo-zsh-custom/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh"
if command -v fzf >/dev/null 2>&1; then
eval "$(fzf --zsh)"
fi
EOF
}
apply_terminal_look_kali() {
clone_or_update_repo "https://github.com/zsh-users/zsh-autosuggestions.git" "$ZSH_CUSTOM_DIR/plugins/zsh-autosuggestions"
clone_or_update_repo "https://github.com/zsh-users/zsh-syntax-highlighting.git" "$ZSH_CUSTOM_DIR/plugins/zsh-syntax-highlighting"
clone_or_update_repo "https://github.com/zsh-users/zsh-history-substring-search.git" "$ZSH_CUSTOM_DIR/plugins/zsh-history-substring-search"
mkdir -p "$ASSETS_DIR"
backup_if_exists "$ASSETS_DIR/kali-prompt.zsh"
cp "$REPO_DIR/terminal/kali/prompt.zsh" "$ASSETS_DIR/kali-prompt.zsh"
backup_if_exists "$HOME/.zshrc"
write_zshrc_kali "$HOME/.zshrc"
}
chsh_to_zsh() {
local zsh_path
zsh_path="$(command -v zsh)"
if [[ "${SHELL:-}" != "$zsh_path" ]]; then
chsh -s "$zsh_path"
fi
}
apply_terminal_look() {
if ! install_terminal_prereqs; then
echo "dotevo: skipping Terminal look -- prerequisites unavailable"
return 0
fi
run_terminal_look_picker
case "$PICK_TERMINAL_LOOK" in
P10k) apply_terminal_look_p10k ;;
Kali) apply_terminal_look_kali ;;
*) echo "dotevo: unknown terminal look: $PICK_TERMINAL_LOOK" >&2; return 1 ;;
esac
chsh_to_zsh
}
main() {
ensure_repo_files
run_component_picker
if [[ " ${PICK_COMPONENTS[*]} " == *" Hyprland "* ]]; then
apply_hyprland
fi
if [[ " ${PICK_COMPONENTS[*]} " == *" TerminalLook "* ]]; then
apply_terminal_look
fi
echo "dotevo: done."
}
# `return` only succeeds at top level when the script is being sourced (as
# tests/install.bats does). Direct execution -- a real file, `bash -c "..."`,
# or `bash <(curl ...)` process substitution -- all fail `return`, so `main`
# runs. This is more robust here than comparing $BASH_SOURCE/$0: under
# `bash -c "$(curl ...)"` there's no backing file at all, so BASH_SOURCE is
# empty and $0 is just "bash" -- they'd never match even with a safe default.
if ! (return 0 2>/dev/null); then
main "$@"
fi