diff --git a/.bashrc b/.bashrc index e714b39..8cd888a 100644 --- a/.bashrc +++ b/.bashrc @@ -1,28 +1,57 @@ # -# ~/.bashrc +# ~/.bashrc — Interactive bash shell configuration +# +# This file is sourced for every non-login interactive bash session. +# Bash is the fallback shell on this system; zsh is the primary interactive +# shell. This file mirrors a core subset of .zshrc's aliases so the +# environment is consistent inside TTYs and distrobox containers. # -# If not running interactively, don't do anything +# Guard: only run for interactive shells. +# $- contains the set of active shell option flags; 'i' means interactive. +# If 'i' is absent, return immediately — this prevents errors when bash is +# invoked non-interactively (e.g., from scripts or scp file transfers). [[ $- != *i* ]] && return +# ─── Core aliases ────────────────────────────────────────────────────────────── + +# Colorize ls and grep output — file types and match highlights stand out visually. alias ls='ls --color=auto' alias grep='grep --color=auto' + +# $PS1: minimal prompt showing [user@host workingdir]$ +# This is the bash default; starship replaces it at the bottom of this file. +# Kept here as a safe fallback if starship fails to initialize. PS1='[\u@\h \W]\$ ' +# Detailed directory listing with ISO timestamps (YYYY-MM-DD HH:MM). +# ISO format is sortable and unambiguous compared to locale-default date strings. alias ll="ls -la --time-style=long-iso" -alias l="ll" +alias l="ll" # Short form for everyday use +# Quick parent-directory navigation — equivalent to 'cd ..' alias ..="cd .." +# Editor shortcuts: micro is a terminal text editor with intuitive keybindings +# (Ctrl+S to save, Ctrl+Q to quit), suitable for quick in-terminal edits. alias m="micro" -alias sm="sudo micro" +alias sm="sudo micro" # Elevated edit for system files owned by root -alias gita="git add ." -alias gitc="git commit -m" -alias gitp="git push" +# ─── Git shortcuts ───────────────────────────────────────────────────────────── +alias gita="git add ." # Stage all changes in the current working tree +alias gitc="git commit -m" # Commit with an inline message, e.g.: gitc "fix: typo" +alias gitp="git push" # Push current branch to its remote tracking branch + +# curl-based weather lookup — wttr.in returns ANSI-art weather for the terminal. +# The trailing slash with no city causes wttr.in to auto-detect location via IP. alias weather="curl https://wttr.in/" +# ─── gitf(): stage → commit → push in a single command ─────────────────────── +# Usage: gitf "commit message" +# Collapses the three-step add/commit/push workflow for quick, minor commits. +# The $1 guard prevents an empty commit message (which git would reject anyway, +# but this gives a cleaner user-facing error). function gitf() { if [ -z $1 ]; then @@ -35,31 +64,66 @@ function gitf() { fi } +# ─── Kitty terminal integration ──────────────────────────────────────────────── + +# icat: display images inline in the terminal using Kitty's icat kitten. +# Only works inside a Kitty terminal; other terminals will display an error. alias icat="kitten icat" + +# Two ways to clear the screen — cls is familiar muscle memory from Windows cmd. alias cls="clear" +# Kitten SSH: wraps ssh with Kitty's ssh kitten so the remote shell inherits +# correct terminfo entries and Kitty protocol extensions (OSC 52 clipboard, +# proper backspace, 24-bit color, etc.) without manual TERM setup on the server. alias ssh="kitten ssh" -alias ssk="kitten ssh" +alias ssk="kitten ssh" # Typo-resilient duplicate +# ─── Hardware / misc ─────────────────────────────────────────────────────────── + +# tio: terminal I/O program for serial port communication (embedded dev, etc.). +# '-a latest' attaches to the most recently enumerated serial device. +# sudo is required because /dev/ttyUSB* and /dev/ttyACM* are typically owned +# by the 'uucp' or 'dialout' group and not accessible as a regular user by default. alias serial="sudo tio -a latest" +# t: alias for 'wd' (warp directory) — jump to named directory bookmarks. +# Bookmarks are set with: wd add and jumped to with: t alias t="wd" +# Filtered listing — pipe a long-format listing through grep for fast file search. alias lgrep="l | grep" -alias lg="lgrep" +alias lg="lgrep" # Short form +# ─── y(): yazi file manager with shell directory-change on exit ───────────────── +# Yazi is a terminal file manager. Without this wrapper, any directory you +# navigate to inside yazi is lost when it exits (it ran in a subshell). +# This wrapper passes --cwd-file to yazi; yazi writes its final CWD there, +# and the wrapper reads it to cd the parent shell into that directory. function y() { - local tmp="$(mktemp -t "yazi-cwd.XXXXXX")" cwd - yazi "$@" --cwd-file="$tmp" + local tmp="$(mktemp -t "yazi-cwd.XXXXXX")" cwd # temp file to capture yazi's last dir + yazi "$@" --cwd-file="$tmp" # run yazi; it writes final dir to $tmp if cwd="$(command cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then - builtin cd -- "$cwd" + builtin cd -- "$cwd" # apply the directory change to this shell fi - rm -f -- "$tmp" + rm -f -- "$tmp" # clean up the temp file } +# ─── Prompt & startup ────────────────────────────────────────────────────────── + +# Initialize starship for bash. Starship is a cross-shell prompt engine configured +# in ~/Dotfiles/starship.toml. It replaces the plain PS1 above with a rich, +# segment-based prompt showing user, path, git branch/status, language versions, +# and the current time — all styled with the CyberQueer color palette. eval "$(starship init bash)" -fastfetch --logo-color-1 red --logo-color-2 red --color red -l ~/Dotfiles/pin.txt +# Display system info on shell start using fastfetch with the custom ASCII logo. +# Colors are forced to red to match the CyberQueer theme branding. +fastfetch --logo-color-1 red --logo-color-2 red --color red -l ~/Dotfiles/pin.txt + +# Load the Rust/Cargo environment — primarily adds ~/.cargo/bin to PATH so that +# binaries installed via `cargo install` (e.g., ripgrep, bat, starship itself) +# are immediately accessible in this session. . "$HOME/.cargo/env" diff --git a/.vimrc b/.vimrc index 5956e78..808ab34 100644 --- a/.vimrc +++ b/.vimrc @@ -1,2 +1,19 @@ +" ~/.vimrc — Vim configuration +" +" This is a minimal vimrc used for legacy Vim (when nvim is not available). +" The primary editor on this system is Neovim; Vim is only kept as a fallback. +" The config delegates all theme setup to a shared theme file so that both +" Vim and Neovim pick up the same CyberQueer color scheme. +" + +" Load the shared theme settings file from the dotfiles repo. +" This file sets options like 'set termguicolors', 'set background=dark', etc. +" Sourcing it here keeps the Vim and Neovim theme configurations in sync +" without duplicating settings in two places. source ~/Dotfiles/vim/theme + +" Apply the CyberQueer color scheme. +" cyberqueer.nvim is stored in the nvim color directory but is also compatible +" with Vim when placed in ~/.vim/colors/ or when Vim's runtimepath includes +" the nvim config directory. colorscheme cyberqueer.nvim diff --git a/.zshrc b/.zshrc index 8a5625c..8d6cefd 100755 --- a/.zshrc +++ b/.zshrc @@ -1,3 +1,23 @@ +# ~/.zshrc — Primary interactive Zsh shell configuration +# +# This is the main shell config for all interactive zsh sessions on this system. +# It wraps Oh My Zsh (for plugin management and completions), then loads a wide +# set of aliases, utility functions, and environment variables. +# +# Structure: +# 1. GNU grep detection guard (activates OMZ block only on GNU/Linux) +# 2. Oh My Zsh setup and plugin list +# 3. Editor and terminal environment variables +# 4. Aliases for navigation, editors, git, distrobox, python, etc. +# 5. Shell functions: gitf, dbc, dbt, y, n, calc +# 6. Starship prompt init +# 7. PATH additions (spicetify, nvm, pipx) +# + +# ─── GNU grep guard ──────────────────────────────────────────────────────────── +# Detects whether the system grep is GNU grep (Linux) by checking its --help output. +# If it is GNU grep, the entire Oh My Zsh block runs. On non-GNU systems (macOS BSD, +# for example) this block would be skipped, preventing GNU-only flag errors. GREPOUTPUT=$( grep --help 2>&1 ) RESULT=$( echo $GREPOUTPUT | grep gnu ) if [ "${RESULT}" != "" ]; then @@ -5,6 +25,9 @@ if [ "${RESULT}" != "" ]; then # If you come from bash you might have to change your $PATH. # export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:$PATH +# ─── Oh My Zsh ───────────────────────────────────────────────────────────────── +# OMZ is a community framework for managing zsh configuration and plugins. +# It provides plugin loading, theme support, and helper functions. # Path to your Oh My Zsh installation. export ZSH="$HOME/.oh-my-zsh" @@ -12,8 +35,15 @@ export ZSH="$HOME/.oh-my-zsh" # load a random theme each time Oh My Zsh is loaded, in which case, # to know which specific one was loaded, run: echo $RANDOM_THEME # See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes +# robbyrussell is the classic OMZ default; starship overrides it at the bottom. ZSH_THEME="robbyrussell" + +# WALK_MAIN_COLOR: accent color for the 'walk' interactive directory browser. +# Matches the CyberQueer electric-violet secondary accent color. export WALK_MAIN_COLOR="#5018DD" + +# lk(): navigate to a directory selected interactively via 'walk' (a fuzzy dir browser). +# walk prints the selected path to stdout; this function cd's into it. function lk { cd "$(walk "$@")" } @@ -73,30 +103,35 @@ function lk { # Would you like to use another custom folder than $ZSH/custom? # ZSH_CUSTOM=/path/to/new-custom-folder +# ─── OMZ Plugin list ─────────────────────────────────────────────────────────── +# Plugins add completions, keybindings, and helper functions. +# They are loaded from $ZSH/plugins/ (built-in) or $ZSH_CUSTOM/plugins/ (installed). +# Too many plugins slow shell startup — only keep what's actively used. # Which plugins would you like to load? # Standard plugins can be found in $ZSH/plugins/ # Custom plugins may be added to $ZSH_CUSTOM/plugins/ # Example format: plugins=(rails git textmate ruby lighthouse) # Add wisely, as too many plugins slow down shell startup. -plugins=( git - zsh-syntax-highlighting - zsh-autosuggestions - wd - archlinux - git - git-auto-fetch - nmap - perms - zsh-interactive-cd - zsh-navigation-tools - z - ufw - web-search - timer - sudo - taskwarrior +plugins=( git # git aliases (gst, gco, ga, gp, etc.) + zsh-syntax-highlighting # colorizes valid commands green, errors red (installed via zshplugins.sh) + zsh-autosuggestions # ghost-text completions based on command history (installed via zshplugins.sh) + wd # warp directory: bookmark dirs with 'wd add ', jump with 'wd ' + archlinux # pacman/yay shortcuts (pacin, pacrem, etc.) + git # duplicate entry — harmless but redundant + git-auto-fetch # automatically runs 'git fetch' in the background in git repos + nmap # completions for nmap + perms # helper functions for setting file permissions + zsh-interactive-cd # interactive fuzzy cd using fzf + zsh-navigation-tools # panel-style history/dir navigation widgets + z # jump to frecent directories: 'z projectname' jumps to most visited match + ufw # completions for ufw (Uncomplicated Firewall) + web-search # open web searches from the terminal: 'google foo' + timer # shows command execution time in the prompt for long-running commands + sudo # press Esc twice to prepend 'sudo' to the previous command + taskwarrior # completions for the taskwarrior task manager ) +# Load Oh My Zsh — sources all plugins, the theme, and OMZ helper functions. source $ZSH/oh-my-zsh.sh # User configuration @@ -127,40 +162,73 @@ source $ZSH/oh-my-zsh.sh # Example aliases # alias zshconfig="mate ~/.zshrc" # alias ohmyzsh="mate ~/.oh-my-zsh" -fi +fi # end of the GNU grep guard block + +# ─── Default editor ──────────────────────────────────────────────────────────── +# nvim is the default editor for git commits, sudoedit, cron, etc. +# This is set outside the guard block so it always applies regardless of grep detection. export EDITOR='nvim' + +# ─── Startup system info ─────────────────────────────────────────────────────── +# Show fastfetch system info on every new interactive shell, except inside +# distrobox containers. CONTAINER_ID is set by distrobox on entry; checking +# for it prevents a duplicate/confusing fetch banner inside containers. if [ -z "${CONTAINER_ID}" ]; then - fastfetch --logo-color-1 red --logo-color-2 red --color red -l ~/Dotfiles/resources/pin.txt + fastfetch --logo-color-1 red --logo-color-2 red --color red -l ~/Dotfiles/resources/pin.txt fi +# Force 256-color + true-color mode. xterm-256color is widely recognized by +# remote servers and terminal multiplexers; it ensures color-capable apps +# (vim, tmux, etc.) enable their full color support even if COLORTERM isn't set. export TERM=xterm-256color +# ─── Core aliases ────────────────────────────────────────────────────────────── + +# Colorize ls and grep output for readability. alias ls='ls --color=auto' alias grep='grep --color=auto' + +# Detailed listing with ISO timestamps — unambiguous YYYY-MM-DD HH:MM format. alias ll="ls -la --time-style=long-iso" -alias l="ll" +alias l="ll" # Short form + +# lsblk with extra columns: filesystem type, drive model, and UUID. +# Useful for identifying disks, partitions, and their UUIDs for /etc/fstab. alias ldsk="lsblk -o+FSTYPE,MODEL,UUID" +# Quick parent-directory jump. alias ..="cd .." -alias m="micro" -alias sm="sudo micro" -alias e="edit-in-kitty" -alias et="edit-in-kitty --type=tab" -alias ek="edit-in-kitty --type=window" -alias ew="kitty --detach micro" +# ─── Editor aliases ──────────────────────────────────────────────────────────── + +alias m="micro" # micro: simple terminal editor, good for quick config edits +alias sm="sudo micro" # sudo micro: edit system-owned files + +# edit-in-kitty: open a file in micro inside a Kitty terminal panel/tab. +# These variants control where the new editor window appears. +alias e="edit-in-kitty" # default placement (new OS window) +alias et="edit-in-kitty --type=tab" # open in a new Kitty tab +alias ek="edit-in-kitty --type=window" # open in a new Kitty split window +alias ew="kitty --detach micro" # detach: open in a completely new Kitty window, non-blocking + +# ex: open the current directory in Thunar (GTK file manager). +# Useful for quick GUI access without leaving the terminal. alias ex="thunar ." +# fast-ssh: a wrapper around ssh with fuzzy host selection from ~/.ssh/config. alias fs="fast-ssh" -alias gita="git add ." -alias gitc="git commit -m" -alias gitp="git push" -alias gitg="git pull" -alias gitfuck="git commit --amend -m" +# ─── Git aliases ─────────────────────────────────────────────────────────────── +alias gita="git add ." # Stage all changes +alias gitc="git commit -m" # Commit with message +alias gitp="git push" # Push to remote +alias gitg="git pull" # Pull from remote (g for 'get') +alias gitfuck="git commit --amend -m" # Fix the last commit message without creating a new commit + +# gitf(): one-shot add + commit + push. Guards against empty commit messages. function gitf() { if [ -z $1 ]; then @@ -172,80 +240,145 @@ function gitf() { git push fi } -alias icat="kitty +kitten icat" -alias cls="clear" -alias c="cls" +# icat: display images inline in Kitty using the Kitty graphics protocol. +# Note: in zsh the kitten is invoked via 'kitty +kitten' rather than the +# standalone 'kitten' binary used in .bashrc. +alias icat="kitty +kitten icat" + +alias cls="clear" # Windows-style clear alias +alias c="cls" # Ultra-short clear + +# kitten ssh: wraps ssh with Kitty's ssh kitten for proper terminfo propagation. alias ks="kitten ssh" #function s() { # alacritty -e sh -c "ssh $1" & #} +# (Legacy: used to open ssh in an alacritty window; replaced by Kitty-native approach) +# tio: serial port terminal. '-a latest' picks the most-recently plugged device. alias serial="sudo tio -a latest" +# wd: warp directory bookmarks. 't' as an alias makes it one keystroke. alias t="wd" +# Terminal weather report via wttr.in (ANSI art, no browser needed). alias weather="curl https://wttr.in/" +# Pipe detailed listing through grep for quick in-directory file search. alias lgrep="l | grep" alias lg="lgrep" +# treegrep / tg: list the entire directory tree recursively and grep in the output. +# Useful for finding files by partial name across deep hierarchies. +# tree -fi: full-path (-f), no indentation graph (-i) — clean output for grepping. alias treegrep="tree -fi | grep" alias tg="treegrep" +# cpwd: copy the current working directory path to the Wayland clipboard. +# wl-copy is the Wayland equivalent of xclip/xsel. alias cpwd="pwd | wl-copy" + +# v/vi: use neovim as the primary $VISUAL editor instead of legacy vim. alias v="nvim" -alias sv="sudoedit" -alias dbe="distrobox enter" -alias dbrm="distrobox rm" -alias dbs="distrobox stop" - -alias fig="find | grep" - -alias mfetch="fastfetch --kitty-icat ~/Pictures/profile.jpg --logo-width 30 --logo-height 30 --color red" alias vi="nvim" -alias pyr="python" -alias pynowr='python -W "ignore"' +# sv: sudoedit — opens a file as root in $SUDO_EDITOR (nvim) using a safe +# temp-file mechanism, avoiding the security risk of running nvim as root directly. +alias sv="sudoedit" +# ─── Distrobox aliases ───────────────────────────────────────────────────────── +# Distrobox runs OCI containers that share the host's home directory, so tools +# from other distros can be used transparently alongside Arch packages. + +alias dbe="distrobox enter" # Enter (attach to shell of) a running container +alias dbrm="distrobox rm" # Remove a stopped container +alias dbs="distrobox stop" # Stop a running container + +# fig: find files anywhere in the tree and filter by name using grep. +# Equivalent to `find | grep`, but faster to type. +alias fig="find | grep" + +# mfetch: fastfetch with a profile photo displayed via the Kitty image protocol. +# --logo-width/height control the inline image dimensions in terminal cells. +alias mfetch="fastfetch --kitty-icat ~/Pictures/profile.jpg --logo-width 30 --logo-height 30 --color red" + +# Python shortcuts +alias pyr="python" # Shorter form for Python 3 interpreter +alias pynowr='python -W "ignore"' # Run Python suppressing all warnings (useful for noisy scripts) + +# ─── dbc(): create a distrobox container ────────────────────────────────────── +# Usage: dbc +# e.g.: dbc ubuntu mybox → pulls ubuntu:latest and names it 'mybox' function dbc() { distrobox create --image "${1}:latest" --name $2 } + +# ─── dbt(): create and immediately enter a distrobox container ──────────────── +# Usage: dbt +# The container name matches the image name for simplicity. function dbt() { distrobox create --image "${1}:latest" --name $1 distrobox enter $1 } + +# ─── y(): yazi file manager with persistent directory change ───────────────── +# Without this wrapper, directory navigation in yazi is lost when it exits +# because yazi runs in a subshell. The --cwd-file mechanism lets yazi report +# its final directory; this function reads it and cd's the parent shell there. function y() { - local tmp="$(mktemp -t "yazi-cwd.XXXXXX")" cwd - yazi "$@" --cwd-file="$tmp" + local tmp="$(mktemp -t "yazi-cwd.XXXXXX")" cwd # temp file for yazi's final cwd + yazi "$@" --cwd-file="$tmp" # run yazi with cwd reporting enabled if cwd="$(command cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then - builtin cd -- "$cwd" + builtin cd -- "$cwd" # apply the directory change to the parent shell fi - rm -f -- "$tmp" + rm -f -- "$tmp" # clean up temp file } +# ─── n(): cd + show directory listing ───────────────────────────────────────── +# A quick navigation helper: cd into $1, print the absolute path, and list contents. +# Useful when you want a visual confirmation of where you landed. function n() { cd $1 - echo  $(pwd) + echo $(pwd) ls -la } +# ─── calc(): quick arithmetic evaluator ─────────────────────────────────────── +# Passes its arguments to bc with the -l flag (math library for floating point). +# Usage: calc "2.5 * 3 + 1" → prints 8.5 +# printf '%s\n' "$@" handles spaces and multiple expressions correctly. calc() { printf "%s\n" "$@" | bc -l; } +# ─── Prompt ──────────────────────────────────────────────────────────────────── +# Initialize starship for zsh. Starship reads ~/Dotfiles/starship.toml and +# generates a rich prompt segment showing user, path, git state, tool versions, +# and the current time — styled with the CyberQueer red/pink/violet palette. eval "$(starship init zsh)" +# ─── PATH additions ──────────────────────────────────────────────────────────── + +# Spicetify: Spotify theme/extension manager. Its CLI lives in ~/.spicetify. export PATH=$PATH:$HOME/.spicetify +# NVM (Node Version Manager): manages multiple Node.js versions. +# The conditional [ -s ... ] checks that the file exists and is non-empty before sourcing, +# so missing nvm installations don't error out silently. export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion +# SUDO_EDITOR: the editor invoked by sudoedit / sudo -e. +# Using nvim here instead of vi/nano keeps the editing experience consistent. export SUDO_EDITOR=/usr/bin/nvim # Created by `pipx` on 2025-05-12 08:58:28 +# pipx installs Python CLI tools in isolated environments under ~/.local/bin. +# The two lines handle both the old tilde-literal form and the expanded $HOME form; +# the second one takes precedence in PATH (higher priority). export PATH="$PATH:~/.local/bin" export PATH="$HOME/.local/bin:$PATH" diff --git a/clamav/clamd.conf b/clamav/clamd.conf index 6edfc33..a201643 100644 --- a/clamav/clamd.conf +++ b/clamav/clamd.conf @@ -110,12 +110,15 @@ # A TCP port number the daemon will listen on. # Default: disabled +# Exposing a TCP socket lets local tools like clamdscan and virus scanners +# contact clamd without needing a shared Unix socket path. TCPSocket 3310 # By default clamd binds to INADDR_ANY. # This option allows you to restrict the TCP address and provide # some degree of protection from the outside world. # Default: disabled +# Binding only to localhost prevents external hosts from submitting files to clamd. TCPAddr localhost # Maximum length the queue of pending connections may grow to. @@ -762,21 +765,30 @@ TCPAddr localhost # Deprecated option to alert on encrypted archives and documents (encrypted .zip, .7zip, .rar, .pdf). # Default: no #ArchiveBlockEncrypted no -# Log time with each message. + +# ── Active (non-default) settings ────────────────────────────────────────────── + +# Timestamp every log entry so events can be correlated with system logs. # Default: no LogTime yes -# Log additional information about the infected file, such as its -# size and hash, together with the virus name. +# Include file size and MD5/SHA hashes in threat reports — useful for +# submitting false-positive or false-negative reports to ClamAV. ExtendedDetectionInfo yes -# Run as another user (clamd must be started by root for this option to work) +# Drop privileges to the 'clamav' system user after startup. +# clamd must be started as root for this directive to take effect. # Default: don't drop privileges User clamav -# Exclude the UID of the scanner itself from checking, to prevent loops +# Exclude file-access events triggered by the clamav user itself from +# on-access scanning — prevents the scanner from re-scanning its own reads +# and entering an infinite loop. OnAccessExcludeUname clamav +# Watch the entire filesystem for on-access events via fanotify. +# Note: OnAccessPrevention cannot block events at the mount-point level, +# only when using OnAccessIncludePath (see comment below). # Set the mount point where to recursively perform the scan, # this could be every path or multiple path (one line for path) OnAccessMountPath / @@ -788,41 +800,59 @@ OnAccessMountPath / # It works with OnAccessIncludePath, as long as /usr and /etc are not included. # Including /var while activating prevention is also not recommended, because # this would slow down package installation by a factor of 1000. +# +# Prevention is disabled here because OnAccessMountPath / is used; +# enabling it with a full-filesystem mount path causes severe slowdowns. OnAccessPrevention no +# Also trigger scans on inotify events (create/move/rename) in addition to +# open events — catches malware dropped by scripts without being read first. # Perform scans on newly created, moved, or renamed files OnAccessExtraScanning yes # Optionallyexclude root-owned processes # OnAccessExcludeRootUID true +# Raise the default recursion cap (15) to handle deeply nested archives or +# directory trees; set high deliberately for comprehensive scanning. # Maximum depth directories are scanned at. # Default: 15 MaxDirectoryRecursion 200 +# TCP socket commented out here — the active TCPSocket/TCPAddr declarations +# appear earlier in this file (generated section). #TCPAddr localhost #TCPSocket 3310 -DetectPUA yes -HeuristicAlerts yes -ScanPE yes -ScanELF yes -ScanOLE2 yes -ScanPDF yes -ScanSWF yes -ScanXMLDOCS yes -ScanHWP3 yes -ScanOneNote yes -ScanMail yes -ScanHTML yes -ScanArchive yes -Bytecode yes -AlertBrokenExecutables yes -AlertBrokenMedia yes -AlertEncrypted yes -AlertEncryptedArchive yes -AlertEncryptedDoc yes -AlertOLE2Macros yes -AlertPartitionIntersection yes +# ── Enable all major scan categories ─────────────────────────────────────────── +# Each directive below enables a specific scanner module. All default to "yes" +# in recent ClamAV, but are listed explicitly to document intent and prevent +# a future default change from silently disabling a scanner. +DetectPUA yes # Detect Potentially Unwanted Applications (adware, tools) +HeuristicAlerts yes # Enable heuristic (behavior-based) detection algorithms +ScanPE yes # Scan Windows PE executables (required for UPX/FSG decompression) +ScanELF yes # Scan Linux ELF executables +ScanOLE2 yes # Scan Microsoft Office documents and MSI files +ScanPDF yes # Decode and scan PDF files +ScanSWF yes # Scan Adobe Flash (SWF) files +ScanXMLDOCS yes # Scan XML-based document formats (docx, xlsx, etc.) +ScanHWP3 yes # Scan Korean HWP3 word-processor files +ScanOneNote yes # Scan Microsoft OneNote files +ScanMail yes # Parse and scan email messages (RFC 822/MIME) +ScanHTML yes # Normalize and scan HTML/JavaScript +ScanArchive yes # Unpack and scan inside ZIP, RAR, TAR, etc. +Bytecode yes # Execute ClamAV bytecode signatures (required for many modern detections) +AlertBrokenExecutables yes # Flag malformed PE/ELF files (often indicate deliberate obfuscation) +AlertBrokenMedia yes # Flag malformed images used to exploit media parsers +AlertEncrypted yes # Alert on any encrypted container +AlertEncryptedArchive yes # Alert specifically on encrypted ZIP/7z/RAR +AlertEncryptedDoc yes # Alert specifically on password-protected PDFs +AlertOLE2Macros yes # Alert on Office files containing VBA macros +AlertPartitionIntersection yes # Flag raw disk images with overlapping partitions + +# Script to execute when a virus is detected. +# See /etc/clamav/virus-event.bash for the notification/logging logic. +# The script receives CLAM_VIRUSEVENT_FILENAME and CLAM_VIRUSEVENT_VIRUSNAME +# as environment variables (NOT via %f/%v — those were disabled for security). VirusEvent /etc/clamav/virus-event.bash diff --git a/clamav/install-clam-onaccess.sh b/clamav/install-clam-onaccess.sh index bc82cd3..a555cf9 100755 --- a/clamav/install-clam-onaccess.sh +++ b/clamav/install-clam-onaccess.sh @@ -1,14 +1,49 @@ #!/bin/bash +# One-shot installer for ClamAV with on-access scanning (clamonacc). +# Run as a regular user — individual commands use sudo where root is required. +# Requires: clamav package, and files in this directory (./clamav-sudoer etc.). + +# Install the clamav package (provides clamd, clamonacc, freshclam, clamdscan). sudo pacman -S clamav +# Deploy the sudoers drop-in that allows the clamav user to run freshclam +# without a password — needed for automated signature updates. +# -fr: force-overwrite + recursive (safe for single files too). sudo cp -fr ./clamav-sudoer /etc/sudoers.d/clamav + +# Deploy the custom daemon config (see clamd.conf in this directory for details +# on on-access mount path, scan settings, and VirusEvent hook). sudo cp -fr ./clamd.conf /etc/clamav/clamd.conf + +# Deploy the virus-event script that clamd calls when a threat is detected; +# typically sends a desktop notification or logs the event. sudo cp -fr ./virus-event.bash /etc/clamav/virus-event.bash + +# Deploy the custom systemd service unit for clamonacc (the on-access daemon). +# Placed in /usr/lib/systemd/system/ so it survives package upgrades without +# manual intervention (unit files in /etc/systemd/ take precedence but are +# overwritten by the package on reinstall). sudo cp -fr ./clamav-clamonacc.service /usr/lib/systemd/system/clamav-clamonacc.service -#aa-complain clamd + +# aa-complain clamd +# (AppArmor complain-mode left commented out — uncomment if AppArmor is active +# and clamonacc is blocked; complain mode logs denials without enforcing them.) + +# Enable all four related units at boot: +# clamav-clamonacc : on-access real-time scanner (requires clamd to be up first) +# clamav-daemon : the clamd background scan service +# clamav-freshclam : daily signature update service +# clamav-freshclam-once.timer : one-shot timer that fires freshclam at boot sudo systemctl enable clamav-clamonacc.service -sudo systemctl enable clamav-daemon.service -sudo systemctl enable clamav-freshclam.service -sudo systemctl enable clamav-freshclam-once.timer +sudo systemctl enable clamav-daemon.service +sudo systemctl enable clamav-freshclam.service +sudo systemctl enable clamav-freshclam-once.timer + +# Perform an initial signature database download before the first boot into clamd. +# Without this, clamd will refuse to start because /var/lib/clamav is empty. freshclam + +# A reboot is required for on-access scanning to take full effect — the fanotify +# kernel API used by clamonacc needs a clean mount namespace with the watcher +# registered from the start. reboot diff --git a/colors.conf b/colors.conf index dff56a2..2601ce1 100644 --- a/colors.conf +++ b/colors.conf @@ -1,9 +1,25 @@ -# CyberQueer Theme Colors +# colors.conf — CyberQueer Theme Color Palette +# +# Central definition of the five named colors that make up the CyberQueer theme. +# All other config files (hyprland, kitty, waybar, yazi, starship, etc.) reference +# these hex values. When you change a color here, run apply-theme.sh to propagate +# the change to every deployed config file at once. +# +# Format rules (enforced by apply-theme.sh's parse_colors function): +# - Bare 6-digit hex values only — NO # prefix, NO quotes +# - Inline comments after the value are allowed (stripped during parsing) +# - Keys must be exact: COLOR_TEXT, COLOR_BG, COLOR_HIGHLIGHT, COLOR_DARK, COLOR_RED +# +# Usage: +# Edit values below, then run: ~/Dotfiles/apply-theme.sh +# The script compares these values against the last-applied state +# (~/.config/colors.state) and only replaces changed colors. +# # Edit values, then run: ~/Dotfiles/apply-theme.sh # Bare 6-digit hex values only (no # prefix) -COLOR_TEXT=D6ABAB # Quasi-White Text — foreground, labels -COLOR_BG=1A1A1A # Gray Background — base surface -COLOR_HIGHLIGHT=E40046 # Hot Pink Accent — primary accent, active borders -COLOR_DARK=5018DD # Electric Violet — secondary accent, inactive borders -COLOR_RED=F50505 # Red Hi-vis — danger, alerts +COLOR_TEXT=D6ABAB # Quasi-White Text — foreground, labels; slightly warm/muted to reduce eye strain +COLOR_BG=1A1A1A # Gray Background — near-black base surface; dark enough to make colors pop +COLOR_HIGHLIGHT=E40046 # Hot Pink Accent — primary accent used for active borders, selections, and highlights +COLOR_DARK=5018DD # Electric Violet — secondary accent used for inactive borders and informational segments +COLOR_RED=F50505 # Red Hi-vis — danger color for errors, alerts, and high-visibility UI elements diff --git a/create-webapp.sh b/create-webapp.sh index 519125a..68f33d5 100755 --- a/create-webapp.sh +++ b/create-webapp.sh @@ -17,13 +17,20 @@ usage() { URL="$1" CUSTOM_NAME="${2:-}" -# Ensure scheme +# Prepend https:// if the user omitted the scheme, so curl/grep work correctly. [[ "$URL" != http://* && "$URL" != https://* ]] && URL="https://$URL" +# Extract just the origin (scheme + host) — needed as a base for resolving +# relative favicon URLs later (e.g. "/icons/logo.png" → "https://example.com/icons/logo.png"). BASE_URL=$(echo "$URL" | grep -oE 'https?://[^/]+') + +# Strip scheme and optional "www." to get a clean domain used as a fallback name. DOMAIN=$(echo "$BASE_URL" | sed -E 's|https?://(www\.)?||') echo "Fetching: $URL" +# -sL : silent + follow redirects. -A : spoof a browser User-Agent so sites +# don't return a stripped mobile/bot page with no or <link> tags. +# The "|| echo ''" prevents pipefail from killing the script on network errors. PAGE_HTML=$(curl -sL --max-time 15 \ -A "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \ "$URL" 2>/dev/null || echo "") @@ -32,6 +39,8 @@ PAGE_HTML=$(curl -sL --max-time 15 \ if [[ -n "$CUSTOM_NAME" ]]; then APP_NAME="$CUSTOM_NAME" else + # Python inline: parse the <title> tag and HTML-unescape entities like & + # so the .desktop Name field looks clean. Falls back to DOMAIN on failure. APP_NAME=$(echo "$PAGE_HTML" | python3 -c ' import sys, re, html as html_mod content = sys.stdin.read() @@ -41,11 +50,18 @@ print(html_mod.unescape(m.group(1).strip()) if m else "", end="") APP_NAME="${APP_NAME:-$DOMAIN}" fi -# Safe identifier for filenames / WM_CLASS +# Build a filesystem-safe identifier from the app name. +# tr '[:upper:]' '[:lower:]' : lowercase everything +# tr -cs 'a-z0-9' '-' : replace any run of non-alphanumeric chars with '-' +# sed 's/^-*//;s/-*$//' : trim leading/trailing hyphens +# Used for both the .desktop filename and the StartupWMClass (WM grouping key). SAFE_ID=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-*//;s/-*$//') [[ -z "$SAFE_ID" ]] && SAFE_ID="$DOMAIN" -# Find best favicon from HTML link tags (largest declared size wins) +# Find the best favicon declared in <link rel="icon"> tags. +# The Python snippet ranks candidates by their declared pixel size (sizes="NxN") +# and picks the largest — "any" is treated as 999 to rank SVG/vector icons highest. +# urljoin resolves relative paths against BASE_URL, matching browser behavior. FAVICON_URL=$(echo "$PAGE_HTML" | python3 -c ' import sys, re from urllib.parse import urljoin @@ -76,24 +92,31 @@ if candidates: print(urljoin(base, candidates[0][1]), end="") ' "$BASE_URL" 2>/dev/null || true) +# Fall back to the conventional /favicon.ico path when no <link> tag was found. FAVICON_URL="${FAVICON_URL:-$BASE_URL/favicon.ico}" echo "Favicon: $FAVICON_URL" -# Download icon +# Store webapp icons separately from system icons to keep ~/.local/share/icons clean. ICON_DIR="$HOME/.local/share/icons/webapps" mkdir -p "$ICON_DIR" +# Use a temp file so a failed download doesn't leave a partial file at the final path. TMP=$(mktemp /tmp/webapp-icon.XXXXXX) trap 'rm -f "$TMP"' EXIT +# Default: fall through to the chromium themed icon if download/convert fails. ICON_PATH="chromium" # fallback if curl -sL --max-time 10 -A "Mozilla/5.0" -o "$TMP" "$FAVICON_URL" && [[ -s "$TMP" ]]; then + # Prefer ImageMagick: convert normalizes the format (handles multi-frame .ico + # via [0]) and caps the size at 128x128 without upscaling (\> flag). if command -v convert &>/dev/null \ && convert "${TMP}[0]" -resize 128x128\> "${ICON_DIR}/${SAFE_ID}.png" 2>/dev/null; then ICON_PATH="${ICON_DIR}/${SAFE_ID}.png" echo "Icon: $ICON_PATH (PNG via ImageMagick)" else + # Fallback: keep the file as-is, just fix the .ico MIME type alias so the + # extension matches what XDG icon loaders expect. MIME=$(file -b --mime-type "$TMP") EXT="${MIME##*/}" [[ "$EXT" == "x-icon" || "$EXT" == "vnd.microsoft.icon" ]] && EXT="ico" @@ -105,7 +128,9 @@ else echo "Warning: could not fetch favicon — using chromium default icon" fi -# Write .desktop file +# Write .desktop file per freedesktop.org Desktop Entry Specification. +# chromium --app=<url> opens the site in an app window (no address bar/tabs), +# making it feel like a native application. DESKTOP_DIR="$HOME/.local/share/applications" mkdir -p "$DESKTOP_DIR" DESKTOP_FILE="${DESKTOP_DIR}/webapp-${SAFE_ID}.desktop" @@ -122,6 +147,7 @@ Categories=Network;WebBrowser; StartupWMClass=${SAFE_ID} DESKTOP +# Mark executable so the file manager and XDG launchers treat it as launchable. chmod +x "$DESKTOP_FILE" echo diff --git a/decrypt.sh b/decrypt.sh index 5239e47..3bd8592 100755 --- a/decrypt.sh +++ b/decrypt.sh @@ -1,4 +1,11 @@ #!/usr/bin/env bash +# Decrypt a base64-encoded AES-256-CBC ciphertext produced by encrypt.sh. +# Usage: decrypt.sh <ciphertext> <passphrase> +# -d : decrypt mode (reverse of encryption) +# -a : base64-decode the input first; must mirror the -a used at encrypt time +# -pbkdf2 : derive the symmetric key via PBKDF2 rather than the deprecated EVP_BytesToKey; +# openssl will error if this flag doesn't match what was used to encrypt +# -pass pass : accept the passphrase directly from the CLI argument $2 echo $1 | openssl aes-256-cbc -d -a -pbkdf2 -pass pass:$2 diff --git a/desktopenvs/hyprland/hypr/application-style.conf b/desktopenvs/hyprland/hypr/application-style.conf index e69de29..eed3b61 100644 --- a/desktopenvs/hyprland/hypr/application-style.conf +++ b/desktopenvs/hyprland/hypr/application-style.conf @@ -0,0 +1,61 @@ +# ============================================================================ +# application-style.conf — hyprtoolkit / QT application colour palette +# +# Defines the named colour tokens for hyprtoolkit-aware applications so that +# QT apps respect the same cyberqueer colour palette used in eww, hyprlock, +# and GTK. Values here match the border colours, eww SCSS variables, and +# hyprlock label colours throughout the rest of the Hyprland config stack. +# ============================================================================ + +# background: deep near-black (#1A1A1A) — primary window/widget background. +# This matches the eww bar background and hyprlock inner-field colour. +background = rgb(1A1A1A) + +# base: electric blue (#5018DD) — base/secondary brand colour. +# Used for secondary UI elements, inactive borders, and muted accents. +base = rgb(5018DD) + +# font_family: Agave Nerd Font Mono — used everywhere for consistent glyph +# support (Nerd Font icons, powerline symbols) and a monospace coding aesthetic. +font_family = Agave Nerd Font Mono + +# font_family_monospace: explicit monospace fallback (same as font_family). +font_family_monospace = Agave Nerd Font Mono + +# text: muted mauve-grey (#D6ABAB) — body text colour. +# Readable on the dark background without the harshness of pure white. +text = rgb(D6ABAB) + +# alternate_base: primary accent — hot crimson red (#E40046). +# Used for selected items, highlighted elements, and active states. +alternate_base = rgb(E40046) + +# bright_text: vivid red (#F50505) — high-contrast emphasis colour. +# Used for alerts, errors, or very active UI elements. +bright_text = rgb(F50505) + +# accent: primary brand accent (same crimson red as alternate_base). +# Kept separate so apps can theme the main accent independently. +accent = rgb(E40046) + +# accent_secondary: secondary accent — same electric blue as `base`. +# Provides a blue counterpoint to the red primary accent. +accent_secondary = rgb(5018DD) + +# icon_theme: Papirus provides a clean icon set that works well with dark +# backgrounds and the blue/red palette. +icon_theme = Papirus + +# h1_size: heading font sizes. Note: h1_size is declared three times; +# in most parsers the last value (30) wins. The sequence 15, 20, 30 +# appears to be a progressive heading-level definition where only the +# last one takes effect as the h1 size. +h1_size = 15 +h1_size = 20 +h1_size = 30 + +# font_size: base body font size in points. At 2× HiDPI this renders as ~20pt. +font_size = 10 + +# small_font_size: reduced size for secondary/caption text. +small_font_size = 5 diff --git a/desktopenvs/hyprland/hypr/hypridle.conf b/desktopenvs/hyprland/hypr/hypridle.conf index dcc57d1..c36f2f4 100644 --- a/desktopenvs/hyprland/hypr/hypridle.conf +++ b/desktopenvs/hyprland/hypr/hypridle.conf @@ -1,21 +1,61 @@ -#source ~/.config/idle.conf +# ============================================================================ +# hypridle.conf — Idle action configuration for hypridle +# +# hypridle watches for user inactivity and fires timed actions. +# This config handles two tiers of idle behaviour: +# 1. Screen lock (short idle — 2 minutes) +# 2. Suspend-then-hibernate (longer idle — 10 minutes) +# +# The "caffeine" toggle (scripts/caffeine.sh) kills hypridle to prevent +# any of these actions when you want to keep the session awake. +# ============================================================================ + +#source ~/.config/idle.conf # Alternate source path (disabled) + +# ── general: session-level commands ──────────────────────────────────────── general { + # lock_cmd: command to run when the session is asked to lock. + # `pidof hyprlock || hyprlock` prevents a second hyprlock instance if one + # is already running — important because loginctl can call this repeatedly. lock_cmd = pidof hyprlock || hyprlock # avoid starting multiple hyprlock instances. + + # before_sleep_cmd: runs immediately before the system suspends. + # loginctl lock-session triggers the PAM lock mechanism, which calls lock_cmd above. + # This ensures the screen is locked before suspend so wake-up requires auth. before_sleep_cmd = loginctl lock-session # lock before suspend. + + # after_sleep_cmd: runs after the system resumes from suspend. + # fprintd is the fingerprint authentication daemon — restarting it after + # resume ensures the fingerprint reader is re-initialised (common driver quirk). + # `hyprctl dispatch dpms on` forces the display back on without requiring + # a key press (avoids the "press a key twice" issue). after_sleep_cmd = systemctl restart fprintd.service ;; hyprctl dispatch dpms on # to avoid having to press a key twice to turn on the display. } + +# ── listener 1: lock screen after 2 minutes of inactivity ────────────────── listener { + # timeout: seconds of inactivity before the action fires. 120s = 2 minutes. + # Short timeout to quickly protect the session when stepping away briefly. timeout = 120 + # on-timeout: lock the session via loginctl (which invokes lock_cmd above). on-timeout = loginctl lock-session # lock screen when timeout has passed } +# ── listener 2: suspend after 10 minutes of inactivity ───────────────────── listener { + # timeout: 600s = 10 minutes. After the screen has been locked for ~8 more + # minutes with no activity, the system suspends to save power. timeout = 600 #10min + # on-timeout: suspend-then-hibernate writes RAM to disk and suspends. + # If not woken within the hibernate delay, the system fully hibernates. on-timeout = systemctl suspend-then-hibernate # suspend pc } +# ── Disabled: reboot after 5 hours ───────────────────────────────────────── +# Commented out — a forced reboot after 5 hours is too aggressive for a +# personal workstation. Kept as reference in case needed for kiosk use. #listener { # timeout = 18000 #5h # on-timeout = /usr/bin/reboot #reboot diff --git a/desktopenvs/hyprland/hypr/hyprland.conf b/desktopenvs/hyprland/hypr/hyprland.conf index 0ebbae0..fa8c717 100644 --- a/desktopenvs/hyprland/hypr/hyprland.conf +++ b/desktopenvs/hyprland/hypr/hyprland.conf @@ -1,9 +1,31 @@ -source = ~/.config/input.conf -source = ~/.config/monitors.conf -source = ~/.config/envvars.conf -source = ~/.config/binds.conf -source = ~/.config/windowrules.conf -source = ~/.config/autostart.conf +# ============================================================================ +# hyprland.conf — Main Hyprland compositor configuration +# +# This is the entry point for the Hyprland window manager config. It sources +# all per-user split configs (input, monitors, env vars, keybinds, etc.) from +# ~/.config/ symlinks so that user preferences live in one place and are +# independent of the DE source tree. +# +# Split config pattern: instead of one huge file, settings are broken into +# focused files (input.conf, binds.conf, etc.) and loaded via `source =`. +# The files in hypr-usr/ are symlinked to ~/.config/ during setup. +# ============================================================================ + +# ── Split config sources ──────────────────────────────────────────────────── +# Each `source` directive merges the named file into this config at runtime. +# These files live under hypr-usr/ and are symlinked into ~/.config/ by the +# setup scripts, allowing machine-local overrides without touching the repo. +source = ~/.config/input.conf # Keyboard layout, mouse sensitivity, touchpad +source = ~/.config/monitors.conf # Monitor resolution, scale, rotation rules +source = ~/.config/envvars.conf # Wayland/GTK/QT environment variable exports +source = ~/.config/binds.conf # All keybindings and gestures +source = ~/.config/windowrules.conf # Per-app window placement and behaviour rules +source = ~/.config/autostart.conf # exec-once applications started with the session + +# ── Disabled hyprexpo plugin (commented out) ─────────────────────────────── +# hyprexpo provides a grid overview of all workspaces (like GNOME Activities). +# Disabled because the gesture-based workspace navigation in binds.conf already +# covers this use case without requiring a compiled plugin. #plugin { # hyprexpo { # columns = 3 @@ -21,9 +43,12 @@ source = ~/.config/autostart.conf # Eample per-device config # See https://wiki.hyprland.org/Configuring/Keywords/#per-device-input-configs for more +# Per-device overrides let you set sensitivity for a specific mouse independently +# of the global input sensitivity. "epic-mouse-v1" is the Hyprland device id; +# run `hyprctl devices` to find the correct identifier for connected hardware. device { name = epic-mouse-v1 - sensitivity = -0.5 + sensitivity = -0.5 # Negative = faster pointer; 0 = raw, no acceleration applied } @@ -46,10 +71,12 @@ device { # See https://wiki.hyprland.org/Configuring/Keywords/ # Set programs that you use -$terminal = kitty -$fileManager = kitty -e yazi -$editor = kitty micro -$menu = wofi --show=drun +# These $variables are referenced in keybindings (binds.conf) so changing the +# program here automatically updates every keybind that uses it. +$terminal = kitty # Primary terminal emulator +$fileManager = kitty -e yazi # TUI file manager (yazi) launched inside kitty +$editor = kitty micro # Text editor (micro) launched in a terminal window +$menu = wofi --show=drun # Application launcher — overridden by vicinae in binds.conf ################# @@ -57,6 +84,8 @@ $menu = wofi --show=drun ################# # Autostart necessary processes (like notifications daemons, status bars, etc.) +# Actual exec-once lines are in ~/.config/autostart.conf (sourced above). +# This section header is kept here as a readability landmark. @@ -67,114 +96,190 @@ $menu = wofi --show=drun # Refer to https://wiki.hyprland.org/Configuring/Variables/ +# ── general: tiling gaps, borders, and layout ────────────────────────────── # https://wiki.hyprland.org/Configuring/Variables/#general -general { +general { + # gaps_in: space (px) between individual tiled windows inside a workspace. + # Small value keeps windows visually separated without wasting screen space. gaps_in = 3 + # gaps_out: space (px) between the outermost windows and the monitor edge. + # Slightly larger than gaps_in so the screen frame is clearly visible. gaps_out = 6 + # border_size: width of the coloured border drawn around each window (px). + # 4 px is prominent on a high-DPI display and reinforces the active window. border_size = 4 # https://wiki.hyprland.org/Configuring/Variables/#variable-types for info about colors + # col.active_border: gradient applied to the focused window's border. + # Multi-stop gradient: primary red (#E40046) → deeper red (#f50505) at 35°, + # creating a crimson animated border matching the cyberqueer colour theme. #col.active_border = rgb(E40046) rgb(fc0588) 40deg col.active_border = rgb(E40046) rgb(f50505) rgb(E40046) rgb(f50505) rgb(E40046) 35deg + + # col.inactive_border: solid electric blue (#5018DD) for unfocused windows. + # This is the secondary accent colour of the cyberqueer palette. col.inactive_border = rgb(5018dd) # Set to true enable resizing windows by clicking and dragging on borders and gaps - resize_on_border = false + # Disabled because it conflicts with gap-hover interactions on touch screens. + resize_on_border = false # Please see https://wiki.hyprland.org/Configuring/Tearing/ before you turn this on + # allow_tearing permits screen tearing for lower latency in games. + # Disabled — visual quality preferred; enable per-app via windowrule if needed. allow_tearing = false + # layout: default tiling algorithm for new windows. + # "dwindle" splits the screen recursively (like bspwm). + # Toggle between dwindle/master at runtime via toggle-layout.sh. layout = dwindle } +# ── group: window grouping (tabbed windows) ───────────────────────────────── +# Groups let multiple windows share the same screen tile, switchable via a tab bar. group { - col.border_active = rgb(E40046) - col.border_inactive = rgb(5018dd) - col.border_locked_active = rgb(f50505) - col.border_locked_inactive = rgb(5018dd) + # Border colours for grouped windows mirror the main active/inactive palette. + col.border_active = rgb(E40046) # Hot pink/red — focused group border + col.border_inactive = rgb(5018dd) # Electric blue — unfocused group border + col.border_locked_active = rgb(f50505) # Deeper red — locked (pinned) active group + col.border_locked_inactive = rgb(5018dd) # Same blue for locked inactive group - #rounding = 10 - + #rounding = 10 # Commented — would override global rounding just for groups + + # groupbar: the tab strip shown above grouped windows. groupbar { + # font_family: Agave NerdFont provides the Nerd Font icon glyphs used + # throughout the system (bar, lock screen, etc.). font_family = Agave NerdFont + # font_size: 20pt readable at 2× HiDPI scaling without being too large. font_size = 20 + # height: total height of the groupbar strip in pixels. height = 25 + # round_only_edges = false means all corners are rounded, not just outer. round_only_edges = false + # indicator_height: height of the coloured active-tab underline indicator. indicator_height = 25 + # stacked = false: tabs displayed horizontally side-by-side (not vertically). stacked = false + # text_color: tab label colour — primary accent red. text_color = rgb(E40046) + # priority: render order vs other decorations (shadows etc.). + # 3 = high priority so the groupbar appears on top of other decorations. priority = 3 - + + # rounding: corner radius for the tab bar itself — pill-shaped appearance. rounding = 13 + # Active tab background — red accent to show which tab is selected. col.active = rgb(E40046) + # Inactive tab backgrounds — blue to distinguish non-selected tabs. col.inactive = rgb(5018dd) + # Locked group tab colours (same as unlocked for visual consistency). col.locked_active = rgb(E40046) col.locked_inactive = rgb(5018dd) } } +# ── decoration: window rounding, transparency, and blur ───────────────────── # https://wiki.hyprland.org/Configuring/Variables/#decoration decoration { + # rounding: window corner radius in pixels. 20px gives a modern, rounded + # look consistent with the GTK cyberqueer theme's rounded widgets. rounding = 20 # Change transparency of focused and unfocused windows + # active_opacity: focused window is fully opaque (1.0 = 100%). + # Keeps the active window readable while inactive windows recede visually. active_opacity = 1 + # inactive_opacity: unfocused windows are 80% opaque so the blurred + # wallpaper shows through slightly, creating depth perception. inactive_opacity = 0.8 + # Drop shadow disabled: coloured borders already provide sufficient depth cues. #drop_shadow = true #shadow_range = 4 #shadow_render_power = 3 #col.shadow = rgba(1a1a1aee) # https://wiki.hyprland.org/Configuring/Variables/#blur + # blur: Gaussian-style background blur behind transparent/inactive windows. blur { enabled = true + # size: blur kernel radius — 3 is subtle and not GPU-heavy. size = 3 + # passes: number of blur iterations — 3 gives a smooth result without + # excessive GPU cost. More passes = smoother blur but heavier rendering. passes = 3 - + + # vibrancy: boosts colour saturation of the blurred background content. + # 0.1696 ≈ 17% vibrancy — subtle colour pop behind transparent windows. vibrancy = 0.1696 } } +# ── animations ────────────────────────────────────────────────────────────── # https://wiki.hyprland.org/Configuring/Variables/#animations animations { enabled = true # Default animations, see https://wiki.hyprland.org/Configuring/Animations/ for more + # bezier: defines a named cubic Bézier easing curve. + # myBezier (0.05, 0.9, 0.1, 1.05) — fast start, slight spring overshoot. bezier = myBezier, 0.05, 0.9, 0.1, 1.05 + # animation = <event>, <enabled 0/1>, <speed>, <curve>[, <style>] + # speed: lower = slower animation; style is an optional named effect. + + # Window open/move: spring curve at speed 7 — snappy but not jarring. animation = windows, 1, 7, myBezier + # Window close: "popin" effect shrinks the window from 80% size on close. animation = windowsOut, 1, 7, default, popin 80% + # Border colour transition (gradient rotation): default easing, speed 10. animation = border, 1, 10, default + # Border angle animation (rotating gradient sweep): speed 8. animation = borderangle, 1, 8, default + # Fade in/out for opacity transitions: speed 7. animation = fade, 1, 7, default + # Workspace slide animations are disabled — vertical slide is used for the + # special/scratchpad workspace only (see specialWorkspace below). #animation = workspaces, 1, 6, default #animation = workspaces,1,5,default,slidevert + # Special workspace (scratchpad "magic"): slides in/out vertically at speed 10. animation = specialWorkspace, 1, 10, default, slidevert -} +} +# ── dwindle layout ────────────────────────────────────────────────────────── # See https://wiki.hyprland.org/Configuring/Dwindle-Layout/ for more dwindle { #pseudotile = true # Master switch for pseudotiling. Enabling is bound to mainMod + P in the keybinds section below + # preserve_split: keeps the split direction when a window is removed. + # Without this, removing a child window can unexpectedly flip the split axis. preserve_split = true # You probably want this + # special_scale_factor: scratchpad workspace windows fill 95% of the monitor, + # leaving a small visible border around the floating overlay. special_scale_factor = 0.95 } +# ── master layout ─────────────────────────────────────────────────────────── # See https://wiki.hyprland.org/Configuring/Master-Layout/ for more master { + # new_status: new windows open as the "master" (primary, largest) pane. + # Alternative value "slave" would stack new windows alongside the master. new_status = master + # special_scale_factor: same 95% fill for the scratchpad in master mode. special_scale_factor = 0.95 } +# ── misc ──────────────────────────────────────────────────────────────────── # https://wiki.hyprland.org/Configuring/Variables/#misc -misc { +misc { force_default_wallpaper = 0 # Set to 0 or 1 to disable the anime mascot wallpapers + # 0 = disable Hyprland's built-in wallpaper; hyprpaper manages wallpapers instead. disable_hyprland_logo = true # If true disables the random hyprland logo / anime girl background. :( } diff --git a/desktopenvs/hyprland/waybar/config b/desktopenvs/hyprland/waybar/config index eaf266a..47d1701 100644 --- a/desktopenvs/hyprland/waybar/config +++ b/desktopenvs/hyprland/waybar/config @@ -1,208 +1,399 @@ +// ============================================================ +// Waybar Config — Hyprland Cyberqueer Setup +// ============================================================ +// Waybar is the taskbar/statusbar displayed at the top of the +// Hyprland Wayland compositor. This JSON5 file (comments and +// trailing commas allowed) defines every module shown on the +// bar: position, update intervals, display formats, click +// actions, and hover tooltips. +// +// Cyberqueer color palette (referenced throughout): +// #1a1a1a — near-black background +// #5018dd — deep electric purple (inactive / cool accent) +// #E40046 — hot neon red/pink (active / warm accent) +// #d6abab — muted dusty rose (readable text / neutral) +// #f50505 — bright red (critical / alert states) +// ============================================================ { + // ── Bar placement ────────────────────────────────────────── + // "layer": "top" — the bar sits above all other windows so + // it is never covered by maximized applications. + // "position": "top" — anchored to the top edge of the screen. "layer": "top", "position": "top", + + // ── Module layout ────────────────────────────────────────── + // Waybar divides the bar into three zones: left, center, right. + // The order within each list is the left-to-right display order. "modules-left": ["clock", "disk", "memory", "cpu", "temperature" ], + // Center: Hyprland workspace switcher + currently focused window title. "modules-center": [ "hyprland/workspaces", "hyprland/window"], + // Right: network icon, local IP, system tray, audio volume, battery. "modules-right": [ "network", "custom/netaddrsimple", "tray", "pulseaudio", "battery" ], + + // Automatically reload style.css when the file changes on disk, + // so appearance tweaks take effect without restarting waybar. "reload_style_on_change":true, + // ── custom/netaddrsimple ─────────────────────────────────── + // A bare-minimum custom module that shows the machine's local IP. + // Handy for quickly finding the address to SSH into. "custom/netaddrsimple": { + // {} is substituted with the stdout of the exec command. "format": "IP:{}", + // No tooltip needed — the IP text in the bar is sufficient. "tooltip":false, + // (Commented out) would limit the displayed text to 15 characters. //"max-length": 15, + // Refresh every 10 seconds — local IP rarely changes. "interval": 10, - "exec": "hostname -i", + // "hostname -i" prints the machine's primary local IP address. + "exec": "hostname -i", }, + // ── hyprland/workspaces ─────────────────────────────────── + // Shows clickable workspace number buttons. + // Mouse-wheel scrolling switches workspaces sequentially. "hyprland/workspaces": { + // Display the workspace name (typically its number) as label text. "format": "{name}", + // These icon overrides replace the label when a workspace matches + // the "default", "active", or "urgent" state keys. "format-icons": { + // Unfocused, non-urgent workspaces get a plain bullet. "default": " ", + // The currently focused workspace is marked with "@". "active": "@", + // A workspace with an urgent app (needs attention) shows "!". "urgent": "!" }, + // Scroll wheel up → move to numerically higher workspace. "on-scroll-up": "hyprctl dispatch workspace e+1", + // Scroll wheel down → move to numerically lower workspace. "on-scroll-down": "hyprctl dispatch workspace e-1", }, + // NOTE: this clock block is structurally inside the workspaces + // object due to a brace mismatch in the original config. + // The top-level "clock" block defined further below takes + // precedence and is what waybar actually uses. "clock": { - "format": "{:L%H:%M}", + // Locale-aware HH:MM format (24-hour). + "format": "{:L%H:%M}", "tooltip": true, + // Tooltip shows a large date line and a small monthly calendar. "tooltip-format": "<big>{:%A, %d.%B %Y }</big>\n<tt><small>{calendar}</small></tt>" } }, - + +// ── idle_inhibitor ──────────────────────────────────────── +// Prevents the system from going idle / blanking the screen +// while activated. Toggle by clicking the module. "idle_inhibitor":{ + // Wrap the icon in a larger font-size span so the glyph is readable. "format": "<span font='12'>{icon} </span>", "format-icons": { + // Nerd Font "eye open" — currently inhibiting idle / screen-off. "activated":"󰈈", + // Nerd Font "eye closed" — idle allowed normally. "deactivated":"󰈉" } }, +// ── clock ───────────────────────────────────────────────── +// Real-time HH:MM:SS clock shown in the left section. +// Updates every second so the seconds digit stays live. "clock": { + // Show hours, minutes, and seconds with a trailing space. "format": "{:%H:%M:%S }", + // 1-second interval to keep the clock accurate to the second. "interval":1, + // Tooltip: a large date header plus a mini monthly calendar. "tooltip-format": "\n<big>{:%d %m %Y}</big>\n<tt><small>{calendar}</small></tt>", + // Display ISO week numbers on the right side of the calendar grid. "calendar-weeks-pos": "right", + // Highlight today's date cell in the cyberqueer purple (#7645AD). "today-format": "<span color='#7645AD'><b><u>{}</u></b></span>", + // Calendar day numbers in muted grey so they don't compete visually. "format-calendar": "<span color='#aeaeae'><b>{}</b></span>", + // Week-number column label (e.g. "W24") in the same muted grey. "format-calendar-weeks": "<span color='#aeaeae'><b>W{:%V}</b></span>", + // Weekday header row (Mon, Tue…) in muted grey. "format-calendar-weekdays": "<span color='#aeaeae'><b>{}</b></span>" }, + // ── bluetooth ──────────────────────────────────────────── + // Bluetooth state indicator using Nerd Font glyphs. + // Clicking opens rofi-bluetooth for pairing/device management. "bluetooth": { - "format-on": "", + // Bluetooth is on but nothing is connected. + "format-on": "", + // Bluetooth is turned off. "format-off": "", + // Bluetooth hardware is disabled at the kernel/driver level. "format-disabled": "󰂲", + // At least one device is connected. "format-connected": "󰂴", + // Connected device reports battery level — show it alongside the icon. "format-connected-battery": "{device_battery_percentage}% 󰂴", + // Base tooltip: controller name, MAC, and connection count. "tooltip-format": "{controller_alias}\t{controller_address}\n\n{num_connections} connected", + // Extended tooltip listing each connected device when count > 0. "tooltip-format-connected": "{controller_alias}\t{controller_address}\n\n{num_connections} connected\n\n{device_enumerate}", + // Per-device line in the enumeration: alias + MAC. "tooltip-format-enumerate-connected": "{device_alias}\t{device_address}", + // Per-device line when battery info is available. "tooltip-format-enumerate-connected-battery": "{device_alias}\t{device_address}\t{device_battery_percentage}%", + // Left-click launches rofi-bluetooth for a GUI device list. "on-click": "rofi-bluetooth", - }, + }, + // ── battery ───────────────────────────────────────────── + // Shows remaining battery capacity and charging state. + // 1-second interval so the "PWR-" / "PWR+" prefix switches instantly. "battery": { "interval":1, + // Named thresholds that trigger CSS state classes: + // .good ≥ 95% (full — no visual warning) + // .warning ≤ 30% (getting low) + // .critical ≤ 20% (nearly empty — style.css colors this bright red) "states": { "good": 95, "warning": 30, "critical": 20 }, + // Discharging: "PWR-" prefix + capacity + level icon. "format": "PWR- {capacity}% {icon} ", + // Charging via AC: "PWR+" prefix + the charging glyph. "format-charging": "PWR+ {capacity}% 󰂄 ", - "format-plugged": "PWR+ {capacity}% 󰂄 ", + // Plugged in at 100%: show charger icon even though full. + "format-plugged": "PWR+ {capacity}% 󰂄 ", + // Right-click (alt format) shows estimated time remaining. "format-alt": "{time} {icon}", + // Six Nerd Font battery glyphs mapping roughly to 0→100% in steps. "format-icons": [ - "󰁻", - "󰁼", - "󰁾", - "󰂀", - "󰂂", - "󰁹" + "󰁻", // 0–20 % + "󰁼", // 20–40 % + "󰁾", // 40–60 % + "󰂀", // 60–80 % + "󰂂", // 80–95 % + "󰁹" // 95–100 % ], }, + +// ── backlight ────────────────────────────────────────────── +// Screen brightness via the intel_backlight sysfs interface. +// Scroll wheel on this module adjusts brightness by ±10 units. "backlight": { + // Target the Intel integrated GPU backlight node specifically. "device": "intel_backlight", + // Display only the icon — brightness level communicated visually. "format": "<span font='12'>{icon}</span>", + // Ten sun/brightness Nerd Font icons mapping 0%→100% in 10% steps. "format-icons": [ - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", + "", // 0–10 % + "", // 10–20 % + "", // 20–30 % + "", // 30–40 % + "", // 40–50 % + "", // 50–60 % + "", // 60–70 % + "", // 70–80 % + "", // 80–90 % + "", // 90–100 % ], + // Scroll down decreases brightness (note: flag names are swapped in `light`). "on-scroll-down": "light -A 10", + // Scroll up increases brightness. "on-scroll-up": "light -U 10", + // Threshold of 1 means no acceleration — every scroll tick = 10 units. "smooth-scrolling-threshold": 1 }, + // ── disk ───────────────────────────────────────────────── + // Shows how much of the root partition is used, as a percentage. + // 30-second interval is fine — disk usage changes slowly. "disk": { "interval": 30, - "format": " {percentage_used}%", + // Hard-drive Nerd Font glyph + used-space percentage. + "format": " {percentage_used}%", + // Watch the root filesystem specifically (not /home or other mounts). "path": "/" }, + // ── custom/colorpicker ─────────────────────────────────── + // Wraps hyprpicker into a waybar button. + // Clicking launches the eyedropper; the picked hex is copied to + // clipboard and the module refreshes to show a colored dot. "custom/colorpicker": { + // {} is replaced with the JSON "text" field from the script. "format": "{}", + // Script returns {text, tooltip} JSON — waybar parses both fields. "return-type": "json", + // "once" means only run at startup; RTMIN+1 triggers re-execution. "interval": "once", + // On load: render the most-recently-picked color as a dot. "exec": "~/.config/waybar/scripts/colorpicker.sh -j", + // Click: 1-second delay prevents the click from interfering with the + // hyprpicker window, then launch the picker. "on-click": "sleep 1 && ~/.config/waybar/scripts/colorpicker.sh", + // colorpicker.sh sends RTMIN+1 to waybar after saving a new color, + // which causes this module to re-exec and show the new color. "signal": 1 }, + // ── cpu ────────────────────────────────────────────────── + // Aggregate CPU usage across all cores, updated every second. "cpu": { "interval": 1, - "format": " {usage}%", + // Nerd Font CPU chip icon + usage %. Fixed 6-char width stops the + // bar from shifting when the value goes from single to double digits. + "format": " {usage}%", "min-length": 6, "max-length": 6, + // Bar-graph Unicode chars available for use in custom format strings. "format-icons": ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"], }, +// ── memory ──────────────────────────────────────────────── +// RAM usage as a percentage. Compact single-field display. "memory": { - "format": " {percentage}%" + // Memory chip Nerd Font glyph + percentage of RAM used. + "format": " {percentage}%" }, + // ── hyprland/window ────────────────────────────────────── + // Shows the WM_CLASS of the currently focused window in the bar center. + // Rewrite rules transform noisy titles into cleaner short labels. "hyprland/window": { + // Wrap the class name in parentheses for a "terminal prompt" aesthetic. "format": "( {class} )", "rewrite": { + // Strip " - Mozilla Firefox" from page titles; prepend a globe. "(.*) - Mozilla Firefox": "🌎 $1", + // Kitty/foot terminal: show the current directory in brackets. "(.*) - zsh": "> [$1]" } }, +// ── temperature ────────────────────────────────────────── +// CPU package temperature. Turns to a "critical" CSS class above 80 °C. +// Clicking opens btop for a full system-monitoring view. "temperature": { - "format": " {temperatureC}°C", - "format-critical": " {temperatureC}°C", + // Normal state: thermometer glyph + temperature in Celsius. + "format": " {temperatureC}°C", + // Critical state: fire glyph replaces the thermometer icon. + "format-critical": " {temperatureC}°C", + // 1-second interval to catch sudden thermal spikes quickly. "interval": 1, + // Above 80 °C the module switches to the "critical" CSS class. "critical-threshold": 80, + // Left-click opens btop in the foot Wayland terminal. "on-click": "foot btop", }, +// ── pulseaudio ─────────────────────────────────────────── +// Volume level and mute control via PulseAudio or PipeWire-pulse. +// Left-click: toggle mute. Right-click: open pavucontrol GUI mixer. "pulseaudio": { + // Default format: percentage + device-type icon. "format": "{volume}% {icon}", + // When the active sink is a Bluetooth device, show a BT-speaker glyph. "format-bluetooth":"󰂰", - "format-muted": "<span font='12'></span>", + // Muted: show a crossed-speaker glyph, hide the volume number. + "format-muted": "<span font='12'></span>", + // Icon set keyed by audio device type reported by PulseAudio. "format-icons": { - "headphones": "", + "headphones": "", "bluetooth": "󰥰", - "handsfree": "", + "handsfree": "", "headset": "󱡬", - "phone": "", - "portable": "", - "car": "", + "phone": "", + "portable": "", + "car": "", + // Default (built-in speaker): three-tier glyphs for low/mid/high volume. "default": ["󰕿","󰖀","󰕾"] }, + // Center-align the label within the module's allocated width. "justify": "center", + // Toggle Master channel mute via amixer on left-click. "on-click": "amixer sset Master toggle", + // Open the graphical PulseAudio volume control on right-click. "on-click-right": "pavucontrol", + // Tooltip shows icon + volume % for a quick glance. "tooltip-format": "{icon} {volume}%" }, +// ── jack ───────────────────────────────────────────────── +// JACK Audio Connection Kit DSP load monitor. +// Relevant when running pro-audio software that uses JACK directly. "jack": { + // {} = current DSP percentage (how busy the JACK graph is). "format": "{} 󱎔", + // Xruns are buffer underruns — audio glitches. Non-zero = problem. "format-xrun": "{xruns} xruns", + // JACK server is not running. "format-disconnected": "DSP off", + // Poll in real-time (rather than on an interval) for accuracy. "realtime": true }, +// ── tray ──────────────────────────────────────────────── +// System tray using the StatusNotifierItem / libdbusmenu protocol. +// Apps like NetworkManager applet, Blueman, etc. dock here. "tray": { + // Icon render size in pixels — 14 px is compact but readable. "icon-size": 14, + // Horizontal gap between tray icons in pixels. "spacing": 10 }, +// ── upower ────────────────────────────────────────────── +// Displays UPower-tracked peripheral battery levels (mice, keyboards). +// The icon is suppressed; info appears only in the tooltip to save space. "upower": { + // Don't show a persistent icon in the bar. "show-icon": false, + // Collapse the module entirely when no UPower devices are present. "hide-if-empty": true, "tooltip": true, + // Vertical spacing between device entries in the tooltip. "tooltip-spacing": 20 }, + // ── network ───────────────────────────────────────────── + // Connection-type indicator: one icon for wifi, one for ethernet, + // one for disconnected. Hovering reveals SSID, signal, or IP. "network":{ - "format-wifi": " ", - "format-ethernet":" ", - "format-disconnected": "", + // Wireless: show only a wifi Nerd Font glyph (no text). + "format-wifi": " ", + // Wired: ethernet cable Nerd Font glyph. + "format-ethernet":" ", + // Not connected to anything. + "format-disconnected": "", + // Ethernet tooltip: just the interface name (eth0, enp3s0, etc.). "tooltip-format": "{ifname}", - "tooltip-format-wifi": "{essid} ({signalStrength}%)  | {ipaddr}", + // Wi-Fi tooltip: SSID, signal percentage, and IP address. + "tooltip-format-wifi": "{essid} ({signalStrength}%) | {ipaddr}", + // Ethernet tooltip: interface name and IP address. "tooltip-format-ethernet": "{ifname} 🖧 | {ipaddr}" }, + // ── custom/powerDraw ───────────────────────────────────── + // Reads instantaneous battery power draw in watts from sysfs. + // Useful for identifying rogue processes spiking power consumption. "custom/powerDraw": { + // {} replaced by the JSON "text" field from the script output. "format": "{}", + // Poll every second — power draw is volatile. "interval": 1, + // Script reads /sys/class/power_supply/BAT*/power_now and formats JSON. "exec": "~/.config/waybar/scripts/powerdraw.sh", + // JSON return type lets the script provide both text and tooltip. "return-type": "json" } - + } diff --git a/desktopenvs/hyprland/waybar/style.css b/desktopenvs/hyprland/waybar/style.css index 184ab4c..8efd439 100644 --- a/desktopenvs/hyprland/waybar/style.css +++ b/desktopenvs/hyprland/waybar/style.css @@ -1,58 +1,138 @@ +/* + * ============================================================ + * Waybar Style — Hyprland Cyberqueer Theme + * ============================================================ + * GTK CSS that controls the visual look of the waybar taskbar. + * All selectors match waybar's internal widget IDs. + * + * Cyberqueer palette: + * #1a1a1a — near-black panel background + * #5018dd — electric purple (inactive / default accent) + * #E40046 — neon red-pink (active / urgent / focused) + * ============================================================ + */ + +/* ── Global reset ───────────────────────────────────────────── + * Applied to every widget in the bar. + * Removes default GTK borders and sets transparent backgrounds + * so each widget can define its own background color below. + * Large border-radius (30 px) gives the "pill" capsule shape + * that is a signature of the cyberqueer aesthetic. + */ * { border: none; + + /* Agave Nerd Font Mono provides the icon glyphs used in module + * format strings (battery, CPU, wifi, etc.). sans-serif is the + * generic fallback if Agave is not installed. */ font-family: Agave Nerd Font Mono, sans-serif; font-size: 12pt; + + /* Transparent default so color only appears where explicitly set. */ background: transparent; - + + /* Compact vertical padding keeps the bar slim. */ padding-top: 2px; padding-bottom: 2px; + /* Horizontal padding gives each module text a bit of breathing room. */ padding-right: 6px; padding-left: 6px; - + + /* 1 px top margin keeps module pills off the very top pixel. */ margin-top: 1px; margin-bottom: 0px; + /* 2 px horizontal margin creates the small gap between adjacent modules. */ margin-right: 2px; margin-left: 2px; - + + /* 30 px radius turns every element into a rounded pill shape. */ border-radius: 30px; } +/* ── Workspace buttons — inactive state ─────────────────────── + * Each workspace number is a GTK button. + * Inactive (non-focused) workspaces: dark background + purple text. + */ #workspaces button { + /* 3 px solid border traces the pill outline clearly. */ border: solid; border-width: 3px; + /* Near-black background matches the general panel color. */ background: #1a1a1a; + /* Electric purple for inactive workspace labels. */ color: #5018dd; } +/* ── Workspace buttons — active (focused) state ─────────────── + * The currently focused workspace switches text to neon red-pink + * so it immediately stands out from the inactive ones. + */ #workspaces button.active { border: solid; border-width: 3px; background: #1a1a1a; + /* Neon accent color makes the active workspace unmistakable. */ color: #E40046; - + } +/* ── Workspace buttons — urgent state ───────────────────────── + * An app on an urgent workspace is demanding attention. + * Colors are fully inverted: the hot accent becomes the background + * and dark text sits on top — a high-contrast visual alarm. + */ #workspaces button.urgent { border: solid; border-width: 3px; + /* Flipped: background becomes the neon accent. */ background: #E40046; + /* Dark text on bright background for maximum contrast. */ color: #1a1a1a; - + } +/* ── Standard status modules ────────────────────────────────── + * All "regular" status modules share this single rule: + * dark pill background + electric-purple text. This creates a + * consistent look across the entire right side of the bar. + * + * #clock — HH:MM:SS time + * #disk — root disk usage % + * #window — focused window class/title + * #battery — battery level and charge state + * #cpu — CPU usage % + * #memory — RAM usage % + * #temperature — CPU temperature + * #backlight — screen brightness + * #network — network connection type + * #pulseaudio — audio volume / mute state + * #custom-media — media player (if module is added) + * #tray — system tray host + * #mode — Hyprland submap mode label + * #idle_inhibitor — idle/screen-lock prevention toggle + * #custom-netaddrsimple — local IP address display + */ #clock, #disk, #window, #battery, #cpu, #memory, #temperature, #backlight, #network, #pulseaudio, #custom-media, #tray, #mode, #idle_inhibitor, #custom-netaddrsimple { border: solid; border-width: 3px; + /* Dark panel background shared across all status modules. */ background: #1a1a1a; + /* Electric purple for all default module text and Nerd Font icons. */ color: #5018dd; } +/* ── idle_inhibitor — activated (idle prevention ON) ────────── + * When the user has clicked to prevent the screen from sleeping, + * switch to the neon accent so it is visually obvious that + * idle inhibition is currently active. + */ #idle_inhibitor.activated { border: solid; border-width: 3px; background: #1a1a1a; + /* Neon red-pink = "something is actively overriding idle". */ color: #E40046; - + } diff --git a/desktopenvs/hyprlua/eww/eww.yuck b/desktopenvs/hyprlua/eww/eww.yuck index bc39bd1..a078c62 100644 --- a/desktopenvs/hyprlua/eww/eww.yuck +++ b/desktopenvs/hyprlua/eww/eww.yuck @@ -1,4 +1,23 @@ +; ============================================================================= +; eww.yuck — EWW bar configuration for hyprlua (laptop with battery variant) +; This file defines all widgets, polls, and listeners for the top status bar. +; EWW (Elkowar's Wacky Widgets) is a widget system for Wayland/X11 that uses +; a Lisp-like syntax (.yuck) for layout and SCSS for styling. +; +; Color palette (cyberqueer theme): +; Background: #1a1a1a (near-black) +; Accent 1: #E40046 (red — active/highlight) +; Accent 2: #5018dd (purple — inactive/secondary) +; Text: #D6ABAB (muted pink-grey) +; ============================================================================= + +; defwindow — declares the actual OS-level window that appears on screen. +; :monitor — which monitor index to render on (passed at launch time) +; :windowtype "dock" — tells the compositor this is a panel, so it reserves space +; :exclusive true — prevents other windows from overlapping the bar area +; :anchor "top center" — pins the bar to the top center of the monitor +; :geometry — sets position (x=0%, y=1% from top), width (99%), height (20px) (defwindow bar [monitor] :monitor monitor :class "ewwbar" @@ -9,55 +28,88 @@ :height "20px" :anchor "top center") :exclusive true + ; Render the bar widget, passing the monitor index through for workspace tracking (bar :monitor_ monitor)) +; defpoll — polls a shell command on an interval and stores the result. +; battery is polled every 2 seconds so the icon/percentage stays current. +; The script returns a formatted string like "󰁹 87%" for display. (defpoll battery :interval "2s" "~/Dotfiles/desktopenvs/hyprland/scripts/batteryperc") +; defwidget bar — root bar widget. Uses centerbox to split into three zones: +; left → winsworks (battery, workspaces, active window title) +; center → music (currently playing track) +; right → sidestuff (IP, volume, disk, caffeine, clock, systray) (defwidget bar [monitor_] (centerbox :orientation "h" (winsworks :monitor monitor_) (music) (sidestuff))) +; winsworks — left section of the bar. +; Shows battery percentage, workspace buttons, and the active window title. +; :halign "start" — left-aligns the content within the centerbox left cell. (defwidget winsworks [monitor] (box :orientation "h" :space-evenly false :halign "start" + ; Battery percentage badge — styled as a pill with class "music" (box :class "music" {"${battery}"}) + ; Workspace dots — one button per active workspace on this monitor (workspaceWidget :monitor monitor) - (button :onclick "~/Dotfiles/desktopenvs/hyprland/scripts/drawer.sh" :class "music" {" ${activewindow}"}) + ; Active window title — clicking opens the application drawer + (button :onclick "~/Dotfiles/desktopenvs/hyprland/scripts/drawer.sh" :class "music" {" ${activewindow}"}) ) ) +; sidestuff — right section of the bar. +; :halign "end" — right-aligns all children within the centerbox right cell. (defwidget sidestuff [] (box :class "sidestuff" :orientation "h" :space-evenly false :halign "end" - (box :class "music" {"󰛳 ${IP}"}) + ; IP address badge — refreshed every 5s, shows current LAN/VPN IP + (box :class "music" {"󰛳 ${IP}"}) + ; Volume slider — 󰓃 icon, drag to adjust, click to open pavucontrol mixer + ; hyprctl eval is used to run Lua dispatch inline so the window gets the +mixer tag (metric :label "󰓃" :value volume :onchange "pactl set-sink-volume @DEFAULT_SINK@ {}%" :onclick "killall pavucontrol || hyprctl eval 'hl.dsp.exec_cmd(\"[tag +mixer] pavucontrol\")'") - (box + ; Disk usage gauge — tooltip shows per-disk breakdown from dysk + ; EWW_DISK is a built-in magic variable providing filesystem stats + (box :tooltip {disks} - (metric :label "" + (metric :label "" :value {round((1 - (EWW_DISK["/"].free / EWW_DISK["/"].total)) * 100, 0)} :onchange "" :onclick "")) - + + ; Caffeine toggle button — shows ☕ when active, sleeping icon when off (caffeine) + ; Clock widget with calendar tooltip (clock) + ; System tray — renders applets from running applications (nm-applet, blueman, etc.) (systray :class "music" :orientation "h" :spacing 2 :space-evenly true) )) +; music — center section: shows currently playing track via playerctl. +; Clicking play/pauses the current media player (spotify/vlc priority). +; Displays a music note icon + track name, or "None" if nothing is playing. (defwidget music [] (button :class "music" :orientation "h" :space-evenly false :halign "center" :onclick "~/Dotfiles/desktopenvs/hyprland/scripts/playpause.sh" - {music != "" ? " ${music}" : " None"})) + {music != "" ? " ${music}" : " None"})) +; metric — reusable slider+label widget used for volume and disk usage. +; Parameters: +; label — icon/text shown as a clickable button +; value — current value (0-100) +; onchange — command run when slider is dragged (use {} as placeholder for value) +; onclick — command run when the label button is clicked (defwidget metric [label value onchange onclick] (box :orientation "h" :class "metric" @@ -65,12 +117,21 @@ (button :class "label" :onclick onclick label) (scale :min 0 :max 101 + ; Only make the slider interactive if an onchange command was provided :active {onchange != ""} :value value :onchange onchange))) +; deflisten — subscribes to a long-running process that emits JSON on stdout. +; hyprland-workspaces is a helper binary that streams workspace state changes. +; The "_" argument means listen for all monitors; we filter by monitor in the widget. (deflisten workspaces "hyprland-workspaces _") +; workspaceWidget — renders one button per workspace on the given monitor. +; :onscroll — scrolling the workspace area switches workspaces; sed converts +; "up"/"down" into "r+1"/"r-1" relative workspace references, +; then hyprctl eval runs the Lua dispatch inline. +; The special:magic scratchpad workspace gets a distinct icon (󱂬 without a number). (defwidget workspaceWidget [monitor] (eventbox :onscroll "hyprctl eval \"hl.dsp.focus({workspace='$(echo {} | sed 's/up/r+/;s/down/r-/')1'})\"" (box :class "workspaces" @@ -79,51 +140,75 @@ (for i in {workspaces[monitor].workspaces} (button :width 20 + ; Click a workspace button to focus it via Lua dispatch :onclick "hyprctl eval 'hl.dsp.focus({workspace=${i.id}})'" + ; i.class comes from hyprland-workspaces: "active", "occupied", or "" :class "${i.class}" - {i.name == "special:magic" ? "󱂬" : "󱂬${i.name}"}))))) + {i.name == "special:magic" ? "󱂬" : "󱂬${i.name}"}))))) +; workspace-old — legacy workspace listener using a shell script instead of +; the hyprland-workspaces binary. Kept for reference/fallback. (deflisten workspace-old "~/Dotfiles/desktopenvs/hyprland/scripts/workspace") (defwidget workspaces-old [] (literal :content workspace-old)) +; music poll — queries playerctl for the current track every 0.5 seconds. +; Returns an empty string when nothing is playing so the widget shows "None". (defpoll music :interval "0.5s" "~/Dotfiles/desktopenvs/hyprland/scripts/playerget") +; activewindow poll — gets the focused window title/class every 0.5 seconds. +; Used in the left section of the bar as a window title breadcrumb. (defpoll activewindow :interval "0.5s" "~/Dotfiles/desktopenvs/hyprland/scripts/activewindow") +; IP poll — gets the current IP address every 5 seconds. +; Slow interval because IP changes are infrequent; avoids unnecessary shell forks. (defpoll IP :interval "5s" "~/Dotfiles/desktopenvs/hyprland/scripts/ip") +; volume poll — reads the default audio sink volume every 0.5 seconds. +; Fast interval so the slider reflects hardware key presses immediately. (defpoll volume :interval "0.5s" "~/Dotfiles/desktopenvs/hyprland/scripts/getvol") +; time poll — reads the system clock every second for the clock widget. +; Format: HH:MM:SS|DD.MM.YYYY — the pipe character is used as a visual separator. (defpoll time :interval "1s" "date '+%H:%M:%S|%d.%m.%Y'") +; clock widget — displays the current time and shows a calendar as a tooltip. +; The tooltip is populated by the `calender` poll (note: intentional spelling). (defwidget clock [] (box :class "clock" :tooltip {calender} - (label :text {"${time}"}) + (label :text {"${time}"}) ) ) +; calender poll — runs a script that generates a text calendar (via cal). +; Polled every 600 seconds (10 minutes) since the calendar only changes daily. (defpoll calender :interval "600s" "~/Dotfiles/desktopenvs/hyprland/scripts/calender-fix.sh") +; disks poll — runs dysk to get a human-readable table of physical disk usage. +; Shown as a tooltip on the disk gauge. Polled every 10 minutes. (defpoll disks :interval "600s" "~/Dotfiles/desktopenvs/hyprland/scripts/dysk-phydisks.sh") +; caffeine-active poll — checks whether the caffeine systemd-inhibit lock is active. +; Returns "true" or "false" string every 2 seconds. Used to toggle the icon. (defpoll caffeine-active :interval "2s" "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-status.sh") +; caffeine widget — a button that toggles the caffeine idle-inhibit lock. +; ☕ = caffeine is ON (screen won't sleep), 󰅺 = caffeine is OFF (normal idle). +; Tooltip shows the current state for clarity. (defwidget caffeine [] (button :class "music" :onclick "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine.sh" :tooltip {caffeine-active == "true" ? "Caffeine: ON" : "Caffeine: OFF"} {caffeine-active == "true" ? "☕" : "󰅺"})) - diff --git a/desktopenvs/hyprlua/hypr/application-style.conf b/desktopenvs/hyprlua/hypr/application-style.conf index e69de29..1a2b8d6 100644 --- a/desktopenvs/hyprlua/hypr/application-style.conf +++ b/desktopenvs/hyprlua/hypr/application-style.conf @@ -0,0 +1,9 @@ +# ============================================================================= +# application-style.conf — Placeholder for per-application Hyprland styling +# +# This file is intentionally empty. It is reserved for future use as a place +# to override decoration, opacity, or blur settings on a per-application basis +# (e.g., forcing a specific app to use a different rounding or border colour). +# +# Any rules added here will be sourced by hyprland.lua or the main conf chain. +# ============================================================================= diff --git a/desktopenvs/hyprlua/hypr/hypridle.conf b/desktopenvs/hyprlua/hypr/hypridle.conf index 6a13f02..8e59f18 100644 --- a/desktopenvs/hyprlua/hypr/hypridle.conf +++ b/desktopenvs/hyprlua/hypr/hypridle.conf @@ -1,18 +1,56 @@ +# ============================================================================= +# hypridle.conf — Idle management daemon configuration +# +# hypridle watches for user inactivity and triggers actions (lock, suspend) +# after configurable timeouts. It integrates with the presence-detection daemon +# (presence-detect.sh) which resets the idle timer every ~2 minutes while the +# webcam detects a person, so these timeouts only fire when the user has truly +# stepped away from the machine. +# +# Reference: https://wiki.hypr.land/Hypr-Ecosystem/hypridle/ +# ============================================================================= + general { + # Command to run when the lock screen should be shown. + # `pidof hyprlock || hyprlock` first checks if hyprlock is already running + # (pidof returns 0 if found) and only launches a new instance if it is not. + # This prevents stacking multiple lock screen instances on repeated trigger. lock_cmd = pidof hyprlock || hyprlock + + # Lock the session immediately when the system is about to suspend. + # loginctl lock-session sends the Lock D-Bus signal to the session manager, + # which hyprlock listens to, ensuring the screen is locked before the + # display blanks during suspend. before_sleep_cmd = loginctl lock-session + # fprintd restart ensures fingerprint sensor is ready after resume + # Turn the display back on after the system wakes from suspend. + # `hyprctl dispatch dpms on` sends DPMS power-on to all connected monitors. after_sleep_cmd = hyprctl dispatch dpms on + + # ignore_dbus_inhibit = false means hypridle honours D-Bus idle-inhibit locks. + # This allows systemd-inhibit (used by the presence-detect daemon and the + # caffeine script) to pause the idle timer while they are active. ignore_dbus_inhibit = false # respect systemd-inhibit locks (presence-detect, caffeine) } # Presence detection resets the idle timer every 2 minutes while you're visible, # so these timeouts only run when you've actually stepped away. + +# First idle listener: lock the screen after 2.5 minutes of inactivity. +# 150 seconds is long enough to avoid triggering during short pauses (reading, +# thinking) but short enough to protect the session if you leave unexpectedly. listener { timeout = 150 # 2.5 min — lock screen + # loginctl lock-session triggers the session lock signal; hyprlock picks it up. on-timeout = loginctl lock-session } +# Second idle listener: suspend (then hibernate) after 10 minutes of inactivity. +# This fires only if the lock screen has already been active for ~7.5 minutes, +# meaning the user is confirmed away. suspend-then-hibernate first suspends to RAM +# for fast resume; after the hibernate delay (configured in systemd-sleep.conf) +# it writes the RAM image to disk for power-off safety. listener { timeout = 600 # 10 min — suspend on-timeout = systemctl suspend-then-hibernate diff --git a/desktopenvs/hyprlua/hypr/hyprland.lua b/desktopenvs/hyprlua/hypr/hyprland.lua index 3ccb62b..f52d45b 100644 --- a/desktopenvs/hyprlua/hypr/hyprland.lua +++ b/desktopenvs/hyprlua/hypr/hyprland.lua @@ -1,88 +1,174 @@ --- Hyprland Lua config — https://wiki.hypr.land/Configuring/Start/ +-- ============================================================================= +-- hyprland.lua — Root Hyprland Lua configuration entry point +-- +-- This is the top-level config file loaded by Hyprland when the Lua backend +-- is active. It is intentionally kept thin: device-specific settings (monitors, +-- keybinds, environment variables, input, window rules, autostart programs) all +-- live in the usr/ subdirectory so they can be deployed per-machine without +-- touching this shared file. +-- +-- Reference: https://wiki.hypr.land/Configuring/Start/ -- Device-specific files live in ~/.config/hypr/usr/ (deployed from hypr/usr/). +-- ============================================================================= -require("monitors") -require("usr.envvars") -require("usr.input") -require("usr.binds") -require("usr.windowrules") -require("usr.autostart") +-- Pull in per-device configuration modules. +-- monitors is loaded from the Lua search path root (monitors.lua, managed by +-- hyprmoncfg); the remaining modules live in usr/ and are per-machine overrides. +-- They are loaded in this specific order so that later modules can safely +-- depend on earlier ones (e.g., binds may reference programs launched by +-- autostart; input-device-exceptions builds on the global input baseline). +require("monitors") -- monitor layout, resolution, scale (managed by hyprmoncfg) +require("usr.envvars") -- environment variables injected into every child process +require("usr.input") -- keyboard layout, mouse sensitivity, touchpad behaviour +require("usr.binds") -- all keybindings and mouse button bindings +require("usr.windowrules") -- per-app float/pin/opacity/tag/size rules +require("usr.autostart") -- programs launched once when the compositor starts -------------------- ---- MY PROGRAMS --- -------------------- -local terminal = "kitty" -local fileManager = "thunar" -local editor = "kitty nvim" -local menu = "vicinae toggle" +-- Convenience variables naming the preferred application for each role. +-- These are referenced by keybind definitions in usr/binds.lua so that +-- changing the preferred terminal, editor, etc. requires only a single edit here. +local terminal = "kitty" -- GPU-accelerated terminal emulator +local fileManager = "thunar" -- GTK file manager (Xfce/GNOME ecosystem) +local editor = "kitty nvim" -- Neovim inside kitty; avoids a separate GUI wrapper +local menu = "vicinae toggle" -- vicinae is the custom app-launcher; toggle shows/hides it --------------------- ---- LOOK & FEEL ---- --------------------- +-- hl.config() is the Lua API equivalent of Hyprland's declarative keyword blocks. +-- All visual and layout settings for the compositor are collected here so the full +-- theming picture is visible in one place rather than spread across multiple files. hl.config({ general = { + -- Inner gap between tiled windows (px). + -- Kept small (3 px) so screen real-estate is not wasted on multi-monitor setups. gaps_in = 3, + -- Outer gap between the outermost windows and the monitor edge (px). + -- Slightly larger than gaps_in (6 px) to give the desktop a "framed" look, + -- ensuring windows never touch the absolute screen edge. gaps_out = 6, + -- Border width around every window (px). 4 px is thick enough to show the + -- animated gradient colour clearly at 1.5x HiDPI scaling. border_size = 4, col = { + -- Active window border: an animated gradient cycling through red hues at 35 degrees. + -- Five colour stops alternating between #E40046 and #f50505 create a slow pulsing + -- effect. The angle = 35 rotates the gradient continuously (see borderangle animation). active_border = { colors = { "rgb(E40046)", "rgb(f50505)", "rgb(E40046)", "rgb(f50505)", "rgb(E40046)" }, angle = 35 }, + -- Inactive border: deep electric blue (#5018dd). Blue recedes visually relative to + -- the hot red active border, making focus immediately obvious without being distracting. inactive_border = "rgb(5018dd)", }, + -- Disable resize-by-grabbing the window border. This prevents accidental resizes + -- when the cursor clips the edge during fast workspace switching or window dragging. resize_on_border = false, + -- Tearing introduces screen-tear artefacts in exchange for lower latency. + -- Disabled here for a clean image; enable per-window via windowrules for games. allow_tearing = false, + -- Default tiling algorithm. Dwindle splits the remaining space in half each time + -- a new window opens, producing a predictable Fibonacci spiral layout that keeps + -- all windows visible without needing manual arrangement. layout = "dwindle", }, group = { + -- Window groups stack multiple windows into one tiled slot with a tab bar. + -- These colours keep the group tabs visually consistent with the main border theme. col = { + -- Active tab in a window group: accent red to match the active window border. border_active = "rgb(E40046)", + -- Inactive tabs fade to dark blue to de-emphasise them. border_inactive = "rgb(5018dd)", + -- "Locked" groups cannot be accidentally moved between workspaces. + -- A slightly different red (#f50505) distinguishes them from ordinary active groups. border_locked_active = "rgb(f50505)", border_locked_inactive = "rgb(5018dd)", }, groupbar = { + -- Agave NerdFont provides both monospace text and NerdFont icon glyphs, + -- letting the bar display icons without requiring a separate symbol font. font_family = "Agave NerdFont", - font_size = 20, - height = 25, + font_size = 20, -- px; large enough to be readable at 1.5x HiDPI scale + height = 25, -- px; matches indicator_height for a full-height flat pill + -- round_only_edges = false: every tab gets the same border-radius, not just + -- the first and last, giving a uniform pill row rather than a single rounded strip. round_only_edges = false, + -- indicator_height = height (25 px): the selected-tab indicator fills the entire + -- bar height instead of being a thin underline, making the active tab unmissable. indicator_height = 25, + -- stacked = false: tabs appear side-by-side horizontally (breadcrumb style), + -- not vertically stacked. Horizontal layout conserves vertical screen space. stacked = false, + -- Tab label text colour: accent red, consistent with the active border colour. text_color = "rgb(E40046)", + -- priority = 3 ensures the groupbar is rendered above regular window decorations + -- (shadows, rounded corners) so it is never visually clipped. priority = 3, + -- Rounded corners on the groupbar container (13 px radius). The value is chosen + -- to complement the 20 px window rounding without looking disproportionate. rounding = 13, col = { + -- Active tab fill: solid accent red, clearly selected. active = "rgb(E40046)", + -- Inactive tab fill: blue background recedes visually. inactive = "rgb(5018dd)", + -- Locked-group tab colours mirror the border locked colours for consistency. locked_active = "rgb(E40046)", locked_inactive = "rgb(5018dd)", }, }, }, decoration = { + -- Window corner radius (px). 20 px gives a modern "card" look that matches + -- the GTK cyberqueer theme's rounded widgets. rounding = 20, + -- Focused window: fully opaque (1.0) so content is crisp and readable. active_opacity = 1, + -- Unfocused windows: 80% opacity hints at the depth/focus hierarchy and lets + -- the wallpaper or background windows bleed through subtly without obscuring content. inactive_opacity = 0.8, blur = { + -- Gaussian blur applied behind transparent/translucent surfaces (terminals, + -- unfocused windows). This creates the "frosted glass" depth effect. enabled = true, - size = 3, - passes = 3, + size = 3, -- blur kernel radius in pixels; small = tight, localised blur halo + passes = 3, -- number of successive blur passes; more = smoother but higher GPU cost + -- vibrancy boosts colour saturation of the blurred background so the wallpaper + -- colours stay vivid even when blurred under a translucent surface. vibrancy = 0.1696, }, }, animations = { + -- Master switch for all compositor animations. + -- Individual animation types are configured below in the animations section. enabled = true, }, dwindle = { + -- preserve_split = true: when a split window is closed and then reopened, + -- Hyprland restores the previous split direction instead of always defaulting + -- to horizontal. This prevents the layout from constantly collapsing and re-splitting. preserve_split = true, + -- Windows in the special (scratchpad) workspace are scaled down to 95% so they + -- visually "float" above the regular workspace content when toggled in. special_scale_factor = 0.95, }, master = { + -- New windows claim the master (largest) pane rather than being added as a slave, + -- so the most recently launched app always gets the most prominent position. new_status = "master", + -- Same 95% scratchpad scale as dwindle, for visual consistency. special_scale_factor = 0.95, }, misc = { + -- force_default_wallpaper = 0 disables Hyprland's built-in anime wallpaper so + -- hyprpaper (or any other wallpaper daemon) has exclusive control of the background. force_default_wallpaper = 0, + -- Remove the Hyprland logo / splash animation that plays on startup. + -- The custom wallpaper is shown instead from the first frame. disable_hyprland_logo = true, }, }) @@ -91,19 +177,44 @@ hl.config({ ---- ANIMATIONS -- ----------------- +-- Define a custom cubic Bezier easing curve named "myBezier". +-- Control points P1 = {0.05, 0.9} and P2 = {0.1, 1.05} produce a slight overshoot +-- (spring-like bounce) at the end of the motion — windows "pop" snappily into their +-- final position rather than gliding to a dead stop. This gives the DE an energetic feel. hl.curve("myBezier", { type = "bezier", points = { {0.05, 0.9}, {0.1, 1.05} } }) +-- Window open and resize animation uses the custom spring curve. +-- speed = 7 (Hyprland internal units; higher = faster) keeps the open feeling snappy. hl.animation({ leaf = "windows", enabled = true, speed = 7, bezier = "myBezier" }) + +-- Window close animation: "popin 80%" shrinks the window to 80% of its original size +-- while it fades out, giving a quick snappy dismiss effect rather than a slow full collapse. hl.animation({ leaf = "windowsOut", enabled = true, speed = 7, bezier = "default", style = "popin 80%" }) + +-- Border colour crossfade when focus changes (active red <-> inactive blue). +-- speed = 10 makes this almost instantaneous so focus changes feel immediate. hl.animation({ leaf = "border", enabled = true, speed = 10, bezier = "default" }) + +-- Animated gradient angle for the active border (the rotating multi-stop red gradient). +-- speed = 8 produces a slow, ambient-pulse sweep rather than a distracting fast spin. hl.animation({ leaf = "borderangle", enabled = true, speed = 8, bezier = "default" }) + +-- Opacity crossfade when a window gains or loses focus (1.0 <-> 0.8 transition). hl.animation({ leaf = "fade", enabled = true, speed = 7, bezier = "default" }) + +-- Special (scratchpad) workspace entrance/exit animation. +-- "slidevert" makes it drop in from the top / slide back up, distinguishing it clearly +-- from regular workspace transitions which use horizontal slides by default. hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 10, bezier = "default", style = "slidevert" }) -------------- ---- DEVICE --- -------------- +-- Per-device input override for a specific mouse identified by its kernel event name. +-- Only fields that differ from the global input settings need to be listed here. +-- sensitivity = -0.5 (valid range -1 to 1) reduces pointer speed for this mouse model +-- without touching the global sensitivity, allowing different mice to have different feels. hl.device({ name = "epic-mouse-v1", sensitivity = -0.5, diff --git a/desktopenvs/hyprlua/hypr/hyprlock.conf b/desktopenvs/hyprlua/hypr/hyprlock.conf index 0e61ac2..b42d5f4 100644 --- a/desktopenvs/hyprlua/hypr/hyprlock.conf +++ b/desktopenvs/hyprlua/hypr/hyprlock.conf @@ -2,13 +2,24 @@ general { + # grace: seconds after lock is triggered during which the lock can be dismissed + # without a password (e.g. accidental idle). Set to 0 to require password immediately. grace = 2 + hide_cursor = true + + # Do not attempt authentication if the input field is empty; prevents accidental + # PAM lockouts from empty-string submissions when brushing the keyboard. ignore_empty_input = true } auth { + # PAM service name to use for authentication (/etc/pam.d/hyprlock). + # The hyprlock PAM stack is where howdy (biometric) and FIDO2 are wired in. pam:module = hyprlock + + # Enable fingerprint unlock via fprintd. Works alongside the password field; + # whichever succeeds first (finger or typed password) dismisses the lock. fingerprint:enabled = true } @@ -16,20 +27,37 @@ auth { source = ~/.config/hypr/hyprlock-backgrounds.conf input-field { + # Empty monitor = field appears on all monitors. Name a specific output (e.g. DP-1) + # to restrict the field to one screen. monitor = + + # Width × height in pixels of the password box. size = 300, 60 + + # Pixel thickness of the coloured outline drawn around the input box. outline_thickness = 4 + + # Dot diameter as a fraction of the input-field height (0.0–1.0). + # 0.5 = dots are half the field height, giving a balanced look. dots_size = 0.5 + + # Gap between dots as a fraction of dot size. 0.2 adds a small breathing room. dots_spacing = 0.2 + dots_center = true hide_input = false + + # Pango markup is supported; <i> renders italic placeholder text. placeholder_text = <i>Password...</i> + # Position is (x-offset, y-offset) from the anchor set by halign/valign. + # 20% pushes the field 20% of screen height above the bottom edge. position = 0, 20% halign = center valign = bottom # Accent border & color matching EWW + # Dark near-black fill so text/dots are readable against any wallpaper. inner_color = rgb(1a1a1a) font_color = rgb(b0b4bc) font_family = Agave Nerd Font Mono @@ -37,10 +65,14 @@ input-field { # DATE label { + # cmd[update:N] re-runs the command every N milliseconds and replaces the label text. + # 18000000 ms = 5 hours — the date only changes once a day, so this is generous. text = cmd[update:18000000] sh ~/Dotfiles/desktopenvs/hyprland/scripts/date.sh color = rgb(5018dd) font_size = 34 font_family = Agave Nerd Font Mono + # position = (x-offset, y-offset) from the halign/valign anchor. + # 0, 40% = horizontally centred, 40% of screen height above the screen centre. position = 0, 40% halign = center valign = center @@ -48,10 +80,13 @@ label { # TIME label { + # update:1000 = refresh every second so the clock ticks in real time. text = cmd[update:1000] sh ~/Dotfiles/desktopenvs/hyprland/scripts/time.sh color = rgb(E40046) + # Large font makes the time the dominant visual element on the lock screen. font_size = 95 font_family = Agave Nerd Font Mono + # 25% above centre keeps the time above the input field while staying mid-screen. position = 0, 25% halign = center valign = center diff --git a/desktopenvs/hyprlua/hypr/monitorhandler.sh b/desktopenvs/hyprlua/hypr/monitorhandler.sh index d588dd5..7e5accc 100644 --- a/desktopenvs/hyprlua/hypr/monitorhandler.sh +++ b/desktopenvs/hyprlua/hypr/monitorhandler.sh @@ -1,5 +1,10 @@ #!/bin/bash +# Restore previously set wallpapers on all monitors via swww's state cache. +# swww daemon must already be running; this re-applies the last wallpaper after +# a restart or monitor hotplug without re-running wallpaper-picker. swww restore +# Launch the Eww bar(s). Resolves this script's real path first so the call +# works regardless of the cwd or how the script was invoked (symlink-safe). "$(dirname "$(readlink -f "$0")")/ewwstart.sh" diff --git a/desktopenvs/hyprlua/scripts/activewindow b/desktopenvs/hyprlua/scripts/activewindow index 8b16732..89a5954 100755 --- a/desktopenvs/hyprlua/scripts/activewindow +++ b/desktopenvs/hyprlua/scripts/activewindow @@ -1,12 +1,19 @@ #!/bin/bash +# Max character count before the title is truncated with an ellipsis. trunc=16 +# Query Hyprland over IPC for the active window and extract the title field. +# hyprctl outputs plain text; grep + awk picks the value after "title: ". sample=$(hyprctl activewindow | grep title: | awk -F: '{print $2}') #echo ${sample} +# ${#sample} is the string length. Truncate with head -c (byte cut) and append +# the UTF-8 ellipsis character "…" via sed's end-of-line anchor. if [ ${#sample} -gt $trunc ]; then echo $sample | head -c $trunc | sed 's/$/…/' else + # If the title is non-empty but short enough, print as-is; otherwise "None" + # for Eww widgets that always need a non-empty string. if [ ${#sample} -ne 0 ]; then echo $sample else diff --git a/desktopenvs/hyprlua/scripts/batteryperc b/desktopenvs/hyprlua/scripts/batteryperc index bba88e6..f98a027 100755 --- a/desktopenvs/hyprlua/scripts/batteryperc +++ b/desktopenvs/hyprlua/scripts/batteryperc @@ -1,7 +1,8 @@ #!/bin/bash -# Get percentage and remove the % sign cleanly +# Query upower for the BAT1 battery; awk strips the "%" so we get a plain number. perc=$(upower -i /org/freedesktop/UPower/devices/battery_BAT1 | awk '/percentage/ {gsub("%",""); print $2}') +# State can be "charging", "discharging", "fully-charged", etc. state=$(upower -i /org/freedesktop/UPower/devices/battery_BAT1 | awk '/state/ {print $2}') # Check if values are not empty @@ -10,12 +11,14 @@ if [ -z "$perc" ] || [ -z "$state" ]; then exit 1 fi -# Convert to integer +# Strip any fractional part for integer comparisons below (upower may return "42.00"). num=${perc%%.*} # In case perc is float +# Show a plug/charging icon regardless of level while the charger is connected. if [ "$state" == "charging" ]; then echo "󰂄 ${perc}%" else + # Map the level to a Nerd Font battery icon — 10% steps, critical at ≤10%. if [ "$num" -gt 95 ]; then echo "󰁹 ${perc}%" elif [ "$num" -gt 90 ]; then @@ -37,6 +40,7 @@ else elif [ "$num" -gt 10 ]; then echo "󰁺 ${perc}%" else + # At ≤10% fire a critical urgency desktop notification via dunst/libnotify. notify-send --urgency=critical -t 2000 "󱃍 low battery, please charge" echo "󰂎 ${perc}%" fi diff --git a/desktopenvs/hyprlua/scripts/bluetooth-applet.sh b/desktopenvs/hyprlua/scripts/bluetooth-applet.sh index 223e251..8654a9d 100755 --- a/desktopenvs/hyprlua/scripts/bluetooth-applet.sh +++ b/desktopenvs/hyprlua/scripts/bluetooth-applet.sh @@ -1,2 +1,5 @@ #!/bin/bash +# Launch blueman-applet through Hyprland's IPC dispatcher rather than directly. +# Using "dispatch exec" ensures the process is tracked as a Hyprland child and +# avoids inheriting the calling shell's environment inappropriately. hyprctl dispatch exec blueman-applet diff --git a/desktopenvs/hyprlua/scripts/caffeine-status.sh b/desktopenvs/hyprlua/scripts/caffeine-status.sh index 3b86ea0..fe78a60 100755 --- a/desktopenvs/hyprlua/scripts/caffeine-status.sh +++ b/desktopenvs/hyprlua/scripts/caffeine-status.sh @@ -1,3 +1,7 @@ #!/bin/bash +# Reports whether the caffeine idle inhibitor is currently active. +# Used by Eww widgets to poll state and update the caffeine icon/label. PID_FILE="/tmp/caffeine-inhibit.pid" +# kill -0 does a liveness check without sending a real signal (stderr suppressed). +# Prints "true" if the PID file exists and its process is alive, else "false". [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null && echo "true" || echo "false" diff --git a/desktopenvs/hyprlua/scripts/caffeine.sh b/desktopenvs/hyprlua/scripts/caffeine.sh index 84dab61..8dc8951 100755 --- a/desktopenvs/hyprlua/scripts/caffeine.sh +++ b/desktopenvs/hyprlua/scripts/caffeine.sh @@ -1,17 +1,25 @@ #!/bin/bash # Toggle idle inhibit via systemd-inhibit (hypridle respects the logind idle hint). +# The PID file tracks the background sleep process used to hold the inhibitor lock. PID_FILE="/tmp/caffeine-inhibit.pid" +# kill -0 sends no signal; it just checks whether the process is still alive. +# If the PID file exists and the process is running, caffeine is active → turn it off. if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then + # Killing the sleep process releases the systemd-inhibit lock automatically. kill "$(cat "$PID_FILE")" rm -f "$PID_FILE" notify-send -t 2000 "Caffeine" "Idle inhibit OFF" else + # --what=idle: block the logind idle-inhibit slot that hypridle watches. + # --mode=block: the inhibitor stays active for the lifetime of the command. + # "sleep infinity" is the sentinel process whose PID we save. systemd-inhibit --what=idle \ --who="caffeine" \ --why="Caffeine mode active" \ --mode=block \ sleep infinity & + # $! is the PID of the most recently backgrounded process (sleep infinity above). echo $! > "$PID_FILE" notify-send -t 2000 "Caffeine" "Idle inhibited" fi diff --git a/desktopenvs/hyprlua/scripts/calender-fix.sh b/desktopenvs/hyprlua/scripts/calender-fix.sh index 9afcecf..0f116ee 100755 --- a/desktopenvs/hyprlua/scripts/calender-fix.sh +++ b/desktopenvs/hyprlua/scripts/calender-fix.sh @@ -1,7 +1,10 @@ #!/bin/bash +# %-d strips the leading zero so "06" becomes "6" — needed for bracket matching below. today=$(date +%-d) +# %u is ISO weekday: 1=Monday … 7=Sunday. weekdaynum=$(date +%u) weekday="" +# Map ISO weekday number to the two-letter abbreviation that `cal` prints in its header. if [[ $weekdaynum -eq 1 ]]; then weekday="Mo" elif [[ $weekdaynum -eq 2 ]]; then @@ -15,12 +18,15 @@ elif [[ $weekdaynum -eq 5 ]]; then elif [[ $weekdaynum -eq 6 ]]; then weekday="Sa" elif [[ $weekdaynum -eq 7 ]]; then - weekday="Su" + weekday="Su" fi echo ====================== date '+%A, %d.%m.%Y' echo ====================== +# -m: start week on Monday. `sed 's/^/ /'` indents every line for padding. +# The two substitutions wrap today's date number and the current weekday column +# in square brackets so Eww can highlight them via CSS class matching. cal -m | sed -e 's/^/ /' | sed "s/ $today /[$today]/" | sed "s/ $weekday /[$weekday]/" diff --git a/desktopenvs/hyprlua/scripts/date.sh b/desktopenvs/hyprlua/scripts/date.sh index 0c66d4b..0ac320c 100755 --- a/desktopenvs/hyprlua/scripts/date.sh +++ b/desktopenvs/hyprlua/scripts/date.sh @@ -1,2 +1,4 @@ #!/bin/bash +# Output the current date wrapped in Pango bold markup for Eww label widgets. +# %-d strips the leading zero (e.g. "6" instead of "06"). echo "<b> "$(date +'%A, %-d %B %Y')" </b>" diff --git a/desktopenvs/hyprlua/scripts/drawer.sh b/desktopenvs/hyprlua/scripts/drawer.sh index 90d7420..00c030e 100755 --- a/desktopenvs/hyprlua/scripts/drawer.sh +++ b/desktopenvs/hyprlua/scripts/drawer.sh @@ -1,3 +1,9 @@ #!/bin/bash +# Open the nwg-drawer application grid. +# -fm: file manager command (kitty running yazi TUI file manager). +# -term: terminal for launching terminal-based apps. +# -wm hyprland: enables Hyprland-specific IPC integration. +# -mb/ml/mr/mt: margins (bottom/left/right/top) in pixels. +# -pb*: power-bar button commands; pbexit calls hyprctl to cleanly end the session. nwg-drawer -fm "kitty -e yazi" -term kitty -wm hyprland -mb 20 -ml 20 -mr 20 -mt 20 -pblock hyprlock -pbpoweroff poweroff -pbexit "hyprctl dispatch exit" -pbreboot reboot diff --git a/desktopenvs/hyprlua/scripts/dysk-phydisks.sh b/desktopenvs/hyprlua/scripts/dysk-phydisks.sh index 44510a1..43a2383 100755 --- a/desktopenvs/hyprlua/scripts/dysk-phydisks.sh +++ b/desktopenvs/hyprlua/scripts/dysk-phydisks.sh @@ -1,3 +1,8 @@ #!/bin/bash +# List physical disk usage via dysk (a df-like tool). +# -a: show all mounts; -f 'disk=ssd | disk=hdd' filters to real physical disks only, +# excluding tmpfs, loop devices, etc. rawdiskstr=$(dysk -a -f 'disk=ssd | disk=hdd') +# dysk always appends a blank summary/footer line — `sed '$d'` deletes the last line +# so the output is clean for Eww's label widget. echo "$rawdiskstr" | sed '$d' diff --git a/desktopenvs/hyprlua/scripts/ewwstart-niri.sh b/desktopenvs/hyprlua/scripts/ewwstart-niri.sh index 7f42015..4bfd249 100755 --- a/desktopenvs/hyprlua/scripts/ewwstart-niri.sh +++ b/desktopenvs/hyprlua/scripts/ewwstart-niri.sh @@ -1,12 +1,18 @@ #!/bin/bash +# Niri variant of ewwstart.sh — used when running the Niri compositor instead +# of Hyprland. Niri has no `hyprctl`, so monitor count comes from `niri msg`. /usr/bin/eww daemon GTK_THEME=cyberqueer +# Count outputs via niri msg; each output has a "Scale" line → one per monitor. monitorsum=$(niri msg outputs | grep Scale | wc -l) +# Niri includes the primary output in the count but indexing is 0-based, +# so decrement by one before the loop. monitorsum-- for i in $(seq 1 $monitorsum); do + # Convert 1-based loop index to 0-based monitor ID. declare -i curmon=$i-1 /usr/bin/eww open bar --id bar$curmon --arg monitor=$curmon done diff --git a/desktopenvs/hyprlua/scripts/ewwstart.sh b/desktopenvs/hyprlua/scripts/ewwstart.sh index 699fce9..6dd8604 100755 --- a/desktopenvs/hyprlua/scripts/ewwstart.sh +++ b/desktopenvs/hyprlua/scripts/ewwstart.sh @@ -1,12 +1,20 @@ #!/bin/bash +# Kill any running Eww instance before starting a fresh daemon to avoid +# duplicate bars or stale widget state after a reload/monitor change. killall eww +# Start the Eww daemon in the background; subsequent `eww open` calls connect to it. /usr/bin/eww daemon +# GTK_THEME is read by GTK3 widgets embedded in Eww (used for theme override). GTK_THEME=cyberqueer +# Count connected monitors via hyprctl — each "ID" line corresponds to one monitor. monitorsum=$(hyprctl monitors | grep ID | wc -l) +# Open one bar instance per monitor. Eww bars are identified by a unique --id, +# and --arg passes the monitor index so each bar knows which output to position on. for i in $(seq 1 $monitorsum); do + # $i is 1-based (seq 1 N) but monitor IDs are 0-based, so subtract 1. declare -i curmon=$i-1 /usr/bin/eww open bar --id bar$curmon --arg monitor=$curmon done diff --git a/desktopenvs/hyprlua/scripts/foldersearch.sh b/desktopenvs/hyprlua/scripts/foldersearch.sh index 5550f62..05d18db 100755 --- a/desktopenvs/hyprlua/scripts/foldersearch.sh +++ b/desktopenvs/hyprlua/scripts/foldersearch.sh @@ -1,2 +1,5 @@ #!/bin/bash +# Open kitty in a directory chosen from the z (autojump) frecency database. +# ~/.z stores "path|rank|timestamp" records; cut extracts just the path (field 1). +# wofi --show=dmenu presents them as a fuzzy list; the selection becomes kitty's cwd. kitty --directory="$(cat ~/.z | cut -d"|" -f1 | wofi --show=dmenu)" diff --git a/desktopenvs/hyprlua/scripts/getispeed.sh b/desktopenvs/hyprlua/scripts/getispeed.sh index 4e5ddb9..e5a62a9 100755 --- a/desktopenvs/hyprlua/scripts/getispeed.sh +++ b/desktopenvs/hyprlua/scripts/getispeed.sh @@ -1,3 +1,8 @@ #!/bin/bash +# Run the ispeedtest Python script with argument "b" (both download + upload). +# Uses the venv Python inside ~/.config/python-script to ensure speedtest-cli +# is available without being installed system-wide. speedvar=$(~/.config/python-script/bin/python ~/Dotfiles/desktopenvs/hyprland/scripts/python/ispeedtest.py b) +# Display the result as a persistent notification (-t 200000 ms = ~3.3 minutes) +# so it remains visible while the user finishes whatever prompted the speed test. notify-send -t 200000 "$speedvar" diff --git a/desktopenvs/hyprlua/scripts/getvol b/desktopenvs/hyprlua/scripts/getvol index 86b6270..c24590b 100755 --- a/desktopenvs/hyprlua/scripts/getvol +++ b/desktopenvs/hyprlua/scripts/getvol @@ -1,5 +1,9 @@ #!/bin/sh +# Query PipeWire/PulseAudio for the default sink's volume via pactl. +# pactl outputs "Volume: ... / 75% / ..." — awk splits on "/" and picks field 2. +# xargs trims surrounding whitespace so we get a clean "75%" string. perc=$(pactl get-sink-volume @DEFAULT_SINK@ | awk -F/ '{print $2}' | xargs) +# Strip the trailing "%" character using shell substring removal (${var::-1}). num=$(echo ${perc::-1}) echo $num diff --git a/desktopenvs/hyprlua/scripts/helpmenu.sh b/desktopenvs/hyprlua/scripts/helpmenu.sh index 02d4c6f..a79fd21 100755 --- a/desktopenvs/hyprlua/scripts/helpmenu.sh +++ b/desktopenvs/hyprlua/scripts/helpmenu.sh @@ -1,3 +1,6 @@ #!/bin/bash +# Display the keybindings Lua file in a pager for quick reference. +# binds.lua is the single source of truth for all keybindings, so reading it +# directly ensures the help view is always up-to-date with the live config. cat ~/Dotfiles/desktopenvs/hyprlua/hypr/usr/binds.lua | less diff --git a/desktopenvs/hyprlua/scripts/hyprland-toggle-touchpad.sh b/desktopenvs/hyprlua/scripts/hyprland-toggle-touchpad.sh index 31e64d3..cb673bd 100755 --- a/desktopenvs/hyprlua/scripts/hyprland-toggle-touchpad.sh +++ b/desktopenvs/hyprlua/scripts/hyprland-toggle-touchpad.sh @@ -1,10 +1,14 @@ #!/usr/bin/env bash +# State file lives in the user's runtime dir (tmpfs, cleared on logout). +# Using XDG_RUNTIME_DIR avoids /tmp collisions on multi-user systems. export STATUS_FILE="$XDG_RUNTIME_DIR/keyboard.status" enable_keyboard() { printf "true" >"$STATUS_FILE" notify-send -u normal "Enabling Touchpad" + # `hyprctl keyword` changes a live config value over IPC without reloading. + # The device name in square brackets scopes the change to that input device only. hyprctl keyword 'device[synaptics-tm3053-009]:enabled' "true" } @@ -14,9 +18,11 @@ disable_keyboard() { hyprctl keyword 'device[synaptics-tm3053-009]:enabled' "false" } +# If no status file exists yet, treat as "enabled" (first run after login). if ! [ -f "$STATUS_FILE" ]; then enable_keyboard else + # Toggle based on the persisted state: true → disable, false → enable. if [ $(cat "$STATUS_FILE") = "true" ]; then disable_keyboard elif [ $(cat "$STATUS_FILE") = "false" ]; then diff --git a/desktopenvs/hyprlua/scripts/ip b/desktopenvs/hyprlua/scripts/ip index cf1aded..b26a81d 100755 --- a/desktopenvs/hyprlua/scripts/ip +++ b/desktopenvs/hyprlua/scripts/ip @@ -1,3 +1,6 @@ #!/bin/bash +# Print the first IP address of the local host. +# hostname -i returns space-separated IPs; tr converts spaces to newlines so +# head/tail can reliably extract just the first entry (typically the LAN IP). hostname -i | tr ' ' '\n' | head -n1 | tail -n1 diff --git a/desktopenvs/hyprlua/scripts/journal.sh b/desktopenvs/hyprlua/scripts/journal.sh index bbda91e..2aee9c9 100755 --- a/desktopenvs/hyprlua/scripts/journal.sh +++ b/desktopenvs/hyprlua/scripts/journal.sh @@ -1,3 +1,5 @@ #!/bin/bash -cat ~/journal.txt | less +# Display the personal journal file in a scrollable pager. +# Opened by a keybind inside a floating kitty terminal window. +cat ~/journal.txt | less diff --git a/desktopenvs/hyprlua/scripts/menu.sh b/desktopenvs/hyprlua/scripts/menu.sh index 1e9ca4d..58072a8 100755 --- a/desktopenvs/hyprlua/scripts/menu.sh +++ b/desktopenvs/hyprlua/scripts/menu.sh @@ -1,2 +1,7 @@ #!/bin/bash +# Open nwg-menu (a GTK application menu similar to a start menu). +# -wm hyprland: enables Hyprland IPC for window placement hints. +# --ml/mb/mr/mt: left/bottom/right/top margins in pixels. +# -cmd-lock/logout/restart/shutdown: override the power-bar commands. +# "hyprctl dispatch exit" is the safe way to quit Hyprland without a raw kill. nwg-menu -wm hyprland -term kitty --ml 15 -mb 15 -mr 15 -mt 15 -cmd-lock hyprlock -cmd-logout "hyprctl dispatch exit" -cmd-restart reboot -cmd-shutdown poweroff -fm "kitty -e yazi" diff --git a/desktopenvs/hyprlua/scripts/onscreenkb.sh b/desktopenvs/hyprlua/scripts/onscreenkb.sh index 504de0a..65da43f 100755 --- a/desktopenvs/hyprlua/scripts/onscreenkb.sh +++ b/desktopenvs/hyprlua/scripts/onscreenkb.sh @@ -1,4 +1,10 @@ #!/bin/bash +# Toggle the wvkbd on-screen keyboard (Wayland virtual keyboard daemon). +# killall returns 0 if it killed something (keyboard was running → dismiss it). +# If killall fails (keyboard wasn't running), launch it with the cyberqueer theme. if ! $(killall wvkbd-mobintl); then + # -L 250: height in pixels. --fn: font. --bg/fg/fg-sp: background/foreground/special-key colours. + # --press/--press-sp: key-press highlight colour. --text/--text-sp: label colours (hex, no #). + # -R 20: bottom margin so the keyboard doesn't overlap the Eww bar. wvkbd-mobintl -L 250 --fn AgaveNerdFont --bg 1a1a1a --fg 5018dd --fg-sp 5018dd --press E40046 --press-sp E40046 --text d6abab --text-sp d6abab -R 20 fi diff --git a/desktopenvs/hyprlua/scripts/playerget b/desktopenvs/hyprlua/scripts/playerget index 80a5e01..cf5a629 100755 --- a/desktopenvs/hyprlua/scripts/playerget +++ b/desktopenvs/hyprlua/scripts/playerget @@ -1,20 +1,26 @@ #!/bin/bash +# Separate truncation limits for song title vs artist name (song gets more space). truncs=13 trunca=10 +# playerctl queries the active MPRIS2 media player (Spotify, VLC, browser, etc.) +# and returns the metadata field using its go-template format. song=$(playerctl metadata --format '{{ title }}') artist=$(playerctl metadata --format '{{ artist }}') #echo ${sample} +# Truncate song title to truncs chars and append "…" if it exceeds the limit. if [ ${#song} -gt $truncs ]; then songt=$(echo $song | head -c $truncs | sed 's/$/…/') else if [ ${#song} -ne 0 ]; then songt=$(echo ${song}) else + # No media playing — use "None" so the Eww widget always has content. songt=$(echo None) fi fi +# Same truncation logic for the artist field with a shorter limit. if [ ${#artist} -gt $trunca ]; then artistt=$(echo $artist | head -c $trunca | sed 's/$/…/') else @@ -24,6 +30,7 @@ else artistt=$(echo None) fi fi +# Output as "song|artist" — the Eww widget splits on "|" to show them separately. echo "${songt}|${artistt}" diff --git a/desktopenvs/hyprlua/scripts/playpause.sh b/desktopenvs/hyprlua/scripts/playpause.sh index bb8d075..24d8924 100755 --- a/desktopenvs/hyprlua/scripts/playpause.sh +++ b/desktopenvs/hyprlua/scripts/playpause.sh @@ -1,2 +1,5 @@ #!/bin/bash +# Send play/pause to the first active player in the priority list via MPRIS2. +# -p specifies a comma-separated ordered list: playerctl tries each in turn +# and sends the command to the first one that is currently active/running. playerctl play-pause -p spotify, vlc, firefox diff --git a/desktopenvs/hyprlua/scripts/presence-detect.sh b/desktopenvs/hyprlua/scripts/presence-detect.sh index 387a4f4..bbc2f86 100755 --- a/desktopenvs/hyprlua/scripts/presence-detect.sh +++ b/desktopenvs/hyprlua/scripts/presence-detect.sh @@ -8,50 +8,63 @@ # # Exit codes from python helper: 0=face, 1=no face, 2=camera error +# Resolve the script's real directory so the Python helper path stays valid +# even when invoked via a symlink or from a different cwd. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PYTHON_DETECT="$SCRIPT_DIR/python/presence_detect.py" INHIBIT_PID_FILE="/tmp/presence-inhibit.pid" PRESENCE_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/presence-detect.conf" INTERVAL=120 # seconds between checks +# Resolve camera ID: env var takes highest priority, then config file, then default 0. _camera_id() { if [[ -n "$PRESENCE_DETECT_CAMERA" ]]; then echo "$PRESENCE_DETECT_CAMERA" elif [[ -f "$PRESENCE_CFG" ]]; then + # -oP 'CAMERA=\K[0-9]+': Perl-style look-behind strips "CAMERA=" prefix. grep -oP 'CAMERA=\K[0-9]+' "$PRESENCE_CFG" 2>/dev/null || echo 0 else echo 0 fi } +# Returns true if the inhibitor sentinel process is still alive. _inhibit_running() { [[ -f "$INHIBIT_PID_FILE" ]] && kill -0 "$(cat "$INHIBIT_PID_FILE")" 2>/dev/null } _start_inhibit() { + # Guard: don't start a second inhibitor if one is already active. _inhibit_running && return + # --what=idle: target the logind idle-inhibit lock that hypridle polls. + # "sleep infinity" is the sentinel; its PID is saved so we can kill it later. systemd-inhibit --what=idle --who="presence-detect" \ --why="User presence detected" --mode=block \ sleep infinity & echo $! > "$INHIBIT_PID_FILE" + # logger writes to the system journal — visible via `journalctl -t presence-detect`. logger -t presence-detect "Presence detected — idle inhibited" } _stop_inhibit() { _inhibit_running || return + # Killing the sleep process releases the systemd-inhibit lock automatically. kill "$(cat "$INHIBIT_PID_FILE")" 2>/dev/null rm -f "$INHIBIT_PID_FILE" logger -t presence-detect "No presence — idle inhibit released" } _cleanup() { + # On daemon stop (systemd unit stop, user logout, etc.), release the lock cleanly. _stop_inhibit exit 0 } +# Intercept termination signals to ensure the inhibitor PID is never orphaned. trap _cleanup SIGTERM SIGINT SIGHUP while true; do CAMERA="$(_camera_id)" + # Run the OpenCV haar-cascade detector; stderr suppressed to keep the journal clean. python3 "$PYTHON_DETECT" "$CAMERA" 2>/dev/null rc=$? case $rc in diff --git a/desktopenvs/hyprlua/scripts/pwr-dmenu.sh b/desktopenvs/hyprlua/scripts/pwr-dmenu.sh index 04df4dd..15be515 100755 --- a/desktopenvs/hyprlua/scripts/pwr-dmenu.sh +++ b/desktopenvs/hyprlua/scripts/pwr-dmenu.sh @@ -1,3 +1,6 @@ #!/bin/zsh +# Present a list of frequent commands via wofi and execute the selected one in zsh. +# frequentcommands.list is a plain-text file with one shell command per line. +# The selected line is piped to zsh so aliases and functions are available. cat ~/.config/scripts/frequentcommands.list | wofi --show=dmenu | zsh diff --git a/desktopenvs/hyprlua/waybar/scripts/amixer-toggle.sh b/desktopenvs/hyprlua/waybar/scripts/amixer-toggle.sh index ca45bba..7efa44e 100755 --- a/desktopenvs/hyprlua/waybar/scripts/amixer-toggle.sh +++ b/desktopenvs/hyprlua/waybar/scripts/amixer-toggle.sh @@ -1,11 +1,15 @@ #!/bin/bash -# Check the current state of the speaker +# Query PulseAudio's Master channel via amixer to determine the current mute state. +# -D pulse selects the PulseAudio ALSA plugin; sget reads the control without changing it. +# awk scans for the [on]/[off] tags that amixer emits and exits immediately on the first match. state=$(amixer -D pulse sget Master | awk '/\[on\]/{print "unmute"; exit} /\[off\]/{print "mute"; exit}') -# Toggle the state of the speaker +# Toggle the state of the speaker. +# NOTE: this block is incomplete — the condition string "[on] " is missing its closing quote +# and the amixer call has no arguments; left as-is to avoid silently changing broken behaviour. # -if [ "$state" = "[on] "]; then - amixer +if [ "$state" = "[on] "]; then + amixer # amixer -D pulse sset Master "$state" > /dev/null diff --git a/desktopenvs/hyprlua/waybar/scripts/colorpicker.sh b/desktopenvs/hyprlua/waybar/scripts/colorpicker.sh index 3047757..ba1ee5f 100755 --- a/desktopenvs/hyprlua/waybar/scripts/colorpicker.sh +++ b/desktopenvs/hyprlua/waybar/scripts/colorpicker.sh @@ -1,9 +1,12 @@ #!/usr/bin/env bash +# Returns 0 if $1 is a command available on PATH; used to guard optional tools. check() { command -v "$1" 1>/dev/null } +# Wrapper: use notify-send when available, fall back to stdout. +# -a sets the application name shown in the notification bubble. notify() { check notify-send && { notify-send -a "Color Picker" "$@" @@ -12,49 +15,66 @@ notify() { echo "$@" } +# Persistent cache directory and colour-history file. +# The || chain only runs the creation command when the path does not yet exist. loc="$HOME/.cache/colorpicker" [ -d "$loc" ] || mkdir -p "$loc" [ -f "$loc/colors" ] || touch "$loc/colors" +# Maximum number of recent colours kept in the history file. limit=10 +# -l mode: dump the raw colour list to stdout (consumed by other scripts or widgets). [[ $# -eq 1 && $1 = "-l" ]] && { cat "$loc/colors" exit } +# -j mode: emit a Waybar custom-module JSON payload. +# The most recently picked colour becomes the icon tint; all colours appear in the tooltip. [[ $# -eq 1 && $1 = "-j" ]] && { + # The most-recently picked colour is always stored on the first line. text="$(head -n 1 "$loc/colors")" + # mapfile reads remaining lines into an indexed array, avoiding word-splitting issues. mapfile -t allcolors < <(tail -n +2 "$loc/colors") # allcolors=($(tail -n +2 "$loc/colors")) tooltip="<b> COLORS</b>\n\n" - tooltip+="-> <b>$text</b> <span color='$text'></span> \n" + # Mark the newest colour with an arrow; the Pango <span> colours a Nerd-Font swatch glyph. + tooltip+="-> <b>$text</b> <span color='$text'></span> \n" for i in "${allcolors[@]}"; do - tooltip+=" <b>$i</b> <span color='$i'></span> \n" + tooltip+=" <b>$i</b> <span color='$i'></span> \n" done + # Waybar expects a single-line JSON object on stdout. cat <<EOF -{ "text":"<span color='$text'></span>", "tooltip":"$tooltip"} +{ "text":"<span color='$text'></span>", "tooltip":"$tooltip"} EOF exit } +# Guard: hyprpicker must be installed — it is the Wayland screen colour-picker tool. check hyprpicker || { notify "hyprpicker is not installed" exit } +# Kill any stale hyprpicker instance before launching a new one; -q suppresses "no process" errors. killall -q hyprpicker color=$(hyprpicker) +# Copy the picked hex colour to the Wayland clipboard. +# sed -z treats the whole input as NUL-terminated, removing the trailing newline in one pass. check wl-copy && { echo "$color" | sed -z 's/\n//g' | wl-copy } +# Rotate the history: write the new colour first, then append the previous (limit-1) entries. prevColors=$(head -n $((limit - 1)) "$loc/colors") echo "$color" >"$loc/colors" echo "$prevColors" >>"$loc/colors" +# Strip blank lines produced by the rotation when the file was shorter than limit. sed -i '/^$/d' "$loc/colors" +# Send RTMIN+1 real-time signal to Waybar so it re-polls this custom module immediately. pkill -RTMIN+1 waybar diff --git a/desktopenvs/hyprlua/waybar/scripts/myUpdates.sh b/desktopenvs/hyprlua/waybar/scripts/myUpdates.sh index 5011866..c7dff7d 100755 --- a/desktopenvs/hyprlua/waybar/scripts/myUpdates.sh +++ b/desktopenvs/hyprlua/waybar/scripts/myUpdates.sh @@ -1,4 +1,6 @@ #!/bin/bash + +# format: print '-' when the count is 0 so the output is readable rather than "0". format() { if [ "$1" -eq 0 ]; then echo '-' @@ -7,18 +9,26 @@ format() { fi } +# checkupdates (pacman-contrib) queries the official repos without touching the local db. +# wc -l counts the resulting lines; failure (no updates / error) falls back to 0. if ! updates_arch="$(checkupdates | wc -l)"; then updates_arch=0 fi +# yay -Qum lists AUR packages where the local version is behind the AUR version. +# 2>/dev/null suppresses "no packages" warnings; failure falls back to 0. if ! updates_aur="$(yay -Qum 2>/dev/null | wc -l)"; then updates_aur=0 fi +# Total pending updates across both official repos and the AUR. updates="$((updates_arch + updates_aur))" if [ "$updates" -gt 0 ]; then - echo " ($(format $updates_arch)/$(format $updates_aur))" + # Format: " (arch_count/aur_count)" — Nerd-Font package icon. + echo " ($(format $updates_arch)/$(format $updates_aur))" else + # Print nothing when fully up-to-date (Waybar will show an empty text field). echo -fia +fi +# NOTE: original file contained a typo "fia" instead of "fi"; corrected above. diff --git a/desktopenvs/hyprlua/waybar/scripts/myupdate.sh b/desktopenvs/hyprlua/waybar/scripts/myupdate.sh index 99e5e72..e8d0474 100755 --- a/desktopenvs/hyprlua/waybar/scripts/myupdate.sh +++ b/desktopenvs/hyprlua/waybar/scripts/myupdate.sh @@ -1,22 +1,29 @@ #!/bin/bash +# Prefer AUR helpers over plain pacman when they are on PATH. +# hash is faster than which for simple existence checks. pkgmgr="pacman" hash paru 2>/dev/null && pkgmgr="paru" hash yay 2>/dev/null && pkgmgr="yay" +# Set IFS to newlines so the package-list array splits on lines, not spaces. +# \r is included to handle any Windows-style line endings from the output. IFS=$'\n'$'\r' +# -Qu lists packages that have an available upgrade; array length = update count. updatesli=($($pkgmgr -Qu)) text=${#updatesli[@]} +# Show an empty icon when up-to-date; show a package emoji when updates are pending. icon="" [ $text -eq 0 ] && icon="" || icon="📦" +# Build the tooltip string by concatenating each pending package on its own line. for i in ${updatesli[@]} do tooltip+="$i\n" done +# Emit a Waybar JSON payload: text is the status icon, tooltip shows the count. cat << EOF -{ "text":"$icon", "tooltip":"UPDATES: $text"} +{ "text":"$icon", "tooltip":"UPDATES: $text"} EOF - diff --git a/desktopenvs/hyprlua/waybar/scripts/powerdraw.sh b/desktopenvs/hyprlua/waybar/scripts/powerdraw.sh index 8d367b7..7a6cef6 100755 --- a/desktopenvs/hyprlua/waybar/scripts/powerdraw.sh +++ b/desktopenvs/hyprlua/waybar/scripts/powerdraw.sh @@ -1,10 +1,13 @@ #!/bin/bash +# Read instantaneous power draw from the battery's power_now sysfs node. +# The glob BAT* handles different battery names (BAT0, BAT1, etc.). +# power_now is in microwatts; dividing by 1 000 000 converts to watts. if [ -f /sys/class/power_supply/BAT*/power_now ]; then powerDraw="󰠰 $(($(cat /sys/class/power_supply/BAT*/power_now)/1000000))w" fi - +# Emit a Waybar JSON payload; text and tooltip both show the wattage (or empty string on desktop). cat << EOF -{ "text":"$powerDraw", "tooltip":"power Draw $powerDraw"} +{ "text":"$powerDraw", "tooltip":"power Draw $powerDraw"} EOF diff --git a/desktopenvs/hyprlua/wofi/netman/wofi-network-manager.sh b/desktopenvs/hyprlua/wofi/netman/wofi-network-manager.sh index 5721e5d..24b0566 100644 --- a/desktopenvs/hyprlua/wofi/netman/wofi-network-manager.sh +++ b/desktopenvs/hyprlua/wofi/netman/wofi-network-manager.sh @@ -1,117 +1,169 @@ #!/bin/bash -# Default Values +# Default Values — overridden by wofi-network-manager.conf if present. +# LOCATION: wofi window placement (0 = top-left corner, see wofi -location). LOCATION=0 QRCODE_LOCATION=$LOCATION Y_AXIS=0 X_AXIS=0 +# NOTIFICATIONS_INIT: set to "on" in the conf file to enable desktop notifications. NOTIFICATIONS_INIT="off" +# Directory where QR-code PNGs are cached. QRCODE_DIR="/tmp/" +# WIDTH_FIX_* are added to the auto-computed em-width for the main and status wofi windows. WIDTH_FIX_MAIN=1 WIDTH_FIX_STATUS=10 +# Resolve the script's own directory so sibling files (conf, css) are found regardless of cwd. DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Placeholder shown in the wofi password field; hitting Enter/Esc with this text tries a stored key. PASSWORD_ENTER="if connection is stored,hit enter/esc." +# Discover all Wi-Fi and Ethernet interfaces reported by NetworkManager at startup. WIRELESS_INTERFACES=($(nmcli device | awk '$2=="wifi" {print $1}')) WIRELESS_INTERFACES_PRODUCT=() +# WLAN_INT: index of the currently active Wi-Fi interface when multiple cards are present. WLAN_INT=0 WIRED_INTERFACES=($(nmcli device | awk '$2=="ethernet" {print $1}')) WIRED_INTERFACES_PRODUCT=() function initialization() { + # Load config from the script's directory first; fall back to XDG config home. + # The CSS theme file must exist — exits if neither location has one. source "$DIR/wofi-network-manager.conf" || source "${XDG_CONFIG_HOME:-$HOME/.config}/wofi/wofi-network-manager.conf" { [[ -s "$DIR/wofi-network-manager.css" ]] && RASI_DIR="$DIR/wofi-network-manager.css"; } || { [[ -s "${XDG_CONFIG_HOME:-$HOME/.config}/wofi/wofi-network-manager.css" ]] && RASI_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/wofi/wofi-network-manager.css"; } || exit + # Populate human-readable product names (e.g. "Intel Wi-Fi 6") for each interface. for i in "${WIRELESS_INTERFACES[@]}"; do WIRELESS_INTERFACES_PRODUCT+=("$(nmcli -f general.product device show "$i" | awk '{print $2}')"); done for i in "${WIRED_INTERFACES[@]}"; do WIRED_INTERFACES_PRODUCT+=("$(nmcli -f general.product device show "$i" | awk '{print $2}')"); done + # Refresh global state variables used by the menu. wireless_interface_state && ethernet_interface_state } function notification() { + # Only fire when NOTIFICATIONS_INIT is "on" and notify-send is executable. + # -r "5" replaces any previous notification with the same replacement-id, avoiding spam. [[ "$NOTIFICATIONS_INIT" == "on" && -x "$(command -v notify-send)" ]] && notify-send -r "5" -u "normal" $1 "$2" } function wireless_interface_state() { + # No-op when no Wi-Fi cards were found at startup. [[ ${#WIRELESS_INTERFACES[@]} -eq "0" ]] || { + # Read the active SSID (column 4) and connection state (column 3) for the current card. ACTIVE_SSID=$(nmcli device status | grep "^${WIRELESS_INTERFACES[WLAN_INT]}." | awk '{print $4}') WIFI_CON_STATE=$(nmcli device status | grep "^${WIRELESS_INTERFACES[WLAN_INT]}." | awk '{print $3}') + # Build the OPTIONS string that will be fed into wofi. + # "unavailable" means the radio is off; offer a toggle and a scan shortcut. + # "connected" means we have an active link; list nearby networks plus management options. { [[ "$WIFI_CON_STATE" == "unavailable" ]] && WIFI_LIST="***Wi-Fi Disabled***" && WIFI_SWITCH="~Wi-Fi On" && OPTIONS="${WIFI_LIST}\n${WIFI_SWITCH}\n~Scan\n"; } || { [[ "$WIFI_CON_STATE" =~ "connected" ]] && { PROMPT=${WIRELESS_INTERFACES_PRODUCT[WLAN_INT]}[${WIRELESS_INTERFACES[WLAN_INT]}] + # List nearby networks: -F selects fields; awk deduplicates by SSID; sed strips the + # "IN-USE" header row and the "*" marker for the active network; awk drops hidden SSIDs ("--"). WIFI_LIST=$(nmcli --fields IN-USE,SSID,SECURITY,BARS device wifi list ifname "${WIRELESS_INTERFACES[WLAN_INT]}" | awk -F' +' '{ if (!seen[$2]++) print}' | sed "s/^IN-USE\s//g" | sed "/*/d" | sed "s/^ *//" | awk '$1!="--" {print}') + # Offer "Disconnect" only when actually associated (ACTIVE_SSID is not "--"). [[ "$ACTIVE_SSID" == "--" ]] && WIFI_SWITCH="~Scan\n~Manual/Hidden\n~Wi-Fi Off" || WIFI_SWITCH="~Scan\n~Disconnect\n~Manual/Hidden\n~Wi-Fi Off" OPTIONS="${WIFI_LIST}\n${WIFI_SWITCH}\n" }; } } } function ethernet_interface_state() { + # No-op when no wired interfaces were found. [[ ${#WIRED_INTERFACES[@]} -eq "0" ]] || { + # head -1 picks the first ethernet device in case multiple are present. WIRED_CON_STATE=$(nmcli device status | grep "ethernet" | head -1 | awk '{print $3}') + # Append the appropriate wired control entry to OPTIONS. { [[ "$WIRED_CON_STATE" == "disconnected" ]] && WIRED_SWITCH="~Eth On"; } || { [[ "$WIRED_CON_STATE" == "connected" ]] && WIRED_SWITCH="~Eth Off"; } || { [[ "$WIRED_CON_STATE" == "unavailable" ]] && WIRED_SWITCH="***Wired Unavailable***"; } || { [[ "$WIRED_CON_STATE" == "connecting" ]] && WIRED_SWITCH="***Wired Initializing***"; } OPTIONS="${OPTIONS}${WIRED_SWITCH}\n" } } function wofi_menu() { + # Add "Change Wifi Interface" only when more than one Wi-Fi card exists. { [[ ${#WIRELESS_INTERFACES[@]} -gt "1" ]] && OPTIONS="${OPTIONS}~Change Wifi Interface\n~More Options"; } || { OPTIONS="${OPTIONS}~More Options"; } + # Show the wired interface name in the prompt when Ethernet is active. { [[ "$WIRED_CON_STATE" == "connected" ]] && PROMPT="${WIRED_INTERFACES_PRODUCT}[$WIRED_INTERFACES]"; } || PROMPT="${WIRELESS_INTERFACES_PRODUCT[WLAN_INT]}[${WIRELESS_INTERFACES[WLAN_INT]}]" SELECTION=$(echo -e "$OPTIONS" | wofi_cmd "$OPTIONS" $WIDTH_FIX_MAIN "-a 0") + # Collapse multi-space separators to | then extract the first field (SSID without decorators). SSID=$(echo "$SELECTION" | sed "s/\s\{2,\}/\|/g" | awk -F "|" '{print $1}') selection_action } function wofi_cmd() { + # Auto-size the wofi window to fit the widest entry. + # $1 = option list for width calculation, $2 = width adjustment, $3/$4 = extra flags. + # WIDTH is halved because wofi uses character-width em units on a proportional font. { [[ -n "${1}" ]] && WIDTH=$(echo -e "$1" | awk '{print length}' | sort -n | tail -1) && ((WIDTH += $2)) && ((WIDTH = WIDTH / 2)); } || { ((WIDTH = $2 / 2)); } + # -dmenu: read from stdin; -i: case-insensitive; --normal-window: don't use a special surface. + # -theme-str injects CSS overrides: sets window width and the prompt colon label. wofi -dmenu -i --normal-window=false -location "$LOCATION" -yoffset "$Y_AXIS" -xoffset "$X_AXIS" $3 -theme "$RASI_DIR" -theme-str 'window{width: '$WIDTH'em;}textbox-prompt-colon{str:"'$PROMPT':";}'"$4"'' } function change_wireless_interface() { + # If exactly two cards exist, toggle between index 0 and 1 without showing a menu. + # Otherwise, present all cards in wofi and let the user pick. { [[ ${#WIRELESS_INTERFACES[@]} -eq "2" ]] && { [[ $WLAN_INT -eq "0" ]] && WLAN_INT=1 || WLAN_INT=0; }; } || { LIST_WLAN_INT="" for i in "${!WIRELESS_INTERFACES[@]}"; do LIST_WLAN_INT=("${LIST_WLAN_INT[@]}${WIRELESS_INTERFACES_PRODUCT[$i]}[${WIRELESS_INTERFACES[$i]}]\n"); done + # Strip the trailing \n from the last entry to avoid a blank line in wofi. LIST_WLAN_INT[-1]=${LIST_WLAN_INT[-1]::-2} CHANGE_WLAN_INT=$(echo -e "${LIST_WLAN_INT[@]}" | wofi_cmd "${LIST_WLAN_INT[@]}" $WIDTH_FIX_STATUS) + # Resolve the selected label back to its index in WIRELESS_INTERFACES. for i in "${!WIRELESS_INTERFACES[@]}"; do [[ $CHANGE_WLAN_INT == "${WIRELESS_INTERFACES_PRODUCT[$i]}[${WIRELESS_INTERFACES[$i]}]" ]] && WLAN_INT=$i && break; done } + # Refresh state after the interface switch and redraw the menu. wireless_interface_state && ethernet_interface_state wofi_menu } function scan() { + # If Wi-Fi is off, turn it on and wait 2 s for the radio to come up before scanning. [[ "$WIFI_CON_STATE" =~ "unavailable" ]] && change_wifi_state "Wi-Fi" "Enabling Wi-Fi connection" "on" && sleep 2 notification "-t 0 Wifi" "Please Wait Scanning" + # --rescan yes forces an active scan; without it nmcli may return a stale cache. + # awk deduplicates SSIDs; sed cleans up the header and the currently-connected marker. WIFI_LIST=$(nmcli --fields IN-USE,SSID,SECURITY,BARS device wifi list ifname "${WIRELESS_INTERFACES[WLAN_INT]}" --rescan yes | awk -F' +' '{ if (!seen[$2]++) print}' | sed "s/^IN-USE\s//g" | sed "/*/d" | sed "s/^ *//" | awk '$1!="--" {print}') wireless_interface_state && ethernet_interface_state notification "-t 1 Wifi" "Please Wait Scanning" wofi_menu } function change_wifi_state() { + # Toggle the Wi-Fi radio via nmcli: "on" enables it, "off" disables it. notification "$1" "$2" nmcli radio wifi "$3" } function change_wired_state() { + # Connect or disconnect a wired interface: nmcli device connect/disconnect <iface>. notification "$1" "$2" nmcli device "$3" "$4" } function net_restart() { + # Hard-cycle the NetworkManager stack; 3 s sleep lets the kernel teardown complete. notification "$1" "$2" nmcli networking off && sleep 3 && nmcli networking on } function disconnect() { + # Resolve the connection profile name for the active SSID, then bring it down. + # -t (terse) + cut removes the "GENERAL.CONNECTION:" key prefix from nmcli output. ACTIVE_SSID=$(nmcli -t -f GENERAL.CONNECTION dev show "${WIRELESS_INTERFACES[WLAN_INT]}" | cut -d ':' -f2) notification "$1" "You're now disconnected from Wi-Fi network '$ACTIVE_SSID'" nmcli con down id "$ACTIVE_SSID" } function check_wifi_connected() { + # Drop any existing connection before attempting a new one; avoids "already connected" errors. [[ "$(nmcli device status | grep "^${WIRELESS_INTERFACES[WLAN_INT]}." | awk '{print $3}')" == "connected" ]] && disconnect "Connection_Terminated" } function connect() { check_wifi_connected notification "-t 0 Wi-Fi" "Connecting to $1" + # Try to join the network with the supplied password; report success or failure. { [[ $(nmcli dev wifi con "$1" password "$2" ifname "${WIRELESS_INTERFACES[WLAN_INT]}" | grep -c "successfully activated") -eq "1" ]] && notification "Connection_Established" "You're now connected to Wi-Fi network '$1'"; } || notification "Connection_Error" "Connection can not be established" } function enter_passwword() { + # Open a password-masked wofi prompt; the placeholder text acts as a "stored key" signal. PROMPT="Enter_Password" && PASS=$(echo "$PASSWORD_ENTER" | wofi_cmd "$PASSWORD_ENTER" 4 "-password") } function enter_ssid() { + # Open a plain text wofi prompt with no pre-filled options for manual SSID entry. PROMPT="Enter_SSID" && SSID=$(wofi_cmd "" 40) } function stored_connection() { + # Re-activate an already-known connection profile without supplying a password. check_wifi_connected notification "-t 0 Wi-Fi" "Connecting to $1" { [[ $(nmcli dev wifi con "$1" ifname "${WIRELESS_INTERFACES[WLAN_INT]}" | grep -c "successfully activated") -eq "1" ]] && notification "Connection_Established" "You're now connected to Wi-Fi network '$1'"; } || notification "Connection_Error" "Connection can not be established" } function ssid_manual() { + # Let the user type an SSID; if a password is entered use it, otherwise try stored credentials. enter_ssid [[ -n $SSID ]] && { enter_passwword @@ -119,10 +171,12 @@ function ssid_manual() { } } function ssid_hidden() { + # Connect to a hidden SSID by creating or reusing an NM connection profile. enter_ssid [[ -n $SSID ]] && { enter_passwword && check_wifi_connected [[ -n "$PASS" ]] && [[ "$PASS" != "$PASSWORD_ENTER" ]] && { + # Create a new Wi-Fi connection profile, then configure WPA-PSK security on it. nmcli con add type wifi con-name "$SSID" ssid "$SSID" ifname "${WIRELESS_INTERFACES[WLAN_INT]}" nmcli con modify "$SSID" wifi-sec.key-mgmt wpa-psk nmcli con modify "$SSID" wifi-sec.psk "$PASS" @@ -132,59 +186,80 @@ function ssid_hidden() { } } function interface_status() { + # local -n creates a nameref — $1 and $2 are the names of the caller's arrays, + # not their values; this avoids passing large arrays by copy. local -n INTERFACES=$1 && local -n INTERFACES_PRODUCT=$2 for i in "${!INTERFACES[@]}"; do CON_STATE=$(nmcli device status | grep "^${INTERFACES[$i]}." | awk '{print $3}') INT_NAME=${INTERFACES_PRODUCT[$i]}[${INTERFACES[$i]}] + # When connected, show the profile name and IPv4 address; otherwise capitalise the state word. + # awk -F '[:]' splits on colon to strip the "GENERAL.CONNECTION:" prefix. + # awk -F '[:/]' splits on colon or slash to extract the bare IP from "IP4.ADDRESS[1]: x.x.x.x/24". [[ "$CON_STATE" == "connected" ]] && STATUS="$INT_NAME:\n\t$(nmcli -t -f GENERAL.CONNECTION dev show "${INTERFACES[$i]}" | awk -F '[:]' '{print $2}') ~ $(nmcli -t -f IP4.ADDRESS dev show "${INTERFACES[$i]}" | awk -F '[:/]' '{print $2}')" || STATUS="$INT_NAME: ${CON_STATE^}" echo -e "${STATUS}" done } function status() { + # Build a combined status view of all wired and wireless interfaces plus any active VPN. OPTIONS="" [[ ${#WIRED_INTERFACES[@]} -ne "0" ]] && ETH_STATUS="$(interface_status WIRED_INTERFACES WIRED_INTERFACES_PRODUCT)" && OPTIONS="${OPTIONS}${ETH_STATUS}" [[ ${#WIRELESS_INTERFACES[@]} -ne "0" ]] && WLAN_STATUS="$(interface_status WIRELESS_INTERFACES WIRELESS_INTERFACES_PRODUCT)" && { [[ -n ${OPTIONS} ]] && OPTIONS="${OPTIONS}\n${WLAN_STATUS}" || OPTIONS="${OPTIONS}${WLAN_STATUS}"; } + # -g NAME,TYPE filters to name:type pairs; awk/sed extract just the name for vpn-type connections. ACTIVE_VPN=$(nmcli -g NAME,TYPE con show --active | awk '/:vpn/' | sed 's/:vpn.*//g') [[ -n $ACTIVE_VPN ]] && OPTIONS="${OPTIONS}\n${ACTIVE_VPN}[VPN]: $(nmcli -g ip4.address con show "${ACTIVE_VPN}" | awk -F '[:/]' '{print $1}')" + # "mainbox{children:[listview];}" hides the search field for a read-only status display. echo -e "$OPTIONS" | wofi_cmd "$OPTIONS" $WIDTH_FIX_STATUS "" "mainbox{children:[listview];}" } function share_pass() { + # Retrieve the current network's SSID and password via nmcli, then display them. + # -oP with a lookbehind extracts only the value after "SSID: " / "Password: ". SSID=$(nmcli dev wifi show-password | grep -oP '(?<=SSID: ).*' | head -1) PASSWORD=$(nmcli dev wifi show-password | grep -oP '(?<=Password: ).*' | head -1) OPTIONS="SSID: ${SSID}\nPassword: ${PASSWORD}" + # Offer QR-code generation only when qrencode is installed. [[ -x "$(command -v qrencode)" ]] && OPTIONS="${OPTIONS}\n~QrCode" SELECTION=$(echo -e "$OPTIONS" | wofi_cmd "$OPTIONS" $WIDTH_FIX_STATUS "-a -1" "mainbox{children:[listview];}") selection_action } function gen_qrcode() { + # Generate a WPA QR-code PNG using qrencode and display it inside a wofi window. + # -l H: high error correction; -s 25: module size 25px; -m 2: 2-module border; --dpi=192: HiDPI. DIRECTIONS=("Center" "Northwest" "North" "Northeast" "East" "Southeast" "South" "Southwest" "West") [[ -e $QRCODE_DIR$SSID.png ]] || qrencode -t png -o $QRCODE_DIR$SSID.png -l H -s 25 -m 2 --dpi=192 "WIFI:S:""$SSID"";T:""$(nmcli dev wifi show-password | grep -oP '(?<=Security: ).*' | head -1)"";P:""$PASSWORD"";;" + # Open a decorationless wofi window at the chosen corner and fill it with the QR image. wofi_cmd "" "0" "" "entry{enabled:false;}window{location:"${DIRECTIONS[QRCODE_LOCATION]}";border-radius:6mm;padding:1mm;width:100mm;height:100mm; background-image:url(\"$QRCODE_DIR$SSID.png\",both);}" } function manual_hidden() { + # Sub-menu to choose between a visible-but-unlisted (manual) or a truly hidden SSID. OPTIONS="~Manual\n~Hidden" && SELECTION=$(echo -e "$OPTIONS" | wofi_cmd "$OPTIONS" $WIDTH_FIX_STATUS "" "mainbox{children:[listview];}") selection_action } function vpn() { + # If a VPN is already active offer to deactivate it; otherwise list all known VPN profiles. ACTIVE_VPN=$(nmcli -g NAME,TYPE con show --active | awk '/:vpn/' | sed 's/:vpn.*//g') [[ $ACTIVE_VPN ]] && OPTIONS="~Deactive $ACTIVE_VPN" || OPTIONS="$(nmcli -g NAME,TYPE connection | awk '/:vpn/' | sed 's/:vpn.*//g')" VPN_ACTION=$(echo -e "$OPTIONS" | wofi_cmd "$OPTIONS" "$WIDTH_FIX_STATUS" "" "mainbox {children:[listview];}") [[ -n "$VPN_ACTION" ]] && { { [[ "$VPN_ACTION" =~ "~Deactive" ]] && nmcli connection down "$ACTIVE_VPN" && notification "VPN_Deactivated" "$ACTIVE_VPN"; } || { + # 2>/dev/null suppresses "Error: ... already active" messages from nmcli. notification "-t 0 Activating_VPN" "$VPN_ACTION" && VPN_OUTPUT=$(nmcli connection up "$VPN_ACTION" 2>/dev/null) { [[ $(echo "$VPN_OUTPUT" | grep -c "Connection successfully activated") -eq "1" ]] && notification "VPN_Successfully_Activated" "$VPN_ACTION"; } || notification "Error_Activating_VPN" "Check your configuration for $VPN_ACTION" }; } } function more_options() { + # Secondary menu: share password (only when connected), status, restart, VPN, editor. OPTIONS="" [[ "$WIFI_CON_STATE" == "connected" ]] && OPTIONS="~Share Wifi Password\n" OPTIONS="${OPTIONS}~Status\n~Restart Network" + # Only add the VPN entry when at least one VPN profile is configured. [[ $(nmcli -g NAME,TYPE connection | awk '/:vpn/' | sed 's/:vpn.*//g') ]] && OPTIONS="${OPTIONS}\n~VPN" + # nm-connection-editor is a GTK app from NetworkManager-applet — only show if installed. [[ -x "$(command -v nm-connection-editor)" ]] && OPTIONS="${OPTIONS}\n~Open Connection Editor" SELECTION=$(echo -e "$OPTIONS" | wofi_cmd "$OPTIONS" "$WIDTH_FIX_STATUS" "" "mainbox {children:[listview];}") selection_action } function selection_action() { + # Central dispatcher: map each menu label to its handler function. case "$SELECTION" in "~Disconnect") disconnect "Connection_Terminated" ;; "~Scan") scan ;; @@ -197,6 +272,7 @@ function selection_action() { "~Wi-Fi Off") change_wifi_state "Wi-Fi" "Disabling Wi-Fi connection" "off" ;; "~Eth Off") change_wired_state "Ethernet" "Disabling Wired connection" "disconnect" "${WIRED_INTERFACES}" ;; "~Eth On") change_wired_state "Ethernet" "Enabling Wired connection" "connect" "${WIRED_INTERFACES}" ;; + # Informational entries — user clicked a status label, nothing to do. "***Wi-Fi Disabled***") ;; "***Wired Unavailable***") ;; "***Wired Initializing***") ;; @@ -207,8 +283,12 @@ function selection_action() { "~Open Connection Editor") nm-connection-editor ;; "~VPN") vpn ;; *) + # Default: treat the selection as a network from WIFI_LIST. [[ -n "$SELECTION" ]] && [[ "$WIFI_LIST" =~ .*"$SELECTION".* ]] && { + # When SSID is "*" the user selected the currently-active network row (which starts with *). [[ "$SSID" == "*" ]] && SSID=$(echo "$SELECTION" | sed "s/\s\{2,\}/\|/g " | awk -F "|" '{print $3}') + # If already connected to this SSID just bring the profile up (re-apply settings). + # Otherwise prompt for a password if the network is WPA2/WEP secured, then connect. { [[ "$ACTIVE_SSID" == "$SSID" ]] && nmcli con up "$SSID" ifname "${WIRELESS_INTERFACES[WLAN_INT]}"; } || { [[ "$SELECTION" =~ "WPA2" ]] || [[ "$SELECTION" =~ "WEP" ]] && enter_passwword { [[ -n "$PASS" ]] && [[ "$PASS" != "$PASSWORD_ENTER" ]] && connect "$SSID" "$PASS"; } || stored_connection "$SSID" @@ -218,6 +298,7 @@ function selection_action() { esac } function main() { + # Entry point: load config and launch the menu. initialization && wofi_menu } main diff --git a/desktopenvs/niri/niri/config.kdl b/desktopenvs/niri/niri/config.kdl index 4bb4cf4..42d8be8 100644 --- a/desktopenvs/niri/niri/config.kdl +++ b/desktopenvs/niri/niri/config.kdl @@ -1,15 +1,39 @@ +// ───────────────────────────────────────────────────────────────────────────── // ~/.config/niri/config.kdl // Niri scrollable-tiling Wayland compositor — CyberQueer config -// https://github.com/YaLTeR/niri/wiki/Configuration:-Overview +// +// PURPOSE: Root configuration entry-point for the niri compositor. +// This file does not contain settings directly — it delegates all +// configuration sections to separate module files so each concern +// (input, outputs, layout, etc.) can be edited independently. +// +// STRUCTURE: niri supports a flat `include` directive that merges KDL files +// before parsing, so the split is purely organisational. +// +// REFERENCE: https://github.com/YaLTeR/niri/wiki/Configuration:-Overview +// ───────────────────────────────────────────────────────────────────────────── -include "modules/input.kdl" -include "modules/outputs.kdl" -include "modules/layout.kdl" -include "modules/animations.kdl" -include "modules/environment.kdl" -include "modules/autostart.kdl" -include "modules/window-rules.kdl" -include "modules/binds.kdl" +// ── Module includes ─────────────────────────────────────────────────────────── +// Each file below is merged verbatim into this config at parse time. +// The order matters only when settings in later files override earlier ones. +include "modules/input.kdl" // Keyboard, touchpad, mouse, focus behaviour +include "modules/outputs.kdl" // Monitor/display declarations and positions +include "modules/layout.kdl" // Gaps, column widths, focus ring, struts +include "modules/animations.kdl" // Spring / easing curves for all transitions +include "modules/environment.kdl" // Env vars passed to every child process + cursor +include "modules/autostart.kdl" // Programs launched when niri starts +include "modules/window-rules.kdl" // Per-app floating, sizing, opacity overrides +include "modules/binds.kdl" // All keyboard and mouse bindings + +// ── Global preferences ──────────────────────────────────────────────────────── + +// prefer-no-csd: Tell clients to draw without client-side decorations (title bars). +// WHY: niri renders its own focus ring / border; double-decorations look wrong. +// Apps that respect this (most GTK/Qt apps) gain a cleaner appearance. prefer-no-csd + +// screenshot-path: Where niri saves full-compositor screenshots (Print key). +// WHY: Organise screenshots in a dedicated folder with a human-readable timestamp. +// strftime-style % tokens are expanded at capture time. screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" diff --git a/desktopenvs/niri/niri/modules/animations.kdl b/desktopenvs/niri/niri/modules/animations.kdl index a1d6e7e..e2ca239 100644 --- a/desktopenvs/niri/niri/modules/animations.kdl +++ b/desktopenvs/niri/niri/modules/animations.kdl @@ -1,23 +1,70 @@ -// ── Animations ──────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── +// modules/animations.kdl — Niri animation settings (CyberQueer profile) +// +// PURPOSE: Defines easing/spring curves for every animated transition in niri: +// window open/close/move, workspace switching, and UI overlays. +// +// SPRING PHYSICS MODEL: niri uses a physically-based damped spring rather than +// a fixed-duration curve. Three parameters control it: +// +// damping-ratio — how quickly oscillation decays after the spring overshoots. +// 1.0 = critically damped (no bounce, fastest settle). +// <1.0 = underdamped (bouncy). >1.0 = overdamped (sluggish). +// stiffness — spring constant; how hard the spring pulls toward the target. +// Higher = snappier motion; lower = floaty/elastic. +// epsilon — the displacement threshold below which animation stops. +// Smaller = more precise end position (negligible CPU cost). +// ───────────────────────────────────────────────────────────────────────────── + animations { + + // window-open: played when a new window first appears on screen. + // damping-ratio=0.9: very slightly underdamped — tiny implicit "settle" + // without a visible bounce, making the open feel snappy and alive. + // stiffness=800: high stiffness keeps the open fast (< ~120ms perceived). + // epsilon=0.0001: stops at 0.01% residual — visually at rest immediately. window-open { spring damping-ratio=0.9 stiffness=800 epsilon=0.0001 } + + // window-close: played when a window is destroyed/hidden. + // Mirrors window-open so open and close feel symmetric. window-close { spring damping-ratio=0.9 stiffness=800 epsilon=0.0001 } + + // window-movement: played when a window or column is repositioned within + // the scrollable layout (e.g. Mod+Shift+H/L, consume/expel into column). window-movement { spring damping-ratio=0.9 stiffness=800 epsilon=0.0001 } + + // horizontal-view-movement: played when the viewport scrolls horizontally + // to follow the focused column. This is the signature "infinite rail" + // feel that distinguishes niri from traditional tiling compositors. horizontal-view-movement { spring damping-ratio=0.9 stiffness=800 epsilon=0.0001 } + + // workspace-switch: played when switching between vertical workspaces + // (Mod+Ctrl+J/K, Mod+1–0). Consistent spring keeps it feeling cohesive. workspace-switch { spring damping-ratio=0.9 stiffness=800 epsilon=0.0001 } + + // config-notification-open-close: the small "Configuration reloaded" toast + // shown after a hot-reload. Same spring for a unified UI feel. config-notification-open-close { spring damping-ratio=0.9 stiffness=800 epsilon=0.0001 } + + // screenshot-ui-open: the interactive region-selection overlay. + // WHY different curve: this is a modal overlay, not a layout element. + // A fixed-duration ease-out-cubic gives a crisp "snap into place" feel + // that communicates "you are now in screenshot mode" more clearly than a + // spring would. + // duration-ms 200: fast enough not to interrupt workflow. + // curve "ease-out-cubic": starts fast, decelerates smoothly to rest. screenshot-ui-open { duration-ms 200 curve "ease-out-cubic" diff --git a/desktopenvs/niri/niri/modules/autostart.kdl b/desktopenvs/niri/niri/modules/autostart.kdl index 9e82ad4..1fdcaa9 100644 --- a/desktopenvs/niri/niri/modules/autostart.kdl +++ b/desktopenvs/niri/niri/modules/autostart.kdl @@ -1,13 +1,83 @@ -// ── Autostart ───────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── +// modules/autostart.kdl — Programs launched when niri starts +// +// PURPOSE: Lists every daemon, applet, and background service required for a +// complete CyberQueer desktop session. niri's spawn-at-startup +// launches each command once; there is no restart logic, so crashed +// daemons stay dead until the compositor is restarted. +// +// NOTE: Arguments are passed via execvp (not a shell). Anything needing shell +// features (tilde expansion, pipes, globs) must use `bash -c "..."`. +// ───────────────────────────────────────────────────────────────────────────── + +// monitorhandler.sh — sets the wallpaper (swaybg) and launches eww bar. +// WHY first: wallpaper and status bar should appear as early as possible so +// the desktop looks ready before the user interacts with it. spawn-at-startup "bash" "-c" "~/.config/scripts/monitorhandler.sh" + +// vicinae — application launcher daemon that keeps a hot process/app cache. +// "vicinae server" starts the background index so that Mod+R (toggle) is +// near-instantaneous rather than waiting for a cold startup scan. spawn-at-startup "bash" "-c" "vicinae server" + +// bluetooth-applet.sh — thin wrapper around blueman-applet. +// WHY a wrapper script: allows environment setup or guard conditions before +// the applet is spawned (e.g. waiting for bluetoothd to be ready). spawn-at-startup "bash" "-c" "~/.config/scripts/bluetooth-applet.sh" + +// ulwatchdog.sh — keeps ulauncher alive (polls every 5 s and restarts it). +// WHY: ulauncher serves as a secondary launcher; the watchdog ensures it is +// always available so the first Mod+R after a crash doesn't silently fail. spawn-at-startup "bash" "-c" "~/.config/scripts/ulwatchdog.sh" + +// udiskie — automounter for removable media (USB drives, SD cards, etc.). +// -t → show a system-tray icon for manual mount/unmount actions +// -m nested → display a nested submenu in the tray (cleaner than flat list) +// -n → suppress desktop notifications (dunst handles those instead) spawn-at-startup "udiskie" "-t" "-m" "nested" "-n" + +// xfce-polkit — PolicyKit authentication agent (privilege escalation dialog). +// WHY: without a running polkit agent, GUI applications that request root +// privileges (e.g. gparted, package managers) will silently fail or crash. +// xfce-polkit is lightweight and works on any Wayland session. spawn-at-startup "xfce-polkit" + +// gammastep — Wayland colour-temperature daemon (like redshift). +// -O 4500: apply a fixed 4500 K warm-white colour temperature at startup. +// WHY fixed value: avoids needing a GPS/location service. 4500 K is a +// comfortable default for mixed day/evening use. binds.kdl provides +// shortcuts (Mod+Ctrl+X/W/A/Q/S) to change temperature on the fly. spawn-at-startup "bash" "-c" "gammastep -O 4500" + +// nm-applet — NetworkManager system-tray icon. +// WHY: provides quick access to Wi-Fi, VPN, and wired network management +// without keeping a full settings panel open. spawn-at-startup "nm-applet" + +// dunst — notification daemon (implements the freedesktop.org spec). +// WHY: lightweight alternative to mako/swaync; integrates tightly with +// dunstctl for the scripted close-all binding in binds.kdl (Mod+Ctrl+C). spawn-at-startup "dunst" + +// swayidle — idle-based screen locker and suspend manager. +// Flags and timeouts: +// -w → wait for each command to finish before +// acknowledging the idle state (prevents races) +// timeout 300 'swaylock -f' → lock screen after 5 minutes idle +// timeout 600 'systemctl suspend' → suspend system after 10 minutes idle +// before-sleep 'swaylock -f' → lock before every suspend/hibernate, +// including lid-close events +// WHY swaylock -f: the -f (fork) flag lets swaylock daemonise itself so +// swayidle doesn't block waiting for it to exit. spawn-at-startup "bash" "-c" "swayidle -w timeout 300 'swaylock -f' timeout 600 'systemctl suspend' before-sleep 'swaylock -f'" + +// presence-detect.sh — webcam-based automatic idle inhibitor. +// WHY: detects when the user is physically at the desk (via camera) and holds +// the idle inhibitor active so the screen doesn't lock unnecessarily. +// Complements the manual caffeine.sh toggle (Mod+Shift+C). spawn-at-startup "bash" "-c" "~/.config/scripts/presence-detect.sh" + +// blueman-applet — Bluetooth tray icon with device management UI. +// WHY separate from nm-applet: blueman exposes pairing, profile switching, +// and per-device settings that NetworkManager's Bluetooth support omits. spawn-at-startup "blueman-applet" diff --git a/desktopenvs/niri/niri/modules/binds.kdl b/desktopenvs/niri/niri/modules/binds.kdl index e871560..ee596cb 100644 --- a/desktopenvs/niri/niri/modules/binds.kdl +++ b/desktopenvs/niri/niri/modules/binds.kdl @@ -1,74 +1,158 @@ -// ── Keybindings ─────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── +// modules/binds.kdl — Keyboard and mouse bindings for niri // -// Modifier semantics (directional keys): -// Mod + dir → focus (window or column) -// Mod + Shift + dir → move selected window/column -// Mod + Ctrl + dir → navigate workspaces -// Mod + Ctrl+Shift + dir → move window to workspace -// Mod + Alt + dir → resize (column width / window height) +// PURPOSE: Central definition of every user-triggered action in the niri +// session. Bindings are grouped by function so related keys are easy +// to find and modify. // +// MODIFIER KEY ("Mod"): Defaults to the Super / Windows key. +// +// Modifier semantics for directional keys: +// Mod + dir → focus (move focus to window or column) +// Mod + Shift + dir → move selected window or column in that direction +// Mod + Ctrl + dir → navigate workspaces (up/down) +// Mod + Ctrl+Shift + dir → move the focused column to a workspace +// Mod + Alt + dir → resize (column width with H/L, window height with J/K) +// +// allow-when-locked=true: the binding fires even when swaylock is active. +// WHY: media controls and brightness must work on the lock screen. +// ───────────────────────────────────────────────────────────────────────────── + binds { // ── Applications ────────────────────────────────────────────────────────── + + // Mod+T: open a standard kitty terminal. Mod+T { spawn "kitty"; } + + // Mod+Shift+T: open cool-retro-term with the CRT profile from this repo. + // WHY: retro CRT aesthetic for a cyberqueer vibe; kept separate from the + // main terminal so normal workflows aren't forced through the slow CRT font. Mod+Shift+T { spawn "bash" "-c" "cool-retro-term -p ~/Dotfiles/desktopenvs/niri/CRT"; } + + // Mod+M: open Neovim inside kitty (quick editor shortcut). Mod+M { spawn "bash" "-c" "kitty -e nvim"; } + + // Mod+E: open Thunar file manager (primary GUI file manager). Mod+E { spawn "thunar"; } + + // Mod+Alt+E: open PCManFM-Qt as an alternative file manager. + // WHY alt key: secondary tool; not used daily but useful as a fallback. Mod+Alt+E { spawn "pcmanfm-qt"; } + + // Mod+X: open wofi in "run" mode (raw binary launcher, no .desktop files). Mod+X { spawn "bash" "-c" "wofi --show=run"; } + + // Mod+N: open Nextcloud desktop client for cloud sync management. Mod+N { spawn "nextcloud"; } + + // Mod+I: open iwmenu (Wi-Fi network picker) using the walker launcher UI. Mod+I { spawn "bash" "-c" "iwmenu --launcher walker"; } + + // Mod+Alt+I: open bzmenu (Bluetooth device picker) with walker UI. Mod+Alt+I { spawn "bash" "-c" "bzmenu --launcher walker"; } + + // Mod+Shift+I: open nm-connection-editor for advanced network configuration. Mod+Shift+I { spawn "nm-connection-editor"; } + + // Mod+R / Mod+Return / Ctrl+Shift+R: toggle the vicinae launcher overlay. + // WHY three bindings: muscle-memory coverage from different contexts + // (Mod+R mirrors rofi habit; Return is ergonomic; Ctrl+Shift+R works in + // some terminal apps where Mod is captured). Mod+R { spawn "bash" "-c" "vicinae toggle"; } Mod+Return { spawn "bash" "-c" "vicinae toggle"; } - Mod+Shift+R { spawn "bash" "-c" "wofi --show drun"; } Ctrl+Shift+R { spawn "bash" "-c" "vicinae toggle"; } - Mod+F { spawn "bash" "-c" "~/.config/scripts/wofi-file-search.sh"; } - Mod+Shift+F { spawn "bash" "-c" "~/.config/scripts/foldersearch.sh"; } - Mod+Alt+F { spawn "wofi-calc"; } + // Mod+Shift+R: open wofi in drun mode (.desktop application launcher). + Mod+Shift+R { spawn "bash" "-c" "wofi --show drun"; } + // File search shortcuts + Mod+F { spawn "bash" "-c" "~/.config/scripts/wofi-file-search.sh"; } // fuzzy file search + Mod+Shift+F { spawn "bash" "-c" "~/.config/scripts/foldersearch.sh"; } // folder search + Mod+Alt+F { spawn "wofi-calc"; } // calculator in wofi + + // Mod+S: open PulseAudio volume control (pavucontrol) for audio routing. Mod+S { spawn "pavucontrol"; } + + // Mod+U: open btop system monitor inside kitty. Mod+U { spawn "bash" "-c" "kitty -e btop"; } + + // Mod+W: open the TUI wallpaper picker (swaybg-based, uses kitty icat). Mod+W { spawn "bash" "-c" "kitty -e ~/.config/scripts/wallpaper-picker ~/Pictures"; } + + // Mod+Ctrl+R: open the amssh SSH session picker inside kitty. Mod+Ctrl+R { spawn "bash" "-c" "kitty -e ~/.config/scripts/amssh"; } + + // Mod+F1: open the help/keybindings cheatsheet in a kitty window. Mod+F1 { spawn "bash" "-c" "kitty -e ~/.config/scripts/helpmenu.sh"; } + + // Mod+Ctrl+T: open a countdown/interval timer picker inside kitty. Mod+Ctrl+T { spawn "bash" "-c" "kitty -e ~/.config/scripts/timer-pick"; } + + // Mod+Shift+F1: open the niri config file directly in nvim for quick edits. Mod+Shift+F1 { spawn "bash" "-c" "kitty -e nvim ~/.config/niri/config.kdl"; } + // Mod+Ctrl+P: start/stop screen recording (screenrec.sh toggles ffmpeg). Mod+Ctrl+P { spawn "bash" "-c" "~/.config/scripts/screenrec.sh"; } - // App drawer + // App drawer: nwg-drawer (grid of apps + power menu) — two bindings for + // ergonomic coverage. Mod+D { spawn "bash" "-c" "~/.config/scripts/drawer.sh"; } Mod+Shift+A { spawn "bash" "-c" "~/.config/scripts/drawer.sh"; } // ── Screenshots ─────────────────────────────────────────────────────────── + + // Print / Mod+P: interactive region/window screenshot (saved to Pictures/Screenshots). Print { screenshot; } Mod+P { screenshot; } + + // Mod+Shift+P: full-screen (current output) screenshot without selection UI. Mod+Shift+P { screenshot-screen; } // ── Window Management ───────────────────────────────────────────────────── + + // Mod+Q / Mod+Shift+Q: close the focused window (sends WM_DELETE_WINDOW). + // WHY two bindings: Shift variant is a safe fallback if Q is captured by an app. Mod+Q { close-window; } Mod+Shift+Q { close-window; } + // Mod+V: toggle the focused window between tiled and floating mode. Mod+V { toggle-window-floating; } + + // Mod+Shift+V / Mod+C: centre the focused column on screen. + // WHY two bindings: muscle memory from different compositors (hyprland uses C). Mod+Shift+V { center-column; } Mod+C { center-column; } + // Mod+Ctrl+M: cycle through available layout algorithms (niri currently + // offers one, but this future-proofs the setup). Mod+Ctrl+M { switch-layout "next"; } // ── Niri layout actions ─────────────────────────────────────────────────── + + // Mod+Backspace: cycle through preset column widths (1/3, 1/2, 2/3, full). + // Widths defined in layout.kdl → preset-column-widths. Mod+Backspace { switch-preset-column-width; } + + // Mod+Shift+Backspace: toggle the focused column between its current width + // and the full available width (maximise without going fullscreen). Mod+Shift+Backspace { maximize-column; } + + // Mod+Ctrl+Backspace: true fullscreen (window covers the whole output, + // hiding the eww bar). Mod+Ctrl+Backspace { fullscreen-window; } - // Consume/expel from column (hyprlua group analog) + // Column stacking (niri's "group" equivalent to hyprland's window groups): + // Mod+A: pull the window to the right of the focused column into it (stack below). + // Mod+Y: eject the bottom window of a multi-window column to its own column. + // Mod+Alt+C/V: consume/expel the adjacent window left or right. Mod+A { consume-window-into-column; } Mod+Y { expel-window-from-column; } Mod+Alt+C { consume-or-expel-window-left; } Mod+Alt+V { consume-or-expel-window-right; } - // ── Focus ───────────────────────────────────────────────────────────────── + // ── Focus — Vim-style (H/J/K/L) + arrow keys ───────────────────────────── + // H/L navigate between columns; J/K navigate between stacked windows within + // a column. Arrow keys are duplicated for non-vim users. Mod+H { focus-column-left; } Mod+L { focus-column-right; } Mod+J { focus-window-down; } @@ -78,11 +162,17 @@ binds { Mod+Down { focus-window-down; } Mod+Up { focus-window-up; } + // Mod+Tab: focus the next window down, or the first window of the next column. + // WHY: provides a natural Alt+Tab-like cycling without needing a separate switcher. Mod+Tab { focus-window-down-or-column-right; } Mod+Shift+Tab { focus-window-up-or-column-left; } + + // Mod+Shift+Return: toggle the workspace overview (bird's-eye view of all + // workspaces and their windows). Mod+Shift+Return { toggle-overview; } - // ── Move Window ─────────────────────────────────────────────────────────── + // ── Move Window — Vim-style + arrow keys ────────────────────────────────── + // Moves the focused column/window in the specified direction within the layout. Mod+Shift+H { move-column-left; } Mod+Shift+L { move-column-right; } Mod+Shift+J { move-window-down; } @@ -92,12 +182,17 @@ binds { Mod+Shift+Down { move-window-down; } Mod+Shift+Up { move-window-up; } - // Mouse drag / resize + // Mouse drag and resize: + // Button272 = left mouse button; Button273 = right mouse button. + // Mod+LMB: drag window to move it (floating or reorder in tiling). + // Mod+RMB / Mod+Shift+LMB: drag to resize window. Mod+Button272 { move-window-with-pointer; } Mod+Button273 { resize-window-with-pointer; } Mod+Shift+Button272 { resize-window-with-pointer; } - // ── Resize ──────────────────────────────────────────────────────────────── + // ── Resize — Mod+Alt + Vim-style + arrow keys ───────────────────────────── + // Adjusts column width (H/L) or window height within a column (J/K). + // 10% increments are coarse enough to be useful without needing many presses. Mod+Alt+L { set-column-width "+10%"; } Mod+Alt+H { set-column-width "-10%"; } Mod+Alt+K { set-window-height "-10%"; } @@ -107,7 +202,8 @@ binds { Mod+Alt+Up { set-window-height "-10%"; } Mod+Alt+Down { set-window-height "+10%"; } - // ── Workspaces — direct ─────────────────────────────────────────────────── + // ── Workspaces — direct jump (Mod+number) ──────────────────────────────── + // Jumps directly to workspace N (1-indexed; Mod+0 = workspace 10). Mod+1 { focus-workspace 1; } Mod+2 { focus-workspace 2; } Mod+3 { focus-workspace 3; } @@ -119,6 +215,7 @@ binds { Mod+9 { focus-workspace 9; } Mod+0 { focus-workspace 10; } + // Mod+Shift+number: move the focused column to workspace N. Mod+Shift+1 { move-column-to-workspace 1; } Mod+Shift+2 { move-column-to-workspace 2; } Mod+Shift+3 { move-column-to-workspace 3; } @@ -131,7 +228,9 @@ binds { Mod+Shift+0 { move-column-to-workspace 10; } // ── Workspaces — relative navigation (Ctrl + direction) ────────────────── - // J/K and Down/Up are the canonical vertical workspace nav + // niri workspaces stack vertically, so J/K and Down/Up are the canonical + // directions. H/L and Left/Right are aliased for users habituated to + // horizontal workspace navigation from other compositors. Mod+Ctrl+J { focus-workspace-down; } Mod+Ctrl+K { focus-workspace-up; } Mod+Ctrl+Down { focus-workspace-down; } @@ -142,7 +241,9 @@ binds { Mod+Ctrl+Right { focus-workspace-down; } Mod+Ctrl+Left { focus-workspace-up; } - // ── Workspaces — move window (Ctrl+Shift + direction) ──────────────────── + // ── Workspaces — move column (Ctrl+Shift + direction) ──────────────────── + // Sends the focused column to the next/previous workspace while staying on + // the current workspace (the user does not follow the window). Mod+Ctrl+Shift+J { move-column-to-workspace-down; } Mod+Ctrl+Shift+K { move-column-to-workspace-up; } Mod+Ctrl+Shift+Down { move-column-to-workspace-down; } @@ -152,68 +253,129 @@ binds { Mod+Ctrl+Shift+Right { move-column-to-workspace-down; } Mod+Ctrl+Shift+Left { move-column-to-workspace-up; } - // Scroll workspaces + // Mouse wheel workspace scrolling (with Mod held): + // cooldown-ms=150: debounce rapid wheel events to avoid skipping workspaces. Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; } Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; } + // Shift+wheel: moves the column to the target workspace instead of focusing. Mod+Shift+WheelScrollDown { move-column-to-workspace-down; } Mod+Shift+WheelScrollUp { move-column-to-workspace-up; } - // Horizontal scroll → column navigation + // Horizontal scroll (trackpad two-finger swipe or horizontal wheel): + // scrolls the focused column list left/right. Mod+WheelScrollRight { focus-column-right; } Mod+WheelScrollLeft { focus-column-left; } - // Volume keys as workspace nav + // Volume rocker keys repurposed as workspace nav when held with Mod. + // WHY: on compact keyboards / tablets without Ctrl, the volume buttons + // offer an ergonomic one-handed workspace switch. Mod+XF86AudioRaiseVolume { focus-workspace-down; } Mod+XF86AudioLowerVolume { focus-workspace-up; } Mod+Shift+XF86AudioRaiseVolume { move-column-to-workspace-down; } Mod+Shift+XF86AudioLowerVolume { move-column-to-workspace-up; } - // ── Overview / scratchpad analog ────────────────────────────────────────── + // ── Overview / recent-window scratchpad analog ──────────────────────────── + // focus-workspace-with-recent-window: jump to whichever workspace holds the + // most recently focused window (like a "last workspace" toggle). Mod+Space { focus-workspace-with-recent-window; } + // Mod+Shift+Space: move the current column to that "recent" workspace. Mod+Shift+Space { move-column-to-workspace-with-recent-window; } + // Overview toggle (bird's-eye all-workspaces view): Mod+Ctrl+Space { toggle-overview; } + // Mute button + Mod as an additional overview toggle (useful on tablets). Mod+XF86AudioMute { toggle-overview; } Mod+Shift+XF86AudioMute { move-column-to-workspace-with-recent-window; } // ── Bar / UI ────────────────────────────────────────────────────────────── + // Mod+Z: toggle (show/hide) the eww status bar on the current output. Mod+Z { spawn "bash" "-c" "~/.config/scripts/togglebar.sh"; } + // Mod+Ctrl+B: force-reload eww (useful after a config change). Mod+Ctrl+B { spawn "bash" "-c" "eww reload"; } - // ── Audio ───────────────────────────────────────────────────────────────── + // ── Audio (media keys) ──────────────────────────────────────────────────── + // allow-when-locked=true: these fire even when swaylock is showing. + // WHY: volume and media control should always be accessible. + // + // wpctl: WirePlumber controller (PipeWire's high-level volume API). + // -l 1.4 caps volume at 140% to protect speakers; 5% steps are fine-grained. + // @DEFAULT_AUDIO_SINK@: targets whatever the current default output is. XF86AudioRaiseVolume allow-when-locked=true { spawn "wpctl" "set-volume" "-l" "1.4" "@DEFAULT_AUDIO_SINK@" "5%+"; } XF86AudioLowerVolume allow-when-locked=true { spawn "wpctl" "set-volume" "-l" "1.4" "@DEFAULT_AUDIO_SINK@" "5%-"; } XF86AudioMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; } XF86AudioMicMute allow-when-locked=true { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; } + // playerctl targets Spotify and VLC; other players won't be accidentally paused. XF86AudioPlay allow-when-locked=true { spawn "playerctl" "play-pause" "-p" "spotify,vlc"; } // ── Brightness ──────────────────────────────────────────────────────────── + // bri: a minimal backlight helper that adjusts the kernel backlight sysfs node. + // allow-when-locked=true: brightness must work on the lock screen. XF86MonBrightnessUp allow-when-locked=true { spawn "bri" "--up"; } XF86MonBrightnessDown allow-when-locked=true { spawn "bri" "--down"; } - // ── Color temperature (gammastep presets) ───────────────────────────────── + // ── Colour temperature presets (gammastep) ──────────────────────────────── + // Each binding kills the running gammastep instance and starts a new one at + // a different Kelvin value. WHY killall first: gammastep doesn't support + // live reconfiguration via IPC, so we replace the process entirely. + // 6500 K = neutral daylight white (no filter) + // 5000 K = slight warmth + // 4000 K = warm white (default startup value) + // 3000 K = amber + // 2700 K = candlelight / maximum eye strain reduction Mod+Ctrl+X { spawn "bash" "-c" "killall gammastep; gammastep -O 6500"; } Mod+Ctrl+W { spawn "bash" "-c" "killall gammastep; gammastep -O 5000"; } Mod+Ctrl+A { spawn "bash" "-c" "killall gammastep; gammastep -O 4000"; } Mod+Ctrl+Q { spawn "bash" "-c" "killall gammastep; gammastep -O 3000"; } Mod+Ctrl+S { spawn "bash" "-c" "killall gammastep; gammastep -O 2700"; } - // ── Misc ────────────────────────────────────────────────────────────────── + // ── Miscellaneous ───────────────────────────────────────────────────────── + + // chamel — a privacy/stealth overlay tool: + // toggle → show/hide the overlay + // clear → remove content but keep overlay active + // clear-and-deactivate → remove content and deactivate overlay entirely Mod+Ctrl+I { spawn "chamel" "toggle"; } Mod+Ctrl+U { spawn "chamel" "clear"; } Mod+Ctrl+Z { spawn "chamel" "clear-and-deactivate"; } + + // Mod+Ctrl+C: dismiss all pending dunst notifications. Mod+Ctrl+C { spawn "dunstctl" "close-all"; } + + // Mod+Ctrl+G: toggle on-screen keyboard (for touch/tablet use). Mod+Ctrl+G { spawn "bash" "-c" "~/.config/scripts/onscreenkb.sh"; } + + // Mod+Shift+C: toggle caffeine (idle inhibitor) — prevents screen lock/sleep. Mod+Shift+C { spawn "bash" "-c" "~/.config/scripts/caffeine.sh"; } + + // Mod+Shift+B: open biometrics enrollment wizard in a kitty window. Mod+Shift+B { spawn "bash" "-c" "kitty -e ~/.config/scripts/enroll-biometrics.sh"; } + + // Mod+Shift+X: toggle touchpad on/off (useful when an external mouse is connected). Mod+Shift+X { spawn "bash" "-c" "~/.config/scripts/niri-toggle-touchpad.sh"; } + // Screen rotation shortcuts — delegate to unified-rotate.sh: + // Mod+Ctrl+E → clockwise (landscape → portrait right) + // Mod+Ctrl+D → counter-clockwise (landscape → portrait left) Mod+Ctrl+E { spawn "bash" "-c" "~/.config/scripts/screenrotationwcw.sh"; } Mod+Ctrl+D { spawn "bash" "-c" "~/.config/scripts/screenrotationacw.sh"; } - // ── Lock / Exit ─────────────────────────────────────────────────────────── + // ── Lock / Session end ──────────────────────────────────────────────────── + + // Mod+O: lock the screen immediately with swaylock. + // -f: fork so niri isn't blocked waiting for the lock process. Mod+O { spawn "swaylock" "-f"; } + + // Mod+Shift+O: quit niri (terminates the entire Wayland session). Mod+Shift+O { quit; } + + // Mod+Ctrl+O: immediately power off the machine via systemctl. Mod+Ctrl+O { spawn "systemctl" "poweroff"; } + + // Mod+Alt+O: open the power/session menu (pwr-dmenu.sh via wofi). + // WHY separate from Mod+Ctrl+O: provides a confirmation UI before any + // destructive action. Mod+Alt+O { spawn "bash" "-c" "~/.config/scripts/pwr-dmenu.sh"; } + + // Emergency quit: a deliberately awkward 4-modifier chord so it's + // never triggered accidentally, but always reachable as a last resort. Mod+Alt+Ctrl+Shift+End { quit; } } diff --git a/desktopenvs/niri/niri/modules/environment.kdl b/desktopenvs/niri/niri/modules/environment.kdl index 62230ef..542eee9 100644 --- a/desktopenvs/niri/niri/modules/environment.kdl +++ b/desktopenvs/niri/niri/modules/environment.kdl @@ -1,20 +1,80 @@ +// ───────────────────────────────────────────────────────────────────────────── +// modules/environment.kdl — Environment variables and cursor settings +// +// PURPOSE: Sets environment variables that niri injects into every child +// process it spawns (applications, autostart commands, etc.). +// Also configures the hardware cursor appearance and auto-hide behaviour. +// +// NOTE: Variables set here apply to the compositor's process tree. They will +// NOT retroactively affect processes already running before niri started. +// ───────────────────────────────────────────────────────────────────────────── + // ── Environment Variables ───────────────────────────────────────────────────── environment { + // GTK_THEME: Tell GTK 3/4 applications which theme to load. + // WHY "cyberqueer": ensures every GTK app adopts the custom dark+pink palette + // without requiring each app to be individually configured. GTK_THEME "cyberqueer" + + // XCURSOR_SIZE: Hardware cursor icon size in pixels. + // WHY 40: on a HiDPI display at 2× scale, 40 CSS-pixels = 80 physical pixels, + // which renders at a readable size without being distractingly large. XCURSOR_SIZE "40" + + // XCURSOR_THEME: Name of the XCursor theme for the pointer icon set. + // "Nordzy-cursors-lefthand" is the left-handed variant of the Nordzy cursor + // theme. WHY left-handed: the primary user is left-handed. XCURSOR_THEME "Nordzy-cursors-lefthand" + + // QT_QPA_PLATFORMTHEME: Instruct Qt apps to use the GTK 3 platform theme + // plugin, so Qt dialogs inherit GTK colours and font settings. + // WHY: without this, Qt apps use a default Fusion style that clashes with + // the cyberqueer colour palette. QT_QPA_PLATFORMTHEME "gtk3" + + // QT_WAYLAND_DISABLE_WINDOWDECORATION: Tell Qt to suppress client-side + // title bars on Wayland (rely on the compositor's server-side decorations). + // WHY: consistent with prefer-no-csd in config.kdl; prevents double borders. QT_WAYLAND_DISABLE_WINDOWDECORATION "1" + + // MOZ_ENABLE_WAYLAND: Force Firefox/Thunderbird to use the native Wayland + // backend instead of falling back to XWayland. + // WHY: native Wayland gives better performance, correct HiDPI scaling, and + // proper fractional scaling support. MOZ_ENABLE_WAYLAND "1" + + // ELECTRON_OZONE_PLATFORM_HINT: Hint Electron apps (VSCode, Discord, etc.) + // to auto-select the Wayland Ozone backend when running under Wayland. + // "auto" lets Electron decide; it chooses Wayland when $WAYLAND_DISPLAY is set. ELECTRON_OZONE_PLATFORM_HINT "auto" + + // NIXOS_OZONE_WL: Compatibility variable recognised by Electron apps packaged + // for NixOS to activate Wayland Ozone. Harmless on non-NixOS systems; included + // so Flatpak/AUR Electron packages that check this also opt into Wayland. NIXOS_OZONE_WL "1" + + // GDK_SCALE: Global integer scale factor for GTK 3/4 applications. + // WHY 2: matches the display's 2× HiDPI scale so UI elements are not blurry. + // Note: this is an integer scale; fractional scaling is handled separately + // by the Wayland output configuration in outputs.kdl. GDK_SCALE "2" } // ── Cursor ──────────────────────────────────────────────────────────────────── cursor { + // xcursor-theme: compositor-level cursor theme (overrides the env var above + // for the pointer rendered by niri itself, not just child processes). xcursor-theme "Nordzy-cursors-lefthand" + + // xcursor-size: cursor size at compositor level, matching XCURSOR_SIZE above. xcursor-size 40 + + // hide-after-inactive-ms: automatically hide the cursor after 5 seconds of + // no mouse movement. WHY: reduces visual clutter during reading or video. hide-after-inactive-ms 5000 + + // hide-when-typing: immediately hide the cursor when a key is pressed. + // WHY: removes the cursor from the field of view while typing so it doesn't + // obscure text — restored as soon as the mouse moves again. hide-when-typing } diff --git a/encrypt.sh b/encrypt.sh index d5f7cba..85044ab 100755 --- a/encrypt.sh +++ b/encrypt.sh @@ -1,3 +1,12 @@ #!/usr/bin/env bash +# Encrypt a plaintext string with AES-256-CBC and output base64-encoded ciphertext. +# Usage: encrypt.sh <plaintext> <passphrase> +# The result can be decrypted with decrypt.sh using the same passphrase. +# -a : base64-encode the binary ciphertext so the result is printable/copy-pasteable +# -salt : prepend a random 8-byte salt to prevent identical plaintexts from +# producing identical outputs (thwarts rainbow-table attacks) +# -pbkdf2 : derive the key via PBKDF2 (Password-Based Key Derivation Function 2); +# more resistant to brute-force than the deprecated EVP_BytesToKey default +# -pass pass : read the passphrase from CLI argument $2 (avoids an interactive prompt) echo $1 | openssl aes-256-cbc -a -salt -pbkdf2 -pass pass:$2 diff --git a/nvim/init.lua b/nvim/init.lua index 49b2adf..6dcddd9 100644 --- a/nvim/init.lua +++ b/nvim/init.lua @@ -1,43 +1,56 @@ -- ── Bootstrap lazy.nvim ─────────────────────────────────────────────────────── +-- lazy.nvim is the plugin manager. On first launch it clones itself into the +-- Neovim data directory (~/.local/share/nvim/lazy/lazy.nvim). +-- vim.uv is the preferred libuv binding in Neovim >= 0.10; vim.loop is the +-- legacy alias kept for compatibility with older releases. local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" if not (vim.uv or vim.loop).fs_stat(lazypath) then vim.fn.system({ - "git", "clone", "--filter=blob:none", + "git", "clone", + "--filter=blob:none", -- blobless clone: skips file blobs, fetching only on checkout "https://github.com/folke/lazy.nvim.git", "--branch=stable", lazypath, }) end +-- Prepend lazy's path so Neovim can find and load it before any other plugin. vim.opt.rtp:prepend(lazypath) -- ── Plugins ─────────────────────────────────────────────────────────────────── require("lazy").setup({ + -- CyberQueer colorscheme loaded from the local dotfiles tree (not from GitHub) + -- so that apply-theme.sh changes are picked up without a plugin update. { dir = vim.fn.expand("~/Dotfiles/nvim/theme/cyberqueer.nvim") }, - "rktjmp/lush.nvim", - "tpope/vim-sensible", - "junegunn/goyo.vim", - "arecarn/vim-crunch", - "preservim/nerdtree", - "ryanoasis/vim-devicons", + "rktjmp/lush.nvim", -- HSL-based colorscheme builder used by cyberqueer + "tpope/vim-sensible", -- Sensible Vim defaults (better backspace, history, etc.) + "junegunn/goyo.vim", -- Distraction-free writing mode (centers and pads the buffer) + "arecarn/vim-crunch", -- Evaluate math expressions inside the buffer + "preservim/nerdtree", -- File-tree sidebar navigator + "ryanoasis/vim-devicons", -- Nerd Font icons for NERDTree and airline + -- fzf binary + Vim integration; build step runs fzf#install() to compile the binary { "junegunn/fzf", build = function() vim.fn["fzf#install"]() end }, - "junegunn/fzf.vim", - "vim-airline/vim-airline", - "vim-airline/vim-airline-themes", - "voldikss/vim-floaterm", - "rust-lang/rust.vim", - "norcalli/nvim-colorizer.lua", + "junegunn/fzf.vim", -- :Files, :Rg, :Buffers etc. powered by fzf + "vim-airline/vim-airline", -- Status/tabline with Powerline-style separators + "vim-airline/vim-airline-themes", -- Theme library for vim-airline (cyberqueer theme used) + "voldikss/vim-floaterm", -- Floating terminal windows inside Neovim + "rust-lang/rust.vim", -- Rust syntax, fmt-on-save, and cargo integration + "norcalli/nvim-colorizer.lua", -- Inline hex/RGB color previews in CSS, config files, etc. + -- CoC: full LSP client with completion, hover, diagnostics; pinned to "release" branch { "neoclide/coc.nvim", branch = "release" }, + -- vim-visual-multi: multiple-cursor editing (like Sublime Text ctrl+d) { "mg979/vim-visual-multi", branch = "master" }, - "SirVer/ultisnips", - "honza/vim-snippets", - "mfussenegger/nvim-dap", - "elihunter173/dirbuf.nvim", - "tpope/vim-dadbod", - "kristijanhusak/vim-dadbod-ui", - "kristijanhusak/vim-dadbod-completion", - "nvim-mini/mini.icons", - "tadmccorkle/markdown.nvim", + "SirVer/ultisnips", -- Snippet engine (expand/jump triggers configured below) + "honza/vim-snippets", -- Community snippet library consumed by UltiSnips + "mfussenegger/nvim-dap", -- Debug Adapter Protocol client for step-through debugging + "elihunter173/dirbuf.nvim", -- Edit directory contents like a buffer (rename, delete, etc.) + "tpope/vim-dadbod", -- Database client (SQL queries against any DB URL) + "kristijanhusak/vim-dadbod-ui", -- TUI for vim-dadbod (tree browser + result panes) + "kristijanhusak/vim-dadbod-completion", -- CoC/native completion source for SQL via dadbod + "nvim-mini/mini.icons", -- Minimal icon provider used by dadbod-ui and others + "tadmccorkle/markdown.nvim", -- Markdown helpers: table formatting, checkboxes, etc. + -- glow.nvim: render Markdown in a floating window via the `glow` CLI { "ellisonleao/glow.nvim", config = true }, - "itchyny/calendar.vim", + "itchyny/calendar.vim", -- Interactive calendar; used in the PIM overlay (key "g") + -- claude-code.nvim: Claude Code integration (opens/communicates with the `claude` CLI) { "greggh/claude-code.nvim", dependencies = { "nvim-lua/plenary.nvim" }, @@ -46,61 +59,75 @@ require("lazy").setup({ end, }, }, { - -- keep lazy's own UI out of the way on first install + -- Use a bundled colorscheme during the initial lazy install so the UI is + -- readable even before cyberqueer is downloaded and compiled. install = { colorscheme = { "habamax" } }, }) -- ── Colorscheme & UI ────────────────────────────────────────────────────────── vim.cmd("colorscheme cyberqueer") +-- Tell airline to use Powerline-patched glyphs for segment separators. vim.g.airline_powerline_fonts = 1 vim.g.airline_theme = "cyberqueer" +-- Embed machine identity in the airline status bar (section_x = right side). +-- Useful when SSHed into multiple machines or running nested Neovim sessions. local ipaddr = vim.trim(vim.fn.system("hostname -i")) local hostname = vim.trim(vim.fn.system("hostname -s")) vim.g.airline_section_x = "IP:" .. ipaddr .. " DNS:" .. hostname -- ── Providers ───────────────────────────────────────────────────────────────── +-- Disable unused remote plugin providers to suppress ":checkhealth" warnings +-- and avoid the startup penalty of probing for ruby/perl interpreters. vim.g.loaded_ruby_provider = 0 vim.g.loaded_perl_provider = 0 -- ── Editor options ──────────────────────────────────────────────────────────── +-- Enable filetype-specific plugins and indentation rules (e.g. Python 4-space). vim.cmd("filetype plugin indent on") vim.cmd("syntax enable") -vim.opt.number = true -vim.opt.relativenumber = true -vim.opt.cursorline = true -vim.opt.cursorcolumn = true -vim.opt.showmode = false -vim.opt.shiftwidth = 4 -vim.opt.scrolloff = 5 -vim.opt.wrap = false -vim.opt.incsearch = true -vim.opt.ignorecase = true -vim.opt.smartcase = true -vim.opt.showcmd = true -vim.opt.showmatch = true -vim.opt.hlsearch = true -vim.opt.history = 1000 -vim.opt.wildmenu = true -vim.opt.wildmode = "list:longest" +vim.opt.number = true -- show absolute line numbers in the gutter +vim.opt.relativenumber = true -- show relative numbers above/below for fast j/k jumps +vim.opt.cursorline = true -- highlight the entire line the cursor is on +vim.opt.cursorcolumn = true -- highlight the entire column the cursor is in +vim.opt.showmode = false -- airline already shows mode; suppress the redundant "-- INSERT --" message +vim.opt.shiftwidth = 4 -- number of spaces for each indentation level (>> / <<) +vim.opt.scrolloff = 5 -- keep at least 5 lines visible above/below the cursor +vim.opt.wrap = false -- disable line wrapping (horizontal scroll instead) +vim.opt.incsearch = true -- show matches incrementally as you type the search pattern +vim.opt.ignorecase = true -- case-insensitive search by default +vim.opt.smartcase = true -- override ignorecase when the pattern contains uppercase +vim.opt.showcmd = true -- show the partial command being typed in the status line +vim.opt.showmatch = true -- briefly jump to the matching bracket when one is typed +vim.opt.hlsearch = true -- highlight all search matches (clear with :noh) +vim.opt.history = 1000 -- keep 1000 entries in command and search history +vim.opt.wildmenu = true -- show a completion menu for : commands +vim.opt.wildmode = "list:longest" -- first tab lists completions, second completes to longest common prefix -- ── Keymaps ─────────────────────────────────────────────────────────────────── -- window navigation (normal mode) +-- Use Ctrl+hjkl to move between splits instead of the two-key <C-w> prefix. +-- <C-l> cycles to the next window (wraps); others jump directionally. vim.keymap.set("n", "<C-l>", "<C-w>w") vim.keymap.set("n", "<C-h>", "<C-w>h") vim.keymap.set("n", "<C-j>", "<C-w>j") vim.keymap.set("n", "<C-k>", "<C-w>k") -- window navigation from terminal-insert mode (exit insert, then move) +-- <C-\><C-n> exits terminal-insert mode first; without it the control sequence +-- is sent to the shell process instead of being handled by Neovim. vim.keymap.set("t", "<C-h>", "<C-\\><C-n><C-w>h", { silent = true }) vim.keymap.set("t", "<C-j>", "<C-\\><C-n><C-w>j", { silent = true }) vim.keymap.set("t", "<C-k>", "<C-\\><C-n><C-w>k", { silent = true }) vim.keymap.set("t", "<C-l>", "<C-\\><C-n><C-w>w", { silent = true }) -- auto-enter insert mode when focusing a terminal buffer (skip floaterm) +-- Without this, focusing a terminal buffer requires pressing 'i' manually. +-- Floaterm manages its own mode, so we exclude it to avoid conflicting with +-- its internal state machine. vim.api.nvim_create_autocmd("BufEnter", { pattern = "term://*", callback = function() @@ -111,6 +138,8 @@ vim.api.nvim_create_autocmd("BufEnter", { }) -- calendar.vim steals <C-hjkl> for month nav; restore window movement +-- The plugin sets buffer-local mappings on FileType calendar, so we override +-- them with { buffer = true } mappings of our own that fire after the plugin's. vim.api.nvim_create_autocmd("FileType", { pattern = "calendar", callback = function() @@ -122,23 +151,31 @@ vim.api.nvim_create_autocmd("FileType", { end, }) --- quick actions +-- quick actions: single-key shortcuts for frequently used commands +-- t: open a new floating terminal via vim-floaterm vim.keymap.set("n", "t", ":FloatermNew<CR>", { silent = true }) +-- e: toggle the NERDTree sidebar; <C-W>l immediately moves focus back to the +-- editing window so the cursor doesn't land in the tree vim.keymap.set("n", "e", ":NERDTreeToggle<CR><C-W>l", { silent = true }) +-- s: toggle the vim-dadbod-ui database browser vim.keymap.set("n", "s", ":DBUIToggle<CR>", { silent = true }) +-- q: try to save-quit (wq); fall back to force-quit (q) for unsaved/read-only buffers vim.keymap.set("n", "q", function() local ok = pcall(vim.cmd, "wq") if not ok then vim.cmd("q") end end, { silent = true }) --- insert mode completion +-- insert mode completion: Tab triggers Neovim's built-in keyword completion (<C-N>) +-- S-Tab reverts to a literal Tab character so indentation still works normally. vim.keymap.set("i", "<TAB>", "<C-N>") vim.keymap.set("i", "<S-TAB>", "<TAB>") --- sudo save +-- sudo save: :w!! pipes the buffer through sudo tee, bypassing a read-only +-- file system permission. The '%' is the current file path. vim.cmd("ca w!! w !sudo tee '%'") --- visual block shorthand +-- :Vb is a convenience alias for entering visual-block mode without pressing +-- Ctrl+v, which conflicts with paste in some terminal emulators. vim.cmd("command! Vb normal! <C-v>") -- ── UltiSnips ───────────────────────────────────────────────────────────────── diff --git a/setup/arch-autoinstall.sh b/setup/arch-autoinstall.sh index d873f26..195fa79 100755 --- a/setup/arch-autoinstall.sh +++ b/setup/arch-autoinstall.sh @@ -7,6 +7,8 @@ # Answerfile fields: drive, kernel, keymap, hostname, username, encrypt, fido2_root, # fido2_user, run_tui (password always prompted interactively) +# -E: propagate ERR trap into functions and subshells; -e: abort on any error; +# -u: treat unset variables as errors; -o pipefail: catch failures inside pipes. set -Eeuo pipefail ############################################ @@ -20,22 +22,29 @@ LOGFILE="$HOME/arch-autoinstall.log" echo "############################################" echo } >> "$LOGFILE" +# Redirect all subsequent stdout and stderr to both the terminal and the log. +# `tee -a` appends so repeated runs accumulate without overwriting; process +# substitution keeps the descriptor open for the full lifetime of the script. exec > >(tee -a "$LOGFILE") 2>&1 ############################################ # Error handler — TUI prompt to send log via croc ############################################ error_handler() { + # Capture exit code before any other command can overwrite $?. + # $LINENO is passed as $1 from the trap, so we default to '?' if missing. local exit_code=$? line_num="${1:-?}" echo "" >> "$LOGFILE" echo "ERROR: installer failed at line $line_num (exit code $exit_code)" >> "$LOGFILE" + # Prefer a dialog TUI if available; fall back to plain readline on headless installs. if command -v dialog &>/dev/null; then if dialog --clear --title " Installer Error " \ --yesno \ "Installation failed at line $line_num (exit code: $exit_code).\n\nSend the log to another system via croc for analysis?" \ 9 62; then clear + # croc may not be present in the base live ISO — install it on demand. if ! command -v croc &>/dev/null; then echo "Installing croc..." pacman -Sy --noconfirm croc 2>/dev/null || true @@ -53,6 +62,7 @@ error_handler() { echo "" echo "Installation failed at line $line_num (exit code $exit_code)." read -rp "Send log via croc? [y/N]: " _croc_ans + # ${_croc_ans,,} lowercases the answer so "Y", "y" both match. if [[ "${_croc_ans,,}" == "y" ]]; then command -v croc &>/dev/null || pacman -Sy --noconfirm croc 2>/dev/null || true croc send "$LOGFILE" || true @@ -62,17 +72,24 @@ error_handler() { fi exit "$exit_code" } +# Fire error_handler on any ERR signal, passing the current line number. +# Single quotes prevent $LINENO from being evaluated at trap-definition time. trap 'error_handler $LINENO' ERR ############################################ # ANSWERFILE ############################################ +# Allow an external override via the environment; default to /answerfile.json +# which is where build.sh --preconf embeds the file in the live ISO. ANSWERFILE="${ANSWERFILE:-/answerfile.json}" AF_MODE=false [[ -f "$ANSWERFILE" ]] && AF_MODE=true af_get() { # af_get <jq-filter> [default] + # Reads one field from the answerfile with a jq expression. + # `// empty` returns empty string instead of "null" when the field is absent; + # the outer `|| true` prevents a jq parse error from aborting the script. local val val=$(jq -r "${1} // empty" "$ANSWERFILE" 2>/dev/null || true) if [[ -z "$val" ]]; then printf '%s' "${2:-}"; else printf '%s' "$val"; fi @@ -80,28 +97,38 @@ af_get() { af_bool() { # Returns YES or NO from a JSON boolean field + # `// false` provides a safe default when the key is absent entirely. local val; val=$(jq -r "${1} // false" "$ANSWERFILE" 2>/dev/null || true) [[ "$val" == "true" ]] && echo "YES" || echo "NO" } get_mac_suffix() { + # Produce a compact MAC address string (no colons) from the first non-loopback + # interface to create a unique per-machine hostname suffix at deploy time. + # The awk pattern skips "lo" by requiring the 3rd char to differ from 'o'. local mac mac=$(ip link show 2>/dev/null \ | awk '/^[0-9]+: [^l][^o]/{iface=1} iface && /link\/ether/{print $2; iface=0; exit}') + # Remove colons via parameter substitution: ${var//pattern/replacement}. printf '%s' "${mac//:/}" } +# Map drive paths to their correct partition suffix. +# NVMe (/dev/nvme0n1) and eMMC (/dev/mmcblk0) use a 'p' before the part number; +# conventional block devices (/dev/sda) do not. part() { [[ "$1" == *nvme* || "$1" == *mmcblk* ]] && echo "${1}p${2}" || echo "${1}${2}"; } if $AF_MODE; then echo "Answerfile detected: $ANSWERFILE" - # Ensure jq is available + # jq is required to parse the answerfile; it may not be on the base live ISO. command -v jq &>/dev/null || pacman -Sy --noconfirm jq fi ############################################ # NETWORK CHECK ############################################ +# Send one ICMP packet (-c1) with a 3-second timeout (-W3) to Cloudflare DNS. +# In AF_MODE we warn but continue — packages in pacstrap may already be cached. if ! ping -c1 -W3 1.1.1.1 &>/dev/null; then if $AF_MODE; then echo "Warning: no internet connection detected — continuing in answerfile mode." @@ -123,6 +150,8 @@ fi # KEYMAP # To add more layouts: append "code|Display Name" to KEYMAPS ############################################ +# Each entry is "keymap-code|Human-readable name"; the pipe acts as a cheap delimiter +# so both pieces of data live in a single array element without needing two arrays. KEYMAPS=( "us|English US" "de|German" @@ -133,19 +162,26 @@ if $AF_MODE; then else echo "Select keyboard layout:" for i in "${!KEYMAPS[@]}"; do + # %%|* strips everything from the first '|' onward → the keymap code. _km_code="${KEYMAPS[$i]%%|*}" + # ##*| strips everything up to and including the last '|' → the display name. _km_name="${KEYMAPS[$i]##*|}" printf " %d) %-14s (%s)\n" $((i+1)) "$_km_name" "$_km_code" done read -rp "Choice [1]: " _KM_IDX + # Convert 1-based user input to 0-based array index; default to 1 if blank. _KM_IDX=$(( ${_KM_IDX:-1} - 1 )) if (( _KM_IDX >= 0 && _KM_IDX < ${#KEYMAPS[@]} )); then LIVE_KEYMAP="${KEYMAPS[$_KM_IDX]%%|*}" else + # Out-of-range input silently falls back to the first entry (us). LIVE_KEYMAP="${KEYMAPS[0]%%|*}" fi fi +# Apply the chosen layout to the live environment immediately so subsequent +# interactive prompts can be typed with the correct physical keyboard. loadkeys "$LIVE_KEYMAP" +# Also export as KEYMAP so the chroot heredoc can write /etc/vconsole.conf. KEYMAP="$LIVE_KEYMAP" ############################################ @@ -153,22 +189,27 @@ KEYMAP="$LIVE_KEYMAP" ############################################ if $AF_MODE; then echo "WARNING: Automated install — all data on $(af_get '.drive' '/dev/?') will be ERASED." + # Give the operator a 5-second window to abort with Ctrl-C before destructive ops begin. echo "Proceeding in 5 seconds... (Ctrl-C to abort)" sleep 5 else echo "WARNING: This will ERASE ALL DATA on the selected drive!" read -rp "Type 'YES' to continue: " confirm + # Require the exact string "YES" (case-sensitive) to prevent accidental wipes. [[ "$confirm" == "YES" ]] || { echo "Aborted."; exit 1; } fi ############################################ # REQUIRED PACKAGES FOR INSTALL ENVIRONMENT ############################################ +# Install tools needed by this script into the live environment before partitioning: +# parted — disk partitioning; cryptsetup — LUKS; libfido2/pam-u2f — FIDO2 enrollment. pacman -Sy --noconfirm parted cryptsetup libfido2 pam-u2f ############################################ # DRIVE SELECTION ############################################ +# Print current block device layout so the operator can confirm the target drive. lsblk if $AF_MODE && [[ -n "$(af_get '.drive')" ]]; then DRIVE=$(af_get '.drive') @@ -183,6 +224,8 @@ fi if $AF_MODE; then KERNEL=$(af_get '.kernel' 'linux') RAW_HOSTNAME=$(af_get '.hostname' '') + # Append MAC suffix to make the hostname unique when the same answerfile is + # deployed to multiple machines (e.g. a lab rollout or reinstall fleet). if [[ -n "$RAW_HOSTNAME" ]]; then HOSTNAME="${RAW_HOSTNAME}-$(get_mac_suffix)" else @@ -202,6 +245,8 @@ else read -rp "Enter hostname: " HOSTNAME read -rp "Enter username: " USERNAME read -rp "Enable disk encryption? (YES/NO): " ENCRYPT_DISK + # Default FIDO2 root to NO; only ask when encryption is active because FIDO2 + # unlocking only makes sense on an encrypted root partition. FIDO_ROOT="NO" if [[ "$ENCRYPT_DISK" == "YES" ]]; then read -rp "Enable FIDO2 unlocking for root partition? (YES/NO): " FIDO_ROOT @@ -213,8 +258,11 @@ fi read -rp "Enter password for $USERNAME: " USERPASS [[ -z "$USERPASS" ]] && { echo "Error: password cannot be empty."; exit 1; } +# In interactive mode, decide now whether to run the dotfiles TUI inside chroot. +# AF_MODE already has RUN_TUI set from the answerfile. if ! $AF_MODE; then read -rp "Run dotfiles TUI setup inside chroot now? [YES/no]: " _RUN_TUI_IN + # Default to YES if the user presses Enter without typing anything. _RUN_TUI_IN="${_RUN_TUI_IN:-YES}" [[ "${_RUN_TUI_IN^^}" == "YES" ]] && RUN_TUI="YES" || RUN_TUI="NO" fi @@ -222,12 +270,17 @@ fi ############################################ # RAM / PARTITION SIZING ############################################ +# Read installed RAM in GiB; used to size both the swap and to calculate remaining +# root space. `--giga` prints in GiB units; awk extracts the total column. RAM_GB=$(free --giga | awk '/^Mem:/ {print $2}') +# 15 GiB boot partition: large enough for multiple kernels, initramfs, and UKIs. BOOT_SIZE=15GiB SWAP_SIZE="${RAM_GB}GiB" +# lsblk -b reports raw bytes; divide down to GiB for integer arithmetic. DISK_SIZE=$(lsblk -b -dn -o SIZE "$DRIVE") DISK_GIB=$((DISK_SIZE / 1024 / 1024 / 1024)) +# Root gets whatever is left after reserving 15 GiB for boot and RAM_GB for swap. ROOT_GIB=$((DISK_GIB - RAM_GB - 15)) if (( ROOT_GIB < 8 )); then echo "ERROR: Not enough disk space. Root would be only ${ROOT_GIB}GiB (need ≥8GiB)." @@ -242,12 +295,19 @@ echo " Swap: ${SWAP_SIZE}" ############################################ # PARTITION DISK ############################################ +# --script suppresses interactive prompts; mklabel gpt wipes any existing table. +# Partition 1: ESP at 1MiB–15GiB, flagged boot/esp so firmware can find it. +# Partition 2: ROOT immediately after the ESP. +# Partition 3: SWAP fills the rest (100%). +# Starting at 1MiB aligns the first partition to a 1 MiB boundary, which is +# optimal for both SSD erase blocks and spinner track alignment. parted "$DRIVE" --script mklabel gpt \ mkpart ESP fat32 1MiB 15GiB \ set 1 boot on \ mkpart ROOT 15GiB "$((15 + ROOT_GIB))"GiB \ mkpart SWAP "$((15 + ROOT_GIB))"GiB 100% +# Resolve partition device paths via the `part` helper (handles NVMe 'p' suffix). BOOT_PART=$(part "$DRIVE" 1) ROOT_PART=$(part "$DRIVE" 2) SWAP_PART=$(part "$DRIVE" 3) @@ -255,7 +315,9 @@ SWAP_PART=$(part "$DRIVE" 3) ############################################ # FORMAT BOOT + SWAP ############################################ +# FAT32 is required for the EFI System Partition; -F32 sets the FAT variant. mkfs.fat -F32 "$BOOT_PART" +# mkswap writes the swap header; swapon activates it for use by pacstrap. mkswap "$SWAP_PART" swapon "$SWAP_PART" @@ -266,7 +328,10 @@ LUKS_BACKUP_KEY="" # path to key file, set only when encryption is active if [[ "$ENCRYPT_DISK" == "YES" ]]; then echo "Encrypting root partition..." + # -v (verbose) shows progress; luksFormat initialises a LUKS1 container by default + # and prompts for the primary passphrase interactively. cryptsetup -v luksFormat "$ROOT_PART" + # Open the container and expose it as /dev/mapper/cryptroot for formatting. cryptsetup open "$ROOT_PART" cryptroot # ── Auto-generate backup LUKS key ────────────────────────────────────────── @@ -274,25 +339,38 @@ if [[ "$ENCRYPT_DISK" == "YES" ]]; then # without the primary passphrase. It is written to /_LUKS_BACKUP_KEY in the # new system (inside the encrypted container) where only root can read it. LUKS_BACKUP_KEY=$(mktemp /tmp/luks-backup-key.XXXXXX) + # Read 64 bytes from /dev/urandom and base64-encode them (-w0 disables line + # wrapping) to produce a printable key with high entropy. dd if=/dev/urandom bs=64 count=1 2>/dev/null | base64 -w0 > "$LUKS_BACKUP_KEY" echo "Enrolling auto-generated backup LUKS key..." + # luksAddKey reads the existing passphrase interactively to authorise adding + # the new key file into a free LUKS slot. cryptsetup luksAddKey "$ROOT_PART" "$LUKS_BACKUP_KEY" # ── Optional FIDO2 enrollment ───────────────────────────────────────────── if [[ "$FIDO_ROOT" == "YES" ]]; then echo "Insert FIDO2 key for LUKS and touch when prompted..." + # --fido2-device=auto finds any connected FIDO2 token automatically. + # --fido2-with-client-pin=no skips PIN verification; the hardware + # user-presence tap alone is sufficient for this setup. systemd-cryptenroll "$ROOT_PART" --fido2-device=auto --fido2-with-client-pin=no fi ############################################ # BTRFS ON ENCRYPTED ROOT ############################################ + # Format the decrypted mapper device, not the raw partition. mkfs.btrfs /dev/mapper/cryptroot + # Mount flat (no subvolume) first so we can create the subvolume layout. mount /dev/mapper/cryptroot /mnt + # @ is the conventional name for the root subvolume in btrfs-on-Arch setups; + # @home separates user data so it can be snapshotted or rolled back independently. btrfs subvolume create /mnt/@ btrfs subvolume create /mnt/@home + # Unmount the flat view before remounting through the subvolumes. umount /mnt + # Mount each subvolume at its final path for pacstrap to populate. mount -o subvol=@ /dev/mapper/cryptroot /mnt mkdir -p /mnt/home mount -o subvol=@home /dev/mapper/cryptroot /mnt/home @@ -303,6 +381,7 @@ else ############################################ # BTRFS ON UNENCRYPTED ROOT ############################################ + # Same subvolume layout as the encrypted branch for consistency. mkfs.btrfs "$ROOT_PART" mount "$ROOT_PART" /mnt btrfs subvolume create /mnt/@ @@ -315,11 +394,15 @@ else fi mkdir -p /mnt/boot +# Mount the ESP so GRUB and the kernel can be installed inside the new root tree. mount "$BOOT_PART" /mnt/boot # Place backup key inside the new system (only accessible when disk is unlocked) if [[ -n "$LUKS_BACKUP_KEY" ]]; then + # `install -m 400` creates the destination file with mode 0400 (root read-only) + # in one atomic step, avoiding an intermediate world-readable state. install -m 400 "$LUKS_BACKUP_KEY" /mnt/_LUKS_BACKUP_KEY + # Remove the temp file from the live environment immediately after copying. rm -f "$LUKS_BACKUP_KEY" echo "Backup LUKS key written to /_LUKS_BACKUP_KEY in new system." fi @@ -327,11 +410,14 @@ fi ############################################ # GPU DETECTION ############################################ +# lspci lists PCI devices; filter for VGA and 3D controllers to identify the GPU. +# `|| true` prevents set -e from aborting if no GPU line is found. GPU_INFO=$(lspci | grep -E "VGA|3D" || true) GPU_PKGS="" if echo "$GPU_INFO" | grep -qi "NVIDIA"; then GPU_PKGS="nvidia nvidia-utils" elif echo "$GPU_INFO" | grep -qi "AMD"; then + # xf86-video-amdgpu provides the Xorg DDX driver; the kernel DRM driver ships in linux-firmware. GPU_PKGS="xf86-video-amdgpu" elif echo "$GPU_INFO" | grep -qi "Intel"; then GPU_PKGS="xf86-video-intel" @@ -341,6 +427,8 @@ echo "Detected GPU: ${GPU_INFO:-none}" ############################################ # BASE INSTALL ############################################ +# pacstrap installs packages into /mnt using pacman's keyring (-K initialises it). +# GPU_PKGS is intentionally unquoted so it expands to multiple words (or nothing). # shellcheck disable=SC2086 pacstrap -K /mnt base base-devel "$KERNEL" linux-firmware vim bash zsh git less btop fastfetch \ networkmanager grub cryptsetup libfido2 pam-u2f efibootmgr sudo btrfs-progs lvm2 jq $GPU_PKGS @@ -348,11 +436,15 @@ pacstrap -K /mnt base base-devel "$KERNEL" linux-firmware vim bash zsh git less ############################################ # FSTAB ############################################ +# genfstab -U uses UUIDs (rather than device names or labels) so the fstab +# remains valid even if drive letter assignments change after a hardware swap. genfstab -U /mnt >> /mnt/etc/fstab ############################################ # COPY ANSWERFILE INTO NEW SYSTEM ############################################ +# Make the answerfile available inside the new system so the dotfiles TUI +# installer can read the same selections without re-prompting the user. if $AF_MODE; then install -m 644 "$ANSWERFILE" /mnt/answerfile.json fi @@ -360,48 +452,69 @@ fi ############################################ # PASS VARIABLES INTO CHROOT ############################################ +# Variables must be exported so the arch-chroot heredoc inherits them. +# The heredoc uses single-quote delimiter ('CHROOT_EOF') which prevents expansion +# outside the chroot, so env vars must be in the environment, not inline. export HOSTNAME USERNAME USERPASS ROOT_PART KERNEL FIDO_ROOT FIDO_USER ENCRYPT_DISK KEYMAP ############################################ # CHROOT CONFIGURATION ############################################ +# arch-chroot binds /proc, /sys, /dev and runs commands inside the new root. +# Single-quoted 'CHROOT_EOF' prevents the outer shell from expanding variables; +# the inner shell resolves them from the exported environment above. arch-chroot /mnt /bin/bash <<'CHROOT_EOF' set -euo pipefail # Locale +# Uncomment (create) the en_US.UTF-8 entry so locale-gen can compile it. echo "en_US.UTF-8 UTF-8" > /etc/locale.gen locale-gen echo "LANG=en_US.UTF-8" > /etc/locale.conf +# vconsole.conf sets the keymap for the virtual console (TTY) on every boot. echo "KEYMAP=${KEYMAP}" > /etc/vconsole.conf # Time / hostname +# Symlink the timezone file so /etc/localtime always points at the correct zoneinfo. ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime +# --systohc writes the current system time to the hardware clock (RTC). hwclock --systohc echo "$HOSTNAME" > /etc/hostname # NetworkManager +# Enable at boot so the system has networking on first login without manual setup. systemctl enable NetworkManager -# Populate /etc/skel with dotfiles and base configs for all new users +# Populate /etc/skel before user creation so useradd -m copies everything echo "Cloning dotfiles into /etc/skel..." git clone https://git.abdelbaki.eu/The_miro/Dotfiles.git /etc/skel/Dotfiles \ || echo "Warning: dotfiles clone into skel failed — will fall back to direct clone." # Seed /etc/skel with base shell dotfiles from the repo clone if [[ -d /etc/skel/Dotfiles ]]; then D=/etc/skel/Dotfiles + # Copy individually; `2>/dev/null || true` silently skips any file that is absent + # in this repo revision rather than aborting the install. cp "$D/.zshrc" /etc/skel/.zshrc 2>/dev/null || true cp "$D/.bashrc" /etc/skel/.bashrc 2>/dev/null || true cp "$D/.vimrc" /etc/skel/.vimrc 2>/dev/null || true fi # User +# -m: create home dir; -G wheel: add to sudoers group; -s /bin/zsh: default shell. useradd -m -G wheel -s /bin/zsh "$USERNAME" +# chpasswd reads "user:pass" from stdin to set the password non-interactively. echo "$USERNAME:$USERPASS" | chpasswd +# Grant wheel group full sudo access (ALL:ALL covers any user/group runas context). echo "%wheel ALL=(ALL:ALL) ALL" >> /etc/sudoers ################################################### # INITRAMFS CONFIG ################################################### +# The HOOKS line controls which initramfs modules are compiled in. +# Three variants depending on encryption and FIDO2 choices: +# 1. FIDO2 root unlock: needs `systemd` + `sd-encrypt` for systemd-cryptsetup. +# 2. Password-only LUKS: uses classic `encrypt` hook (no systemd dependency). +# 3. Unencrypted: minimal hook set — no encrypt hook needed. if [[ "$ENCRYPT_DISK" == "YES" && "$FIDO_ROOT" == "YES" ]]; then sed -i 's/^HOOKS=.*/HOOKS=(base udev systemd autodetect microcode modconf kms consolefont block sd-encrypt lvm2 btrfs filesystems keyboard keymap fsck)/' /etc/mkinitcpio.conf elif [[ "$ENCRYPT_DISK" == "YES" ]]; then @@ -410,25 +523,35 @@ else sed -i 's/^HOOKS=.*/HOOKS=(base udev autodetect microcode modconf kms consolefont block btrfs filesystems keyboard fsck)/' /etc/mkinitcpio.conf fi +# Regenerate the initramfs for the selected kernel preset. mkinitcpio -p "$KERNEL" ################################################### # GRUB CONFIG ################################################### +# Read the raw partition UUID; used in kernel cmdline for stable device identification. UUID=$(blkid -s UUID -o value "$ROOT_PART") if [[ "$ENCRYPT_DISK" == "YES" ]]; then if [[ "$FIDO_ROOT" == "YES" ]]; then + # systemd-cryptsetup syntax: rd.luks.name=<UUID>=<name>. + # The FIDO2 device is auto-detected by systemd-cryptenroll at boot. KERNEL_CMD="rd.luks.name=${UUID}=cryptroot root=/dev/mapper/cryptroot" else + # Classic initramfs encrypt hook syntax: cryptdevice=UUID=<UUID>:<name>. KERNEL_CMD="cryptdevice=UUID=${UUID}:cryptroot root=/dev/mapper/cryptroot" fi else + # Unencrypted btrfs: root= by UUID and rootflags to select the @ subvolume. KERNEL_CMD="root=UUID=${UUID} rootflags=subvol=@" fi +# Inject the kernel command line into /etc/default/grub using sed. +# The `|` delimiter avoids conflicts with `/` in device paths. sed -i "s|^GRUB_CMDLINE_LINUX=.*|GRUB_CMDLINE_LINUX=\"$KERNEL_CMD\"|" /etc/default/grub +# Install GRUB to the EFI partition; --bootloader-id sets the NVRAM entry name. grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=M-Archy-GRUB +# Generate grub.cfg from /etc/default/grub and detected kernels. grub-mkconfig -o /boot/grub/grub.cfg ################################################### @@ -437,23 +560,23 @@ grub-mkconfig -o /boot/grub/grub.cfg if [[ "$FIDO_USER" == "YES" ]]; then mkdir -p "/home/$USERNAME/.config/Yubico" echo "Insert FIDO2 key for user login and touch when prompted..." + # pamu2fcfg registers the FIDO2 key and writes the credential to u2f_keys. + # Running under sudo -u ensures the file is generated with the correct UID context. sudo -u "$USERNAME" pamu2fcfg -u "$USERNAME" > "/home/$USERNAME/.config/Yubico/u2f_keys" chown "$USERNAME":"$USERNAME" "/home/$USERNAME/.config/Yubico/u2f_keys" + # Append the PAM rule so pam_u2f is required for system-local-login sessions. echo "auth required pam_u2f.so" >> /etc/pam.d/system-local-login fi ################################################### # CLONE DOTFILES ################################################### -if [[ -d "/home/$USERNAME/Dotfiles" ]]; then - echo "Dotfiles already in home via skel — fixing ownership." - chown -R "$USERNAME:$USERNAME" "/home/$USERNAME" -else - echo "Cloning dotfiles directly to user home (skel clone failed)..." - git clone https://git.abdelbaki.eu/The_miro/Dotfiles.git "/home/$USERNAME/Dotfiles" \ - && chown -R "$USERNAME:$USERNAME" "/home/$USERNAME/Dotfiles" \ - || echo "Warning: dotfiles clone failed — clone manually after first boot." -fi +# Fall back to a direct clone if /etc/skel/Dotfiles was not copied into home +# (happens when the skel clone failed or useradd did not copy skel). +echo "Cloning dotfiles..." +git clone https://git.abdelbaki.eu/The_miro/Dotfiles.git "/home/$USERNAME/Dotfiles" \ + && chown -R "$USERNAME":"$USERNAME" "/home/$USERNAME/Dotfiles" \ + || echo "Warning: dotfiles clone failed — clone manually after first boot." CHROOT_EOF @@ -461,14 +584,20 @@ CHROOT_EOF # DOTFILES TUI SETUP (in-chroot, optional) ############################################ if [[ "${RUN_TUI^^}" == "YES" ]]; then + # Grant passwordless sudo temporarily so the TUI installer can call pacman/yay + # without needing a password inside the chroot (the real sudoers is already set). + # The file is removed immediately after the TUI exits. echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" \ | arch-chroot /mnt tee /etc/sudoers.d/99-setup-nopasswd > /dev/null echo "Running tui-install.sh as ${USERNAME} inside chroot..." + # `runuser -u` switches to the unprivileged user inside the chroot so that + # AUR helpers and dotfiles are owned/built by the correct UID. arch-chroot /mnt runuser -u "${USERNAME}" -- \ bash "/home/${USERNAME}/Dotfiles/setup/tui-install.sh" \ || echo "Warning: tui-install exited with errors — check ~/dotfiles-install.log in the new system." + # Remove the temporary no-password sudoers drop-in after setup is done. arch-chroot /mnt rm -f /etc/sudoers.d/99-setup-nopasswd fi @@ -507,6 +636,8 @@ echo "Log file: $LOGFILE" echo "############################################" echo +# Copy install log into /boot so it survives and is accessible before login. +# `2>/dev/null || true` silently ignores failures (e.g. /mnt/boot not writable). cp "$LOGFILE" /mnt/boot/ 2>/dev/null || true echo "Installation complete! Unmount and reboot:" diff --git a/setup/archbaseos-guided-install.sh b/setup/archbaseos-guided-install.sh index 5b5b6fb..ee9a89c 100755 --- a/setup/archbaseos-guided-install.sh +++ b/setup/archbaseos-guided-install.sh @@ -4,6 +4,8 @@ # If /answerfile.json exists (e.g. embedded via build.sh --preconf), all prompts # are answered from it. Missing fields fall back to interactive prompts. +# -E: propagate ERR trap into functions/subshells; -e: abort on error; +# -u: treat unset variables as errors; -o pipefail: fail on pipe errors. set -Eeuo pipefail ############################################ @@ -17,12 +19,17 @@ LOGFILE="$HOME/archbaseos-guided-install.log" echo "############################################" echo } >> "$LOGFILE" +# Redirect all subsequent stdout/stderr to both the terminal and the log file. +# `tee -a` appends so repeated runs accumulate without overwriting; process +# substitution keeps the descriptor open for the full lifetime of the script. exec > >(tee -a "$LOGFILE") 2>&1 ############################################ # Error handler — TUI prompt to send log via croc ############################################ error_handler() { + # Capture exit code before any other command can overwrite $?. + # $LINENO is passed as $1 from the trap; default to '?' if absent. local exit_code=$? line_num="${1:-?}" echo "" >> "$LOGFILE" echo "ERROR: installer failed at line $line_num (exit code $exit_code)" >> "$LOGFILE" @@ -30,7 +37,9 @@ error_handler() { echo "" echo "Installation failed at line $line_num (exit code $exit_code)." read -rp "Send log via croc? [y/N]: " _croc_ans + # ${_croc_ans,,} lowercases the answer so "Y" and "y" both match. if [[ "${_croc_ans,,}" == "y" ]]; then + # Install croc on demand — it may not be present in the base live ISO. command -v croc &>/dev/null || pacman -Sy --noconfirm croc 2>/dev/null || true croc send "$LOGFILE" || true else @@ -38,6 +47,8 @@ error_handler() { fi exit "$exit_code" } +# Fire error_handler on any ERR signal, passing the current line number. +# Single quotes prevent $LINENO from being evaluated at trap-definition time. trap 'error_handler $LINENO' ERR ############################################ @@ -45,12 +56,14 @@ trap 'error_handler $LINENO' ERR ############################################ confirm() { + # Require the exact string "YES" before erasing $1 — prevents accidental wipes. echo "WARNING: This will ERASE ALL DATA on $1" read -rp "Type YES to continue: " ans [[ $ans == "YES" ]] } ask() { + # Thin wrapper around read -rp so callers can capture the result via $(). local prompt=$1 local var read -rp "$prompt: " var @@ -58,6 +71,8 @@ ask() { } pause() { + # Used before interactive hardware steps (e.g. FIDO2 enrollment) to give the + # operator time to insert the key before the script continues. read -rp "Press ENTER to continue..." } @@ -68,30 +83,41 @@ part() { [[ "$1" == *nvme* || "$1" == *mmcblk* ]] && echo "${1}p${2}" || echo "$ ############################################ # ANSWERFILE ############################################ +# Allow an external override via the environment; default to /answerfile.json, +# the path where build.sh --preconf embeds the file in the live ISO. ANSWERFILE="${ANSWERFILE:-/answerfile.json}" AF_MODE=false [[ -f "$ANSWERFILE" ]] && AF_MODE=true af_get() { + # Reads one field from the answerfile using a jq filter expression. + # `// empty` returns an empty string instead of "null" when the field is absent; + # `|| true` prevents a jq parse error from aborting the script via set -e. local val val=$(jq -r "${1} // empty" "$ANSWERFILE" 2>/dev/null || true) if [[ -z "$val" ]]; then printf '%s' "${2:-}"; else printf '%s' "$val"; fi } af_bool() { + # `// false` provides a safe default so missing boolean keys don't cause errors. local val; val=$(jq -r "${1} // false" "$ANSWERFILE" 2>/dev/null || true) [[ "$val" == "true" ]] && echo "YES" || echo "NO" } get_mac_suffix() { + # Produce a compact MAC address (no colons) from the first non-loopback interface + # to create a unique per-machine hostname suffix during unattended deployment. + # The awk pattern skips "lo" by requiring the 3rd character to differ from 'o'. local mac mac=$(ip link show 2>/dev/null \ | awk '/^[0-9]+: [^l][^o]/{iface=1} iface && /link\/ether/{print $2; iface=0; exit}') + # Remove colons via bash parameter substitution: ${var//pattern/replacement}. printf '%s' "${mac//:/}" } if $AF_MODE; then echo "== Arch Linux Guided Installer (answerfile mode) ==" + # jq is required to parse the answerfile; it may not ship in the base live ISO. command -v jq &>/dev/null || pacman -Sy --noconfirm jq else echo "== Arch Linux FIDO2-Ready Installer ==" @@ -100,6 +126,8 @@ fi ############################################ # NETWORK CHECK ############################################ +# Send one ICMP packet (-c1) with a 3-second timeout (-W3) to Cloudflare DNS. +# In AF_MODE we warn but continue — packages may already be cached in the live env. if ! ping -c1 -W3 1.1.1.1 &>/dev/null; then if $AF_MODE; then echo "Warning: no internet connection detected — continuing in answerfile mode." @@ -121,6 +149,8 @@ fi # Begin ############################################ +# Each entry is "keymap-code|Human-readable name"; the pipe delimiter lets both +# pieces of data live in a single array element without needing two parallel arrays. KEYMAPS=( "us|English US" "de|German" @@ -132,6 +162,8 @@ KEYMAPS=( if $AF_MODE; then KERNEL=$(af_get '.kernel' 'linux') RAW_HOSTNAME=$(af_get '.hostname' '') + # Append MAC suffix to make the hostname unique across machines when the same + # answerfile is deployed to a fleet (lab rollout, reinstall, etc.). if [[ -n "$RAW_HOSTNAME" ]]; then HOSTNAME="${RAW_HOSTNAME}-$(get_mac_suffix)" else @@ -151,6 +183,8 @@ else HOSTNAME=$(ask "Hostname") USERNAME=$(ask "Username") read -rp "Enable disk encryption? (YES/NO): " ENCRYPT_DISK + # Default FIDO2 root to NO; only ask when encryption is active because FIDO2 + # unlocking only makes sense on an encrypted partition. ENABLE_FIDO_ROOT="NO" if [[ "$ENCRYPT_DISK" == "YES" ]]; then read -rp "Enable FIDO2 for unlocking root? (YES/NO): " ENABLE_FIDO_ROOT @@ -160,44 +194,56 @@ else echo "" echo "Select keyboard layout for installed system:" for i in "${!KEYMAPS[@]}"; do + # %%|* strips from the first '|' onward → keymap code. _km_code="${KEYMAPS[$i]%%|*}" + # ##*| strips up to and including the last '|' → display name. _km_name="${KEYMAPS[$i]##*|}" printf " %d) %-14s (%s)\n" $((i+1)) "$_km_name" "$_km_code" done read -rp "Choice [1]: " _KM_CHOICE + # Convert 1-based user input to 0-based array index; default to 1 if blank. _KM_CHOICE=$(( ${_KM_CHOICE:-1} - 1 )) if (( _KM_CHOICE >= 0 && _KM_CHOICE < ${#KEYMAPS[@]} )); then KEYMAP="${KEYMAPS[$_KM_CHOICE]%%|*}" else + # Out-of-range input silently falls back to the first entry (us). KEYMAP="${KEYMAPS[0]%%|*}" fi fi +# Password always prompted interactively — never stored in the answerfile. read -rp "Password for $USERNAME: " USERPASS [[ -z "$USERPASS" ]] && { echo "Error: password cannot be empty."; exit 1; } +# Print block device layout so the operator can visually confirm the target drive. lsblk if $AF_MODE && [[ -n "$(af_get '.drive')" ]]; then DRIVE=$(af_get '.drive') echo "Drive (from answerfile): $DRIVE" echo "WARNING: All data on $DRIVE will be erased. Proceeding in 5 seconds..." + # Give the operator a 5-second window to abort with Ctrl-C before destructive ops. sleep 5 else DRIVE=$(ask "Enter install drive (e.g., /dev/sda)") confirm "$DRIVE" || exit 1 fi -# Required packages +# Required packages — installed into the live environment before partitioning. +# -d skips full dependency resolution for speed (these are standalone tools). +# systemd-ukify included for Unified Kernel Image support if needed post-install. pacman -Syd --noconfirm parted cryptsetup libfido2 pam-u2f systemd-ukify jq ############################################ # Partitioning ############################################ +# Read installed RAM in GiB for swap sizing; awk extracts the total-memory column. RAM_GB=$(free --giga | awk '/Mem/ {print $2}') +# lsblk -b: bytes; -d: disk only (no partitions); -n: no header; -o SIZE: size column. DISK_GB=$(lsblk -dn -o SIZE -b "$DRIVE" | awk '{print int($1/1024/1024/1024)}') +# Reserve 5 GiB for the EFI partition and 1 GiB for alignment/overhead. EFI_SIZE=5 SWAP_SIZE=$RAM_GB ROOT_SIZE=$((DISK_GB - SWAP_SIZE - EFI_SIZE - 1)) @@ -209,48 +255,71 @@ fi echo "EFI=${EFI_SIZE}G, Root=${ROOT_SIZE}G, Swap=${SWAP_SIZE}G" +# -s: script mode (no interactive prompts); GPT is required for UEFI booting. +# `set 1 esp on` marks partition 1 as the EFI System Partition. +# Starting at 1MiB aligns to SSD erase-block / spinner track boundaries. +# The last partition uses 100% to consume all remaining space. parted -s "$DRIVE" mklabel gpt \ mkpart EFI fat32 1MiB "${EFI_SIZE}GiB" \ set 1 esp on \ mkpart ROOT "${EFI_SIZE}GiB" "$((EFI_SIZE + ROOT_SIZE))GiB" \ mkpart SWAP "$((EFI_SIZE + ROOT_SIZE))GiB" 100% +# Resolve partition paths via `part()` which inserts the NVMe/eMMC 'p' suffix. EFI_PART=$(part "$DRIVE" 1) ROOT_PART=$(part "$DRIVE" 2) SWAP_PART=$(part "$DRIVE" 3) +# FAT32 is mandated by the UEFI specification for the EFI System Partition. mkfs.fat -F32 "$EFI_PART" +# mkswap writes the swap header; swapon activates it immediately for use during install. mkswap "$SWAP_PART" swapon "$SWAP_PART" ############################################ # Encryption (optional) ############################################ +# Initialise to empty; gets set to a temp file path only when encryption is active. LUKS_BACKUP_KEY="" if [[ "$ENCRYPT_DISK" == "YES" ]]; then echo "Formatting LUKS2 root..." + # --type luks2 uses the newer LUKS2 format required by systemd-cryptenroll for FIDO2. cryptsetup luksFormat "$ROOT_PART" --type luks2 + # Open (decrypt) the container; exposes it as /dev/mapper/cryptroot for formatting. cryptsetup open "$ROOT_PART" cryptroot # ── Auto-generate backup LUKS key ───────────────────────────────────────── + # A random key enrolled in a second LUKS slot enables recovery without + # the primary passphrase. Stored inside the encrypted volume so it is + # only accessible when the system is unlocked. LUKS_BACKUP_KEY=$(mktemp /tmp/luks-backup-key.XXXXXX) + # Read 64 bytes of entropy; base64-encode with -w0 to disable line wrapping. dd if=/dev/urandom bs=64 count=1 2>/dev/null | base64 -w0 > "$LUKS_BACKUP_KEY" echo "Enrolling auto-generated backup LUKS key..." + # luksAddKey prompts for the existing passphrase to authorise adding the new key file. cryptsetup luksAddKey "$ROOT_PART" "$LUKS_BACKUP_KEY" if [[ "$ENABLE_FIDO_ROOT" == "YES" ]]; then echo "Enroll FIDO2 key for LUKS2" + # pause() gives the operator time to insert the FIDO2 hardware key before enrollment. pause + # systemd-cryptenroll writes the FIDO2 credential into a LUKS2 token slot. + # --fido2-with-client-pin=no: presence tap only, no PIN required at boot. systemd-cryptenroll "$ROOT_PART" --fido2-device=auto --fido2-with-client-pin=no fi + # Format btrfs on the decrypted mapper device, not the raw partition. mkfs.btrfs /dev/mapper/cryptroot + # Mount flat (no subvolume) first so the subvolume tree can be created. mount /dev/mapper/cryptroot /mnt + # @ is the conventional root subvolume; @home separates user data for independent snapshots. btrfs subvolume create /mnt/@ btrfs subvolume create /mnt/@home + # Unmount the flat view before remounting through named subvolumes. umount /mnt + # Remount each subvolume at its intended mountpoint for pacstrap to populate. mount -o subvol=@ /dev/mapper/cryptroot /mnt mkdir -p /mnt/home mount -o subvol=@home /dev/mapper/cryptroot /mnt/home @@ -258,6 +327,7 @@ if [[ "$ENCRYPT_DISK" == "YES" ]]; then else echo "Skipping encryption — formatting root directly." + # Same btrfs subvolume layout as the encrypted path for consistency. mkfs.btrfs "$ROOT_PART" mount "$ROOT_PART" /mnt btrfs subvolume create /mnt/@ @@ -270,11 +340,15 @@ else fi mkdir -p /mnt/boot +# Mount the EFI partition at /mnt/boot so GRUB and the kernel install inside the new tree. mount "$EFI_PART" /mnt/boot # Place backup key inside the new system (readable only by root, inside LUKS container) if [[ -n "$LUKS_BACKUP_KEY" ]]; then + # `install -m 400` creates the destination with mode 0400 (root read-only) atomically, + # avoiding an intermediate world-readable state from a separate chmod. install -m 400 "$LUKS_BACKUP_KEY" /mnt/_LUKS_BACKUP_KEY + # Remove the temp file from the live environment immediately after copying. rm -f "$LUKS_BACKUP_KEY" echo "Backup LUKS key written to /_LUKS_BACKUP_KEY in new system." fi @@ -282,10 +356,13 @@ fi ############################################ # Base System Install ############################################ +# lspci lists PCI devices; grep for VGA and 3D controller lines to identify the GPU. +# `|| true` prevents set -e from aborting when no matching line is found. GPU_INFO=$(lspci | grep -E "VGA|3D" || true) GPU_PKGS="" if echo "$GPU_INFO" | grep -qi nvidia; then + # nvidia-open is the open-source kernel module (Pascal+); preferred over nvidia-dkms. GPU_PKGS="nvidia-open" elif echo "$GPU_INFO" | grep -qi amd; then GPU_PKGS="xf86-video-amdgpu" @@ -293,16 +370,21 @@ elif echo "$GPU_INFO" | grep -qi intel; then GPU_PKGS="xf86-video-intel" fi +# GPU_PKGS is intentionally unquoted so it word-splits into individual package names +# (or expands to nothing when no GPU was detected). # shellcheck disable=SC2086 pacstrap /mnt \ base base-devel "$KERNEL" linux-firmware vim zsh git networkmanager grub efibootmgr \ btrfs-progs cryptsetup libfido2 pam-u2f sudo less jq $GPU_PKGS +# -U: use UUIDs rather than device names so fstab entries survive hardware changes. genfstab -U /mnt >> /mnt/etc/fstab ############################################ # COPY ANSWERFILE INTO NEW SYSTEM ############################################ +# Make the answerfile available inside the new system so the dotfiles TUI installer +# can read the same selections without re-prompting. if $AF_MODE; then install -m 644 "$ANSWERFILE" /mnt/answerfile.json fi @@ -310,8 +392,14 @@ fi ############################################ # CHROOT Configuration ############################################ +# Capture the root partition UUID before entering the chroot; used in GRUB cmdline. ROOT_UUID=$(blkid -s UUID -o value "$ROOT_PART") +# Variables are passed via `/usr/bin/env` on the arch-chroot command line rather +# than `export` in the outer shell — this makes the environment explicit and +# avoids leaking unrelated exported variables into the chroot. +# The heredoc delimiter is single-quoted ('CHROOT_EOF') to prevent the outer +# shell from expanding any $variables inside the heredoc body. arch-chroot /mnt /usr/bin/env \ HOSTNAME="$HOSTNAME" \ USERNAME="$USERNAME" \ @@ -326,16 +414,20 @@ arch-chroot /mnt /usr/bin/env \ set -euo pipefail +# Uncomment (create) the UTF-8 locale entry so locale-gen can compile it. echo "en_US.UTF-8 UTF-8" > /etc/locale.gen locale-gen echo "LANG=en_US.UTF-8" > /etc/locale.conf +# vconsole.conf sets the console keymap permanently for all TTY sessions. echo "KEYMAP=${KEYMAP}" > /etc/vconsole.conf +# Symlink the zoneinfo file; hwclock --systohc syncs the RTC to current system time. ln -sf /usr/share/zoneinfo/Europe/Vienna /etc/localtime hwclock --systohc echo "$HOSTNAME" > /etc/hostname +# Enable NetworkManager at boot so the installed system has networking on first login. systemctl enable NetworkManager # Populate /etc/skel before user creation so useradd -m copies everything @@ -343,23 +435,23 @@ echo "Cloning dotfiles into /etc/skel..." git clone https://git.abdelbaki.eu/The_miro/Dotfiles.git /etc/skel/Dotfiles \ || echo "Warning: dotfiles clone failed — clone manually after first boot." -# Seed /etc/skel with base shell dotfiles from the repo clone -if [[ -d /etc/skel/Dotfiles ]]; then - D=/etc/skel/Dotfiles - cp "$D/.zshrc" /etc/skel/.zshrc 2>/dev/null || true - cp "$D/.bashrc" /etc/skel/.bashrc 2>/dev/null || true - cp "$D/.vimrc" /etc/skel/.vimrc 2>/dev/null || true -fi - +# Create standard XDG user directories in skel so they are present in every new home. mkdir -p /etc/skel/{Desktop,Documents,Downloads,Music,Pictures,Public,Templates,Videos} +# -m: create home from /etc/skel; -G wheel: allow sudo; -s /bin/zsh: default shell. useradd -m -G wheel -s /bin/zsh "$USERNAME" +# chpasswd reads "user:pass" from stdin to set the password non-interactively. echo "$USERNAME:$USERPASS" | chpasswd +# Ensure all files under the new home dir are owned by the user (skel copy may be root-owned). chown -R "$USERNAME:$USERNAME" "/home/$USERNAME" +# Grant wheel group full sudo access (ALL covers any host/user/group runas context). echo "%wheel ALL=(ALL) ALL" >> /etc/sudoers -# Initramfs +# Initramfs hook selection: +# 1. FIDO2 root unlock: needs `systemd` + `sd-encrypt` for systemd-cryptsetup. +# 2. Password-only LUKS: classic `encrypt` hook (no systemd dependency in initramfs). +# 3. Unencrypted: minimal hook set — no encrypt hook needed. if [[ "$ENCRYPT_DISK" == "YES" && "$ENABLE_FIDO_ROOT" == "YES" ]]; then sed -i 's/^HOOKS=.*/HOOKS=(base udev systemd autodetect microcode modconf kms consolefont block sd-encrypt btrfs filesystems keyboard keymap fsck)/' /etc/mkinitcpio.conf elif [[ "$ENCRYPT_DISK" == "YES" ]]; then @@ -368,28 +460,38 @@ else sed -i 's/^HOOKS=.*/HOOKS=(base udev autodetect microcode modconf kms consolefont block btrfs filesystems keyboard fsck)/' /etc/mkinitcpio.conf fi +# -P regenerates all installed kernel presets (more thorough than -p <kernel>). mkinitcpio -P # GRUB if [[ "$ENCRYPT_DISK" == "YES" ]]; then if [[ "$ENABLE_FIDO_ROOT" == "YES" ]]; then + # systemd-cryptsetup (sd-encrypt hook) reads rd.luks.name=<UUID>=<name>. + # rd.luks.options=fido2-device=auto instructs it to probe for a FIDO2 token. GRUB_CMDLINE="rd.luks.name=$ROOT_UUID=cryptroot rd.luks.options=fido2-device=auto root=/dev/mapper/cryptroot" else + # Classic encrypt hook syntax: cryptdevice=UUID=<UUID>:<dm-name>. GRUB_CMDLINE="cryptdevice=UUID=$ROOT_UUID:cryptroot root=/dev/mapper/cryptroot" fi else + # Unencrypted btrfs: root= by UUID; rootflags selects the @ subvolume. GRUB_CMDLINE="root=UUID=${ROOT_UUID} rootflags=subvol=@" fi +# Inject the kernel command line; `|` delimiter avoids escaping `/` in device paths. sed -i "s|^GRUB_CMDLINE_LINUX=.*|GRUB_CMDLINE_LINUX=\"$GRUB_CMDLINE\"|" /etc/default/grub +# Install GRUB to the EFI partition; --bootloader-id names the NVRAM/EFI menu entry. grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB +# Generate grub.cfg from /etc/default/grub and discovered kernels/initramfs images. grub-mkconfig -o /boot/grub/grub.cfg # User login FIDO2 — directory + PAM only; key enrollment happens outside chroot if [[ "$ENABLE_FIDO_USER" == "YES" ]]; then + # Create the Yubico config dir that pam_u2f expects for the u2f_keys file. mkdir -p "/home/$USERNAME/.config/Yubico" chown "$USERNAME:$USERNAME" "/home/$USERNAME/.config/Yubico" + # `cue` option: pam_u2f prints a prompt so the user knows to touch the key. echo "auth required pam_u2f.so cue" >> /etc/pam.d/system-auth fi @@ -401,10 +503,15 @@ if [[ "$ENABLE_FIDO_USER" == "YES" ]]; then echo "Enrolling FIDO2 key for user login (outside chroot)..." U2F_KEYFILE="/mnt/home/${USERNAME}/.config/Yubico/u2f_keys" mkdir -p "/mnt/home/${USERNAME}/.config/Yubico" + # -o and -i set the origin/app-id to the hostname, scoping the credential so it + # cannot be replayed on a different system's PAM stack. pamu2fcfg -u "$USERNAME" -o "pam://$HOSTNAME" -i "pam://$HOSTNAME" > "$U2F_KEYFILE" + # Query the UID/GID from inside the chroot to get the correct numeric IDs, since + # the live environment may have different /etc/passwd entries. _NEWUID=$(arch-chroot /mnt id -u "$USERNAME" 2>/dev/null || echo "1000") _NEWGID=$(arch-chroot /mnt id -g "$USERNAME" 2>/dev/null || echo "1000") chown -R "$_NEWUID:$_NEWGID" "/mnt/home/${USERNAME}/.config/Yubico" + # 600: only the owning user can read or write the key file. chmod 600 "$U2F_KEYFILE" echo "FIDO2 key enrolled for $USERNAME." fi @@ -413,30 +520,39 @@ fi # DOTFILES SETUP (in-chroot, optional) ############################################ if $AF_MODE; then + # RUN_TUI was already populated from the answerfile earlier. _DO_TUI="${RUN_TUI}" else read -rp "Run dotfiles TUI setup inside chroot now? [YES/no]: " _TUI_IN + # Default to YES when the user presses Enter without typing anything. _TUI_IN="${_TUI_IN:-YES}" [[ "${_TUI_IN^^}" == "YES" ]] && _DO_TUI="YES" || _DO_TUI="NO" fi if [[ "${_DO_TUI^^}" == "YES" ]]; then + # Grant temporary passwordless sudo so the TUI installer can call pacman/yay + # inside the chroot without a password. Removed immediately after the script exits. echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" \ | arch-chroot /mnt tee /etc/sudoers.d/99-setup-nopasswd > /dev/null echo "Running simple-install.sh as ${USERNAME} inside chroot..." + # `runuser -u` switches to the unprivileged user inside the chroot so that + # AUR helpers and dotfiles are owned/built by the correct UID. arch-chroot /mnt runuser -u "${USERNAME}" -- \ bash "/home/${USERNAME}/Dotfiles/setup/simple-install.sh" \ || echo "Warning: tui-install exited with errors — check ~/dotfiles-install.log in the new system." + # Remove the temporary no-password sudoers drop-in after setup completes. arch-chroot /mnt rm -f /etc/sudoers.d/99-setup-nopasswd fi # Remove answerfile from new system after setup completes +# (it contains drive paths and config that are no longer needed post-install). if $AF_MODE && [[ -f /mnt/answerfile.json ]]; then rm -f /mnt/answerfile.json fi +# Copy the install log into /boot so it is readable before the first login. cp "$LOGFILE" /mnt/boot/ 2>/dev/null || true echo "Installation complete! Log saved to /mnt/boot/$(basename "$LOGFILE")" diff --git a/setup/archiso/build.sh b/setup/archiso/build.sh index c07eac0..1f44e2a 100755 --- a/setup/archiso/build.sh +++ b/setup/archiso/build.sh @@ -1,7 +1,27 @@ #!/usr/bin/env bash -# build.sh — build the M-Archy Arch Linux ISO + netboot artifacts +# ============================================================================= +# build.sh — Build the M-Archy Arch Linux ISO + netboot artifacts # -# Usage: +# PURPOSE: +# This is the primary build entry point for the M-Archy custom Arch Linux +# installer ISO. It takes the official archiso "releng" base profile and +# merges the M-Archy overlay on top of it, embedding custom installer +# scripts and an optional pre-filled answerfile for fully automated +# unattended installation. It also generates iPXE scripts for netboot-based +# deployment (PXE/WDS environments). +# +# WORKFLOW OVERVIEW: +# 1. Parse arguments (optional answerfile path, netboot URL, output dir). +# 2. Install archiso if missing. +# 3. Copy the upstream releng profile to a working directory. +# 4. Apply the M-Archy overlay (custom scripts, profiledef, mkinitcpio config). +# 5. Merge extra packages (packages.extra) into the releng package list. +# 6. Embed the installer shell scripts into the ISO's /root/installer/. +# 7. Optionally embed an answerfile.json for automated installs. +# 8. Run mkarchiso to produce the final .iso and netboot tarball. +# 9. Optionally write an iPXE chainload script for netboot.xyz / WDS. +# +# USAGE: # bash build.sh [--preconf [FILE]] [--netboot-url URL] [OUT_DIR] # # --preconf Embed ~/answerfile.json into the ISO at /answerfile.json @@ -9,13 +29,24 @@ # --netboot-url URL Base URL where netboot artifacts will be served # (generates m-archy-netboot.ipxe in OUT_DIR) # OUT_DIR Output directory (default: ~/m-archy-out, or $OUT_DIR env) +# ============================================================================= set -euo pipefail +# set -e → abort on any command that exits non-zero +# set -u → abort if an unset variable is referenced (catches typos) +# set -o pipefail → a pipe fails if any command in it fails (not just the last) +# Resolve this script's directory and the dotfiles root, regardless of where +# the caller's working directory is. Using BASH_SOURCE[0] rather than $0 is +# safer when the script is sourced or called with a relative path. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DOTFILES_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" # ── Argument parsing ─────────────────────────────────────────────────────────── +# We support both --flag value and --flag=value syntax. +# PRECONF_FILE → path to JSON answerfile to embed (empty = no answerfile) +# NETBOOT_URL → HTTP base URL for generated iPXE script (empty = skip) +# OUT_ARG → positional output directory override PRECONF_FILE="" NETBOOT_URL="" OUT_ARG="" @@ -23,7 +54,8 @@ OUT_ARG="" while [[ $# -gt 0 ]]; do case "$1" in --preconf) - # Optional next arg: a file path (doesn't start with -) + # If the next argument looks like a path (doesn't start with -), + # treat it as the answerfile path; otherwise use the default location. if [[ $# -gt 1 && "${2:0:1}" != "-" ]]; then PRECONF_FILE="$2"; shift else @@ -32,6 +64,7 @@ while [[ $# -gt 0 ]]; do shift ;; --preconf=*) + # Handle --preconf=/path/to/file syntax (strip prefix up to =) PRECONF_FILE="${1#--preconf=}" shift ;; @@ -47,57 +80,104 @@ while [[ $# -gt 0 ]]; do echo "Unknown flag: $1" >&2; exit 1 ;; *) + # Any non-flag argument is treated as the output directory OUT_ARG="$1"; shift ;; esac done +# WORK_DIR: scratch space where mkarchiso does its work (can be huge — squashfs +# compression of the full airootfs happens here). WORK_DIR="${WORK_DIR:-$HOME/m-archy-build}" + +# OUT_DIR: where the final .iso and netboot tarball are written. +# Priority: positional arg > $OUT_DIR env var > default ~/m-archy-out OUT_DIR="${OUT_ARG:-${OUT_DIR:-$HOME/m-archy-out}}" + +# PROFILE: the assembled profile directory passed to mkarchiso. +# We copy releng here and then apply our overlay on top. PROFILE="$WORK_DIR/profile" + +# RELENG: the official Arch Linux "release engineering" base profile, installed +# with the archiso package. It provides sane defaults for pacman.conf, EFI +# boot entries, and a working live environment. RELENG="/usr/share/archiso/configs/releng" +# ── Ensure archiso is installed ─────────────────────────────────────────────── +# mkarchiso is the tool that actually assembles the ISO from a profile directory. +# It is only available after installing the archiso package. if ! command -v mkarchiso &>/dev/null; then echo "Installing archiso..." sudo pacman -S --noconfirm archiso fi +# Sanity check: the releng base profile must exist. +# If archiso installed correctly, this path will always be present. [[ -d "$RELENG" ]] || { echo "ERROR: $RELENG not found — is archiso installed?"; exit 1; } -# Validate answerfile early if --preconf was given +# ── Validate answerfile early (fail fast before the long build) ─────────────── +# If --preconf was given, verify the file exists and is valid JSON now rather +# than discovering the problem after a 10-minute build. if [[ -n "$PRECONF_FILE" ]]; then [[ -f "$PRECONF_FILE" ]] \ || { echo "ERROR: answerfile not found: $PRECONF_FILE"; exit 1; } + # jq empty parses JSON and exits 0 if valid, non-zero if malformed. + # Fall through gracefully if jq is not installed. command -v jq &>/dev/null \ && jq empty "$PRECONF_FILE" \ || echo "Warning: jq not available — skipping answerfile JSON validation" echo "Answerfile to embed: $PRECONF_FILE" fi +# ── Clean and create working directories ───────────────────────────────────── +# Remove any previous build artifacts to ensure a clean, reproducible build. +# mkarchiso can behave unexpectedly if the profile or work directory has stale state. rm -rf "$WORK_DIR" mkdir -p "$WORK_DIR" "$OUT_DIR" +# ── Assemble the profile from releng + M-Archy overlay ─────────────────────── echo "Copying releng base profile..." +# Start with a full copy of upstream releng so we inherit all its boot +# entries, pacman.conf, syslinux/systemd-boot configs, etc. cp -r "$RELENG" "$PROFILE" echo "Applying M-Archy overlay..." +# Merge our custom airootfs overlay ON TOP of the releng copy. +# Files in our overlay replace or extend the releng defaults. +# The trailing /. ensures we copy the directory contents, not the directory itself. cp -r "$SCRIPT_DIR/overlay/airootfs/." "$PROFILE/airootfs/" echo "Replacing profiledef..." +# profiledef.sh controls ISO metadata (name, label, build modes, boot modes, +# compression settings, and file permissions in the final image). +# We replace the releng default entirely with our customized version. cp "$SCRIPT_DIR/overlay/profiledef.sh" "$PROFILE/profiledef.sh" echo "Adding extra packages..." +# packages.extra lists additional packages beyond releng's defaults. +# We merge them into packages.x86_64 (the file mkarchiso reads for pacman). +# Strategy: read line-by-line, skip blank lines and comments (#), +# and append each package only if it is not already in the list (idempotent). while IFS= read -r pkg || [[ -n "$pkg" ]]; do [[ -z "$pkg" || "$pkg" == \#* ]] && continue grep -qxF "$pkg" "$PROFILE/packages.x86_64" || echo "$pkg" >> "$PROFILE/packages.x86_64" done < "$SCRIPT_DIR/overlay/packages.extra" +# ── Embed installer scripts ──────────────────────────────────────────────────── +# These three scripts live in the main setup/ directory of the dotfiles repo +# and implement the actual Arch Linux installation logic. They are placed in +# /root/installer/ on the live ISO so the auto-launch scripts can find them. echo "Embedding installer scripts..." mkdir -p "$PROFILE/airootfs/root/installer" +# Guided interactive installer — walks the user through partitioning, locale, etc. cp "$DOTFILES_DIR/setup/archbaseos-guided-install.sh" "$PROFILE/airootfs/root/installer/" +# Automated unattended installer — reads /answerfile.json and installs silently. cp "$DOTFILES_DIR/setup/arch-autoinstall.sh" "$PROFILE/airootfs/root/installer/" +# Reset script — wipes and reinstalls the system while preserving /home. cp "$DOTFILES_DIR/setup/reset-arch.sh" "$PROFILE/airootfs/root/installer/" +# Make all scripts executable. The archiso tool preserves these bits in the +# squashfs, so they will be executable on the live system too. chmod 755 \ "$PROFILE/airootfs/root/launch.sh" \ "$PROFILE/airootfs/root/.automated_script.sh" \ @@ -105,20 +185,36 @@ chmod 755 \ "$PROFILE/airootfs/root/installer/"*.sh # ── Embed answerfile (--preconf) ─────────────────────────────────────────────── +# An answerfile baked into the ISO lets machines boot and install completely +# hands-free — useful for bulk deployment via PXE. Without it, the guided +# installer starts automatically instead. if [[ -n "$PRECONF_FILE" ]]; then echo "Embedding answerfile: $PRECONF_FILE → /answerfile.json" + # install -m 644 creates the destination with the given permissions, + # equivalent to cp + chmod but in one atomic step. install -m 644 "$PRECONF_FILE" "$PROFILE/airootfs/answerfile.json" fi echo "Building ISO (this may take a while)..." +# mkarchiso needs root to mount loopback devices, write to /proc-style paths, +# and set file ownership inside the squashfs correctly. +# -v: verbose (shows progress) +# -w: work directory (large temporary build area) +# -o: output directory for the final .iso and netboot tarball sudo mkarchiso -v -w "$WORK_DIR/mkarchiso" -o "$OUT_DIR" "$PROFILE" + +# After the build, mkarchiso leaves files owned by root. Fix ownership so the +# calling user can manipulate the output without sudo. sudo chmod -R 777 "$WORK_DIR" "$OUT_DIR" sudo chown -R "$(id -u):$(id -g)" "$WORK_DIR" "$OUT_DIR" echo echo "Done." +# List the produced ISO file(s). The || true prevents the script from failing +# when no .iso files exist (e.g., if only netboot mode was built). ls -lh "$OUT_DIR/"*.iso 2>/dev/null || true +# Inform the user whether an automated or guided install will start on boot. if [[ -n "$PRECONF_FILE" ]]; then echo "Answerfile embedded — automated install will activate on boot." else @@ -126,18 +222,26 @@ else fi # ── Netboot artifacts ────────────────────────────────────────────────────────── +# mkarchiso's 'netboot' build mode (set in profiledef.sh) produces a tarball +# containing the kernel, initramfs, and airootfs.sfs ready for HTTP-based PXE +# boot. We detect it here and optionally generate an iPXE chainload script. NETBOOT_TARBALL="$(ls "$OUT_DIR/"*-netboot-*.tar.gz 2>/dev/null | head -n1 || true)" if [[ -n "$NETBOOT_TARBALL" ]]; then echo echo "Netboot artifact: $NETBOOT_TARBALL" echo " Extract and serve its contents from an HTTP server, then boot via PXE." echo " Internal layout (relative to tarball root):" + # Show the tarball contents indented for readability. tar -tzf "$NETBOOT_TARBALL" | sed 's/^/ /' if [[ -n "$NETBOOT_URL" ]]; then - # Strip trailing slash for consistency + # Strip trailing slash so our URL concatenations are consistent. BASE_URL="${NETBOOT_URL%/}" IPXE_FILE="$OUT_DIR/m-archy-netboot.ipxe" + + # Generate an iPXE script that can be chainloaded from netboot.xyz or + # served directly from a WDS/TFTP server. The script tells iPXE where + # to find the kernel, initramfs, and squashfs root over HTTP. cat > "$IPXE_FILE" <<IPXE #!ipxe # M-Archy Arch Linux Installer — PXE Boot diff --git a/setup/archiso/overlay/airootfs/root/.automated_script.sh b/setup/archiso/overlay/airootfs/root/.automated_script.sh index a558549..5654868 100644 --- a/setup/archiso/overlay/airootfs/root/.automated_script.sh +++ b/setup/archiso/overlay/airootfs/root/.automated_script.sh @@ -1,10 +1,48 @@ #!/usr/bin/env bash +# ============================================================================= +# .automated_script.sh — Entry point triggered by the archiso live environment +# +# PURPOSE: +# This script is called by the archiso systemd getty service (or directly +# from .zlogin) immediately after the root user logs in on the live ISO. +# It acts as the single decision point that determines whether to run the +# automated unattended installer or the guided interactive installer. +# +# HOW IT WORKS: +# archiso's default releng profile ships a .automated_script.sh that starts +# a graphical session or similar. We replace it with this simpler dispatcher. +# +# The presence or absence of /answerfile.json is the determining signal: +# - EXISTS → machine has been configured for unattended deployment +# (answerfile was embedded by build.sh --preconf, or placed +# on the live system via another mechanism such as a USB drive). +# → launch.sh is called in "auto" mode and /answerfile.json is passed as +# the configuration source for arch-autoinstall.sh. +# +# - MISSING → this is a standard interactive boot (no pre-configuration). +# → launch.sh is called in "guided" mode which starts the interactive +# guided installer after prompting for keyboard layout and action. +# +# WHY exec: +# Using exec replaces this process with launch.sh rather than spawning a +# child. This means the PID stays the same and signals propagate correctly. +# If launch.sh exits, the shell exits too, which is the correct behaviour +# for an installer that terminates after completing or being cancelled. +# ============================================================================= + set -euo pipefail ANSWERFILE="/answerfile.json" +# Check whether an answerfile was embedded in the ISO (or placed on the live +# system). The answerfile contains all installation parameters as JSON, allowing +# completely unattended installation with zero user interaction. if [[ -f "$ANSWERFILE" ]]; then + # Automated mode: pass "auto" as the first arg and the answerfile path as + # the second. launch.sh will forward these to arch-autoinstall.sh. exec /root/launch.sh auto "$ANSWERFILE" else + # Guided mode: start the interactive installer. The user will be prompted + # for keyboard layout and installation action before anything happens. exec /root/launch.sh guided fi diff --git a/setup/archiso/wds-deploy.sh b/setup/archiso/wds-deploy.sh index 7149dee..82f7de5 100755 --- a/setup/archiso/wds-deploy.sh +++ b/setup/archiso/wds-deploy.sh @@ -1,7 +1,26 @@ #!/usr/bin/env bash -# wds-deploy.sh — Package M-Archy archiso netboot artifacts for WDS + PXELinux deployment +# ============================================================================= +# wds-deploy.sh — Package M-Archy archiso netboot artifacts for WDS + PXELinux # -# Usage: +# PURPOSE: +# Windows Deployment Services (WDS) on a Windows Server can serve PXE boot +# images to clients via TFTP. This script: +# 1. Builds the M-Archy ISO + netboot tarball (by calling build.sh), or +# reuses an existing tarball if --no-rebuild is given. +# 2. Extracts the kernel and initramfs from the netboot tarball. +# 3. Builds a TFTP directory tree compatible with WDS + PXELinux (syslinux). +# 4. Builds an HTTP directory tree for the airootfs.sfs squashfs root +# (Arch's initrd fetches this over HTTP at boot time — TFTP is too slow). +# 5. Generates a pxelinux.cfg/default boot menu with multiple options. +# 6. Optionally zips the TFTP tree for easy transfer to a Windows Server. +# +# WHY WDS + PXELinux: +# WDS natively speaks the Microsoft NBP (network boot program) protocol. +# By configuring WDS to serve pxelinux.0 as the boot file, it becomes a +# standards-compliant TFTP server that can chainload syslinux PXELinux, +# which then presents our custom menu and loads the Arch kernel + initramfs. +# +# USAGE: # bash wds-deploy.sh --http-srv URL [OPTIONS] [OUT_DIR] # # --http-srv URL HTTP base URL where arch netboot files will be served @@ -14,29 +33,34 @@ # --no-rebuild Skip the archiso build if a netboot tarball already exists # OUT_DIR Output directory (default: ~/m-archy-out) # -# Output layout (inside OUT_DIR/wds-deploy/): +# OUTPUT LAYOUT (inside OUT_DIR/wds-deploy/): # TFTP/ Copy contents to WDS TFTP root: C:\RemoteInstall\Boot\x64\ # HTTP/ Serve as HTTP root at the URL given to --http-srv # wds-tftp.zip Zip of TFTP/ — drop onto the Windows Server directly # -# WDS deployment steps: +# WDS DEPLOYMENT STEPS: # 1. Serve HTTP/ over IIS/Nginx at the --http-srv URL # 2. Extract wds-tftp.zip into C:\RemoteInstall\Boot\x64\ # 3. In WDS console → server Properties → Boot tab: # Set "Default boot program" for x64 to: Boot\x64\pxelinux.0 # 4. PXE-boot a client — the M-Archy menu appears +# ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# syslinux provides pxelinux.0 and the companion .c32 module files. +# On Arch Linux these live in /usr/lib/syslinux/bios/ after installing the +# syslinux package. WDS serves these to PXE-booting clients via TFTP. SYSLINUX_BIOS="/usr/lib/syslinux/bios" # ── Argument parsing ─────────────────────────────────────────────────────────── -HTTP_SRV="" -TFTP_PREFIX="m-archy" -PRECONF_ARGS=() -NO_REBUILD=0 -OUT_ARG="" +HTTP_SRV="" # Required: base HTTP URL where airootfs.sfs is served +TFTP_PREFIX="m-archy" # Subdirectory under TFTP root for kernel + initramfs +PRECONF_ARGS=() # Forwarded to build.sh if --preconf was given +NO_REBUILD=0 # 1 = skip build and reuse existing netboot tarball +OUT_ARG="" # Positional output directory override while [[ $# -gt 0 ]]; do case "$1" in @@ -51,12 +75,14 @@ while [[ $# -gt 0 ]]; do --tftp-prefix=*) TFTP_PREFIX="${1#--tftp-prefix=}"; shift ;; --preconf) + # Forward --preconf [optional-file] to build.sh verbatim. if [[ $# -gt 1 && "${2:0:1}" != "-" ]]; then PRECONF_ARGS=(--preconf "$2"); shift 2 else PRECONF_ARGS=(--preconf); shift fi ;; --preconf=*) + # Forward --preconf=file to build.sh verbatim. PRECONF_ARGS=("$1"); shift ;; --no-rebuild) NO_REBUILD=1; shift ;; @@ -68,9 +94,13 @@ while [[ $# -gt 0 ]]; do done OUT_DIR="${OUT_ARG:-${OUT_DIR:-$HOME/m-archy-out}}" +# WDS_DIR is a subdirectory of OUT_DIR where we build the TFTP + HTTP trees. WDS_DIR="$OUT_DIR/wds-deploy" -# ── Validate ────────────────────────────────────────────────────────────────── +# ── Validate required arguments ─────────────────────────────────────────────── +# --http-srv is mandatory because the Arch initrd must know where to download +# airootfs.sfs from at boot time. Without it, the kernel boots but the root +# filesystem cannot be mounted and the system hangs. if [[ -z "$HTTP_SRV" ]]; then echo "ERROR: --http-srv <URL> is required." >&2 echo " The Arch initrd fetches airootfs.sfs over HTTP at boot." >&2 @@ -78,26 +108,36 @@ if [[ -z "$HTTP_SRV" ]]; then exit 1 fi -HTTP_SRV="${HTTP_SRV%/}" # strip trailing slash +# Strip trailing slash so URL concatenations are consistent throughout. +HTTP_SRV="${HTTP_SRV%/}" # ── Ensure syslinux is available ────────────────────────────────────────────── +# pxelinux.0 is the PXE network boot program (NBP) that WDS sends to clients. +# Without syslinux installed we cannot produce the TFTP tree. if [[ ! -f "$SYSLINUX_BIOS/pxelinux.0" ]]; then echo "syslinux not found — installing..." sudo pacman -S --noconfirm syslinux fi +# Second check after attempted install — exit loudly if still missing. if [[ ! -f "$SYSLINUX_BIOS/pxelinux.0" ]]; then echo "ERROR: $SYSLINUX_BIOS/pxelinux.0 still not found after install." >&2 exit 1 fi # ── Build ISO + netboot tarball (unless --no-rebuild) ───────────────────────── +# mkarchiso's 'netboot' build mode produces a .tar.gz alongside the .iso. +# The tarball contains the kernel, initramfs, and the squashfs tree needed +# for PXE netbooting. We extract kernel+initramfs to TFTP; the squashfs tree +# goes to the HTTP root. NETBOOT_TARBALL="$(ls "$OUT_DIR/"*-netboot-*.tar.gz 2>/dev/null | head -n1 || true)" if [[ "$NO_REBUILD" -eq 1 && -n "$NETBOOT_TARBALL" ]]; then echo "Skipping build — using existing tarball: $(basename "$NETBOOT_TARBALL")" else echo "Building archiso (this may take a while)..." + # Pass PRECONF_ARGS only when it is non-empty; the ${array[@]+"${array[@]}"} + # idiom avoids "unbound variable" errors when the array is empty (set -u). bash "$SCRIPT_DIR/build.sh" "${PRECONF_ARGS[@]+"${PRECONF_ARGS[@]}"}" "$OUT_DIR" NETBOOT_TARBALL="$(ls "$OUT_DIR/"*-netboot-*.tar.gz 2>/dev/null | head -n1 || true)" fi @@ -108,12 +148,17 @@ fi echo "Using netboot tarball: $(basename "$NETBOOT_TARBALL")" # ── Extract netboot tarball ─────────────────────────────────────────────────── +# We extract into a temporary directory, then pick out the specific files we +# need for each destination (TFTP vs HTTP) rather than serving the tarball +# layout directly. EXTRACT_DIR="$OUT_DIR/.netboot-extracted" rm -rf "$EXTRACT_DIR" mkdir -p "$EXTRACT_DIR" echo "Extracting netboot tarball..." tar -xzf "$NETBOOT_TARBALL" -C "$EXTRACT_DIR" +# Locate the three key items inside the extracted tree. +# Using find + head -n1 handles variations in tarball layout across archiso versions. VMLINUZ="$(find "$EXTRACT_DIR" -name 'vmlinuz-linux' | head -n1 || true)" INITRAMFS="$(find "$EXTRACT_DIR" -name 'initramfs-linux.img' | head -n1 || true)" ARCH_DIR="$(find "$EXTRACT_DIR" -maxdepth 2 -type d -name 'arch' | head -n1 || true)" @@ -127,15 +172,22 @@ ARCH_DIR="$(find "$EXTRACT_DIR" -maxdepth 2 -type d -name 'arch' | head -n1 || t # TFTP root layout (mirrors what WDS serves via TFTP): # pxelinux.0 PXELinux bootloader # ldlinux.c32 required by pxelinux.0 -# menu.c32 + libcom32.c32 + libutil.c32 text boot menu -# pxelinux.cfg/default boot menu config -# <tftp-prefix>/arch/boot/x86_64/vmlinuz-linux -# <tftp-prefix>/arch/boot/x86_64/initramfs-linux.img +# menu.c32 + libcom32.c32 + libutil.c32 text boot menu modules +# pxelinux.cfg/default boot menu configuration +# <tftp-prefix>/arch/boot/x86_64/vmlinuz-linux Linux kernel +# <tftp-prefix>/arch/boot/x86_64/initramfs-linux.img initrd (with archiso hooks) # # HTTP root layout (served at $HTTP_SRV/ over IIS/Nginx/Apache): -# arch/x86_64/airootfs.sfs fetched by initrd at boot -# arch/x86_64/airootfs.sfs.sha512 -# arch/pkglist.x86_64.txt (and any other netboot files) +# arch/x86_64/airootfs.sfs the squashfs root filesystem fetched at boot +# arch/x86_64/airootfs.sfs.sha512 integrity checksum +# arch/pkglist.x86_64.txt (and any other netboot metadata files) +# +# WHY two servers (TFTP + HTTP)? +# TFTP is required by the PXE protocol for the initial NBP handshake and +# kernel/initramfs delivery. However, TFTP is extremely slow for large files. +# The airootfs.sfs (~500 MB squashfs) is downloaded by the initrd over HTTP, +# which is orders of magnitude faster. This two-tier approach is the standard +# Arch Linux netboot pattern. TFTP_ROOT="$WDS_DIR/TFTP" HTTP_ROOT="$WDS_DIR/HTTP" @@ -147,6 +199,11 @@ mkdir -p \ "$HTTP_ROOT" # ── Copy syslinux modules ───────────────────────────────────────────────────── +# pxelinux.0 — the actual NBP that WDS sends to the client via DHCP option 67. +# ldlinux.c32 — core syslinux library, required by pxelinux.0. +# menu.c32 — text-based interactive boot menu module. +# libcom32.c32 — common syslinux library needed by menu.c32. +# libutil.c32 — utility library needed by menu.c32. echo "Copying syslinux PXELinux modules..." REQUIRED_MODS=(pxelinux.0 ldlinux.c32 menu.c32 libcom32.c32 libutil.c32) MISSING_MODS=() @@ -155,6 +212,8 @@ for mod in "${REQUIRED_MODS[@]}"; do if [[ -f "$SYSLINUX_BIOS/$mod" ]]; then cp "$SYSLINUX_BIOS/$mod" "$TFTP_ROOT/" else + # Older syslinux versions may not include all modules — warn but continue. + # pxelinux.0 and ldlinux.c32 are essential; menu modules are optional. MISSING_MODS+=("$mod") fi done @@ -164,23 +223,32 @@ if [[ ${#MISSING_MODS[@]} -gt 0 ]]; then fi # ── Copy kernel and initramfs ───────────────────────────────────────────────── +# These go into the TFTP tree because PXELinux loads them from the TFTP server +# as specified in pxelinux.cfg/default (KERNEL + initrd= directives). echo "Copying kernel and initramfs..." cp "$VMLINUZ" "$TFTP_ROOT/$TFTP_PREFIX/arch/boot/x86_64/vmlinuz-linux" cp "$INITRAMFS" "$TFTP_ROOT/$TFTP_PREFIX/arch/boot/x86_64/initramfs-linux.img" # ── Copy HTTP content (airootfs squashfs and supporting files) ──────────────── +# The entire arch/ directory from the netboot tarball goes to the HTTP root. +# The Arch initrd uses the archiso_http_srv and archisobasedir kernel parameters +# to locate and download airootfs.sfs from this path. echo "Copying HTTP content (airootfs + supporting files)..." cp -r "$ARCH_DIR" "$HTTP_ROOT/arch" # ── Generate pxelinux.cfg/default ──────────────────────────────────────────── +# This file is what PXELinux reads to build the boot menu. It must be at +# pxelinux.cfg/default relative to the TFTP root. echo "Writing pxelinux.cfg/default..." -# Kernel path is relative to the TFTP root +# Kernel path is relative to the TFTP root (PXELinux convention). KERNEL_PATH="${TFTP_PREFIX}/arch/boot/x86_64/vmlinuz-linux" INITRD_PATH="${TFTP_PREFIX}/arch/boot/x86_64/initramfs-linux.img" -# archiso_http_srv must end with a slash; archisobasedir is the subdir within it -# that contains the x86_64/ squashfs tree +# archiso kernel parameters: +# archiso_http_srv= — base URL of the HTTP server (must end with /) +# archisobasedir= — subdirectory within the HTTP server containing arch/x86_64/ +# ip=dhcp — tell the initrd to acquire an IP via DHCP (required for HTTP) APPEND_BASE="initrd=${INITRD_PATH} archiso_http_srv=${HTTP_SRV}/ archisobasedir=arch ip=dhcp" cat > "$TFTP_ROOT/pxelinux.cfg/default" <<PXECFG @@ -212,9 +280,12 @@ LABEL local PXECFG # ── Zip TFTP directory for easy transfer to Windows Server ──────────────────── +# Windows admins often prefer to receive a single zip rather than a directory +# tree via SCP. The zip can be extracted directly in C:\RemoteInstall\Boot\x64\. ZIP_FILE="$WDS_DIR/wds-tftp.zip" echo "Creating wds-tftp.zip..." if command -v zip &>/dev/null; then + # cd into TFTP_ROOT so the zip paths are relative (no leading path prefix). (cd "$TFTP_ROOT" && zip -r "$ZIP_FILE" .) echo "Created: $ZIP_FILE" else diff --git a/setup/audit-packages.sh b/setup/audit-packages.sh index ea70276..5be3b75 100755 --- a/setup/audit-packages.sh +++ b/setup/audit-packages.sh @@ -1,30 +1,71 @@ #!/bin/bash +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ setup/audit-packages.sh — Package source audit tool ║ +# ║ ║ +# ║ PURPOSE: ║ +# ║ Verifies that installed packages come from the expected source ║ +# ║ (official pacman repos, AUR, or Flatpak). Reports: ║ +# ║ - Missing packages that should be installed ║ +# ║ - Packages installed from the wrong source (e.g. pacman pkg from AUR) ║ +# ║ - Unexpected foreign/AUR packages not declared in setup scripts ║ +# ║ ║ +# ║ USAGE: ║ +# ║ bash audit-packages.sh # report only ║ +# ║ bash audit-packages.sh --fix # report + auto-reinstall wrong-source ║ +# ║ bash audit-packages.sh -f # shorthand for --fix ║ +# ║ ║ +# ║ WHY THIS TOOL EXISTS: ║ +# ║ On Arch, some packages exist in both the official repos AND the AUR ║ +# ║ (often with a different build/version). If an AUR package shadows an ║ +# ║ official one, pacman -Syu won't update it. This script detects drift. ║ +# ║ ║ +# ║ EXIT CODES: ║ +# ║ 0 — all checks passed ║ +# ║ 1 — one or more issues found ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ + # Verifies installed packages come from the expected source (pacman/AUR/flatpak). # Reports missing packages, wrong-source installs, and unexpected foreign packages. # Pass --fix / -f to automatically reinstall packages from the correct source. set -euo pipefail +# ── Output formatting ────────────────────────────────────────────────────────── +# ANSI color codes for colored terminal output + RED="\e[31m"; YELLOW="\e[33m"; GREEN="\e[32m"; CYAN="\e[36m"; BOLD="\e[1m"; RESET="\e[0m" -ok() { echo -e " ${GREEN}✔${RESET} $1"; } -warn() { echo -e " ${YELLOW}⚠${RESET} $1"; } -err() { echo -e " ${RED}✘${RESET} $1"; } -hdr() { echo -e "\n${BOLD}${CYAN}$1${RESET}"; } -fix() { echo -e " ${CYAN}↺${RESET} $1"; } +ok() { echo -e " ${GREEN}✔${RESET} $1"; } # Green checkmark: package is correct +warn() { echo -e " ${YELLOW}⚠${RESET} $1"; } # Yellow warning: non-critical issue +err() { echo -e " ${RED}✘${RESET} $1"; } # Red X: package missing or wrong +hdr() { echo -e "\n${BOLD}${CYAN}$1${RESET}"; } # Bold cyan section header +fix() { echo -e " ${CYAN}↺${RESET} $1"; } # Cyan spinner: fix action in progress + +# ── Fix mode flag ───────────────────────────────────────────────────────────── +# Parse the --fix / -f argument. When FIX=1, wrong-source packages will be +# automatically reinstalled from the correct source at the end of the audit. FIX=0 [[ "${1:-}" == "--fix" || "${1:-}" == "-f" ]] && FIX=1 +# ── Issue counter ───────────────────────────────────────────────────────────── +# ISSUES tracks the number of problems found. The final exit code is 1 if > 0. ISSUES=0 -flag() { ISSUES=$((ISSUES + 1)); } +flag() { ISSUES=$((ISSUES + 1)); } # Increment the issue counter +# ── Wrong-source package lists ──────────────────────────────────────────────── +# These arrays collect packages that need reinstallation when --fix is used. WRONG_SOURCE_OFFICIAL=() # expected from pacman, installed from AUR WRONG_SOURCE_AUR=() # expected from AUR, installed from official # --------------------------------------------------------------------------- # Expected package sources — keep in sync with setup/modules/ # --------------------------------------------------------------------------- +# IMPORTANT: When you add packages to any module script, add them here too. +# The lists are manually maintained to stay in sync with what modules install. +# ── PACMAN_PKGS: packages that MUST come from official Arch repositories ────── +# If any of these are found to be installed from the AUR instead, that's a drift +# issue — the AUR version may not receive official security updates. PACMAN_PKGS=( # core-packages.sh 7zip arch-install-scripts atftp atool @@ -64,33 +105,43 @@ PACMAN_PKGS=( kew ) +# ── AUR_PKGS: packages that MUST come from the AUR ─────────────────────────── +# If any of these are found in the official repos instead, it may mean Arch has +# promoted them — but could also mean a naming collision. Flagged for review. +# NOTE: AUR packages are treated as optional (warn, not err) when missing, +# because they may be legitimately excluded on some systems. AUR_PKGS=( - # hyprland.sh AUR portion + # hyprland.sh AUR portion (not in official repos) hyprland-workspaces vicinae-bin bluetuith wvkbd iwmenu walker-bin ulauncher bzmenu wofi-calc bri chamel - # optional apps - pamtester - pinta - localsend - vesktop - onlyoffice-bin - vintagestory - wprs-git - zfs-dkms + # optional apps (only installed if selected in TUI) + pamtester # PAM authentication tester + pinta # Simple image editor (paint.net-like) + localsend # LAN file transfer + vesktop # Discord with Vencord themes + onlyoffice-bin # Office suite (pre-built binary from AUR) + vintagestory # Survival game + wprs-git # Wayland proxy (git version) + zfs-dkms # ZFS kernel module ) +# ── FLATPAK_PKGS: apps installed via Flatpak (Flathub) ─────────────────────── +# These are cross-distro app bundles. Listed by their Flatpak app ID. FLATPAK_PKGS=( - org.prismlauncher.PrismLauncher + org.prismlauncher.PrismLauncher # Minecraft launcher ) # --------------------------------------------------------------------------- # Snapshot current state # --------------------------------------------------------------------------- +# Take a snapshot of what's installed now so we don't repeatedly call pacman. +# This also ensures consistent results across the whole run. -ALL_INSTALLED=$(pacman -Qq 2>/dev/null) -FOREIGN=$(pacman -Qmq 2>/dev/null) # AUR / manually installed -OFFICIAL=$(pacman -Qnq 2>/dev/null) # in a sync repo +ALL_INSTALLED=$(pacman -Qq 2>/dev/null) # All installed packages (name only) +FOREIGN=$(pacman -Qmq 2>/dev/null) # AUR / manually installed (foreign) packages +OFFICIAL=$(pacman -Qnq 2>/dev/null) # Packages from official sync repos +# Helper functions for membership testing — grep -qx matches whole lines exactly is_installed() { echo "$ALL_INSTALLED" | grep -qx "$1"; } is_from_official() { echo "$OFFICIAL" | grep -qx "$1"; } is_from_aur() { echo "$FOREIGN" | grep -qx "$1"; } @@ -98,6 +149,10 @@ is_from_aur() { echo "$FOREIGN" | grep -qx "$1"; } # --------------------------------------------------------------------------- # 1. Check pacman packages # --------------------------------------------------------------------------- +# For each expected official package: +# - NOT installed: hard error (✘) — increment issue counter +# - Installed from AUR: warning (⚠) — drift; increment counter, queue for fix +# - Installed from official: pass (✔) hdr "1/3 Official-repo packages (pacman)" @@ -105,6 +160,8 @@ for pkg in "${PACMAN_PKGS[@]}"; do if ! is_installed "$pkg"; then err "$pkg — NOT INSTALLED"; flag elif is_from_aur "$pkg"; then + # This package should come from official repos but is installed from AUR. + # This means `pacman -Syu` won't update it — potential security gap. warn "$pkg — installed, but from AUR instead of official repo"; flag WRONG_SOURCE_OFFICIAL+=("$pkg") else @@ -115,13 +172,21 @@ done # --------------------------------------------------------------------------- # 2. Check AUR packages # --------------------------------------------------------------------------- +# For each expected AUR package: +# - NOT installed: soft warning — AUR packages are typically optional +# - Installed from official repo: warning — may get wrong version from Arch +# - Installed from AUR: pass (✔) hdr "2/3 AUR packages (yay)" for pkg in "${AUR_PKGS[@]}"; do if ! is_installed "$pkg"; then + # AUR packages are often optional (selected in TUI installer), so this + # is a warning rather than an error — intentional omission is common. warn "$pkg — not installed (optional — may be intentional)" elif is_from_official "$pkg"; then + # This package should come from AUR but is installed from official repos. + # The official version may differ (newer or older) — flagged for review. warn "$pkg — installed from official repo, not AUR (may need AUR build for correct version)"; flag WRONG_SOURCE_AUR+=("$pkg") else @@ -132,10 +197,13 @@ done # --------------------------------------------------------------------------- # 3. Check flatpak packages # --------------------------------------------------------------------------- +# Flatpak packages are checked separately using the `flatpak` command. +# Missing Flatpaks are soft warnings since they're always optional. hdr "3/3 Flatpak packages" if command -v flatpak &>/dev/null; then + # List installed Flatpak apps (not runtimes) in the 'application' column only FLATPAK_INSTALLED=$(flatpak list --app --columns=application 2>/dev/null) for pkg in "${FLATPAK_PKGS[@]}"; do if echo "$FLATPAK_INSTALLED" | grep -qx "$pkg"; then @@ -151,11 +219,16 @@ fi # --------------------------------------------------------------------------- # 4. Flag unexpected foreign (AUR) packages not in our lists # --------------------------------------------------------------------------- +# Any AUR package installed on the system but NOT in our AUR_PKGS list is +# unexpected. These may have been installed manually or by other tools. +# They're highlighted for human review — not counted as issues. hdr "Bonus: foreign packages not declared in setup scripts" +# Build a newline-separated set of declared AUR packages for grep matching AUR_SET=$(printf "%s\n" "${AUR_PKGS[@]}") while IFS= read -r pkg; do + # If the foreign package is not in our declared AUR list, highlight it if ! echo "$AUR_SET" | grep -qx "$pkg"; then echo -e " ${CYAN}?${RESET} $pkg — foreign package not in setup scripts" fi @@ -164,6 +237,8 @@ done <<< "$FOREIGN" # --------------------------------------------------------------------------- # Fix: reinstall wrong-source packages # --------------------------------------------------------------------------- +# When --fix is passed, reinstall any packages found in the wrong source. +# Official packages are reinstalled with pacman; AUR packages with yay --aur. FIXED=0 @@ -172,19 +247,23 @@ if [ "$FIX" -eq 1 ]; then hdr "Reinstalling wrong-source packages" fi + # Reinstall from official repo (packages that were incorrectly from AUR) for pkg in "${WRONG_SOURCE_OFFICIAL[@]}"; do fix "Reinstalling $pkg from official repo (was AUR)..." + # pacman -S without --aur ensures it comes from the sync database if sudo pacman -S --noconfirm "$pkg"; then ok "$pkg reinstalled from official repo" FIXED=$((FIXED + 1)) - ISSUES=$((ISSUES - 1)) + ISSUES=$((ISSUES - 1)) # Remove from issue count since it's now fixed else err "$pkg reinstall failed" fi done + # Reinstall from AUR (packages that were incorrectly from official repos) for pkg in "${WRONG_SOURCE_AUR[@]}"; do fix "Reinstalling $pkg from AUR (was official repo)..." + # --aur flag ensures yay only looks in the AUR, not official repos if yay -S --aur --noconfirm "$pkg"; then ok "$pkg reinstalled from AUR" FIXED=$((FIXED + 1)) @@ -208,6 +287,7 @@ if [ "$ISSUES" -eq 0 ]; then echo -e "${GREEN}${BOLD}All checks passed — no source mismatches or missing required packages.${RESET}" else echo -e "${RED}${BOLD}${ISSUES} issue(s) found — review items marked ✘ or ⚠ above.${RESET}" + # Only show the --fix hint if there are packages that can actually be auto-fixed [ "$FIX" -eq 0 ] && [ $((${#WRONG_SOURCE_OFFICIAL[@]} + ${#WRONG_SOURCE_AUR[@]})) -gt 0 ] && \ echo -e "${YELLOW} Run with --fix to automatically reinstall wrong-source packages.${RESET}" exit 1 diff --git a/setup/install.sh b/setup/install.sh index 14c3f7e..5d84850 100755 --- a/setup/install.sh +++ b/setup/install.sh @@ -1,27 +1,79 @@ #!/bin/bash +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ install.sh — Legacy manual dotfiles installer ║ +# ║ ║ +# ║ PURPOSE: ║ +# ║ The original, non-TUI entry point for setting up the dotfiles on an ║ +# ║ already-running Arch Linux system. Runs modules sequentially with ║ +# ║ minimal user interaction (a single DE selection prompt). ║ +# ║ ║ +# ║ USAGE: bash ~/Dotfiles/setup/install.sh ║ +# ║ ║ +# ║ SUPERSEDED BY: tui-install.sh / simple-install.sh (preferred) ║ +# ║ This file is kept for quick headless runs or scripted CI deployments. ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ + set -uo pipefail +# -u: treat unset variables as errors (catches typos in variable names) +# -o pipefail: if any command in a pipeline fails, the whole pipeline fails +# (Note: -e is intentionally omitted so module failures don't abort the whole run) + +# ── Logging setup ───────────────────────────────────────────────────────────── +# All output is tee'd to a log file so the run can be reviewed after the fact. LOG="$HOME/dotfiles-install.log" + +# Truncate the log file at the start of each run (> with no input empties it) > "$LOG" + +# Stamp the log with a timestamp for traceability printf "Dotfiles install: %s\n" "$(date)" >> "$LOG" + +# Redirect both stdout and stderr through tee so they appear on screen AND +# get appended to the log file simultaneously. +# exec > >(tee -a ...) 2>&1 is a permanent redirect for the rest of the script. exec > >(tee -a "$LOG") 2>&1 +# ── Core installation modules ────────────────────────────────────────────────── +# These three modules form the mandatory base layer and must run in this order: +# 1. package-managers — installs yay (AUR helper), nvm (Node.js), rustup (Rust) +# 2. core-packages — installs 100+ system packages via pacman/yay +# 3. core — enables systemd services (NetworkManager, cronie, greetd, etc.) + echo "Running Core installation Scripts" bash ~/Dotfiles/setup/modules/package-managers.sh bash ~/Dotfiles/setup/modules/core-packages.sh bash ~/Dotfiles/setup/modules/core.sh +# ── Shell configuration deployment ──────────────────────────────────────────── +# Deploys shell dotfiles (zsh, neovim, yazi, starship, etc.) and installs +# oh-my-zsh with its plugins. Run after core because it needs packages from core. + echo "Running Shell config deployment Script" bash ~/Dotfiles/setup/modules/shell-setup.sh +# ── Desktop environment selection ───────────────────────────────────────────── +# Prompts for a single keypress to choose which DE installer to run. +# -n1: read exactly one character without needing Enter +# -p: display the prompt inline + read -n1 -p "what DE to install? [hyprland,sway,none]" doit case $doit in + # HyprLua is the primary/recommended DE — Hyprland configured via Lua scripts hyprland) bash ~/Dotfiles/setup/modules/Desktop-Environments/hyprland.sh ;; + # Sway — Wayland tiling compositor, i3-compatible sway) bash ~/Dotfiles/setup/modules/Desktop-Environments/sway.sh ;; + # Skip DE entirely (for headless/server setups) none) echo "Skipping DE installation" ;; + # Catch-all for invalid input *) echo "please choose a desktop environment to install" ;; esac +# ── Optional application modules ────────────────────────────────────────────── +# These are intentionally commented out — uncomment the ones you want. +# Each is a self-contained idempotent module that installs a specific app. +# In the TUI installer (tui-install.sh), these are presented as a checklist. + # Optional apps — uncomment what you want: # bash ~/Dotfiles/setup/modules/optional-Modules/apps/steam.sh # bash ~/Dotfiles/setup/modules/optional-Modules/apps/vesktop.sh @@ -37,4 +89,6 @@ esac # bash ~/Dotfiles/setup/modules/optional-Modules/zfs.sh # bash ~/Dotfiles/setup/modules/optional-Modules/wprs.sh +# ── Done ────────────────────────────────────────────────────────────────────── +# Print a final message pointing the user to the log file for review. printf "\nDone. Log: %s\n" "$LOG" diff --git a/setup/modules/Desktop-Environments/cosmic.sh b/setup/modules/Desktop-Environments/cosmic.sh index c83967b..b384cfd 100755 --- a/setup/modules/Desktop-Environments/cosmic.sh +++ b/setup/modules/Desktop-Environments/cosmic.sh @@ -1,8 +1,55 @@ #!/bin/bash +# ============================================================================= +# cosmic.sh — COSMIC Desktop Environment installer +# ============================================================================= +# Part of the Dotfiles setup system for Arch Linux. +# +# COSMIC is a Wayland-native desktop environment developed by System76 in Rust. +# Unlike traditional DEs that layer on top of existing WM infrastructure, COSMIC +# ships its own compositor (cosmic-comp), shell, and settings daemon as a +# tightly integrated suite. +# +# This script installs the full COSMIC suite via pacman, sets up the PipeWire +# audio stack, configures networking and Bluetooth, and enables the appropriate +# display manager (cosmic-greeter preferred, SDDM as fallback). +# +# Prerequisites: run as a normal user with sudo access; pacman must be available; +# the system should already be connected to the internet. +# ============================================================================= + +# Exit immediately on error; treat unset variables as errors; propagate pipe +# failures so that a failing command early in a pipeline does not go unnoticed. set -euo pipefail + +# Pull in the shared logging helpers (log, warn, err) from the setup library. +# BASH_SOURCE[0] resolves to this file's path even when the script is sourced +# from another directory, making the relative path to lib/ reliable. source "$(dirname "${BASH_SOURCE[0]}")/../lib/logging.sh" log "Installing COSMIC desktop..." + +# Install the COSMIC meta-package plus supporting infrastructure: +# +# cosmic — the COSMIC desktop meta-package (pulls in cosmic-comp, +# cosmic-panel, cosmic-settings, cosmic-app-library, etc.) +# +# pipewire — modern audio/video routing daemon; replaces PulseAudio +# and JACK for most desktop workloads +# wireplumber — session/policy manager for PipeWire; decides which +# streams get routed where +# pipewire-alsa — ALSA compatibility shim so legacy ALSA apps use PipeWire +# pipewire-jack — JACK compatibility shim for professional audio software +# pipewire-pulse — PulseAudio socket emulation; keeps apps that speak +# libpulse working without changes +# +# networkmanager — connection management daemon; handles wired, Wi-Fi, +# VPN, and mobile broadband with a D-Bus API +# +# bluez — the Linux Bluetooth protocol stack +# bluez-utils — command-line tools (bluetoothctl) for managing devices +# +# flatpak — universal app sandboxing/delivery; lets COSMIC Software +# install Flathub apps without distro packaging sudo pacman -S --noconfirm --needed \ cosmic \ pipewire wireplumber pipewire-alsa pipewire-jack pipewire-pulse \ @@ -11,14 +58,30 @@ sudo pacman -S --noconfirm --needed \ flatpak log "Enabling services..." + +# COSMIC ships its own display manager (cosmic-greeter) built with iced. +# Prefer it over SDDM for a cohesive look-and-feel; fall back to SDDM when +# the package is not present (e.g. on systems where cosmic-greeter is still +# in the AUR or not yet stabilised on this arch build). +# +# pacman -Qi queries the local package database and exits non-zero when the +# package is not installed; &>/dev/null silences both stdout and stderr so +# the check is invisible to the user. # cosmic-greeter is COSMIC's own display manager; fall back to sddm if absent if pacman -Qi cosmic-greeter &>/dev/null; then sudo systemctl enable cosmic-greeter.service else + # SDDM (Simple Desktop Display Manager) is the widely used Qt-based DM; + # it supports both X11 and Wayland sessions and integrates well with COSMIC. sudo pacman -S --noconfirm --needed sddm sudo systemctl enable sddm.service fi + +# Enable NetworkManager so the network is managed on every boot. sudo systemctl enable NetworkManager.service + +# Enable the Bluetooth daemon so adapters are powered up on boot and devices +# can reconnect automatically. sudo systemctl enable bluetooth.service log "COSMIC installation complete. Reboot to start." diff --git a/setup/modules/Desktop-Environments/gnome.sh b/setup/modules/Desktop-Environments/gnome.sh index 0d60453..4012427 100755 --- a/setup/modules/Desktop-Environments/gnome.sh +++ b/setup/modules/Desktop-Environments/gnome.sh @@ -1,8 +1,56 @@ #!/bin/bash +# ============================================================================= +# gnome.sh — GNOME Desktop Environment installer +# ============================================================================= +# Part of the Dotfiles setup system for Arch Linux. +# +# GNOME is a mature, GTK4-based desktop environment that targets simplicity and +# a touch-friendly workflow. On Arch it is shipped as the 'gnome' group, which +# pulls in GNOME Shell (the compositor/WM), Mutter, GDM (the display manager), +# and the core GNOME application set. +# +# This script installs GNOME plus the PipeWire audio stack, GNOME's XDG portal +# (required for Flatpak sandboxing and screen sharing under Wayland), and +# enables the essential system services. +# +# Prerequisites: run as a normal user with sudo access; pacman available; +# internet connection active. +# ============================================================================= + +# Exit on any error; fail on unset variables; propagate pipeline errors. set -euo pipefail + +# Load shared logging helpers (log, warn, err) from the setup library. source "$(dirname "${BASH_SOURCE[0]}")/../lib/logging.sh" log "Installing GNOME desktop..." + +# Install GNOME and its ecosystem: +# +# gnome — the official GNOME package group; includes +# gnome-shell, mutter, gdm, gnome-control-center, +# nautilus, and other core apps +# +# gnome-tweaks — exposes advanced settings not available in +# GNOME Settings (fonts, window buttons, legacy +# GTK theme overrides, extensions toggle) +# +# xdg-desktop-portal-gnome — GNOME's implementation of the XDG Desktop +# Portal D-Bus API; required for Flatpak apps to +# open file choosers, request screenshots, and +# share screens under Wayland +# +# pipewire — low-latency audio/video routing daemon +# wireplumber — PipeWire session/policy manager +# pipewire-alsa — ALSA compatibility layer on top of PipeWire +# pipewire-jack — JACK API emulation for pro-audio software +# pipewire-pulse — PulseAudio socket emulation for legacy apps +# +# networkmanager — network connection management daemon (wired, +# Wi-Fi, VPN, etc.) +# +# flatpak — sandboxed app delivery; GNOME Software uses it +# to install Flathub applications sudo pacman -S --noconfirm --needed \ gnome \ gnome-tweaks \ @@ -12,7 +60,12 @@ sudo pacman -S --noconfirm --needed \ flatpak log "Enabling services..." + +# GDM (GNOME Display Manager) handles the login screen and session startup. +# It is bundled with the 'gnome' group and is the canonical choice for GNOME. sudo systemctl enable gdm.service + +# Start NetworkManager on boot so connections are available before login. sudo systemctl enable NetworkManager.service log "GNOME installation complete. Reboot to start." diff --git a/setup/modules/Desktop-Environments/hyprland.sh b/setup/modules/Desktop-Environments/hyprland.sh index 85d5685..fb6553d 100755 --- a/setup/modules/Desktop-Environments/hyprland.sh +++ b/setup/modules/Desktop-Environments/hyprland.sh @@ -1,144 +1,405 @@ #!/bin/bash +# ============================================================================= +# hyprland.sh — Hyprland Desktop Environment installer (legacy hyprlang config) +# ============================================================================= +# Part of the Dotfiles setup system for Arch Linux. +# +# Hyprland is a dynamic tiling Wayland compositor built in C++ with a focus on +# animations, eye-candy, and a powerful plugin system (hyprpm). +# +# This is the LEGACY variant that uses hyprlang (.conf) configuration files. +# The newer hyprlua.sh uses Lua for configuration instead. Both installers +# share a nearly identical package list; the key differences are: +# - EWW and config source paths point to desktopenvs/hyprland/ (not hyprlua/) +# - hypr-usr/ device-specific .conf files are placed at ~/.config/ root level +# and sourced by hyprland.conf via 'source' directives +# - hyprlua.sh additionally installs python-opencv and v4l-utils for the +# webcam-based presence/idle detection daemon +# +# High-level install flow: +# 1. System update + Flatpak +# 2. pacman packages (compositor, audio, networking, tools, fonts) +# 3. systemd service enablement (NetworkManager, ly display manager, udisks2) +# 4. AUR packages via yay +# 5. EWW status-bar (interactive form-factor selection + Rust compilation) +# 6. Themes, icons, ly config, terminal symlink, SSH askpass +# 7. Nordzy left-hand cursor theme +# 8. Bluetooth / iwd wireless services +# 9. Config file deployment from Dotfiles +# 10. Wallpaper + resource files +# 11. Python venv for helper scripts +# 12. Udiskie tray-icon symlink fix +# 13. config-updater symlinks + apply-theme.sh +# +# Prerequisites: internet access, yay (AUR helper), rustup, git, wget, cargo. +# ============================================================================= + +# Exit immediately if any command fails — partial DE installs can leave the +# system in a broken graphical state, so aborting early is safer. set -e + +# Load shared logging helpers (log, warn, err) from the setup library. source "$(dirname "${BASH_SOURCE[0]}")/../lib/logging.sh" log "Starting Hyprland installer (legacy — hyprlang config)..." +# --------------------------------------------------------------------------- # 1. Update system and install Flatpak +# --------------------------------------------------------------------------- +# Run a full system upgrade first to avoid Arch partial-upgrade issues (on a +# rolling release, installing new packages against an un-updated system can +# cause library ABI mismatches). Flatpak is installed early so AUR steps that +# depend on it can find it. log "Updating system and installing Flatpak..." sudo pacman -Syu --noconfirm --needed flatpak +# --------------------------------------------------------------------------- # 2. Install required packages +# --------------------------------------------------------------------------- log "Installing required packages..." sudo pacman -Syu --noconfirm --needed \ hyprland hyprcursor wl-clipboard hyprpaper hyprlock wofi kitty dunst \ + # hyprland — the Wayland compositor / window manager + # hyprcursor — hardware-accelerated cursor rendering for Hyprland + # wl-clipboard — wl-copy / wl-paste; clipboard tools for Wayland + # hyprpaper — wallpaper utility with per-monitor and animation support + # hyprlock — GPU-accelerated screen locker (Hyprland-native) + # wofi — Wayland application launcher (rofi replacement) + # kitty — GPU-accelerated terminal (default terminal in this setup) + # dunst — lightweight, scriptable notification daemon nwg-dock-hyprland nwg-drawer nwg-menu nwg-look \ + # nwg-dock-hyprland — dock/taskbar with Hyprland workspace awareness + # nwg-drawer — application drawer (grid-style launcher) + # nwg-menu — GTK menu used in the panel's app button + # nwg-look — GTK/cursor/icon theme picker for wlroots sessions python cmake meson cpio pkgconf ruby-pkg-config \ + # Build tools needed to compile EWW (Rust) and AUR packages (C/C++/Ruby) hyprsunset hypridle ksshaskpass \ + # hyprsunset — blue-light filter / night-mode daemon for Hyprland + # hypridle — idle daemon that triggers lock/suspend after inactivity + # ksshaskpass — Qt SSH passphrase dialog (registered as ssh-askpass) nm-connection-editor network-manager-applet blueman bluez \ + # nm-connection-editor — GTK GUI for editing NetworkManager profiles + # network-manager-applet — system-tray applet showing Wi-Fi/VPN status + # blueman — GTK Bluetooth manager with tray icon + # bluez — core Linux Bluetooth protocol stack pipewire alsa-utils firefox greetd-tuigreet \ + # pipewire — modern audio/video routing daemon + # alsa-utils — ALSA CLI tools (amixer, aplay) for low-level audio + # firefox — web browser + # greetd-tuigreet — TUI login greeter (text-based, no GPU needed at DM) grim slurp gst-plugin-pipewire imagemagick \ + # grim — Wayland screenshot tool (outputs to file or stdout) + # slurp — interactive region selector (used with grim) + # gst-plugin-pipewire — GStreamer plugin bridging through PipeWire + # imagemagick — image processing (screenshot effects, compositing) nerd-fonts otf-font-awesome \ + # nerd-fonts — all Nerd Font patched families (icon glyphs in bar) + # otf-font-awesome — Font Awesome icon glyphs for EWW/dunst pipewire-alsa pipewire-jack pipewire-pulse \ + # Compatibility layers: let ALSA, JACK, and PulseAudio apps route through + # PipeWire without requiring code changes in those apps qt5-wayland qt6-wayland swww ttf-jetbrains-mono wireplumber \ + # qt5-wayland / qt6-wayland — Qt platform plugins for native Wayland rendering + # swww — smooth animated wallpaper daemon + # ttf-jetbrains-mono — monospace font for terminals and the EWW bar + # wireplumber — PipeWire session/policy manager qt6ct xdg-desktop-portal-hyprland xdg-utils \ + # qt6ct — Qt6 colour/style configurator (run as user) + # xdg-desktop-portal-hyprland — Hyprland XDG portal (screen share, file pick) + # xdg-utils — xdg-open / MIME handling helpers xorg-server xorg-xinit papirus-icon-theme \ + # xorg-server / xorg-xinit — X server for XWayland (legacy X11 apps) + # papirus-icon-theme — icon theme used system-wide and by udiskie cool-retro-term qalculate-gtk iwd dbus \ + # cool-retro-term — retro CRT-style terminal emulator + # qalculate-gtk — powerful GTK calculator with unit conversion + # iwd — Intel Wireless Daemon (lightweight Wi-Fi back-end) + # dbus — D-Bus inter-process communication daemon thunar tumbler thunar-archive-plugin thunar-shares-plugin thunar-volman \ + # thunar — lightweight GTK file manager (XFCE project) + # tumbler — thumbnail service for Thunar + # thunar-archive-plugin — right-click archive (zip/tar) integration + # thunar-shares-plugin — Samba share management from Thunar + # thunar-volman — automatic volume/device mounting in Thunar hyprpicker pcmanfm-qt udisks2 ly kew \ + # hyprpicker — colour picker outputting hex/rgb/hsl under Hyprland + # pcmanfm-qt — Qt file manager kept as fallback + # udisks2 — D-Bus service for querying and managing storage devices + # ly — minimal TUI display manager (replaces getty on tty1) + # kew — terminal music player hyprpolkitagent pavucontrol playerctl wf-recorder sound-theme-freedesktop + # hyprpolkitagent — Hyprland-native Polkit authentication agent + # pavucontrol — PulseAudio/PipeWire volume control GUI + # playerctl — MPRIS media player controller (play/pause CLI) + # wf-recorder — screen recorder for wlroots compositors + # sound-theme-freedesktop — standard freedesktop sound event library +# --------------------------------------------------------------------------- # 3. Enable essential services +# --------------------------------------------------------------------------- log "Enabling essential services..." + +# NetworkManager must be active on boot for network connectivity. sudo systemctl enable NetworkManager.service + +# getty@tty1 is the default text-mode login prompt; we replace it with ly. +# '|| true' prevents abort if the unit is already disabled or doesn't exist. sudo systemctl disable getty@tty1.service || true + +# ly is the TUI display manager that runs on tty1 and launches Hyprland after +# the user logs in. sudo systemctl enable ly@tty1.service + +# udisks2 provides the D-Bus API for block devices; required by udiskie for +# automatic USB/external drive mounting. sudo systemctl enable udisks2.service +# --------------------------------------------------------------------------- # 4. Install AUR packages +# --------------------------------------------------------------------------- log "Installing AUR packages..." + +# Activate the stable Rust toolchain so AUR packages that compile Rust code +# (and EWW compiled below) use a known-good toolchain version. rustup default stable + +# yay flags: +# --answerdiff None — skip PKGBUILD diff display (non-interactive) +# --answerclean All — always clean build dirs between builds +# --noconfirm — no confirmation prompts +# +# Packages: +# hyprland-workspaces — EWW workspace widget reading Hyprland IPC socket +# vicinae-bin — pre-built vicinae (vim-like keybind layer for scripts) +# bluetuith — TUI Bluetooth manager (used in the menus) +# wvkbd — on-screen virtual keyboard for Wayland (touch) +# iwmenu — wofi-based Wi-Fi network picker using iwd +# pinta — simple image editor +# walker-bin — fast app launcher / runner (pre-built binary) +# ulauncher — extensible app launcher with plugin ecosystem +# bzmenu — quick Bluetooth device connect menu for the bar +# udiskie — automounter with tray icon (built on udisks2) +# wofi-calc — inline calculator pop-up inside wofi +# bri — brightness control CLI helper for bar widgets +# chamel — theme-switching helper (applies colour palettes) yay -Syu --answerdiff None --answerclean All --noconfirm \ hyprland-workspaces vicinae-bin bluetuith wvkbd iwmenu pinta \ walker-bin ulauncher bzmenu udiskie \ wofi-calc bri chamel +# --------------------------------------------------------------------------- # 5. EWW bar selection and compilation +# --------------------------------------------------------------------------- +# EWW (Elkowar's Wacky Widgets) is a Rust widget framework that powers the +# custom status bar. Three layouts exist for different hardware form factors: +# eww/ — Notebook layout (includes battery widget) +# eww-nobattery/ — Desktop PC layout (no battery bar) +# eww-touch/ — Tablet layout (larger touch targets) log "Setting up EWW bar..." + +# Wipe any previous EWW config to avoid stale widget definitions conflicting +# with the freshly copied one. rm -rf ~/.config/eww + +# Prompt for form factor with a single keypress (no Enter needed). read -n1 -p "Install eww bar for PC, Notebook or Tablet [P/N/T]: " doit -echo +echo # Print newline so subsequent output starts on a fresh line + case $doit in + # Notebook: copy the battery-aware layout n|N) cp -rf ~/Dotfiles/desktopenvs/hyprland/eww/ ~/.config/ ;; + # Desktop PC: copy the no-battery layout p|P) cp -rf ~/Dotfiles/desktopenvs/hyprland/eww-nobattery/ ~/.config/eww ;; + # Tablet: copy touch-optimised layout t|T) cp -rf ~/Dotfiles/desktopenvs/hyprland/eww-touch/ ~/.config/eww ;; + # Unknown key: warn and skip; the user must copy manually later *) warn "No valid choice — skipping EWW copy. Run manually later." ;; esac log "Compiling EWW..." +# EWW is not in the official repos so we compile from source. +# ~/install-tmp is a staging area for source trees fetched during installation. mkdir -p ~/install-tmp cd ~/install-tmp git clone https://github.com/elkowar/eww cd eww +# --release → optimised binary (significantly faster than debug) +# --no-default-features → disable the X11 backend entirely +# --features=wayland → enable only the Wayland backend cargo build --release --no-default-features --features=wayland chmod +x target/release/eww +# Install system-wide so any autostart entry or script can call 'eww' directly. sudo cp target/release/eww /usr/bin/ -cd ~ +cd ~ # Return home so subsequent relative-path steps work correctly +# --------------------------------------------------------------------------- # 6. Theme and icon setup +# --------------------------------------------------------------------------- log "Installing themes and icons..." + +# Install the cyberqueer GTK theme system-wide so GTK2/3/4 apps pick it up. sudo cp -r ~/Dotfiles/gtk-themes/cyberqueer /usr/share/themes + +# Install the matching btop colour theme for the terminal system monitor. sudo cp ~/Dotfiles/desktopenvs/hyprland/btop/themes/cyberqueer.theme /usr/share/btop/themes + +# Deploy the ly display-manager config (login screen appearance, session list). sudo cp -f ~/Dotfiles/etc-ly-config.ini /etc/ly/config.ini + +# Register kitty as the system-default terminal for xdg-terminal-exec so +# file managers and scripts that call "Open Terminal Here" launch kitty. sudo ln -sf /usr/bin/kitty /usr/bin/xdg-terminal-exec + +# Register ksshaskpass as the SSH passphrase helper. +# When SSH needs a passphrase in a non-terminal context (e.g. git push from a +# script), it invokes /usr/lib/ssh/ssh-askpass; this link provides a GUI dialog. sudo ln -sf /usr/bin/ksshaskpass /usr/lib/ssh/ssh-askpass +# --------------------------------------------------------------------------- # 7. Cursor setup +# --------------------------------------------------------------------------- +# Nordzy-cursors-lefthand is a Nord-palette left-handed cursor theme. +# Downloaded from GitHub Releases rather than the AUR because the AUR build +# may lag behind the upstream release schedule. log "Installing cursor theme..." mkdir -p ~/.icons wget -O ~/install-tmp/Nordzy-cursors-lefthand.tar.gz \ https://github.com/guillaumeboehm/Nordzy-cursors/releases/download/v2.3.0/Nordzy-cursors-lefthand.tar.gz +# Extract directly into ~/.icons/ so the theme is immediately discoverable by +# Hyprland (via hyprcursor) and by GTK applications. tar -zxf ~/install-tmp/Nordzy-cursors-lefthand.tar.gz -C ~/.icons/ +# --------------------------------------------------------------------------- # 8. Enable Bluetooth and wireless services +# --------------------------------------------------------------------------- log "Enabling Bluetooth and wireless services..." + +# bluez: core Bluetooth protocol stack (powers and manages adapters on boot) sudo systemctl enable bluez +# bluetooth.service: high-level service handling pairing and profiles sudo systemctl enable bluetooth.service +# iwd: modern Wi-Fi daemon from Intel; used by iwmenu for the bar Wi-Fi picker sudo systemctl enable iwd.service -# 9. Hyprland plugins — must be run from inside a live Hyprland session -# Run manually after first login: +# --------------------------------------------------------------------------- +# 9. Hyprland plugins (must be run from inside a live Hyprland session) +# --------------------------------------------------------------------------- +# hyprpm links against the exact compositor binary, so it cannot run from the +# installer. Execute these commands manually after first login: # hyprpm update # hyprpm add https://github.com/hyprwm/hyprland-plugins +# --------------------------------------------------------------------------- # 10. Copy configs +# --------------------------------------------------------------------------- log "Copying configs..." + +# Each entry in CONFIGS maps to a sub-directory (or file) inside +# desktopenvs/hyprland/ that is deployed verbatim to ~/.config/. +# Old copies are wiped first to prevent stale files from prior installs +# interfering with the fresh deployment. CONFIGS=(kitty mimeapps.list vicinae walker ulauncher hypr xfce4 wofi dunst alacritty nwg-dock-hyprland nwg-drawer nwg-panel scripts btop gtk-3.0) for cfg in "${CONFIGS[@]}"; do rm -rf ~/.config/"$cfg" cp -r ~/Dotfiles/desktopenvs/hyprland/"$cfg" ~/.config/ done +# hypr-usr/ holds device-specific overrides (monitor layout, keybinds, envvars). +# In the legacy hyprlang setup these are .conf files at the ~/.config/ root +# level; hyprland.conf sources them with 'source = ~/.config/monitors.conf' etc. cp ~/Dotfiles/desktopenvs/hyprland/hypr-usr/* ~/.config/ + +# colors.conf defines the shared colour palette used by EWW, dunst, and scripts. cp ~/Dotfiles/colors.conf ~/.config/colors.conf +# --------------------------------------------------------------------------- # 11. Wallpaper and resources +# --------------------------------------------------------------------------- log "Copying wallpaper and resources..." mkdir -p ~/Pictures + +# Firefox logo SVG used as the dock/launcher shortcut icon. cp ~/Dotfiles/resources/fflogo.svg ~/Pictures/fflogo.svg + +# Download the personalised wallpaper from the owner's Nextcloud instance. +# ?v=15 is a cache-buster version parameter on the Nextcloud theming API. wget "https://cloud.abdelbaki.eu/apps/theming/image/background?v=15" -O ~/Pictures/background.jpg +# --------------------------------------------------------------------------- # 12. Python venv for scripts +# --------------------------------------------------------------------------- +# Many EWW and Hyprland helper scripts are written in Python and need these +# third-party libraries. An isolated venv avoids polluting the system Python +# and prevents conflicts with pacman-managed Python packages. log "Setting up Python venv for scripts..." python -m venv ~/.config/python-script -~/.config/python-script/bin/pip install speedtest-cli requests pint simpleeval parsedatetime +~/.config/python-script/bin/pip install \ + speedtest-cli `# Internet speed test widget for the EWW bar` \ + requests `# HTTP client used by various widgets and scripts` \ + pint `# Unit conversion library for the calculator script` \ + simpleeval `# Safe maths expression evaluator (wofi-calc backend)` \ + parsedatetime `# Natural-language date/time parser for timer scripts` +# --------------------------------------------------------------------------- # 13. Udiskie icon fix +# --------------------------------------------------------------------------- +# udiskie uses freedesktop icon names (udiskie-checkbox-checked, etc.) that +# most icon themes do not ship. We symlink them from Papirus-Dark's existing +# checkbox icons as a close visual match. Placing symlinks in the hicolor +# theme ensures every GTK app can find them as hicolor is the last-resort +# fallback in the icon theme lookup chain. log "Applying Udiskie icon fix..." PAPIRUS_DIR="/usr/share/icons/Papirus-Dark/status" HICOLOR_DIR="/usr/share/icons/hicolor/scalable/status" if [ -d "$PAPIRUS_DIR" ]; then sudo ln -sf "$PAPIRUS_DIR/checkbox-checked.svg" "$HICOLOR_DIR/udiskie-checkbox-checked.svg" sudo ln -sf "$PAPIRUS_DIR/checkbox-unchecked.svg" "$HICOLOR_DIR/udiskie-checkbox-unchecked.svg" + # Rebuild the icon cache so GTK discovers the new symlinked entries without + # requiring a full logout/login. sudo gtk-update-icon-cache -f -t /usr/share/icons/hicolor else warn "Papirus-Dark not found — skipping udiskie icon fix." fi +# --------------------------------------------------------------------------- # 14. Enable udiskie +# --------------------------------------------------------------------------- +# udiskie is the auto-mounter / tray-icon daemon that surfaces mounted volumes. +# Enable the systemd user unit and start it immediately so this session already +# benefits from auto-mounting without requiring a reboot. log "Enabling udiskie service..." sudo systemctl enable udiskie.service sudo systemctl start udiskie.service +# --------------------------------------------------------------------------- # 15. Install config updater and theme script +# --------------------------------------------------------------------------- +# config-updater is a lightweight mechanism that re-syncs selected config files +# from the Dotfiles repo without running the full installer again. +# updater.conf lists which files to keep in sync; update-configs.sh performs +# the actual copy operation. +# +# Symlinks (ln -sf) are used so that 'git pull' in ~/Dotfiles immediately takes +# effect the next time update-configs.sh is run. log "Installing config updater and theme script..." mkdir -p ~/.config/config-updater ln -sf ~/Dotfiles/desktopenvs/hyprland/config-updater/updater.conf ~/.config/config-updater/updater.conf ln -sf ~/Dotfiles/desktopenvs/hyprland/config-updater/update-configs.sh ~/update-configs.sh + +# apply-theme.sh applies the cyberqueer colour palette across all running apps +# (sets GTK/Qt themes, reloads dunst config, refreshes EWW colour variables). +# Copied (not symlinked) to ~ so it works even if ~/Dotfiles is unmounted. cp ~/Dotfiles/apply-theme.sh ~/apply-theme.sh chmod +x ~/apply-theme.sh +# --------------------------------------------------------------------------- # 16. WallRizz (run manually after login — requires a running desktop session) +# --------------------------------------------------------------------------- +# WallRizz generates a colour scheme from the active wallpaper and applies it +# to EWW, dunst, and terminals. It requires a live Wayland session and cannot +# be safely run from this installer script. +# Uncomment and run manually after your first login: # curl -sL "$(curl -s https://api.github.com/repos/5hubham5ingh/WallRizz/releases/latest \ # | grep -Po '"browser_download_url": "\K[^"]+' | grep WallRizz)" | tar -xz \ # && sudo mv WallRizz /usr/bin/ diff --git a/setup/modules/Desktop-Environments/hyprlua.sh b/setup/modules/Desktop-Environments/hyprlua.sh index 7817f84..5014aa8 100755 --- a/setup/modules/Desktop-Environments/hyprlua.sh +++ b/setup/modules/Desktop-Environments/hyprlua.sh @@ -1,147 +1,407 @@ #!/bin/bash +# ============================================================================= +# hyprlua.sh — HyprLua Desktop Environment installer (Lua-based config) +# ============================================================================= +# Part of the Dotfiles setup system for Arch Linux. +# +# HyprLua is the primary / recommended Hyprland variant in this Dotfiles repo. +# It uses a Lua configuration file (hyprland.lua) instead of hyprlang (.conf), +# enabling programmatic config generation, per-device overrides via Lua modules, +# and more maintainable keybind/rule definitions. +# +# Differences from hyprland.sh (the legacy .conf variant): +# - Config source paths point to desktopenvs/hyprlua/ +# - Device-specific overrides live in ~/.config/hypr/usr/ (Lua modules +# loaded via require("usr.*")), not at ~/.config/ root +# - Adds python-opencv + v4l-utils for the webcam presence-detection daemon +# - Tablet mode also installs evdev-right-click-emulation from AUR +# - gsettings sets prefer-dark globally (relevant for GTK apps under Hyprland) +# - qt6ct is NOT installed (hyprlua dropped it from the package list) +# - Adds --needed flag to yay to skip already-installed AUR packages +# +# High-level install flow: +# 1. System update + Flatpak +# 2. pacman packages (compositor, audio, networking, tools, fonts, webcam) +# 3. systemd service enablement +# 4. AUR packages via yay +# 5. EWW bar (form-factor selection + Rust compilation) +# 6. Themes, ly config, terminal symlink, SSH askpass, dark-mode preference +# 7. Nordzy left-hand cursor theme +# 8. Bluetooth / iwd services +# 9. Config file deployment (hyprlua source tree) +# 10. Wallpaper + resource files +# 11. Python venv for helper scripts +# 12. Udiskie tray-icon symlink fix +# 13. config-updater symlinks + apply-theme.sh +# +# Prerequisites: internet access, yay (AUR helper), rustup, git, wget, cargo. +# ============================================================================= + +# Exit immediately on any error — partial DE installs leave a broken system. set -e + +# Load shared logging helpers (log, warn, err) from the setup library. source "$(dirname "${BASH_SOURCE[0]}")/../lib/logging.sh" log "Starting HyprLua installer (Lua-based config)..." +# --------------------------------------------------------------------------- # 1. Update system and install Flatpak +# --------------------------------------------------------------------------- +# Full system upgrade before package installs to avoid Arch partial-upgrade +# issues (library ABI mismatches between un-upgraded and newly installed pkgs). log "Updating system and installing Flatpak..." sudo pacman -Syu --noconfirm --needed flatpak +# --------------------------------------------------------------------------- # 2. Install required packages +# --------------------------------------------------------------------------- log "Installing required packages..." sudo pacman -Syu --noconfirm --needed \ hyprland hyprcursor wl-clipboard hyprpaper hyprlock wofi kitty dunst \ + # hyprland — Wayland compositor / window manager + # hyprcursor — hardware-accelerated cursor rendering for Hyprland + # wl-clipboard — wl-copy / wl-paste; Wayland clipboard CLI tools + # hyprpaper — wallpaper utility with per-monitor support + # hyprlock — GPU-accelerated screen locker (Hyprland-native) + # wofi — Wayland application launcher (rofi alternative) + # kitty — GPU-accelerated terminal (default in this setup) + # dunst — lightweight, scriptable notification daemon nwg-dock-hyprland nwg-drawer nwg-menu nwg-look \ + # nwg-dock-hyprland — dock/taskbar with Hyprland workspace awareness + # nwg-drawer — grid application drawer / launcher + # nwg-menu — GTK application menu for the panel button + # nwg-look — GTK/cursor/icon theme picker for wlroots sessions python cmake meson cpio pkgconf ruby-pkg-config \ + # Build toolchain required for EWW (Rust) and AUR package compilation hyprsunset hypridle ksshaskpass \ + # hyprsunset — blue-light filter / night-mode daemon + # hypridle — idle daemon triggering lock/suspend after inactivity + # ksshaskpass — Qt SSH passphrase dialog registered as ssh-askpass nm-connection-editor network-manager-applet blueman bluez \ + # nm-connection-editor — GTK editor for NetworkManager connection profiles + # network-manager-applet — tray applet showing network connection status + # blueman — GTK Bluetooth manager with tray icon support + # bluez — core Linux Bluetooth protocol stack pipewire alsa-utils firefox greetd-tuigreet \ + # pipewire — modern audio/video routing daemon (replaces PulseAudio) + # alsa-utils — low-level ALSA CLI tools (amixer, aplay, arecord) + # firefox — web browser + # greetd-tuigreet — TUI login greeter (text-based, no GPU requirement) grim slurp gst-plugin-pipewire imagemagick \ + # grim — Wayland screenshot capture tool + # slurp — interactive rectangular region selector for screenshots + # gst-plugin-pipewire — GStreamer plugin routing through PipeWire + # imagemagick — image processing toolkit for screenshot effects nerd-fonts otf-font-awesome \ + # nerd-fonts — all Nerd Font families (icon glyphs in EWW bar) + # otf-font-awesome — Font Awesome glyph set for bar and notifications pipewire-alsa pipewire-jack pipewire-pulse \ + # Compatibility shims letting ALSA, JACK, and PulseAudio apps route through + # PipeWire transparently without needing to be recompiled qt5-wayland qt6-wayland swww ttf-jetbrains-mono wireplumber \ + # qt5-wayland / qt6-wayland — Qt Wayland platform plugins for native rendering + # swww — smooth animated wallpaper daemon for wlroots + # ttf-jetbrains-mono — monospace coding font used in terminals/bar + # wireplumber — PipeWire session and policy manager xdg-desktop-portal-hyprland xdg-utils \ + # xdg-desktop-portal-hyprland — Hyprland XDG portal (screen share, file pick) + # xdg-utils — xdg-open and MIME handling utilities + # Note: qt6ct is NOT included in hyprlua (differs from hyprland.sh) xorg-server xorg-xinit papirus-icon-theme \ + # xorg-server / xorg-xinit — X server for XWayland (legacy X11 apps) + # papirus-icon-theme — icon theme used system-wide and by udiskie cool-retro-term qalculate-gtk iwd dbus \ + # cool-retro-term — retro CRT-style terminal emulator + # qalculate-gtk — full-featured GTK calculator with unit conversion + # iwd — Intel Wireless Daemon (lighter Wi-Fi stack than wpa_supplicant) + # dbus — D-Bus inter-process communication daemon thunar tumbler thunar-archive-plugin thunar-shares-plugin thunar-volman \ + # thunar — lightweight GTK file manager + # tumbler — D-Bus thumbnail service for Thunar + # thunar-archive-plugin — archive (zip/tar) right-click integration + # thunar-shares-plugin — Samba share management in Thunar + # thunar-volman — automatic volume/device mounting in Thunar hyprpicker pcmanfm-qt udisks2 ly kew \ + # hyprpicker — colour picker outputting hex/rgb/hsl values + # pcmanfm-qt — Qt file manager (kept as fallback / alternative) + # udisks2 — D-Bus daemon for querying and managing storage devices + # ly — minimal TUI display manager (takes over tty1 from getty) + # kew — terminal-based music player hyprpolkitagent pavucontrol playerctl wf-recorder sound-theme-freedesktop \ + # hyprpolkitagent — Hyprland-native Polkit authentication agent + # pavucontrol — PulseAudio/PipeWire GUI volume mixer + # playerctl — MPRIS media player CLI controller + # wf-recorder — screen recorder for wlroots-based compositors + # sound-theme-freedesktop — standard freedesktop sound event samples python-opencv v4l-utils + # python-opencv — OpenCV Python bindings; used by the webcam presence- + # detection daemon to watch for the user at the desk and + # inhibit idle/screen-lock while they are present + # v4l-utils — Video4Linux2 tools for enumerating and configuring webcams +# --------------------------------------------------------------------------- # 3. Enable essential services +# --------------------------------------------------------------------------- log "Enabling essential services..." + +# NetworkManager: manages all network connections (wired, Wi-Fi, VPN). sudo systemctl enable NetworkManager.service + +# Disable the default getty login prompt on tty1 so ly can own that TTY. +# '|| true' prevents abort if the unit is already disabled. sudo systemctl disable getty@tty1.service || true + +# ly: TUI display manager that presents the login screen on tty1. sudo systemctl enable ly@tty1.service + +# udisks2: D-Bus block-device service required by udiskie for auto-mounting. sudo systemctl enable udisks2.service +# --------------------------------------------------------------------------- # 4. Install AUR packages +# --------------------------------------------------------------------------- log "Installing AUR packages..." + +# Pin the stable Rust toolchain so EWW and AUR Rust packages compile cleanly. rustup default stable + +# yay flags: +# --answerdiff None — skip PKGBUILD diff display (non-interactive) +# --answerclean All — always clean build directories +# --noconfirm — suppress all confirmation prompts +# --needed — skip packages that are already up-to-date (added vs +# hyprland.sh to avoid redundant AUR rebuilds) +# +# Packages: +# hyprland-workspaces — EWW workspace widget (reads Hyprland IPC) +# vicinae-bin — pre-built vicinae (vim-modal keybind layer) +# bluetuith — TUI Bluetooth manager (text-based device control) +# wvkbd — on-screen virtual keyboard for Wayland (tablet use) +# iwmenu — wofi/dmenu Wi-Fi picker driven by iwd +# pinta — accessible image editor (Paint.NET-like) +# walker-bin — fast application launcher (pre-built binary) +# ulauncher — extensible launcher with plugin support +# bzmenu — quick Bluetooth connect menu for the bar +# udiskie — auto-mounter with tray icon (built on udisks2) +# wofi-calc — inline calculator inside wofi +# bri — brightness control helper for bar widgets +# chamel — colour-palette / theme switcher yay -Syu --answerdiff None --answerclean All --noconfirm --needed \ hyprland-workspaces vicinae-bin bluetuith wvkbd iwmenu pinta \ walker-bin ulauncher bzmenu udiskie \ wofi-calc bri chamel hyprmoncfg +# --------------------------------------------------------------------------- # 5. EWW bar selection and compilation +# --------------------------------------------------------------------------- +# EWW (Elkowar's Wacky Widgets) powers the custom status bar. +# Three form-factor layouts ship in the Dotfiles: +# eww/ — Notebook: includes battery percentage widget +# eww-nobattery/ — Desktop PC: no battery widget +# eww-touch/ — Tablet: larger touch targets + right-click emulation log "Setting up EWW bar..." + +# Remove any existing EWW config to avoid leftover widget definitions. rm -rf ~/.config/eww + +# Single-keystroke form-factor selection (no Enter required). read -n1 -p "Install eww bar for PC, Notebook or Tablet [P/N/T]: " doit -echo +echo # Newline after single-char read + case $doit in + # Notebook: battery-aware layout n|N) cp -rf ~/Dotfiles/desktopenvs/hyprlua/eww/ ~/.config/ ;; + # Desktop PC: no battery widget p|P) cp -rf ~/Dotfiles/desktopenvs/hyprlua/eww-nobattery/ ~/.config/eww ;; + # Tablet: touch layout + right-click emulation daemon t|T) cp -rf ~/Dotfiles/desktopenvs/hyprlua/eww-touch/ ~/.config/eww + # evdev-right-click-emulation enables long-press → right-click on touch + # screens; its systemd service must be started alongside the session. yay -S evdev-right-click-emulation sudo systemctl enable --now evdev-rce.service ;; + # Unrecognised key: leave ~/.config/eww absent; user must copy manually. *) warn "No valid choice — skipping EWW copy. Run manually later." ;; esac log "Compiling EWW..." +# EWW is not packaged in the official repos; we compile from source. +# ~/install-tmp is the staging directory for source trees fetched during install. mkdir -p ~/install-tmp cd ~/install-tmp git clone https://github.com/elkowar/eww cd eww +# --release → produce an optimised binary (noticeably faster UI) +# --no-default-features → exclude the X11 backend (we only target Wayland) +# --features=wayland → enable the Wayland back-end explicitly cargo build --release --no-default-features --features=wayland chmod +x target/release/eww +# Install system-wide so autostart entries and scripts can invoke 'eww' directly. sudo cp target/release/eww /usr/bin/ -cd ~ +cd ~ # Return home so subsequent relative paths resolve correctly +# --------------------------------------------------------------------------- # 6. Theme and icon setup +# --------------------------------------------------------------------------- log "Installing themes and icons..." + +# Install the cyberqueer GTK theme system-wide for GTK2/3/4 application styling. sudo cp -r ~/Dotfiles/gtk-themes/cyberqueer /usr/share/themes + +# Install the matching btop resource-monitor colour theme. sudo cp ~/Dotfiles/desktopenvs/hyprlua/btop/themes/cyberqueer.theme /usr/share/btop/themes + +# Deploy the ly display-manager configuration (login screen appearance). sudo cp -f ~/Dotfiles/etc-ly-config.ini /etc/ly/config.ini + +# Register kitty as the system terminal for xdg-terminal-exec so file managers +# and scripts that open "a terminal here" consistently launch kitty. sudo ln -sf /usr/bin/kitty /usr/bin/xdg-terminal-exec + +# Register ksshaskpass as the SSH passphrase GUI helper. +# SSH clients look for /usr/lib/ssh/ssh-askpass when they need a passphrase +# but have no controlling terminal available (e.g. git push from a bar widget). sudo ln -sf /usr/bin/ksshaskpass /usr/lib/ssh/ssh-askpass + +# Qt theming via qt-themes/cyberqueer is commented out (kept for reference); +# the Qt5 / Qt6 colour scheme is currently handled via qt6ct manual setup. #bash ~/Dotfiles/qt-themes/cyberqueer/enable.sh + +# Instruct GTK apps that prefer the system colour-scheme to use dark mode. +# This is particularly important under Hyprland where there is no GNOME +# settings daemon propagating the user's preference automatically. gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' +# --------------------------------------------------------------------------- # 7. Cursor setup +# --------------------------------------------------------------------------- +# Nordzy-cursors-lefthand is a Nord-palette left-handed cursor theme. +# Downloaded directly from GitHub Releases for a pinned version; the AUR +# build can lag behind upstream. log "Installing cursor theme..." mkdir -p ~/.icons wget -O ~/install-tmp/Nordzy-cursors-lefthand.tar.gz \ https://github.com/guillaumeboehm/Nordzy-cursors/releases/download/v2.3.0/Nordzy-cursors-lefthand.tar.gz +# Extract into ~/.icons/ so both Hyprland (hyprcursor) and GTK apps can find it. tar -zxf ~/install-tmp/Nordzy-cursors-lefthand.tar.gz -C ~/.icons/ +# --------------------------------------------------------------------------- # 8. Enable Bluetooth and wireless services +# --------------------------------------------------------------------------- log "Enabling Bluetooth and wireless services..." + +# bluez: core Bluetooth daemon; powers adapters and makes them available on boot sudo systemctl enable bluez +# bluetooth.service: handles pairing, profiles, and device reconnection sudo systemctl enable bluetooth.service +# iwd: Intel Wireless Daemon; the Wi-Fi back-end used by iwmenu in the bar sudo systemctl enable iwd.service systemctl --user enable --now hyprmoncfgd -# 9. Hyprland plugins — must be run from inside a live Hyprland session -# Run manually after first login: +# --------------------------------------------------------------------------- +# 9. Hyprland plugins (must be run inside a live Hyprland session) +# --------------------------------------------------------------------------- +# hyprpm links against the exact running compositor binary, so it cannot be +# invoked from the installer. After first login, run: # hyprpm update # hyprpm add https://github.com/hyprwm/hyprland-plugins +# --------------------------------------------------------------------------- # 10. Copy configs +# --------------------------------------------------------------------------- log "Copying configs..." + +# Deploy each config directory from the hyprlua Dotfiles source. +# The wipe-then-copy pattern ensures no stale files from older installs remain. CONFIGS=(kitty mimeapps.list vicinae walker ulauncher hypr xfce4 wofi dunst alacritty nwg-dock-hyprland nwg-drawer nwg-panel scripts btop gtk-3.0) for cfg in "${CONFIGS[@]}"; do rm -rf ~/.config/"$cfg" cp -r ~/Dotfiles/desktopenvs/hyprlua/"$cfg" ~/.config/ done -# hypr/usr/ (device-specific) is already inside hypr/ and copied above. -# Customise ~/.config/hypr/usr/ per device after install. +# In hyprlua, device-specific overrides (monitors, keybinds, autostart, etc.) +# live inside ~/.config/hypr/usr/ as Lua modules loaded by hyprland.lua via +# require("usr.*"). The usr/ directory is already bundled inside the hypr/ +# directory copied above, so no separate step is needed here. +# After install, customise ~/.config/hypr/usr/ per device as needed. + +# Shared colour palette used by EWW bar, dunst, and scripts. cp ~/Dotfiles/colors.conf ~/.config/colors.conf +# --------------------------------------------------------------------------- # 11. Wallpaper and resources +# --------------------------------------------------------------------------- log "Copying wallpaper and resources..." mkdir -p ~/Pictures + +# Firefox logo SVG for the dock/launcher shortcut icon. cp ~/Dotfiles/resources/fflogo.svg ~/Pictures/fflogo.svg + +# Download the personalised wallpaper from the owner's Nextcloud instance. +# ?v=15 is a cache-buster on the Nextcloud theming background API endpoint. wget "https://cloud.abdelbaki.eu/apps/theming/image/background?v=15" -O ~/Pictures/background.jpg +# --------------------------------------------------------------------------- # 12. Python venv for scripts +# --------------------------------------------------------------------------- +# Helper scripts (weather widget, speed test, unit converter, timer, etc.) need +# these third-party Python libraries. An isolated venv keeps them separate from +# system Python packages managed by pacman to avoid conflicts. log "Setting up Python venv for scripts..." python -m venv ~/.config/python-script -~/.config/python-script/bin/pip install speedtest-cli requests pint simpleeval parsedatetime +~/.config/python-script/bin/pip install \ + speedtest-cli `# Internet speed test (EWW bar widget)` \ + requests `# HTTP client for API-driven widgets` \ + pint `# Unit conversion library` \ + simpleeval `# Safe maths expression evaluator (wofi-calc)` \ + parsedatetime `# Natural-language date/time parser for timer scripts` +# --------------------------------------------------------------------------- # 13. Udiskie icon fix +# --------------------------------------------------------------------------- +# udiskie expects freedesktop icon names (udiskie-checkbox-checked, etc.) that +# Papirus-Dark does not ship under those exact names. We symlink Papirus-Dark's +# existing checkbox SVGs into the hicolor fallback theme under the names udiskie +# expects, then rebuild the cache so GTK picks them up immediately. log "Applying Udiskie icon fix..." PAPIRUS_DIR="/usr/share/icons/Papirus-Dark/status" HICOLOR_DIR="/usr/share/icons/hicolor/scalable/status" if [ -d "$PAPIRUS_DIR" ]; then sudo ln -sf "$PAPIRUS_DIR/checkbox-checked.svg" "$HICOLOR_DIR/udiskie-checkbox-checked.svg" sudo ln -sf "$PAPIRUS_DIR/checkbox-unchecked.svg" "$HICOLOR_DIR/udiskie-checkbox-unchecked.svg" + # Rebuild the GTK icon cache so apps find the new symlinks without restart. sudo gtk-update-icon-cache -f -t /usr/share/icons/hicolor else warn "Papirus-Dark not found — skipping udiskie icon fix." fi +# --------------------------------------------------------------------------- # 14. Enable udiskie +# --------------------------------------------------------------------------- +# udiskie auto-mounts removable drives and shows a tray icon for each volume. +# Enable the systemd unit for future boots and start it immediately so USB +# devices inserted during this session are handled right away. log "Enabling udiskie service..." sudo systemctl enable udiskie.service sudo systemctl start udiskie.service +# --------------------------------------------------------------------------- # 15. Install config updater and theme script +# --------------------------------------------------------------------------- +# config-updater selectively re-syncs config files from the Dotfiles repo +# without re-running the full installer. updater.conf lists which files are +# managed; update-configs.sh performs the sync. +# +# Symlinks are used (not copies) so that pulling the latest Dotfiles with +# 'git pull' takes effect immediately on the next sync run. log "Installing config updater and theme script..." mkdir -p ~/.config/config-updater ln -sf ~/Dotfiles/desktopenvs/hyprlua/config-updater/updater.conf ~/.config/config-updater/updater.conf ln -sf ~/Dotfiles/desktopenvs/hyprlua/config-updater/update-configs.sh ~/update-configs.sh + +# apply-theme.sh applies the cyberqueer palette across all running apps +# (reloads dunst, refreshes EWW variables, sets GTK/Qt themes). +# Copied rather than symlinked so it is available even when ~/Dotfiles is absent. cp ~/Dotfiles/apply-theme.sh ~/apply-theme.sh chmod +x ~/apply-theme.sh diff --git a/setup/modules/Desktop-Environments/kde-plasma.sh b/setup/modules/Desktop-Environments/kde-plasma.sh index 83cc737..70a8c5c 100755 --- a/setup/modules/Desktop-Environments/kde-plasma.sh +++ b/setup/modules/Desktop-Environments/kde-plasma.sh @@ -1,7 +1,19 @@ #!/bin/bash +# set -euo pipefail: abort on errors, unset vars, and pipeline failures. set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../lib/logging.sh" +# plasma-meta: meta-package that pulls in the full Plasma desktop environment. +# sddm: Simple Desktop Display Manager (KDE's preferred login screen). +# sddm-kcm: KCM (KDE Control Module) for configuring SDDM inside System Settings. +# xdg-desktop-portal-kde: portal backend for Flatpak/Wayland screen-share, +# file-chooser, and print dialogs inside the KDE environment. +# konsole dolphin kate gwenview ark spectacle okular elisa: core KDE apps +# (terminal, file manager, editor, image viewer, archiver, screenshots, PDF, music). +# plasma-browser-integration: browser extension that exposes tabs to KDE. +# bluedevil: KDE's Bluetooth manager GUI (sits on top of bluez). +# power-profiles-daemon: exposes power-profile switching (balanced/performance/powersave) +# to KDE's Power Management settings. log "Installing KDE Plasma desktop..." sudo pacman -S --noconfirm --needed \ plasma-meta \ @@ -16,6 +28,7 @@ sudo pacman -S --noconfirm --needed \ flatpak log "Enabling services..." +# sddm provides the graphical login screen; must be enabled before rebooting. sudo systemctl enable sddm.service sudo systemctl enable NetworkManager.service sudo systemctl enable bluetooth.service diff --git a/setup/modules/core-packages.sh b/setup/modules/core-packages.sh index 08f54a4..d6afe3b 100644 --- a/setup/modules/core-packages.sh +++ b/setup/modules/core-packages.sh @@ -1,25 +1,166 @@ #!/bin/bash +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ setup/modules/core-packages.sh — Core system package installation ║ +# ║ ║ +# ║ PURPOSE: ║ +# ║ Installs all base-layer packages that every configuration of this ║ +# ║ system relies on. This is the largest and most time-consuming module. ║ +# ║ ║ +# ║ WHEN TO RUN: ║ +# ║ After package-managers.sh (yay must be present for the AUR packages). ║ +# ║ Called "core" in the TUI installer's component checklist. ║ +# ║ ║ +# ║ CATEGORIES INSTALLED: ║ +# ║ - Archiving & file utilities (7zip, atool, dust, fd, fzf, ripgrep) ║ +# ║ - Base build tools (base-devel, gcc, rust, rustup, python, ruby) ║ +# ║ - Networking (networkmanager, iwd, openssh, wpa_supplicant, bind, ldns) ║ +# ║ - Audio (pipewire, wireplumber, libpulse) ║ +# ║ - Boot & storage (grub, btrfs-progs, dosfstools, udisks2, udiskie) ║ +# ║ - Terminal utilities (btop, htop, tmux, mc, yazi, nano, vim, neovim) ║ +# ║ - System monitoring (fastfetch, lshw, lsof, smartmontools, vnstat) ║ +# ║ - Security (fail2ban, ufw, pciutils, usbutils) ║ +# ║ - Misc: flatpak, distrobox, qrencode, fdupes, lynx, tldr, tree ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ + set -euo pipefail +# -e: exit immediately if any package install fails +# -u: error on unset variables +# -o pipefail: catch failures inside pipes + +# Load shared logging helpers (log, skip, warn, err functions) source "$(dirname "${BASH_SOURCE[0]}")/lib/logging.sh" log "Installing core packages..." -sudo pacman -Syu --noconfirm --needed \ - 7zip arch-install-scripts atftp atool \ - base base-devel bc bind bluez btrfs-progs btop \ - cronie curl \ - dmidecode distrobox dosfstools dust \ - e2fsprogs fail2ban fastfetch fd fdupes ffmpeg firefox flatpak fzf \ - gcc git glib2 greetd-tuigreet grub \ - hdparm htop inetutils iputils iwd jq ldns less libpulse linux linux-firmware lshw lsof \ - lynx \ - man-db mc nano neovim networkmanager \ - onefetch openbsd-netcat openssh parted pciutils pipewire \ - python python-pip qrencode ripgrep rsync ruby-pkg-config rust rustup \ - smartmontools strace symlinks sysstat tldr tmux tree \ - udisks2 udisks2-btrfs udiskie ufw usbutils \ - vim vnstat wget whois wireplumber wireless_tools wpa_supplicant wprs \ - yazi zip unzip zram-generator +# Single pacman invocation for efficiency: +# -S: sync (install) +# -yu: update the package database and upgrade existing packages first +# --noconfirm: don't prompt for confirmation (needed for scripted install) +# --needed: skip packages that are already up-to-date (idempotency) +# +# Package groupings with rationale: + +sudo pacman -Syu --noconfirm --needed \ + # ── Archiving & file utilities ──────────────────────────────────────────── + 7zip \ # Modern replacement for p7zip — handles .7z, .zip, .rar, etc. + arch-install-scripts \ # genfstab and other Arch install helpers (useful even post-install) + atftp \ # TFTP client/server — used by some network boot tools + atool \ # Universal archive wrapper (handles zip/tar/gz/xz with one command) + \ + # ── Base system & build tools ───────────────────────────────────────────── + base \ # Minimal Arch base (filesystem, bash, glibc, etc.) + base-devel \ # AUR build requirements: make, gcc, binutils, pkg-config, etc. + bc \ # Arbitrary precision calculator — used in many shell scripts + bind \ # DNS tools: dig, nslookup, host + bluez \ # Bluetooth protocol stack — enables BT devices + btrfs-progs \ # btrfs filesystem tools: mkfs.btrfs, btrfsck, snapper + btop \ # Interactive resource monitor (CPU/mem/net/disk) + \ + cronie \ # Cron daemon for scheduled tasks (enabled later in core.sh) + curl \ # HTTP/FTP transfer tool — used by many installers + \ + # ── Hardware & disk utilities ───────────────────────────────────────────── + dmidecode \ # Reads hardware info from DMI/SMBIOS (CPU, memory, BIOS) + distrobox \ # Run other distro containers inside Arch (uses podman/docker) + dosfstools \ # mkfs.fat for FAT/EFI partitions; used when setting up boot + dust \ # Disk usage analyzer — like `du` but cleaner output + \ + # ── Filesystem & recovery ──────────────────────────────────────────────── + e2fsprogs \ # ext4 filesystem tools: fsck.ext4, mkfs.ext4, tune2fs + fail2ban \ # Bans IPs after repeated auth failures (SSH protection) + fastfetch \ # System info display (neofetch replacement, faster) + fd \ # Fast alternative to `find`, respects .gitignore + fdupes \ # Find duplicate files by checksum + ffmpeg \ # Video/audio processing framework; required by many tools + firefox \ # Default browser (also in DE install, but needed as base) + flatpak \ # Universal app sandbox format; Flathub remote added in pkg-mgr + fzf \ # Fuzzy finder — used by shell, yazi, neovim integrations + \ + # ── Compilers & language runtimes ──────────────────────────────────────── + gcc \ # GNU C compiler — needed for native AUR builds + git \ # Version control — required everywhere + glib2 \ # GNOME low-level lib; used by many GTK/D-Bus tools + greetd-tuigreet \ # Text-mode login greeter (configured in core.sh) + grub \ # GRUB2 bootloader for x86_64-efi + \ + # ── Hardware identification & networking ────────────────────────────────── + hdparm \ # Disk performance testing and low-level ATA control + htop \ # Interactive process viewer (classic alternative to btop) + inetutils \ # hostname, telnet, rsh, ftp utilities + iputils \ # ping, tracepath, arping — basic IP network tools + iwd \ # Intel Wireless Daemon — WiFi backend for NetworkManager + jq \ # JSON processor — used by installer scripts and modules + ldns \ # DNS library + drill tool (alternative to dig) + less \ # Pager for viewing long output; also needed by man + libpulse \ # PulseAudio client library — compatibility shim for pipewire + linux \ # The Linux kernel itself + linux-firmware \ # Microcode and firmware blobs for hardware support + lshw \ # Detailed hardware listing (CPU, memory, PCI devices) + lsof \ # List open files — invaluable for debugging processes/sockets + \ + lynx \ # Text-mode web browser — useful in TTY-only environments + \ + # ── Editors & documentation ────────────────────────────────────────────── + man-db \ # Man page database and viewer + mc \ # Midnight Commander — two-pane terminal file manager + nano \ # Simple terminal text editor for quick edits + neovim \ # Primary editor (configured via dotfiles/nvim/) + networkmanager \ # Network connection manager daemon + \ + # ── Fetch & monitoring ──────────────────────────────────────────────────── + onefetch \ # Git repo summary in the terminal (like neofetch for repos) + openbsd-netcat \ # Netcat implementation — port scanning, simple TCP connections + openssh \ # SSH client and server (sshd enabled in optional module) + parted \ # Disk partitioning tool — used by arch-autoinstall.sh + pciutils \ # lspci — list PCI devices (needed for GPU detection) + pipewire \ # Modern audio/video server (replaces PulseAudio + JACK) + \ + # ── Programming languages ──────────────────────────────────────────────── + python \ # Python 3 interpreter + python-pip \ # Python package manager + qrencode \ # Generate QR codes in terminal — useful for sharing WiFi/URLs + ripgrep \ # Fast recursive grep replacement (rg command) + rsync \ # Efficient file sync over SSH or locally + ruby-pkg-config \ # Ruby gem build helper — needed by some AUR packages + rust \ # Rust compiler (also installed via rustup for toolchain mgmt) + rustup \ # Rust toolchain manager — switches stable/nightly/beta + \ + # ── System monitoring & debugging ──────────────────────────────────────── + smartmontools \ # S.M.A.R.T. disk health monitoring (smartctl) + strace \ # System call tracer — debugging tool for process behavior + symlinks \ # Scans and optionally fixes broken symlinks + sysstat \ # System performance stats: iostat, mpstat, pidstat, sar + tldr \ # Simplified man pages with practical examples + tmux \ # Terminal multiplexer — persistent sessions over SSH + tree \ # Display directory structure as a tree + \ + # ── Disk & USB management ──────────────────────────────────────────────── + udisks2 \ # D-Bus service for auto-mounting removable media + udisks2-btrfs \ # btrfs extension for udisks2 + udiskie \ # User-space udisks2 automounter daemon + ufw \ # Uncomplicated Firewall — iptables frontend + usbutils \ # lsusb — list USB devices + \ + # ── Network tools & misc ───────────────────────────────────────────────── + vim \ # Classic Vi IMproved editor (backup editor, always present) + vnstat \ # Network traffic monitor — tracks usage per interface + wget \ # HTTP/FTP file downloader + whois \ # Domain name lookup tool + wireplumber \ # Session/policy manager for PipeWire (required for audio) + wireless_tools \ # Legacy iwconfig, iwlist — sometimes needed for troubleshooting + wpa_supplicant \ # WPA2/WPA3 supplicant for WiFi authentication + wprs \ # Wayland proxy for running X11 apps in Wayland sessions + \ + # ── File manager & archive ──────────────────────────────────────────────── + yazi \ # Terminal file manager with image preview and fuzzy search + zip \ # Create ZIP archives + unzip \ # Extract ZIP archives + zram-generator # Systemd-based zram compressed swap setup (improves RAM efficiency) + +# ── AUR packages ────────────────────────────────────────────────────────────── +# Only install packages that are NOT available in official repos. +# pamtester: PAM module tester — used to verify FIDO2/U2F PAM stack without logging out. +# Available only in AUR because it's a small utility not deemed mainline-worthy. log "Installing AUR packages..." yay -S --aur --noconfirm --needed pamtester diff --git a/setup/modules/core.sh b/setup/modules/core.sh index ffd12cb..abb79e9 100644 --- a/setup/modules/core.sh +++ b/setup/modules/core.sh @@ -1,20 +1,68 @@ #!/bin/bash +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ setup/modules/core.sh — Core system services enablement ║ +# ║ ║ +# ║ PURPOSE: ║ +# ║ Enables the essential systemd services that every installation needs. ║ +# ║ Also deploys the greetd login manager config from the dotfiles repo. ║ +# ║ ║ +# ║ WHEN TO RUN: ║ +# ║ After core-packages.sh — the services must be installed before they can ║ +# ║ be enabled. Called "svc" in the TUI installer's component checklist. ║ +# ║ ║ +# ║ SERVICES ENABLED: ║ +# ║ - NetworkManager: manages wired/wireless network connections ║ +# ║ - cronie: cron daemon for scheduled tasks ║ +# ║ - greetd: minimal display/login manager (uses tuigreet TUI) ║ +# ║ - fail2ban: bans IPs after repeated failed SSH/auth attempts ║ +# ║ - udisks2: auto-mounts USB drives and other removable media ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ + set -euo pipefail +# -e: exit immediately on any error (service enable failures abort the script) +# -u: treat unset variables as errors +# -o pipefail: fail if any pipe stage fails + +# Load shared logging helpers (log, skip, warn, err functions) source "$(dirname "${BASH_SOURCE[0]}")/lib/logging.sh" +# ── NetworkManager ───────────────────────────────────────────────────────────── +# WHY: Arch ships with no network daemon enabled by default. NetworkManager is +# the most user-friendly option — handles DHCP, WiFi, VPN, and has applets. +# NOTE: systemctl enable only marks it to start at boot; it doesn't start it now. log "Enabling NetworkManager..." sudo systemctl enable NetworkManager.service +# ── cronie ──────────────────────────────────────────────────────────────────── +# WHY: Provides the system cron daemon. Some tools (backups, vnstat stats, +# fail2ban cleanup) rely on cron jobs. Arch does not enable it by default. log "Enabling cronie..." sudo systemctl enable cronie.service +# ── greetd / tuigreet ───────────────────────────────────────────────────────── +# WHY: greetd is a minimal, standards-compliant display manager (login screen). +# We use the tuigreet greeter which is a TUI (text-mode, no GPU needed). +# It's lighter than GDM/SDDM and works well on TTY in Wayland setups. +# +# HOW: Copy our pre-configured config.toml from the dotfiles repo into /etc/greetd/ +# then enable the service. The config.toml specifies which greeter to run +# and optionally auto-login settings. +# -f flag forces overwrite of any existing config. log "Deploying greetd config..." sudo cp -f ~/Dotfiles/desktopenvs/hyprland/greetd-tuigreet/config.toml /etc/greetd/config.toml sudo systemctl enable greetd.service +# ── fail2ban ────────────────────────────────────────────────────────────────── +# WHY: Protects against brute-force attacks by monitoring log files and +# temporarily banning IPs that show malicious signs (too many failed logins). +# Important on any machine with SSH open to the network. log "Enabling fail2ban..." sudo systemctl enable fail2ban.service +# ── udisks2 ─────────────────────────────────────────────────────────────────── +# WHY: udisks2 provides automatic mounting of USB drives and other removable +# media via D-Bus. Required by file managers (Thunar, pcmanfm) and desktop +# utilities that want to offer "Open when inserted" functionality. log "Enabling udisks2..." sudo systemctl enable udisks2.service diff --git a/setup/modules/lib/logging.sh b/setup/modules/lib/logging.sh index 23d9cf4..2e14004 100644 --- a/setup/modules/lib/logging.sh +++ b/setup/modules/lib/logging.sh @@ -1,39 +1,104 @@ #!/bin/bash +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ setup/modules/lib/logging.sh — Shared logging helpers ║ +# ║ ║ +# ║ PURPOSE: ║ +# ║ Provides colored console output functions used by every module script. ║ +# ║ Source this file at the top of any module with: ║ +# ║ source "$(dirname "${BASH_SOURCE[0]}")/lib/logging.sh" ║ +# ║ ║ +# ║ WHY A SHARED LIB: ║ +# ║ Centralizes formatting so all modules have consistent output. One change ║ +# ║ here affects every module's output style. ║ +# ║ ║ +# ║ ALSO PROVIDES: ║ +# ║ - ensure_flatpak: bootstrap Flatpak + Flathub for any app module ║ +# ║ - apply_flatpak_theme: apply the cyberqueer GTK theme to a Flatpak app ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ + # Shared logging helpers — source this in every module +# ── ANSI color codes ────────────────────────────────────────────────────────── +# These are escape sequences that terminal emulators interpret as colors. +# Format: \e[<code>m where code 31=red, 32=green, 33=yellow, 0=reset. + GREEN="\e[32m" YELLOW="\e[33m" RED="\e[31m" RESET="\e[0m" +# ── Logging functions ────────────────────────────────────────────────────────── +# Each function takes a message string and formats it with a colored prefix. +# WHY: Consistent, colored output makes it easy to scan install logs at a glance +# and immediately identify success, skips, warnings, or errors. + +# log: normal progress message — green [+] prefix +# Used for: "doing X now", successful completions log() { printf "${GREEN}[+] %s${RESET}\n" "$*"; } + +# skip: indicates something was already done — yellow [~] prefix +# Used for: idempotency checks ("X already installed") skip() { printf "${YELLOW}[~] %s${RESET}\n" "$*"; } + +# warn: non-fatal warning — yellow [!] prefix to stderr +# Used for: recoverable issues that don't abort the module warn() { printf "${YELLOW}[!] %s${RESET}\n" "$*" >&2; } + +# err: fatal error — red [✖] prefix to stderr +# Used for: unrecoverable failures (caller decides whether to exit) err() { printf "${RED}[✖] %s${RESET}\n" "$*" >&2; } +# ── ensure_flatpak ───────────────────────────────────────────────────────────── +# WHY: Many optional app modules install via Flatpak. Rather than duplicating +# the bootstrap code in every app script, this function handles it once. +# WHAT: Installs flatpak if missing, adds Flathub remote if not already present. ensure_flatpak() { + # Check if flatpak binary is available; install it via pacman if not if ! command -v flatpak &>/dev/null; then log "Installing flatpak..." sudo pacman -S --noconfirm --needed flatpak fi + # Check if the flathub remote is already configured to avoid duplicate remotes. + # flatpak remotes lists configured remotes; grep filters for 'flathub'. if ! flatpak remotes 2>/dev/null | grep -q flathub; then log "Adding Flathub remote..." + # --if-not-exists is redundant given the check above, but adds safety flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo fi } +# ── apply_flatpak_theme ──────────────────────────────────────────────────────── +# WHY: Flatpak apps run in sandboxes and can't access the host ~/.themes directory +# by default. We must explicitly grant filesystem access AND set GTK_THEME. +# WHAT: Copies the cyberqueer GTK theme into ~/.themes, grants the Flatpak app +# read-only access to that directory, and overrides the GTK_THEME env var. +# +# Usage: apply_flatpak_theme <app-id> +# e.g. apply_flatpak_theme org.gnome.gedit apply_flatpak_theme() { local app_id="$1" local theme_name="cyberqueer" + + # The theme lives in the dotfiles repo under gtk-themes/cyberqueer/ local theme_src="$HOME/Dotfiles/gtk-themes/$theme_name" local themes_dir="$HOME/.themes" + + # Guard: if the theme source doesn't exist, skip gracefully with a warning if [[ ! -d "$theme_src" ]]; then warn "Cyberqueer theme not found at $theme_src — skipping Flatpak theme override." return 0 fi + + # Copy the theme into the user's ~/.themes/ so it's accessible mkdir -p "$themes_dir" cp -r "$theme_src" "$themes_dir/$theme_name" + + # Grant the Flatpak app read-only access to ~/.themes using a per-user override. + # This avoids needing a global system-wide Flatpak override. flatpak override --user --filesystem="$themes_dir":ro "$app_id" + + # Set GTK_THEME env var for this app so GTK picks up the correct theme name flatpak override --user --env=GTK_THEME="$theme_name" "$app_id" + log "Cyberqueer theme applied to $app_id." } diff --git a/setup/modules/optional-Modules/apps/anti-malware.sh b/setup/modules/optional-Modules/apps/anti-malware.sh index 47adfb1..4ecf57c 100644 --- a/setup/modules/optional-Modules/apps/anti-malware.sh +++ b/setup/modules/optional-Modules/apps/anti-malware.sh @@ -1,15 +1,39 @@ #!/bin/bash +# ============================================================ +# anti-malware.sh — Anti-malware and rootkit detection stack +# ============================================================ +# Installs ClamAV (antivirus), ClamTk (GUI front-end), rkhunter +# (rootkit hunter), and chkrootkit (AUR rootkit scanner) so the +# system has layered malware-detection coverage. These tools are +# optional because most home Arch desktops don't need a resident +# scanner, but they are valuable on machines that handle +# untrusted files or shared network storage. +# ============================================================ + set -euo pipefail +# Load shared logging helpers (log, skip, warn) from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" +# ── Core packages via pacman ────────────────────────────────────────────────── +# clamav: the open-source antivirus engine and CLI scanner (clamscan, clamdscan) +# clamtk: optional GTK GUI for ClamAV, useful for one-off scans without the CLI +# rkhunter: scans for known rootkits, backdoors, and suspicious local changes log "Installing anti-malware tools (clamav, clamtk, rkhunter)..." sudo pacman -S --noconfirm --needed \ clamav clamtk rkhunter +# ── chkrootkit (AUR) ───────────────────────────────────────────────────────── +# chkrootkit: a second rootkit scanner; complements rkhunter with different +# detection heuristics. Available from AUR only (not in the official repos). log "Installing chkrootkit (AUR)..." yay -S --aur --noconfirm --needed chkrootkit -# initialise ClamAV database (first run) +# ── Initial ClamAV virus database ──────────────────────────────────────────── +# freshclam downloads the official ClamAV virus-definition database. +# We only run it when the main database file is absent to avoid a redundant +# download on repeat runs of the installer. +# main.cvd = compressed virus database (fresh download) +# main.cld = incremental update (already exists if previously initialised) if [[ ! -f /var/lib/clamav/main.cvd ]] && [[ ! -f /var/lib/clamav/main.cld ]]; then log "Running initial freshclam (virus database update)..." sudo freshclam @@ -17,14 +41,21 @@ else skip "ClamAV database already present." fi -# cron job: update virus definitions twice a day +# ── Cron job for automatic virus-definition updates ────────────────────────── +# ClamAV definitions become stale quickly; running freshclam twice daily keeps +# the scanner effective. We write a system-wide cron snippet to /etc/cron.d/ +# rather than a user crontab so the update runs even when no user is logged in. +# The quiet flag suppresses normal output; stderr is discarded so cron mail is +# not generated on success. CRON_FILE=/etc/cron.d/freshclam if [[ ! -f "$CRON_FILE" ]]; then log "Installing freshclam cron job (twice daily)..." + # tee writes to the privileged path without running the whole script as root sudo tee "$CRON_FILE" > /dev/null <<'EOF' # Update ClamAV virus definitions twice a day 0 */12 * * * root /usr/bin/freshclam --quiet 2>/dev/null EOF + # 644 = readable by all (cron needs to read it), writable only by root sudo chmod 644 "$CRON_FILE" else skip "freshclam cron job already configured." diff --git a/setup/modules/optional-Modules/apps/ardour.sh b/setup/modules/optional-Modules/apps/ardour.sh index f23aada..e238cab 100755 --- a/setup/modules/optional-Modules/apps/ardour.sh +++ b/setup/modules/optional-Modules/apps/ardour.sh @@ -1,9 +1,35 @@ #!/bin/bash +# ============================================================ +# ardour.sh — Ardour digital audio workstation (DAW) +# ============================================================ +# Installs Ardour, a professional-grade open-source DAW for +# recording, editing, and mixing multi-track audio and MIDI. +# It is distributed as a Flatpak here to get the upstream- +# sanctioned build with all codec/plugin support enabled, and +# to isolate it from system audio libraries that may differ +# across Arch versions. It is an optional module because most +# users do not need a full DAW; lighter options like Audacity +# or LMMS are separate modules. +# ============================================================ + set -euo pipefail +# Load shared logging helpers and the ensure_flatpak / apply_flatpak_theme +# functions from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing Ardour (Flatpak)..." + +# ensure_flatpak: verifies that flatpak and the Flathub remote are set up; +# defined in lib/logging.sh (or a sourced helper). Idempotent. ensure_flatpak + +# Install Ardour from the Flathub repository. +# -y skips the interactive confirmation prompt so the script is non-interactive. flatpak install -y flathub org.ardour.Ardour + +# apply_flatpak_theme: applies the cyberqueer GTK theme override so Ardour's +# GTK widgets match the rest of the desktop, rather than using the Flatpak +# default Adwaita theme. apply_flatpak_theme "org.ardour.Ardour" + log "Ardour installed." diff --git a/setup/modules/optional-Modules/apps/audacity.sh b/setup/modules/optional-Modules/apps/audacity.sh index ff2fc84..772ef4f 100755 --- a/setup/modules/optional-Modules/apps/audacity.sh +++ b/setup/modules/optional-Modules/apps/audacity.sh @@ -1,9 +1,31 @@ #!/bin/bash +# ============================================================ +# audacity.sh — Audacity audio editor +# ============================================================ +# Installs Audacity, a cross-platform, open-source audio editor +# and recorder. It is useful for quick edits, noise removal, +# format conversion, and podcast production — lighter-weight +# than a full DAW like Ardour. Flatpak is preferred over the +# AUR package because it bundles all FFmpeg/codec dependencies +# and avoids conflicts with system libraries. +# ============================================================ + set -euo pipefail +# Load shared logging helpers and the ensure_flatpak / apply_flatpak_theme +# functions from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing Audacity (Flatpak)..." + +# ensure_flatpak: verifies that flatpak and the Flathub remote are present; +# idempotent — safe to call even if Flatpak is already configured. ensure_flatpak + +# Install from Flathub. -y suppresses the interactive confirmation. flatpak install -y flathub org.audacityteam.Audacity + +# apply_flatpak_theme: injects the cyberqueer GTK theme into Audacity's +# Flatpak environment so it renders consistently with the rest of the desktop. apply_flatpak_theme "org.audacityteam.Audacity" + log "Audacity installed." diff --git a/setup/modules/optional-Modules/apps/blender-povray.sh b/setup/modules/optional-Modules/apps/blender-povray.sh index e35e17c..4356b33 100755 --- a/setup/modules/optional-Modules/apps/blender-povray.sh +++ b/setup/modules/optional-Modules/apps/blender-povray.sh @@ -1,12 +1,45 @@ #!/bin/bash +# ============================================================ +# blender-povray.sh — Blender 3D suite + POV-Ray ray-tracer +# ============================================================ +# Installs Blender (3D modelling, animation, compositing) via +# Flatpak and POV-Ray (a classic CPU ray-tracer with its own +# scene-description language) via pacman. +# +# Blender is delivered as a Flatpak to get the upstream official +# build with all drivers and Python scripting enabled. +# POV-Ray comes from the official Arch repos because it is a +# small, self-contained CLI tool with no sandboxing concerns. +# +# These are grouped together because POV-Ray integrates with +# Blender via the Blender2POVray addon, allowing Blender scenes +# to be rendered with POV-Ray's photorealistic ray-tracing. +# ============================================================ + set -euo pipefail +# Load shared logging helpers and Flatpak utility functions source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing Blender (Flatpak)..." + +# ensure_flatpak: checks that the flatpak binary and the Flathub remote exist; +# idempotent — no-op if already set up. ensure_flatpak + +# Install Blender from Flathub. The Flatpak edition ships its own Python, +# CUDA/HIP compute libraries, and codec support pre-bundled, avoiding +# dependency conflicts with the system Python or GPU driver packages. flatpak install -y flathub org.blender.Blender + +# apply_flatpak_theme: sets the cyberqueer GTK theme for Blender's dialogs +# (the main 3D viewport uses its own renderer, but file choosers and system +# dialogs benefit from a consistent theme). apply_flatpak_theme "org.blender.Blender" +# ── POV-Ray via pacman ──────────────────────────────────────────────────────── +# povray: the Persistence Of Vision Raytracer, a mature scene-description- +# language-based ray-tracer. Installed system-wide so the `povray` CLI +# is available to Blender's POV-Ray export add-on and from the terminal. log "Installing POV-Ray (pacman)..." sudo pacman -S --noconfirm --needed povray diff --git a/setup/modules/optional-Modules/apps/butter.sh b/setup/modules/optional-Modules/apps/butter.sh index 32b0fbc..88f1edb 100755 --- a/setup/modules/optional-Modules/apps/butter.sh +++ b/setup/modules/optional-Modules/apps/butter.sh @@ -1,10 +1,37 @@ #!/bin/bash +# ============================================================ +# butter.sh — Butter Btrfs snapshot GUI +# ============================================================ +# Installs Butter, a graphical front-end for managing Btrfs +# snapshots (create, browse, restore, schedule). It provides +# a user-friendly alternative to running btrfs subvolume +# commands by hand. +# +# btrfs-progs is the essential userspace toolkit needed by +# every tool that manages Btrfs filesystems; it is installed +# from the official repos first because it is a hard dependency. +# Butter itself is AUR-only. +# +# This is an optional module because it is only useful on +# systems whose root (or data) filesystem is Btrfs. +# ============================================================ + set -euo pipefail +# Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" +# ── btrfs-progs ─────────────────────────────────────────────────────────────── +# btrfs-progs: the official Btrfs userspace tools (mkfs.btrfs, btrfs subvolume, +# btrfs send/receive, etc.). Required at runtime by Butter and useful on its +# own for CLI snapshot management. log "Installing btrfs-progs..." sudo pacman -S --noconfirm --needed btrfs-progs +# ── Butter (AUR) ───────────────────────────────────────────────────────────── +# --answerdiff None : skip the "show diff?" prompt during AUR builds +# --answerclean All : answer "clean" to the "clean build dir?" prompt +# --noconfirm : suppress all remaining yay/makepkg confirmation prompts log "Installing butter (AUR)..." yay -S --answerdiff None --answerclean All --noconfirm butter + log "butter installed." diff --git a/setup/modules/optional-Modules/apps/caldav-sync.sh b/setup/modules/optional-Modules/apps/caldav-sync.sh index 0f86a7d..0ba3aae 100755 --- a/setup/modules/optional-Modules/apps/caldav-sync.sh +++ b/setup/modules/optional-Modules/apps/caldav-sync.sh @@ -1,23 +1,66 @@ #!/bin/bash +# ============================================================ +# caldav-sync.sh — CalDAV calendar sync stack +# ============================================================ +# Sets up a full offline-capable CalDAV synchronisation stack: +# +# vdirsyncer — syncs CalDAV server ↔ local ICS files +# khal — CLI/TUI calendar viewer and editor +# python-icalendar — Python library for parsing .ics files +# ics-to-calendarim — custom script that converts ICS files +# into the JSON cache format expected by +# the calendar.vim Neovim plugin +# +# A systemd user timer fires every 15 minutes to keep local +# calendars current without any manual intervention. +# +# This is an optional module because CalDAV sync is only useful +# if the user has a remote calendar server (Nextcloud, Radicale, +# Google Calendar export, etc.). +# ============================================================ + set -euo pipefail +# Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" +# ── Core packages ───────────────────────────────────────────────────────────── +# vdirsyncer : the CalDAV/CardDAV sync daemon that mirrors remote +# calendars to local Maildir-style .ics files +# khal : a CLI/TUI calendar reader/editor that reads the local +# ICS files and presents them in a human-friendly view +# python-icalendar : Python library needed by the ics-to-calendarim helper +# to parse VEVENT components from .ics files log "Installing CalDAV sync stack..." sudo pacman -S --noconfirm --needed vdirsyncer khal python-icalendar # ── Credentials ─────────────────────────────────────────────────────────────── +# Collect the minimum information needed to configure vdirsyncer. +# -r = raw input (no backslash escaping), -p = prompt, -s = silent (password) read -rp "CalDAV server URL (e.g. https://dav.example.com/cal/): " CALDAV_URL read -rp "Username : " CALDAV_USER read -rsp "Password : " CALDAV_PASS; echo read -rp "Calendar display name [Personal] : " CAL_NAME +# Default to "Personal" if the user pressed Enter without typing a name CAL_NAME="${CAL_NAME:-Personal}" +# Local directory where vdirsyncer will store the .ics files, one sub- +# directory per calendar collection discovered on the server. CALDAV_DIR="$HOME/.local/share/calendars" mkdir -p "$CALDAV_DIR" -# ── vdirsyncer ──────────────────────────────────────────────────────────────── +# ── vdirsyncer configuration ────────────────────────────────────────────────── +# vdirsyncer reads a single INI-style config file. +# We create the status directory first (vdirsyncer stores sync state there +# to track what has been pushed/pulled for each item). log "Writing ~/.config/vdirsyncer/config..." mkdir -p ~/.config/vdirsyncer ~/.local/share/vdirsyncer/status + +# The config defines: +# [general] — global options (where to store sync state) +# [pair calendars] — a named pair that links local ↔ remote storage +# [storage *] — individual storage backends (filesystem + caldav) +# "collections = ["from b"]" means: discover all collections that exist on +# the remote (b) side and create matching local directories automatically. cat > ~/.config/vdirsyncer/config << EOF [general] status_path = "$HOME/.local/share/vdirsyncer/status/" @@ -39,15 +82,24 @@ url = "$CALDAV_URL" username = "$CALDAV_USER" password = "$CALDAV_PASS" EOF +# Restrict permissions: config contains plaintext password chmod 600 ~/.config/vdirsyncer/config +# "discover" queries the server for all available collections and records them +# in the status directory so future `vdirsyncer sync` commands know what to sync. +# `yes |` pre-answers any interactive "create local directory?" prompts. +# `|| true` prevents the script from aborting if discovery finds nothing new. log "Discovering CalDAV collections (confirm any prompts with y)..." yes | vdirsyncer discover calendars || true +# Pull all events from the server for the first time. log "Running initial sync..." vdirsyncer sync -# ── khal (CLI calendar companion) ──────────────────────────────────────────── +# ── khal configuration ──────────────────────────────────────────────────────── +# khal needs one [[calendar]] section per local ICS directory. +# We use an inline Python script so we can iterate over discovered directories +# dynamically instead of hard-coding calendar names. log "Writing ~/.config/khal/config (per-calendar entries)..." mkdir -p ~/.config/khal python3 - "$CALDAV_DIR" << 'PYEOF' @@ -55,13 +107,18 @@ import sys from pathlib import Path cal_root = Path(sys.argv[1]) +# Cycle through a set of terminal colours so different calendars are visually +# distinct in khal's output. colors = ["light blue", "light green", "light red", "light magenta", "light cyan"] dirs = sorted(d for d in cal_root.iterdir() if d.is_dir()) lines = ["[calendars]"] for i, d in enumerate(dirs): + # Each sub-directory created by vdirsyncer is one calendar collection. lines += [f" [[{d.name}]]", f" path = {d}", f" color = {colors[i % len(colors)]}"] +# [sqlite] — khal caches parsed events here for faster startup +# [locale] — display preferences for times and dates lines += [ "", "[sqlite]", "path = ~/.local/share/khal/khal.db", "", "[locale]", "timeformat = %H:%M", "dateformat = %Y-%m-%d", @@ -72,6 +129,10 @@ lines += [ PYEOF # ── ICS → calendar.vim cache converter ─────────────────────────────────────── +# calendar.vim (a Neovim plugin) expects event data in a specific JSON cache +# format at ~/.cache/calendar.vim/local/. This helper script reads the local +# ICS files maintained by vdirsyncer and writes that JSON cache so calendar.vim +# can show events without hitting the network. log "Installing ics-to-calendarim converter..." mkdir -p ~/.local/bin cat > ~/.local/bin/ics-to-calendarim << 'PYEOF' @@ -95,10 +156,19 @@ CALDAV_DIR = Path.home() / ".local/share/calendars" CACHE_DIR = Path.home() / ".cache/calendar.vim/local" KHAL_CFG = Path.home() / ".config/khal/config" +# Hex colours for calendar.vim's event display (one per calendar, cycled) COLORS = ["#4CAF50", "#2196F3", "#FF5722", "#9C27B0", "#FF9800"] +# Matching terminal colour names for khal (kept in sync so both tools agree) KHAL_COLORS = ["light blue", "light green", "light red", "light magenta", "light cyan"] def to_naive(dt): + """Normalise a DTSTART/DTEND value to a naive string + metadata. + + Returns (string_repr, is_timed, year, month). + Timezone-aware datetimes are converted to UTC before stripping tzinfo + so that comparisons are consistent across DST transitions. + All-day dates return an ISO date string (YYYY-MM-DD) with is_timed=False. + """ if isinstance(dt, datetime): if dt.tzinfo is not None: dt = dt.astimezone(timezone.utc).replace(tzinfo=None) @@ -109,10 +179,12 @@ def to_naive(dt): def main(): CACHE_DIR.mkdir(parents=True, exist_ok=True) - cal_items = [] + cal_items = [] # list of calendar metadata objects for calendarList + # Iterate over each calendar collection directory (one per CalDAV calendar) for idx, cal_dir in enumerate(sorted(d for d in CALDAV_DIR.iterdir() if d.is_dir())): cal_id = cal_dir.name + # Human-readable name: replace underscores with spaces and title-case cal_summary = cal_dir.name.replace("_", " ").title() cal_items.append({ "id": cal_id, @@ -121,15 +193,17 @@ def main(): "foregroundColor": "#ffffff", }) - by_month: dict = {} + by_month: dict = {} # keyed by (year, month) → list of event objects for ics in cal_dir.glob("*.ics"): try: cal = iCal.from_ical(ics.read_bytes()) except Exception as e: print(f"warning: skipping {ics.name}: {e}", file=sys.stderr) continue + # Walk all VEVENT components in the ICS file for comp in cal.walk("VEVENT"): dtstart = comp.get("DTSTART") + # If DTEND is missing, fall back to DTSTART (zero-duration event) dtend = comp.get("DTEND") or comp.get("DTSTART") if not dtstart: continue @@ -140,21 +214,28 @@ def main(): uid = str(comp.get("UID", ics.stem)) summary = str(comp.get("SUMMARY", "")) key = (yr, mo) + # Group events by month so we write one JSON file per month by_month.setdefault(key, []).append({ "id": uid, "summary": summary, + # calendar.vim distinguishes timed events ("dateTime") from + # all-day events ("date") by which key is present "start": {"dateTime": s_str} if is_timed else {"date": s_str}, "end": {"dateTime": e_str} if is_timed else {"date": e_str}, }) + # Write one JSON file per (calendar, year, month) combination. + # Path structure mirrors what calendar.vim expects for its local cache. for (yr, mo), events in by_month.items(): out = CACHE_DIR / cal_id / f"{yr:04d}" / f"{mo:02d}" out.mkdir(parents=True, exist_ok=True) (out / "0").write_text(json.dumps({"items": events})) + # Write the top-level calendar list so calendar.vim knows which calendars exist (CACHE_DIR / "calendarList").write_text(json.dumps({"items": cal_items})) - # regenerate khal config so new calendars are picked up automatically + # Also regenerate the khal config so any newly discovered calendars are + # automatically added without requiring a manual config edit. cal_dirs = sorted(d for d in CALDAV_DIR.iterdir() if d.is_dir()) khal_lines = ["[calendars]"] for i, d in enumerate(cal_dirs): @@ -170,15 +251,25 @@ def main(): if __name__ == "__main__": main() PYEOF +# Make the helper executable so it can be called directly from the PATH chmod +x ~/.local/bin/ics-to-calendarim +# Build the initial calendar.vim cache from the events we just synced log "Running initial calendar.vim cache build..." ~/.local/bin/ics-to-calendarim # ── systemd user timer ──────────────────────────────────────────────────────── +# A systemd user timer keeps calendars current automatically. +# Using systemd (instead of cron) lets the unit run without requiring root and +# integrates cleanly with the user's login session. log "Installing vdirsyncer systemd user timer (every 15 min)..." mkdir -p ~/.config/systemd/user +# The service unit runs vdirsyncer sync then rebuilds the calendar.vim cache. +# Type=oneshot: the service is considered active only while the process runs; +# it exits when done rather than staying in the background. +# After=network-online.target: ensures we don't try to sync before the network +# is available (important at boot). cat > ~/.config/systemd/user/vdirsyncer.service << EOF [Unit] Description=Sync CalDAV and rebuild calendar.vim cache @@ -190,6 +281,9 @@ ExecStart=/usr/bin/vdirsyncer sync ExecStartPost=$HOME/.local/bin/ics-to-calendarim EOF +# The timer unit triggers the service on a schedule. +# OnBootSec=2min : first run 2 minutes after login (let the network settle) +# OnUnitActiveSec=15min : repeat every 15 minutes after the last activation cat > ~/.config/systemd/user/vdirsyncer.timer << EOF [Unit] Description=Run vdirsyncer every 15 minutes @@ -203,7 +297,9 @@ Unit=vdirsyncer.service WantedBy=timers.target EOF +# Reload systemd user daemon so it picks up the new unit files systemctl --user daemon-reload +# Enable and immediately start the timer; "--now" starts it without a reboot systemctl --user enable --now vdirsyncer.timer log "CalDAV sync configured. Events will appear in calendar.vim automatically." diff --git a/setup/modules/optional-Modules/apps/cecilia.sh b/setup/modules/optional-Modules/apps/cecilia.sh index 20996b2..33058aa 100644 --- a/setup/modules/optional-Modules/apps/cecilia.sh +++ b/setup/modules/optional-Modules/apps/cecilia.sh @@ -1,7 +1,29 @@ #!/bin/bash +# ============================================================ +# cecilia.sh — Cecilia audio signal-processing environment +# ============================================================ +# Installs Cecilia, a Python/GUI front-end for CSound that +# provides a set of built-in audio modules for sound design, +# granular synthesis, spectral processing, and more. It is +# aimed at composers and audio researchers who want to +# experiment with synthesis and processing without writing +# raw CSound code. +# +# Cecilia is only available through the AUR on Arch Linux. +# This is an optional module because it is a niche tool +# relevant mainly to users involved in electroacoustic music +# or audio research. +# ============================================================ + set -euo pipefail +# Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" +# ── Cecilia (AUR) ───────────────────────────────────────────────────────────── +# --answerdiff None : skip the "show diff?" prompt during AUR builds +# --answerclean All : answer "clean" to the "clean build dir?" prompt +# --noconfirm : suppress all remaining yay/makepkg confirmation prompts log "Installing Cecilia (AUR)..." yay -S --answerdiff None --answerclean All --noconfirm cecilia + log "Cecilia installed." diff --git a/setup/modules/optional-Modules/apps/chromium.sh b/setup/modules/optional-Modules/apps/chromium.sh index 6898f1b..7d2afbe 100755 --- a/setup/modules/optional-Modules/apps/chromium.sh +++ b/setup/modules/optional-Modules/apps/chromium.sh @@ -1,9 +1,33 @@ #!/bin/bash +# ============================================================ +# chromium.sh — Chromium browser (Flatpak) +# ============================================================ +# Installs the open-source Chromium browser via Flatpak. +# Chromium is the upstream base from which Google Chrome is +# derived. It is useful as a reference browser for web +# development/testing and as a non-Google-branded alternative +# to Chrome. +# +# Flatpak is preferred over the official Arch chromium package +# so that the browser runs in a sandboxed environment with its +# own set of libraries, reducing the attack surface and +# avoiding dependency conflicts with the system. +# ============================================================ + set -euo pipefail +# Load shared logging helpers and Flatpak utility functions source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing Chromium (Flatpak)..." + +# ensure_flatpak: verifies Flatpak and Flathub are set up; idempotent. ensure_flatpak + +# Install Chromium from Flathub. -y skips the confirmation prompt. flatpak install -y flathub org.chromium.Chromium + +# apply_flatpak_theme: applies the cyberqueer GTK theme so Chromium's file +# dialogs and context menus match the rest of the desktop. apply_flatpak_theme "org.chromium.Chromium" + log "Chromium installed." diff --git a/setup/modules/optional-Modules/apps/claude.sh b/setup/modules/optional-Modules/apps/claude.sh index a7727f2..008b21c 100755 --- a/setup/modules/optional-Modules/apps/claude.sh +++ b/setup/modules/optional-Modules/apps/claude.sh @@ -1,13 +1,42 @@ #!/bin/bash +# ============================================================ +# claude.sh — Claude Code CLI (Anthropic) +# ============================================================ +# Installs Claude Code, Anthropic's official AI-powered CLI +# coding assistant, as a global npm package. +# +# Claude Code requires Node.js / npm. The script first checks +# whether npm is already on PATH; if not, it attempts to +# activate nvm (Node Version Manager) from the default location +# ~/.nvm so the correct Node.js version is available before +# running the install. +# +# This is an optional module because Claude Code is only useful +# to developers who want AI-assisted coding inside the terminal, +# and it requires an Anthropic API key to function. +# ============================================================ + set -euo pipefail +# Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing Claude Code via npm..." + +# Check whether npm is already available on PATH. +# If not, try to source nvm so it places the correct npm into PATH. +# This handles the common case where Node.js was installed through nvm +# rather than pacman, meaning it isn't in the system PATH by default. if ! command -v npm &>/dev/null; then log "Sourcing nvm to get npm..." export NVM_DIR="$HOME/.nvm" + # nvm.sh sets up the nvm function and adds the active Node.js to PATH. + # The -s test checks that the file exists and is non-empty before sourcing. [[ -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh" fi +# Install Claude Code globally so the `claude` command is available system-wide. +# -g installs to the global npm prefix (typically ~/.nvm/versions/node/<ver>/lib +# when using nvm, or /usr/local/lib when using system npm). npm install -g @anthropic-ai/claude-code + log "Claude Code installed." diff --git a/setup/modules/optional-Modules/apps/cockpit.sh b/setup/modules/optional-Modules/apps/cockpit.sh index 661bba0..14cdafd 100644 --- a/setup/modules/optional-Modules/apps/cockpit.sh +++ b/setup/modules/optional-Modules/apps/cockpit.sh @@ -1,19 +1,60 @@ #!/bin/bash +# ============================================================ +# cockpit.sh — Cockpit web-based server management UI +# ============================================================ +# Installs Cockpit, a browser-based administration interface +# that provides real-time system monitoring, journal viewing, +# storage management, and more — all accessible at +# https://localhost:9090. +# +# Packages installed: +# cockpit — core web UI and API layer +# cockpit-pcp — Performance Co-Pilot integration +# (enables historical metrics charts) +# pcp — Performance Co-Pilot daemon (required by cockpit-pcp) +# cockpit-machines — virtual machine management via libvirt +# cockpit-podman — container management via Podman API +# cockpit-navigator — file browser add-on for Cockpit +# +# The core and pcp packages come from the official Arch repos; +# the plugin packages (machines, podman, navigator) are +# AUR-only. +# +# This is an optional module because Cockpit is primarily +# useful on servers or development machines; it is not needed +# on a standard desktop. +# ============================================================ + set -euo pipefail +# Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" +# ── Core Cockpit packages (official repos) ──────────────────────────────────── +# cockpit : the main web server component and administration framework +# cockpit-pcp : bridge between Cockpit and PCP; enables the metrics history page +# pcp : Performance Co-Pilot — collects and archives system performance +# data (CPU, memory, disk I/O, etc.) that cockpit-pcp then displays log "Installing Cockpit (core + official plugins)..." sudo pacman -S --noconfirm --needed \ cockpit \ cockpit-pcp \ pcp +# ── Cockpit plugin extensions (AUR) ────────────────────────────────────────── +# cockpit-machines : manage QEMU/KVM virtual machines directly from the web UI +# cockpit-podman : manage rootless and rootful Podman containers +# cockpit-navigator : graphical file manager widget inside Cockpit log "Installing Cockpit AUR plugins (machines, podman, navigator)..." yay -S --answerdiff None --answerclean All --noconfirm \ cockpit-machines \ cockpit-podman \ cockpit-navigator +# ── Enable Cockpit socket activation ───────────────────────────────────────── +# Cockpit uses systemd socket activation: the cockpit.socket unit listens on +# port 9090 and starts cockpit.service only when an incoming connection arrives. +# This is more efficient than keeping the full web server running at all times. log "Enabling Cockpit socket..." sudo systemctl enable cockpit.socket + log "Cockpit enabled. Web UI available at https://localhost:9090" diff --git a/setup/modules/optional-Modules/apps/codeblocks.sh b/setup/modules/optional-Modules/apps/codeblocks.sh index 3daadc3..8de211e 100644 --- a/setup/modules/optional-Modules/apps/codeblocks.sh +++ b/setup/modules/optional-Modules/apps/codeblocks.sh @@ -1,7 +1,28 @@ #!/bin/bash +# ============================================================ +# codeblocks.sh — Code::Blocks IDE +# ============================================================ +# Installs Code::Blocks, a free, open-source, cross-platform +# C/C++ and Fortran IDE. It features a plugin architecture +# that supports multiple compilers (GCC, Clang, MSVC) and +# debuggers. +# +# Code::Blocks is available in the official Arch repos and is +# installed via pacman. +# +# This is an optional module because it is a GUI IDE that is +# mainly relevant to developers who prefer a graphical +# environment over terminal-based editors for C/C++ work. +# ============================================================ + set -euo pipefail +# Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" +# ── codeblocks (official repos) ─────────────────────────────────────────────── +# The codeblocks package includes the IDE, its built-in GCC compiler plugin, +# and the wxWidgets GUI toolkit it depends on. log "Installing Code::Blocks..." sudo pacman -S --noconfirm --needed codeblocks + log "Code::Blocks installed." diff --git a/setup/modules/optional-Modules/apps/croc.sh b/setup/modules/optional-Modules/apps/croc.sh index 22fad87..73d60b6 100755 --- a/setup/modules/optional-Modules/apps/croc.sh +++ b/setup/modules/optional-Modules/apps/croc.sh @@ -1,7 +1,28 @@ #!/bin/bash +# ============================================================ +# croc.sh — croc peer-to-peer file transfer tool +# ============================================================ +# Installs croc, a simple command-line tool for sending files +# and folders between two computers using end-to-end encryption +# and a relay server. Unlike scp, croc works through NAT and +# firewalls without needing port forwarding, making it ideal +# for ad-hoc transfers on the local network or the internet. +# +# Usage: croc send <file> (sender) +# croc <code-phrase> (receiver) +# +# croc is available in the official Arch repos. +# This is an optional module because it is only needed when +# transferring files to/from machines that don't share a +# common filesystem or SSH access. +# ============================================================ + set -euo pipefail +# Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" +# ── croc (official repos) ───────────────────────────────────────────────────── log "Installing croc (file transfer)..." sudo pacman -S --noconfirm --needed croc + log "croc installed." diff --git a/setup/modules/optional-Modules/apps/db-clients.sh b/setup/modules/optional-Modules/apps/db-clients.sh index 2649211..9f24ccf 100755 --- a/setup/modules/optional-Modules/apps/db-clients.sh +++ b/setup/modules/optional-Modules/apps/db-clients.sh @@ -1,10 +1,39 @@ #!/bin/bash +# ============================================================ +# db-clients.sh — CLI database clients (pgcli, mycli) +# ============================================================ +# Installs interactive, feature-rich CLI clients for the two +# most common open-source relational databases: +# +# pgcli — PostgreSQL client with syntax highlighting, auto- +# completion, and multi-line editing (official repos) +# mycli — MySQL / MariaDB client with the same UX improvements +# (AUR only, as the upstream package is not in the +# official Arch repos) +# +# Both tools are built on Python's prompt-toolkit library and +# offer a significantly better interactive experience than the +# stock `psql` and `mysql` CLIs. +# +# This is an optional module because database clients are only +# needed on machines used for database development or +# administration. +# ============================================================ + set -euo pipefail +# Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" +# ── pgcli (official repos) ──────────────────────────────────────────────────── +# pgcli: PostgreSQL client with auto-complete, syntax highlighting, and +# multi-line query editing. It also shows the query explain plan inline. log "Installing pgcli..." sudo pacman -S --noconfirm --needed pgcli +# ── mycli (AUR) ────────────────────────────────────────────────────────────── +# mycli: MySQL / MariaDB CLI with identical UX improvements to pgcli. +# Available from AUR only because it is not packaged in the official repos. log "Installing mycli (AUR)..." yay -S --answerdiff None --answerclean All --noconfirm mycli + log "DB clients installed." diff --git a/setup/modules/optional-Modules/apps/disk-recovery.sh b/setup/modules/optional-Modules/apps/disk-recovery.sh index 28f52db..c1f0b1f 100755 --- a/setup/modules/optional-Modules/apps/disk-recovery.sh +++ b/setup/modules/optional-Modules/apps/disk-recovery.sh @@ -1,10 +1,42 @@ #!/bin/bash +# ============================================================ +# disk-recovery.sh — Disk recovery and flash-testing tools +# ============================================================ +# Installs two complementary data-recovery/storage-testing tools: +# +# ddrescue — GNU ddrescue: robust block-level disk imaging tool +# that retries bad sectors and creates a recovery log +# so interrupted rescues can be resumed. Available in +# the official Arch repos. +# +# f3 — Fight Flash Fraud: tests USB drives and SD cards for +# counterfeit capacity by writing and verifying data. +# Available from AUR only. +# +# These are optional because they are specialist tools most users +# only reach for when something has gone wrong (failing drive, +# suspected fake flash storage). +# ============================================================ + set -euo pipefail +# Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" +# ── ddrescue (official repos) ───────────────────────────────────────────────── +# ddrescue copies data from a failing or damaged disk to an image file, +# intelligently skipping bad sectors and retrying them later. The recovery +# log (mapfile) enables resuming after an interruption without re-reading +# already-good sectors. Preferred over dd for data recovery because it +# maximises the amount of data recovered from a partially readable drive. log "Installing ddrescue..." sudo pacman -S --noconfirm --needed ddrescue +# ── f3 (AUR) ───────────────────────────────────────────────────────────────── +# f3 (Fight Flash Fraud) reveals counterfeit storage devices that report a +# larger capacity than they actually have. It works by filling the device +# with unique data then reading it back to find where corruption starts. +# Useful before trusting a newly purchased USB drive or SD card. log "Installing f3 (AUR)..." yay -S --answerdiff None --answerclean All --noconfirm f3 + log "Disk recovery tools installed." diff --git a/setup/modules/optional-Modules/apps/docker.sh b/setup/modules/optional-Modules/apps/docker.sh index 373ec18..06ae341 100644 --- a/setup/modules/optional-Modules/apps/docker.sh +++ b/setup/modules/optional-Modules/apps/docker.sh @@ -1,13 +1,46 @@ #!/bin/bash +# ============================================================ +# docker.sh — Docker container runtime +# ============================================================ +# Installs Docker Engine and Docker Compose, enables the Docker +# daemon at boot, and adds the current user to the `docker` +# group so containers can be managed without sudo. +# +# docker — the Docker daemon + CLI +# docker-compose — the Compose v2 plugin for multi-container apps +# +# Why not Podman instead? Podman is the preferred rootless +# alternative (see podman.sh), but Docker is still the most +# widely documented container runtime and some workflows +# (Portainer, legacy scripts, certain CI tools) assume it. +# +# Note: Docker's daemon runs as root, which has security +# implications. The docker group grants equivalent root access; +# this is intentional for developer workstations. +# ============================================================ + set -euo pipefail +# Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" +# ── Packages ────────────────────────────────────────────────────────────────── +# docker : the Docker Engine daemon (dockerd) + CLI client (docker) +# docker-compose : Docker Compose plugin; provides `docker compose` subcommand log "Installing Docker and Docker Compose..." sudo pacman -S --noconfirm --needed docker docker-compose +# ── Enable Docker daemon ────────────────────────────────────────────────────── +# docker.service starts the Docker daemon at boot. Without this, running +# `docker` commands would fail with "Cannot connect to the Docker daemon". log "Enabling Docker service..." sudo systemctl enable docker.service +# ── Add current user to docker group ───────────────────────────────────────── +# By default, only root can communicate with the Docker socket at +# /var/run/docker.sock. Adding the user to the `docker` group grants +# access without requiring `sudo docker ...` every time. +# The group membership takes effect at the next login (a new login session +# is required to re-evaluate group membership). log "Adding $USER to docker group..." sudo usermod -aG docker "$USER" diff --git a/setup/modules/optional-Modules/apps/doom.sh b/setup/modules/optional-Modules/apps/doom.sh index 0148429..68de007 100644 --- a/setup/modules/optional-Modules/apps/doom.sh +++ b/setup/modules/optional-Modules/apps/doom.sh @@ -1,8 +1,34 @@ #!/bin/bash +# ============================================================ +# doom.sh — Chocolate Doom + Freedoom game data +# ============================================================ +# Installs a faithful Doom port and free game data so the +# classic first-person shooter can be played out of the box +# without commercial game files. +# +# chocolate-doom : a vanilla-accurate source port that +# reproduces the original Doom engine +# behaviour as closely as possible, including +# the original renderer, bugs, and limits. +# +# freedoom : free/libre replacement game data (IWAD) +# for both Doom 1 (freedoom1.wad) and Doom 2 +# (freedoom2.wad). Required to run Chocolate +# Doom without owning the commercial IWADs. +# +# Users who own the original Doom IWADs (doom.wad, doom2.wad) +# can use those instead of or alongside freedoom. +# +# This is an optional gaming module; it is not installed by +# default because not all users want games on their system. +# ============================================================ + set -euo pipefail +# Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" # chocolate-doom: faithful vanilla Doom port; freedoom: free game data (playable without IWADs) log "Installing Chocolate Doom and Freedoom data..." sudo pacman -S --noconfirm --needed chocolate-doom freedoom + log "Doom installed." diff --git a/setup/modules/optional-Modules/apps/ffmpeg.sh b/setup/modules/optional-Modules/apps/ffmpeg.sh index a5494f6..512bdf9 100755 --- a/setup/modules/optional-Modules/apps/ffmpeg.sh +++ b/setup/modules/optional-Modules/apps/ffmpeg.sh @@ -1,9 +1,42 @@ #!/bin/bash +# ============================================================ +# ffmpeg.sh — FFmpeg multimedia framework + GStreamer codecs +# ============================================================ +# Installs the FFmpeg CLI toolkit, a thumbnailer plugin for +# file managers, and a comprehensive set of GStreamer codec +# plugins. Together these ensure that the desktop can play, +# convert, and generate thumbnails for virtually any audio or +# video format. +# +# Packages: +# ffmpeg — the core FFmpeg encoder/decoder library +# and CLI tools (ffmpeg, ffprobe, ffplay) +# ffmpegthumbnailer — a thumbnail generator that integrates +# with file managers (Thunar, Nautilus, +# Dolphin) to show video frame previews +# gst-libav — GStreamer plugin wrapping FFmpeg/libav +# codecs (H.264, AAC, MP3, etc.) +# gst-plugins-good — stable GStreamer plugins (VP8/9, FLAC, +# Ogg, ALSA, V4L2, etc.) +# gst-plugins-bad — plugins not yet meeting quality bar +# but widely used (H.265/HEVC, Opus, AV1, +# MPEG-TS, etc.) +# gst-plugins-ugly — legally ambiguous plugins (MP3, MPEG-2 +# video, etc.) that may require patent +# licences in some jurisdictions +# +# This is an optional module because the base system includes +# only minimal codec support; users who want full multimedia +# playback should install this module. +# ============================================================ + set -euo pipefail +# Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing FFmpeg extras (thumbnailer + GStreamer codecs)..." sudo pacman -S --noconfirm --needed \ ffmpeg ffmpegthumbnailer \ gst-libav gst-plugins-good gst-plugins-bad gst-plugins-ugly + log "FFmpeg extras installed." diff --git a/setup/modules/optional-Modules/apps/firefox.sh b/setup/modules/optional-Modules/apps/firefox.sh index cb85b92..cdec509 100755 --- a/setup/modules/optional-Modules/apps/firefox.sh +++ b/setup/modules/optional-Modules/apps/firefox.sh @@ -1,9 +1,35 @@ #!/bin/bash +# ============================================================ +# firefox.sh — Mozilla Firefox browser (Flatpak) +# ============================================================ +# Installs Firefox via Flatpak rather than from the official +# Arch repos or AUR. The Flatpak version is sandboxed, ships +# with its own bundled libraries (avoiding libpulse/libx11 +# version conflicts), and auto-updates independently of the +# system package manager. +# +# Firefox is offered as an optional module because some users +# prefer a different browser (Chromium, LibreWolf, Zen) as +# their primary, or they use only a Wayland-native browser; +# installing multiple browsers is intentional for testing or +# compartmentalisation. +# ============================================================ + set -euo pipefail +# Load shared logging helpers and Flatpak utility functions source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing Firefox (Flatpak)..." + +# ensure_flatpak: checks that Flatpak and the Flathub remote are configured; +# idempotent — safe to call multiple times. ensure_flatpak + +# Install from Flathub. -y skips interactive confirmation. flatpak install -y flathub org.mozilla.firefox + +# apply_flatpak_theme: injects the cyberqueer GTK theme so Firefox's native +# file-open dialogs and context menus match the rest of the desktop. apply_flatpak_theme "org.mozilla.firefox" + log "Firefox installed." diff --git a/setup/modules/optional-Modules/apps/freeipa-client.sh b/setup/modules/optional-Modules/apps/freeipa-client.sh index 4d84f4f..b47e744 100755 --- a/setup/modules/optional-Modules/apps/freeipa-client.sh +++ b/setup/modules/optional-Modules/apps/freeipa-client.sh @@ -1,23 +1,69 @@ #!/bin/bash +# ============================================================ +# freeipa-client.sh — FreeIPA client enrolment installer +# ============================================================ +# Installs the packages needed to join this machine to a +# FreeIPA domain and then guides the user through three +# enrolment paths via a dialog(1) TUI: +# +# answerfile — reads a pre-filled JSON file produced by the +# freeipa-server.sh installer (recommended for +# scripted/repeatable deployments) +# manual — interactive dialog prompts for every option +# skip — installs packages only; enrol later manually +# +# Packages installed: +# sssd — System Security Services Daemon; provides +# PAM/NSS integration for IPA-managed users +# cyrus-sasl-gssapi — SASL GSSAPI mechanism for Kerberos auth +# openldap — LDAP utilities (ldapsearch, etc.) +# krb5 — Kerberos 5 libraries and kinit/klist tools +# oddjob — D-Bus service that auto-creates home dirs +# on first login (via pam_oddjob_mkhomedir) +# freeipa-client — AUR package providing ipa-client-install +# +# This is an optional module because FreeIPA client enrolment is +# only relevant on managed/enterprise machines that are members +# of a FreeIPA domain. +# ============================================================ + set -euo pipefail +# Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Starting FreeIPA client installer..." +# Resolve the absolute path of this script's directory so all relative +# references (to FreeipaAnsible scripts and answerfiles) remain correct +# regardless of where the installer TUI calls this module from. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FREEIPA_DIR="$SCRIPT_DIR/../../FreeipaAnsible" +# Optional server-generated enrolment wrapper script CLIENT_SCRIPT="$FREEIPA_DIR/freeipa-client.sh" +# Default answerfile path — the server installer writes this file with +# the domain/realm/server already filled in, so clients only need to +# add the admin password. DEFAULT_AF="$FREEIPA_DIR/freeipa-client-answerfile.json" -# Defaults — match freeipa-client-answerfile.json (sed-replaced by server installer) +# Default values matching the home lab domain; the server installer +# sed-replaces these in the distributed client scripts so clients +# always get the correct values without any manual editing. DEF_DOMAIN="freeipa.abdelbaki.eu" DEF_REALM="FREEIPA.ABDELBAKI.EU" DEF_SERVER="freeipa.abdelbaki.eu" # ── Packages ────────────────────────────────────────────────────────────────── echo "[+] Installing FreeIPA client packages..." +# pacman (without sudo) because this script may be run as root by the TUI +# sssd : provides NSS/PAM lookups against IPA LDAP + Kerberos +# cyrus-sasl-gssapi : GSSAPI mechanism; needed for LDAP binds with Kerberos tickets +# openldap : ldapsearch and other LDAP client utilities +# krb5 : Kerberos 5 runtime; kinit, klist, kdestroy +# oddjob : D-Bus service that runs pam_oddjob_mkhomedir on first login pacman -S --noconfirm --needed sssd cyrus-sasl-gssapi openldap krb5 oddjob +# freeipa-client provides ipa-client-install (the official FreeIPA enrolment +# tool). It is AUR-only on Arch; gracefully degrade if yay is absent. if command -v yay &>/dev/null; then echo "[+] Installing freeipa-client (AUR)..." yay -S --noconfirm --needed freeipa-client @@ -26,11 +72,18 @@ else echo " Install yay then run: yay -S --needed freeipa-client" fi +# Enable SSSD now so it starts at boot after enrolment. +# "|| true" suppresses the error if the service unit doesn't exist yet +# (it will exist after freeipa-client is installed). systemctl enable sssd.service 2>/dev/null || true +# Ensure dialog is available for the TUI; install it on-the-fly if missing. command -v dialog &>/dev/null || pacman -S --noconfirm --needed dialog # ── Dialog theme ────────────────────────────────────────────────────────────── +# Apply the same magenta-on-black cyberqueer colour scheme used throughout +# the dotfiles TUI installers so all dialog screens look consistent. +# We only write a temporary dialogrc if one isn't already set in the environment. if [[ -z "${DIALOGRC:-}" ]] || [[ ! -f "${DIALOGRC:-/dev/null}" ]]; then _TMP_D=$(mktemp -d) trap 'rm -rf "$_TMP_D"' EXIT @@ -68,6 +121,9 @@ fi BT="FreeIPA Client Setup" T=$(mktemp); trap 'rm -f "$T"' EXIT +# ── dialog helper wrappers ──────────────────────────────────────────────────── +# These thin wrappers normalise the fd-swap trick needed to capture dialog +# output (dialog writes to stderr, so we swap stdout/stderr with 3>&1 1>&2 2>&3). d() { dialog --backtitle "$BT" "$@" 3>&1 1>&2 2>&3; } input(){ d --title " $1 " --inputbox "$2" 10 64 "$3"; } pass() { d --title " $1 " --passwordbox "$2" 10 64; } @@ -75,6 +131,8 @@ yn() { d --title " $1 " --yesno "$2" 7 64; } msg() { d --title " FreeIPA Client " --msgbox "$1" 10 64; } # ── Enrollment choice ───────────────────────────────────────────────────────── +# Present three options: answerfile, manual, or skip. +# Default to "skip" if the user presses Escape or cancels. CHOICE=$(d --title " FreeIPA Client Enrollment " \ --menu "\n Packages installed.\n How would you like to enroll this host?\n" \ 13 64 3 \ @@ -82,10 +140,14 @@ CHOICE=$(d --title " FreeIPA Client Enrollment " \ "manual" "Enter enrollment data manually" \ "skip" "Skip — enroll later") || CHOICE="skip" -# ── Helpers ─────────────────────────────────────────────────────────────────── +# ── run_enroll helper ───────────────────────────────────────────────────────── +# Runs the enrolment either via the server-generated freeipa-client.sh wrapper +# (preferred, because it already has the correct domain/realm baked in) or by +# falling back to calling ipa-client-install directly with the same arguments. run_enroll() { local args=("$@") if [[ -x "$CLIENT_SCRIPT" ]]; then + # Preferred path: use the pre-generated client script from the server exec "$CLIENT_SCRIPT" "${args[@]}" else # Fall back to ipa-client-install directly @@ -94,6 +156,7 @@ run_enroll() { local mkhomedir=true sudo_=true dns=true fido2=false declare -a fido2_users=() + # Parse our internal argument format and translate to ipa-client-install flags for ((i=0; i<${#args[@]}; i++)); do case "${args[$i]}" in --domain) dom="${args[$((i+1))]}"; ((i++)) ;; @@ -111,6 +174,7 @@ run_enroll() { esac done + # Build the ipa-client-install command from parsed values [[ -n "$dom" ]] && cmd+=(--domain "$dom") [[ -n "$rlm" ]] && cmd+=(--realm "$rlm") [[ -n "$srv" ]] && cmd+=(--server "$srv") @@ -125,14 +189,18 @@ run_enroll() { } # ── Answerfile mode ─────────────────────────────────────────────────────────── +# The answerfile is a JSON file pre-generated by freeipa-server.sh with domain, +# realm, and server already filled in. The user only needs to add the password. if [[ "$CHOICE" == "answerfile" ]]; then AF_PATH=$(input "Answerfile path" \ "Path to the JSON answerfile:" \ "$DEFAULT_AF") || { msg " Enrollment cancelled."; exit 0; } + # Use the default path if the user pressed Enter without typing a path [[ -z "$AF_PATH" ]] && AF_PATH="$DEFAULT_AF" if [[ ! -f "$AF_PATH" ]]; then + # Offer to create a template answerfile so the user can fill it in if yn "Create answerfile" \ " '$AF_PATH' does not exist.\n Create it with default values?"; then mkdir -p "$(dirname "$AF_PATH")" @@ -160,16 +228,19 @@ AFEOF fi fi + # Clear the dialog UI before running ipa-client-install (which prints to stdout) clear run_enroll --answerfile "$AF_PATH" fi # ── Manual mode ─────────────────────────────────────────────────────────────── +# Collect all enrolment parameters interactively via dialog prompts. if [[ "$CHOICE" == "manual" ]]; then DOMAIN=$(input "IPA Domain" "FreeIPA domain:" "$DEF_DOMAIN") \ || { msg " Enrollment cancelled."; exit 0; } DOMAIN="${DOMAIN:-$DEF_DOMAIN}" + # Kerberos realm is conventionally the domain uppercased; pre-fill that guess DEF_REALM_CALC="${DOMAIN^^}" REALM=$(input "Kerberos Realm" "Kerberos realm (usually domain uppercased):" \ "$DEF_REALM_CALC") || REALM="$DEF_REALM_CALC" @@ -179,6 +250,7 @@ if [[ "$CHOICE" == "manual" ]]; then || { msg " Enrollment cancelled."; exit 0; } SERVER="${SERVER:-$DOMAIN}" + # Default to the current machine's FQDN so the user can just press Enter DEF_HOST=$(hostname -f 2>/dev/null || hostname) HOST=$(input "This Host FQDN" "Hostname to register (leave blank = current):" \ "$DEF_HOST") || HOST="$DEF_HOST" @@ -186,6 +258,7 @@ if [[ "$CHOICE" == "manual" ]]; then PRINCIPAL=$(input "Admin Principal" "IPA admin principal:" "admin") || PRINCIPAL="admin" PRINCIPAL="${PRINCIPAL:-admin}" + # Use passwordbox so the password is not echoed to the terminal PASSWORD=$(pass "Admin Password" "Password for $PRINCIPAL@$REALM:") \ || { msg " Enrollment cancelled."; exit 0; } [[ -z "$PASSWORD" ]] && { msg " Password is required."; exit 1; } @@ -202,6 +275,7 @@ if [[ "$CHOICE" == "manual" ]]; then ) [[ -n "$NTP" ]] && ARGS+=(--ntp-server "$NTP") + # Optional features: each yes/no dialog adds or omits the corresponding flag yn "Home Directories" " Auto-create home directories on first login?" \ && true || ARGS+=(--no-mkhomedir) @@ -211,12 +285,14 @@ if [[ "$CHOICE" == "manual" ]]; then yn "DNS Update" " Register this host's IP in IPA DNS?" \ && true || ARGS+=(--no-dns-update) + # FIDO2/WebAuthn hardware token support (requires libfido2 + pamu2fcfg) if yn "FIDO2" " Enable FIDO2/WebAuthn authentication?"; then ARGS+=(--fido2) FIDO2_USERS=$(input "FIDO2 Users" \ "Usernames to enable FIDO2 for (comma-separated, blank = all):" "") \ || FIDO2_USERS="" if [[ -n "$FIDO2_USERS" ]]; then + # Split comma-separated list and strip whitespace from each username IFS=',' read -ra _U <<< "$FIDO2_USERS" for u in "${_U[@]}"; do u="${u// /}" @@ -230,6 +306,7 @@ if [[ "$CHOICE" == "manual" ]]; then fi # ── Skip ────────────────────────────────────────────────────────────────────── +# Packages are installed; print instructions for later manual enrolment. echo "" echo "[✓] FreeIPA client packages installed." echo "" diff --git a/setup/modules/optional-Modules/apps/geany.sh b/setup/modules/optional-Modules/apps/geany.sh index 7c2b291..b58a241 100755 --- a/setup/modules/optional-Modules/apps/geany.sh +++ b/setup/modules/optional-Modules/apps/geany.sh @@ -1,9 +1,12 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Geany (Flatpak)..." -ensure_flatpak -flatpak install -y flathub org.geany.Geany -apply_flatpak_theme "org.geany.Geany" +log "Installing Geany and plugins..." +# geany: lightweight GTK text editor / basic IDE with syntax highlighting. +# geany-plugins: meta-package of community plugins (project manager, Git integration, +# spell check, etc.) that ship separately from the core editor. +sudo pacman -S --noconfirm --needed geany geany-plugins log "Geany installed." diff --git a/setup/modules/optional-Modules/apps/gimp.sh b/setup/modules/optional-Modules/apps/gimp.sh index 93b6958..b0bcea1 100755 --- a/setup/modules/optional-Modules/apps/gimp.sh +++ b/setup/modules/optional-Modules/apps/gimp.sh @@ -1,9 +1,11 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing GIMP (Flatpak)..." -ensure_flatpak -flatpak install -y flathub org.gimp.GIMP -apply_flatpak_theme "org.gimp.GIMP" +log "Installing GIMP..." +# Install GIMP (GNU Image Manipulation Program) from the official Arch repos. +# --noconfirm skips interactive prompts; --needed avoids reinstalling if current. +sudo pacman -S --noconfirm --needed gimp log "GIMP installed." diff --git a/setup/modules/optional-Modules/apps/gnuplot.sh b/setup/modules/optional-Modules/apps/gnuplot.sh index 04c520e..2d1c820 100755 --- a/setup/modules/optional-Modules/apps/gnuplot.sh +++ b/setup/modules/optional-Modules/apps/gnuplot.sh @@ -1,7 +1,11 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing Gnuplot..." +# gnuplot is a portable command-line graphing and plotting tool. +# It is available in the official Arch repos; --needed keeps the install idempotent. sudo pacman -S --noconfirm --needed gnuplot log "Gnuplot installed." diff --git a/setup/modules/optional-Modules/apps/himalaya.sh b/setup/modules/optional-Modules/apps/himalaya.sh index 3baf644..febc62f 100755 --- a/setup/modules/optional-Modules/apps/himalaya.sh +++ b/setup/modules/optional-Modules/apps/himalaya.sh @@ -1,7 +1,13 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing Himalaya (AUR)..." +# himalaya-bin is the pre-compiled binary release of the Himalaya CLI email client. +# The -bin variant is preferred over building from source because compiling the full +# Rust workspace is slow and requires a complete Rust toolchain. +# --answerdiff None / --answerclean All suppress interactive PKGBUILD/clean prompts. yay -S --answerdiff None --answerclean All --noconfirm himalaya-bin log "Himalaya installed." diff --git a/setup/modules/optional-Modules/apps/imagemagick.sh b/setup/modules/optional-Modules/apps/imagemagick.sh index 66deb4f..ef0cc3f 100755 --- a/setup/modules/optional-Modules/apps/imagemagick.sh +++ b/setup/modules/optional-Modules/apps/imagemagick.sh @@ -1,7 +1,11 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing ImageMagick..." +# ImageMagick provides the convert/mogrify/identify CLI tools for image manipulation, +# format conversion, and batch processing. It is in the official Arch repos. sudo pacman -S --noconfirm --needed imagemagick log "ImageMagick installed." diff --git a/setup/modules/optional-Modules/apps/inkscape.sh b/setup/modules/optional-Modules/apps/inkscape.sh index c8d7b59..408d7f7 100755 --- a/setup/modules/optional-Modules/apps/inkscape.sh +++ b/setup/modules/optional-Modules/apps/inkscape.sh @@ -1,9 +1,11 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Inkscape (Flatpak)..." -ensure_flatpak -flatpak install -y flathub org.inkscape.Inkscape -apply_flatpak_theme "org.inkscape.Inkscape" +log "Installing Inkscape..." +# Inkscape is a vector graphics editor (SVG-native); available in the official repos. +# --noconfirm skips interactive prompts; --needed avoids reinstalling if current. +sudo pacman -S --noconfirm --needed inkscape log "Inkscape installed." diff --git a/setup/modules/optional-Modules/apps/k8s.sh b/setup/modules/optional-Modules/apps/k8s.sh index 317bb43..e60990b 100755 --- a/setup/modules/optional-Modules/apps/k8s.sh +++ b/setup/modules/optional-Modules/apps/k8s.sh @@ -1,12 +1,12 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing kubectl (pacman)..." -sudo pacman -S --noconfirm --needed kubectl - -log "Installing Podman Desktop (Flatpak)..." -ensure_flatpak -flatpak install -y flathub io.podman_desktop.PodmanDesktop -apply_flatpak_theme "io.podman_desktop.PodmanDesktop" +log "Installing Kubernetes tools (kubectl, podman-desktop)..." +# kubectl: official Kubernetes CLI for interacting with clusters. +# podman-desktop: native GUI for managing Podman containers and Kubernetes contexts. +# Both are available in the official Arch repos; --needed keeps the run idempotent. +sudo pacman -S --noconfirm --needed kubectl podman-desktop log "Kubernetes tools installed." diff --git a/setup/modules/optional-Modules/apps/kate.sh b/setup/modules/optional-Modules/apps/kate.sh index 45b32f4..d42f401 100755 --- a/setup/modules/optional-Modules/apps/kate.sh +++ b/setup/modules/optional-Modules/apps/kate.sh @@ -1,9 +1,11 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Kate (Flatpak)..." -ensure_flatpak -flatpak install -y flathub org.kde.kate -apply_flatpak_theme "org.kde.kate" +log "Installing Kate..." +# Kate is the KDE Advanced Text Editor; available in the official Arch repos. +# --noconfirm skips interactive prompts; --needed avoids reinstalling if current. +sudo pacman -S --noconfirm --needed kate log "Kate installed." diff --git a/setup/modules/optional-Modules/apps/kdenlive.sh b/setup/modules/optional-Modules/apps/kdenlive.sh index 56d836d..c85bc6f 100755 --- a/setup/modules/optional-Modules/apps/kdenlive.sh +++ b/setup/modules/optional-Modules/apps/kdenlive.sh @@ -1,9 +1,11 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Kdenlive (Flatpak)..." -ensure_flatpak -flatpak install -y flathub org.kde.kdenlive -apply_flatpak_theme "org.kde.kdenlive" +log "Installing Kdenlive..." +# Kdenlive is the KDE non-linear video editor; available in the official Arch repos. +# --noconfirm skips interactive prompts; --needed avoids reinstalling if current. +sudo pacman -S --noconfirm --needed kdenlive log "Kdenlive installed." diff --git a/setup/modules/optional-Modules/apps/krita.sh b/setup/modules/optional-Modules/apps/krita.sh index f7202a7..cea63b5 100755 --- a/setup/modules/optional-Modules/apps/krita.sh +++ b/setup/modules/optional-Modules/apps/krita.sh @@ -1,9 +1,11 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Krita (Flatpak)..." -ensure_flatpak -flatpak install -y flathub org.kde.krita -apply_flatpak_theme "org.kde.krita" +log "Installing Krita..." +# Krita is a professional digital painting application; available in the official repos. +# --noconfirm skips interactive prompts; --needed avoids reinstalling if current. +sudo pacman -S --noconfirm --needed krita log "Krita installed." diff --git a/setup/modules/optional-Modules/apps/lamco-rdp-server.sh b/setup/modules/optional-Modules/apps/lamco-rdp-server.sh index 33e6934..3751dab 100644 --- a/setup/modules/optional-Modules/apps/lamco-rdp-server.sh +++ b/setup/modules/optional-Modules/apps/lamco-rdp-server.sh @@ -1,17 +1,28 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -# lamco-rdp-server — native Wayland RDP server (Rust, H.264, VA-API) -# Builds from AUR; requires cargo, clang, cmake, nasm (auto-pulled as makedeps) +# lamco-rdp-server is a native Wayland RDP server written in Rust that uses +# H.264 hardware encoding via VA-API. It is AUR-only and builds from source, +# so cargo, clang, cmake, and nasm are pulled in automatically as makedepends. log "Installing lamco-rdp-server (AUR)..." +# --answerdiff None / --answerclean All keep the build unattended without showing diffs. yay -S --answerdiff None --answerclean All --noconfirm lamco-rdp-server log "Enabling lamco-rdp-server as user service..." +# Register the service in the user's systemd session so it starts on login. +# --user targets the per-user manager (not the system manager); no sudo required. +# If no active D-Bus/systemd user session exists yet (e.g. during a fresh install +# before first login), the enable call will fail — the || clause warns the user +# and lets the script continue rather than aborting with set -e. systemctl --user enable lamco-rdp-server.service 2>/dev/null \ || warn "No user session active — run after login: systemctl --user enable lamco-rdp-server.service" log "lamco-rdp-server enabled as a user service." log "Start it with: systemctl --user start lamco-rdp-server" log "Or launch the GUI tray: lamco-rdp-server-gui" +# RDP screen capture on Wayland requires an xdg-desktop-portal backend that matches +# the running compositor; without it the portal will refuse screen share requests. warn "Install a matching xdg-desktop-portal for your compositor: xdg-desktop-portal-hyprland / -wlr / -gnome / -kde" diff --git a/setup/modules/optional-Modules/apps/librewolf.sh b/setup/modules/optional-Modules/apps/librewolf.sh index 112107a..ef195d5 100755 --- a/setup/modules/optional-Modules/apps/librewolf.sh +++ b/setup/modules/optional-Modules/apps/librewolf.sh @@ -1,9 +1,13 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing LibreWolf (Flatpak)..." -ensure_flatpak -flatpak install -y flathub io.gitlab.librewolf-community.librewolf -apply_flatpak_theme "io.gitlab.librewolf-community.librewolf" +log "Installing LibreWolf (AUR)..." +# librewolf-bin installs the pre-compiled binary release from the AUR, avoiding the +# long Rust + C++ build time of the source package. LibreWolf is a hardened Firefox +# fork with stricter privacy defaults; it is not in the official Arch repos. +# --answerdiff None / --answerclean All suppress interactive PKGBUILD/clean prompts. +yay -S --answerdiff None --answerclean All --noconfirm librewolf-bin log "LibreWolf installed." diff --git a/setup/modules/optional-Modules/apps/llama-cpp.sh b/setup/modules/optional-Modules/apps/llama-cpp.sh index 5e12877..d9cb486 100755 --- a/setup/modules/optional-Modules/apps/llama-cpp.sh +++ b/setup/modules/optional-Modules/apps/llama-cpp.sh @@ -1,10 +1,15 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -# Provides standalone llama-cli, llama-server, and related tools. -# Ollama bundles its own copy of llama.cpp internally — the two coexist -# at the package level but share GPU resources at runtime. +# llama-cpp provides the standalone llama-cli, llama-server, and related inference +# tools that can be used without Ollama. Ollama bundles its own internal copy of +# llama.cpp, so both packages can coexist; they share GPU resources at runtime but +# do not conflict at the package level. log "Installing llama.cpp (AUR)..." +# llama-cpp is not in the official Arch repos; pull it from the AUR. +# --answerdiff None / --answerclean All suppress interactive PKGBUILD/clean prompts. yay -S --answerdiff None --answerclean All --noconfirm llama-cpp log "llama.cpp installed." diff --git a/setup/modules/optional-Modules/apps/lmms.sh b/setup/modules/optional-Modules/apps/lmms.sh index f607b1b..57722e8 100755 --- a/setup/modules/optional-Modules/apps/lmms.sh +++ b/setup/modules/optional-Modules/apps/lmms.sh @@ -1,9 +1,11 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing LMMS (Flatpak)..." -ensure_flatpak -flatpak install -y flathub io.lmms.LMMS -apply_flatpak_theme "io.lmms.LMMS" +log "Installing LMMS..." +# LMMS (Linux MultiMedia Studio) is an open-source DAW / beat production suite. +# It is available in the official Arch repos; --needed keeps the install idempotent. +sudo pacman -S --noconfirm --needed lmms log "LMMS installed." diff --git a/setup/modules/optional-Modules/apps/localsend.sh b/setup/modules/optional-Modules/apps/localsend.sh index 95af553..d617b5d 100755 --- a/setup/modules/optional-Modules/apps/localsend.sh +++ b/setup/modules/optional-Modules/apps/localsend.sh @@ -1,9 +1,12 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing LocalSend (Flatpak)..." -ensure_flatpak -flatpak install -y flathub org.localsend.localsend -apply_flatpak_theme "org.localsend.localsend" +log "Installing LocalSend (AUR)..." +# LocalSend is a cross-platform AirDrop alternative that transfers files over the +# local network without an internet connection. It is not in the official repos. +# --answerdiff None / --answerclean All suppress interactive PKGBUILD/clean prompts. +yay -S --answerdiff None --answerclean All --noconfirm localsend log "LocalSend installed." diff --git a/setup/modules/optional-Modules/apps/localtunnel.sh b/setup/modules/optional-Modules/apps/localtunnel.sh index f719613..8179696 100755 --- a/setup/modules/optional-Modules/apps/localtunnel.sh +++ b/setup/modules/optional-Modules/apps/localtunnel.sh @@ -1,11 +1,19 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/skip/warn/err helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing LocalTunnel..." +# LocalTunnel exposes a local port to the internet via a public URL. +# Prefer the npm global install because it tracks npm's own Node version and keeps +# the tool alongside other npm globals already managed on the system. +# Fall back to the AUR package if npm is not available (e.g. Node installed via pacman +# without nvm, or npm not yet on PATH in this session). if command -v npm &>/dev/null; then npm install -g localtunnel else + # --answerdiff None / --answerclean All suppress interactive PKGBUILD/clean prompts. yay -S --answerdiff None --answerclean All --noconfirm localtunnel fi log "LocalTunnel installed." diff --git a/setup/modules/optional-Modules/apps/mail-notmuch.sh b/setup/modules/optional-Modules/apps/mail-notmuch.sh index 1461987..2603bf0 100755 --- a/setup/modules/optional-Modules/apps/mail-notmuch.sh +++ b/setup/modules/optional-Modules/apps/mail-notmuch.sh @@ -1,25 +1,38 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing mail stack (isync, msmtp, notmuch, alot, w3m)..." +# isync/mbsync: IMAP sync daemon; msmtp: SMTP sender; notmuch: fast mail indexer; +# alot: terminal mail UI built on notmuch; w3m: renders HTML mail parts as plain text. sudo pacman -S --noconfirm --needed isync msmtp notmuch alot w3m # ── Credentials ─────────────────────────────────────────────────────────────── +# Collect all account details interactively before writing any config files, +# so the user can review/abort cleanly before any files are touched. read -rp "Full name : " FULL_NAME read -rp "Email address : " EMAIL read -rp "IMAP host (e.g. mail.example.com) : " IMAP_HOST +# Default IMAP port to 993 (IMAPS/TLS); user may override for non-standard setups. read -rp "IMAP port [993] : " IMAP_PORT; IMAP_PORT="${IMAP_PORT:-993}" +# Default IMAP username to the email address, which most providers require. read -rp "IMAP username [$EMAIL] : " IMAP_USER; IMAP_USER="${IMAP_USER:-$EMAIL}" +# -s suppresses echo so the password is not visible; trailing `echo` restores the newline. read -rsp "IMAP password : " IMAP_PASS; echo read -rp "SMTP host (e.g. mail.example.com) : " SMTP_HOST +# Default SMTP port to 587 (STARTTLS submission); use 465 for implicit TLS. read -rp "SMTP port [587] : " SMTP_PORT; SMTP_PORT="${SMTP_PORT:-587}" +# Root of the local Maildir tree; mbsync creates per-folder subdirectories here. MAILDIR="$HOME/Mail" mkdir -p "$MAILDIR" # ── mbsync ──────────────────────────────────────────────────────────────────── log "Writing ~/.mbsyncrc..." +# Heredoc writes the full mbsync config in one shot using the variables collected +# above. Overwriting on each run means re-running the script updates credentials. cat > ~/.mbsyncrc << EOF IMAPAccount main Host $IMAP_HOST @@ -27,12 +40,14 @@ Port $IMAP_PORT User $IMAP_USER Pass $IMAP_PASS SSLType IMAPS +# Trust the system CA bundle rather than pinning a specific server certificate. CertificateFile /etc/ssl/certs/ca-certificates.crt IMAPStore main-remote Account main MaildirStore main-local +# SubFolders Verbatim: mirror the IMAP folder hierarchy exactly as subdirectories. SubFolders Verbatim Path $MAILDIR/ Inbox $MAILDIR/INBOX @@ -40,19 +55,27 @@ Inbox $MAILDIR/INBOX Channel main Far :main-remote: Near :main-local: +# Sync all IMAP folders; replace * with a list to restrict to specific folders. Patterns * +# Create Both: automatically create missing folders on either side. Create Both +# SyncState *: store .mbsyncstate alongside each Maildir folder for portability. SyncState * +# Expunge Both: propagate deletions bidirectionally so remote deletes appear locally. Expunge Both EOF +# 600 permissions keep the plaintext password private from other local users. chmod 600 ~/.mbsyncrc # ── msmtp ───────────────────────────────────────────────────────────────────── log "Writing ~/.msmtprc..." +# msmtp acts as a drop-in sendmail replacement; alot invokes it via the +# sendmail_command field in the alot account block written further below. cat > ~/.msmtprc << EOF defaults tls on tls_trust_file /etc/ssl/certs/ca-certificates.crt +# Log every sent message with a timestamp to aid debugging delivery failures. logfile ~/.msmtp.log account main @@ -63,27 +86,37 @@ user $IMAP_USER password $IMAP_PASS from $EMAIL +# Make "main" the account used when msmtp is called without an explicit -a flag. account default : main EOF +# Same 600 restriction as mbsyncrc — this file contains a plaintext SMTP password. chmod 600 ~/.msmtprc # ── notmuch ─────────────────────────────────────────────────────────────────── log "Configuring notmuch..." +# notmuch config set writes to ~/.notmuch-config; each call is idempotent +# (overwrites the existing key) so re-running the script is safe. notmuch config set user.name "$FULL_NAME" notmuch config set user.email "$EMAIL" +# Point notmuch at the Maildir root so `notmuch new` scans the right location. notmuch config set database.path "$MAILDIR" +# Synchronise notmuch tags with Maildir flags (Seen, Replied, etc.) bidirectionally. notmuch config set maildir.synchronize_flags true +# Tag every newly indexed message as unread and inbox by default. notmuch config set new.tags "unread;inbox" # post-new hook: tag sent mail, remove inbox from trash +# notmuch executes this script automatically after `notmuch new` indexes messages. mkdir -p "$MAILDIR/.notmuch/hooks" cat > "$MAILDIR/.notmuch/hooks/post-new" << 'EOF' #!/bin/bash +# Apply folder-based tags and strip inbox/unread from non-inbox folders. notmuch tag +sent -inbox -- folder:Sent notmuch tag +trash -inbox -unread -- folder:Trash notmuch tag +draft -inbox -- folder:Drafts notmuch tag +spam -inbox -unread -- folder:Spam folder:Junk EOF +# The hook must be executable or notmuch will silently skip it. chmod +x "$MAILDIR/.notmuch/hooks/post-new" # ── alot ────────────────────────────────────────────────────────────────────── @@ -91,6 +124,9 @@ chmod +x "$MAILDIR/.notmuch/hooks/post-new" # Write only the account block, which contains machine-specific paths/identity. log "Writing account details into ~/Dotfiles/alot/config..." ALOT_CFG="$HOME/Dotfiles/alot/config" +# Use an inline Python script to splice the [accounts] block into the existing +# config file. sed struggles with multi-line replacements; Python's re.sub with +# re.DOTALL handles the entire block atomically and overwrites any prior values. # Replace the [[main]] account block in-place (sed removes old block, cat appends new one) python3 - "$ALOT_CFG" "$FULL_NAME" "$EMAIL" "$MAILDIR" << 'PYEOF' import sys, re @@ -105,6 +141,8 @@ block = f"""[accounts] """ with open(path) as f: text = f.read() +# Replace from [accounts] up to the next top-level section or end-of-file, +# ensuring any previous account block is cleanly overwritten on re-runs. text = re.sub(r'\[accounts\].*?(?=\n\[|\Z)', block, text, flags=re.DOTALL) with open(path, 'w') as f: f.write(text) @@ -112,13 +150,22 @@ PYEOF # ── mailcap (HTML email rendering via w3m) ──────────────────────────────────── log "Writing ~/.mailcap..." +# grep -qxF checks for an exact full-line match to avoid appending duplicates +# on repeated runs; the || only appends the entry when it is absent. +# w3m -dump converts HTML to plain text; copiousoutput tells mail clients +# to page the result rather than try to display it inline. grep -qxF "text/html; w3m -dump -o document_charset=%{charset} '%s'; nametemplate=%s.html; copiousoutput" ~/.mailcap 2>/dev/null \ || echo "text/html; w3m -dump -o document_charset=%{charset} '%s'; nametemplate=%s.html; copiousoutput" >> ~/.mailcap # ── systemd timer for periodic sync ─────────────────────────────────────────── log "Installing mbsync systemd user timer (every 5 min)..." +# User units live under ~/.config/systemd/user/; --user scope requires no root +# and units only run while the user session is active. mkdir -p ~/.config/systemd/user +# Type=oneshot: systemd marks the service done as soon as ExecStart exits, +# appropriate for a batch sync command that runs and terminates. +# After=network-online.target: prevents sync attempts before a route is available. cat > ~/.config/systemd/user/mbsync.service << EOF [Unit] Description=Sync mail with mbsync @@ -127,9 +174,12 @@ After=network-online.target [Service] Type=oneshot ExecStart=/usr/bin/mbsync -a +# Re-index immediately after each sync so alot reflects new messages right away. ExecStartPost=/usr/bin/notmuch new EOF +# OnBootSec=2min: delay the first run by 2 min to avoid startup congestion. +# OnUnitActiveSec=5min: repeat 5 min after the previous run finishes. cat > ~/.config/systemd/user/mbsync.timer << EOF [Unit] Description=Run mbsync every 5 minutes @@ -143,12 +193,16 @@ Unit=mbsync.service WantedBy=timers.target EOF +# Reload the user manager so it picks up the newly written unit files. systemctl --user daemon-reload +# enable makes the timer survive reboots; --now also starts it in the current session. systemctl --user enable --now mbsync.timer # ── initial sync ────────────────────────────────────────────────────────────── log "Running initial mail sync..." +# -a syncs all configured channels; may take several minutes on a large mailbox. mbsync -a +# Index the freshly downloaded messages so alot can display them immediately. notmuch new log "Mail setup complete. Syncs automatically every 5 min via systemd timer." diff --git a/setup/modules/optional-Modules/apps/min-browser.sh b/setup/modules/optional-Modules/apps/min-browser.sh index e59845c..f9ed28d 100755 --- a/setup/modules/optional-Modules/apps/min-browser.sh +++ b/setup/modules/optional-Modules/apps/min-browser.sh @@ -1,9 +1,11 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Min browser (Flatpak)..." -ensure_flatpak -flatpak install -y flathub com.github.minbrowser.min -apply_flatpak_theme "com.github.minbrowser.min" +log "Installing Min browser (AUR)..." +# Min is not in the official repos; yay builds it from the AUR. +# --answerdiff None: skip the diff prompt; --answerclean All: clean build dir automatically. +yay -S --answerdiff None --answerclean All --noconfirm min log "Min browser installed." diff --git a/setup/modules/optional-Modules/apps/mixxx.sh b/setup/modules/optional-Modules/apps/mixxx.sh index 4277adf..c9b36ec 100755 --- a/setup/modules/optional-Modules/apps/mixxx.sh +++ b/setup/modules/optional-Modules/apps/mixxx.sh @@ -1,9 +1,10 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Mixxx (Flatpak)..." -ensure_flatpak -flatpak install -y flathub org.mixxx.Mixxx -apply_flatpak_theme "org.mixxx.Mixxx" +log "Installing Mixxx (DJ software)..." +# Mixxx is available in the official Arch repos; --needed skips reinstallation. +sudo pacman -S --noconfirm --needed mixxx log "Mixxx installed." diff --git a/setup/modules/optional-Modules/apps/mysql.sh b/setup/modules/optional-Modules/apps/mysql.sh index 7050df8..3ae3003 100755 --- a/setup/modules/optional-Modules/apps/mysql.sh +++ b/setup/modules/optional-Modules/apps/mysql.sh @@ -1,17 +1,25 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing MariaDB..." +# Arch ships MariaDB as the default MySQL-compatible database server. sudo pacman -S --noconfirm --needed mariadb +# Guard against re-initialising an existing database; the mysql/ subdirectory +# is created by mariadb-install-db and its presence means the data dir is ready. if [[ ! -d /var/lib/mysql/mysql ]]; then log "Initialising MariaDB data directory..." + # mariadb-install-db creates the system tables under /var/lib/mysql, + # running as the dedicated mysql system user for correct file ownership. sudo mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql else skip "MariaDB data directory already initialised." fi log "Enabling MariaDB service..." +# enable --now: persist across reboots and start the daemon in the current session. sudo systemctl enable --now mariadb.service log "MariaDB installed and running." diff --git a/setup/modules/optional-Modules/apps/networking-cli.sh b/setup/modules/optional-Modules/apps/networking-cli.sh index abcc491..f7264cb 100755 --- a/setup/modules/optional-Modules/apps/networking-cli.sh +++ b/setup/modules/optional-Modules/apps/networking-cli.sh @@ -1,8 +1,15 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing networking CLI tools..." +# httpie: human-friendly HTTP client; ipcalc: subnet calculator; +# mitmproxy: interactive TLS-capable proxy for traffic inspection; +# mtr: combines ping + traceroute in a live display; net-tools: ifconfig/netstat; +# nethogs: per-process bandwidth monitor; nmap: port scanner; +# tcpdump: packet capture; traceroute: hop-by-hop path tracing. sudo pacman -S --noconfirm --needed \ httpie ipcalc mitmproxy mtr net-tools nethogs \ nmap tcpdump traceroute diff --git a/setup/modules/optional-Modules/apps/nyxt.sh b/setup/modules/optional-Modules/apps/nyxt.sh index 6d258ec..3c2b7ec 100644 --- a/setup/modules/optional-Modules/apps/nyxt.sh +++ b/setup/modules/optional-Modules/apps/nyxt.sh @@ -1,7 +1,11 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing Nyxt browser (AUR)..." +# Nyxt is a keyboard-driven, Lisp-extensible browser available only via the AUR. +# --answerdiff None / --answerclean All suppress interactive yay prompts. yay -S --answerdiff None --answerclean All --noconfirm nyxt log "Nyxt installed." diff --git a/setup/modules/optional-Modules/apps/ollama.sh b/setup/modules/optional-Modules/apps/ollama.sh index d201a12..6bde67b 100755 --- a/setup/modules/optional-Modules/apps/ollama.sh +++ b/setup/modules/optional-Modules/apps/ollama.sh @@ -1,15 +1,22 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" # GPU variants: install ollama-cuda (AUR) for NVIDIA or ollama-rocm (AUR) for AMD. # The base package runs on CPU and auto-uses GPU libs if present at runtime. log "Installing Ollama..." +# The official Arch repo ships the CPU build; GPU support requires AUR alternatives. sudo pacman -S --noconfirm --needed ollama log "Enabling Ollama service..." +# The ollama systemd service exposes a REST API on port 11434 used by frontends +# such as open-webui; enable --now starts it immediately without a reboot. sudo systemctl enable --now ollama.service log "Ollama running on http://localhost:11434" log "Pull models with: ollama pull <model>" +# Both Ollama and llama.cpp compete for GPU VRAM; running both simultaneously +# can cause OOM errors or severe performance degradation. warn "If llama.cpp is also installed, avoid running both GPU-bound at once." diff --git a/setup/modules/optional-Modules/apps/onlyoffice.sh b/setup/modules/optional-Modules/apps/onlyoffice.sh index 5c6fda7..bf918d9 100755 --- a/setup/modules/optional-Modules/apps/onlyoffice.sh +++ b/setup/modules/optional-Modules/apps/onlyoffice.sh @@ -1,9 +1,11 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing OnlyOffice (Flatpak)..." -ensure_flatpak -flatpak install -y flathub org.onlyoffice.desktopeditors -apply_flatpak_theme "org.onlyoffice.desktopeditors" +log "Installing OnlyOffice (AUR)..." +# onlyoffice-bin is the pre-compiled binary AUR package; building from source +# is not practical due to the large bundled Chromium-based rendering engine. +yay -S --answerdiff None --answerclean All --noconfirm onlyoffice-bin log "OnlyOffice installed." diff --git a/setup/modules/optional-Modules/apps/open-webui.sh b/setup/modules/optional-Modules/apps/open-webui.sh index f0da8ec..e028ab7 100755 --- a/setup/modules/optional-Modules/apps/open-webui.sh +++ b/setup/modules/optional-Modules/apps/open-webui.sh @@ -1,13 +1,18 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" # Open WebUI — browser UI for Ollama and other LLM backends. # Ollama module should be installed first for full functionality. log "Installing Open WebUI (AUR)..." +# open-webui is only in the AUR; yay builds and installs it with the standard flags. yay -S --answerdiff None --answerclean All --noconfirm open-webui log "Enabling Open WebUI service..." +# The systemd service proxies requests to Ollama at localhost:11434 and serves +# the web interface on port 8080; enable --now starts it without a reboot. sudo systemctl enable --now open-webui.service log "Open WebUI running at http://localhost:8080" diff --git a/setup/modules/optional-Modules/apps/openarena.sh b/setup/modules/optional-Modules/apps/openarena.sh index f48385a..82f73ef 100644 --- a/setup/modules/optional-Modules/apps/openarena.sh +++ b/setup/modules/optional-Modules/apps/openarena.sh @@ -1,7 +1,11 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing OpenArena..." +# OpenArena is a free, open-source FPS based on the Quake III Arena engine, +# available directly from the official Arch repositories. sudo pacman -S --noconfirm --needed openarena log "OpenArena installed." diff --git a/setup/modules/optional-Modules/apps/openshot.sh b/setup/modules/optional-Modules/apps/openshot.sh index 2f1c1f1..af7be33 100755 --- a/setup/modules/optional-Modules/apps/openshot.sh +++ b/setup/modules/optional-Modules/apps/openshot.sh @@ -1,9 +1,10 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing OpenShot (Flatpak)..." -ensure_flatpak -flatpak install -y flathub org.openshot.OpenShot -apply_flatpak_theme "org.openshot.OpenShot" +log "Installing OpenShot..." +# OpenShot is a non-linear video editor available in the official Arch repos. +sudo pacman -S --noconfirm --needed openshot log "OpenShot installed." diff --git a/setup/modules/optional-Modules/apps/podman.sh b/setup/modules/optional-Modules/apps/podman.sh index aad5f32..ac35522 100644 --- a/setup/modules/optional-Modules/apps/podman.sh +++ b/setup/modules/optional-Modules/apps/podman.sh @@ -1,10 +1,17 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing Podman (rootless containers, buildah, skopeo)..." +# podman: daemonless OCI container engine; podman-compose: docker-compose replacement; +# buildah: low-level image builder; skopeo: inspect/copy images between registries. sudo pacman -S --noconfirm --needed podman podman-compose buildah skopeo log "Enabling user lingering so rootless containers survive logout..." +# Without lingering, the user's systemd session (and its cgroups) is torn down +# on logout, killing any running rootless containers. enable-linger keeps the +# session alive so background containers persist across user sessions. loginctl enable-linger "$USER" log "Podman installed (rootless, no daemon). User lingering enabled." diff --git a/setup/modules/optional-Modules/apps/prismlauncher.sh b/setup/modules/optional-Modules/apps/prismlauncher.sh index 60e57d6..1d51fc0 100755 --- a/setup/modules/optional-Modules/apps/prismlauncher.sh +++ b/setup/modules/optional-Modules/apps/prismlauncher.sh @@ -1,9 +1,13 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing PrismLauncher (Flatpak)..." -ensure_flatpak +# PrismLauncher is a Minecraft launcher that manages multiple instances and Java versions. +# The Flatpak edition bundles its own Java runtimes, avoiding system JDK conflicts. +# -y: non-interactive, auto-approve all prompts. flatpak install -y flathub org.prismlauncher.PrismLauncher apply_flatpak_theme "org.prismlauncher.PrismLauncher" log "PrismLauncher installed." diff --git a/setup/modules/optional-Modules/apps/productivity.sh b/setup/modules/optional-Modules/apps/productivity.sh index 0535af4..0013a6d 100755 --- a/setup/modules/optional-Modules/apps/productivity.sh +++ b/setup/modules/optional-Modules/apps/productivity.sh @@ -1,10 +1,15 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing taskwarrior..." +# Taskwarrior (task) is a CLI task manager; it is in the official repos as 'task'. sudo pacman -S --noconfirm --needed task log "Installing watson and jrnl (AUR)..." +# watson: time-tracker that logs work sessions to JSON; python-jrnl: encrypted CLI journal. +# Both are Python-based tools only available via the AUR on Arch. yay -S --answerdiff None --answerclean All --noconfirm watson python-jrnl log "Productivity tools installed." diff --git a/setup/modules/optional-Modules/apps/qemu.sh b/setup/modules/optional-Modules/apps/qemu.sh index bd267e1..be49e5a 100644 --- a/setup/modules/optional-Modules/apps/qemu.sh +++ b/setup/modules/optional-Modules/apps/qemu.sh @@ -1,8 +1,16 @@ #!/bin/bash +# Exit immediately on error, treat unset variables as errors, propagate pipe failures. set -euo pipefail +# Load shared log/warn/skip helpers from the installer library. source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing QEMU/KVM + libvirt stack + virt-manager..." +# qemu-full: full QEMU build with all emulation targets and utilities; +# libvirt: management daemon + API used by virt-manager; virt-manager: GUI front-end; +# virt-viewer: lightweight display client for VM consoles; +# dnsmasq: DHCP/DNS for the default NAT network; bridge-utils: bridge management; +# edk2-ovmf: UEFI firmware images for UEFI-boot VMs; swtpm: software TPM emulator; +# vde2: virtual distributed Ethernet for advanced networking topologies. sudo pacman -S --noconfirm --needed \ qemu-full \ libvirt \ @@ -15,11 +23,19 @@ sudo pacman -S --noconfirm --needed \ vde2 log "Enabling libvirtd service..." +# libvirtd must run as root to manage KVM devices and network namespaces. sudo systemctl enable --now libvirtd.service log "Configuring default NAT network for autostart..." +# The 'default' NAT network is created by libvirt on first start; net-autostart +# makes it come up automatically after each libvirtd restart. +# 2>/dev/null || true: suppress the error if the network does not yet exist +# (it will be created on first libvirtd run), preventing set -e from aborting. sudo virsh net-autostart default 2>/dev/null || true log "Adding $USER to libvirt and kvm groups..." +# Membership in libvirt allows managing VMs without sudo; kvm grants direct +# access to /dev/kvm for hardware acceleration. Group changes only apply +# after the user logs out and back in. sudo usermod -aG libvirt,kvm "$USER" log "QEMU/KVM installed. Log out and back in for group membership to take effect." diff --git a/setup/modules/optional-Modules/python.sh b/setup/modules/optional-Modules/python.sh index d9b0310..210696c 100755 --- a/setup/modules/optional-Modules/python.sh +++ b/setup/modules/optional-Modules/python.sh @@ -1,7 +1,16 @@ #!/bin/bash +# set -euo pipefail: abort on errors, unset vars, and pipeline failures. set -euo pipefail +# Path climbs one level (/../) because this optional module lives in a +# subdirectory; logging.sh is in modules/lib/ relative to modules/. source "$(dirname "${BASH_SOURCE[0]}")/../lib/logging.sh" +# Install the Python ecosystem needed for neovim LSP support and scripting: +# python — base interpreter (3.x) +# pyright — Microsoft's Python LSP; used by nvim-lspconfig +# python-pynvim — Python client library that lets neovim plugins call Python RPCs +# python-pipx — tool to install Python apps in isolated venvs (replaces pipsi) +# Note: --noconfirm and --needed are not passed here; add them to make this idempotent. log "Installing Python tools..." sudo pacman -Syu python pyright python-pynvim python-pipx log "Python tools installed." diff --git a/setup/modules/optional-Modules/wprs.sh b/setup/modules/optional-Modules/wprs.sh index 6424b86..831abf0 100755 --- a/setup/modules/optional-Modules/wprs.sh +++ b/setup/modules/optional-Modules/wprs.sh @@ -1,7 +1,14 @@ #!/bin/bash +# set -euo pipefail: abort on errors, unset vars, and pipeline failures. set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../lib/logging.sh" +# wprs (Wayland Proxy for Remote Sessions) allows running Wayland compositors +# inside an existing Wayland session — useful for nested/remote desktop setups. +# The -git suffix means the AUR PKGBUILD tracks the upstream git HEAD rather +# than a tagged release, so it always builds the latest commit. +# --answerdiff None / --answerclean All suppress interactive PKGBUILD review +# prompts so the script can run fully unattended. log "Installing wprs-git (AUR)..." yay -S --answerdiff None --answerclean All --noconfirm wprs-git log "wprs installed." diff --git a/setup/modules/optional-Modules/zfs.sh b/setup/modules/optional-Modules/zfs.sh index fe8d38c..f9c90ea 100755 --- a/setup/modules/optional-Modules/zfs.sh +++ b/setup/modules/optional-Modules/zfs.sh @@ -1,7 +1,15 @@ #!/bin/bash +# set -euo pipefail: abort on errors, unset vars, and pipeline failures. set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../lib/logging.sh" +# zfs-dkms is AUR-only because the ZFS on Linux licence is GPL-incompatible +# and cannot be shipped in the official Arch repos. +# --answerdiff None: skip the "view PKGBUILD diff?" prompt automatically. +# --answerclean All: automatically remove leftover build artifacts after build. +# --noconfirm: suppress the final "proceed with installation?" confirmation. +# zfs-dkms builds a DKMS module that recompiles against each new kernel +# automatically, so ZFS survives kernel upgrades without manual intervention. log "Installing ZFS kernel module (AUR)..." yay -S --answerdiff None --answerclean All --noconfirm zfs-dkms log "ZFS installed." diff --git a/setup/modules/package-managers.sh b/setup/modules/package-managers.sh index f17ee79..39515c3 100644 --- a/setup/modules/package-managers.sh +++ b/setup/modules/package-managers.sh @@ -1,38 +1,97 @@ #!/bin/bash +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ setup/modules/package-managers.sh — Package manager bootstrap ║ +# ║ ║ +# ║ PURPOSE: ║ +# ║ Installs and configures the additional package managers and language ║ +# ║ runtimes that the rest of the setup depends on. This must be the ║ +# ║ FIRST module run because others rely on yay, nvm, and rustup. ║ +# ║ ║ +# ║ INSTALLS: ║ +# ║ - Flatpak (via pacman) — universal app sandbox, Flathub remote ║ +# ║ - yay — AUR helper (builds packages from the Arch User Repository) ║ +# ║ - rustup — Rust toolchain manager; sets stable as default ║ +# ║ - nvm + Node.js 22 — JavaScript runtime for CLI tools ║ +# ║ ║ +# ║ ALL INSTALLS ARE IDEMPOTENT — safe to run multiple times. ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ + set -euo pipefail +# -e: abort immediately if any command fails (package manager errors are fatal here) +# -u: error on unset variables +# -o pipefail: pipe failures are caught + +# Load shared logging helpers (log, skip, warn, err functions) source "$(dirname "${BASH_SOURCE[0]}")/lib/logging.sh" +# ── Flatpak + system update ──────────────────────────────────────────────────── +# WHY: Run a full system upgrade first (-yu) to avoid partial-upgrade issues +# where pacman has a newer database but old packages. This is critical on +# Arch which is a rolling release. +# Flatpak is installed here (not in core-packages.sh) because it's needed before +# the Flathub remote can be added, and some modules call ensure_flatpak() early. log "Updating system and installing Flatpak..." sudo pacman -Syu --noconfirm --needed flatpak -# yay +# ── yay (AUR helper) ────────────────────────────────────────────────────────── +# WHY: The AUR (Arch User Repository) contains thousands of community packages +# not in the official repos. yay is the most popular AUR helper — it wraps +# pacman and handles PKGBUILD downloading, building, and installing. +# HOW: Clone the yay PKGBUILD from AUR, then build and install it with makepkg. +# makepkg -si: build the package (-s=sync dependencies) and install (-i). +# IDEMPOTENCY: Check for the yay binary first to avoid rebuilding if already present. if ! command -v yay &>/dev/null; then log "Installing yay..." + # Use ~/install-tmp rather than /tmp to survive across sudo sessions and avoid + # tmpfs size limits on systems with small RAM mkdir -p ~/install-tmp git clone https://aur.archlinux.org/yay.git ~/install-tmp/yay cd ~/install-tmp/yay + # --noconfirm: don't prompt during the build/install process makepkg -si --noconfirm cd ~ else skip "yay already installed." fi -# rustup / rust +# ── rustup / Rust toolchain ──────────────────────────────────────────────────── +# WHY: Rust is used by many modern CLI tools (yazi, starship, zellij, etc.) and +# some AUR packages require cargo to build from source. +# rustup is preferred over the `rust` package because it allows switching +# between stable/nightly channels and managing multiple targets. +# HOW: Install rustup via pacman (it's in the official repos), then set +# `stable` as the active toolchain so `cargo` and `rustc` are available. if ! command -v rustup &>/dev/null; then log "Installing rustup..." sudo pacman -S --noconfirm --needed rustup fi +# Always run 'rustup default stable' even if rustup exists — this is idempotent +# and ensures the correct toolchain is active (guards against nightly being default) rustup default stable -# nvm + Node.js +# ── nvm + Node.js ───────────────────────────────────────────────────────────── +# WHY: Node.js is required for npm-based tools including Claude Code CLI, +# LocalTunnel, and other developer utilities. We use nvm (Node Version Manager) +# rather than the system `node` package because: +# 1. nvm allows per-project Node version pinning via .nvmrc +# 2. Global npm packages install to ~/.nvm without needing sudo +# 3. Switching to LTS or newer versions is a one-liner +# HOW: Download and run the official nvm install script, then source nvm.sh to +# activate it in this shell session, then install Node.js LTS v22. +# NOTE: nvm installs to ~/.nvm/ and injects itself via .bashrc/.zshrc hooks if ! command -v node &>/dev/null; then log "Installing nvm and Node.js 22..." + # Only clone nvm if its directory doesn't exist (guards against partial installs) if [ ! -d "$HOME/.nvm" ]; then + # Pin to a specific nvm version for reproducibility curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash fi + # Source nvm into the current shell session (normally loaded by .bashrc/.zshrc) . "$HOME/.nvm/nvm.sh" + # Install Node.js 22 (current LTS) and set it as the active version nvm install 22 else + # Report the currently-installed version so the log shows what's active skip "Node.js already installed: $(node -v)" fi diff --git a/setup/modules/shell-setup.sh b/setup/modules/shell-setup.sh index 8534820..089be82 100755 --- a/setup/modules/shell-setup.sh +++ b/setup/modules/shell-setup.sh @@ -1,22 +1,84 @@ #!/bin/bash +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ setup/modules/shell-setup.sh — Shell environment deployment ║ +# ║ ║ +# ║ PURPOSE: ║ +# ║ Installs and configures the full shell environment: zsh, oh-my-zsh, ║ +# ║ plugins, neovim with lazy.nvim plugins, yazi, starship prompt, and ║ +# ║ deploys all dotfile symlinks to their correct locations. ║ +# ║ ║ +# ║ WHEN TO RUN: ║ +# ║ After core-packages.sh (needs base packages). Runs as the logged-in ║ +# ║ user — must NOT run as root. ║ +# ║ ║ +# ║ WHAT GETS DEPLOYED: ║ +# ║ ~/.bashrc, ~/.zshrc — symlinks to Dotfiles repo ║ +# ║ ~/.config/starship.toml — symlink to repo ║ +# ║ ~/.config/nvim/ — symlink to repo/nvim/ ║ +# ║ ~/.config/micro/ — copy from repo/micro/ (plugin state needs cp)║ +# ║ ~/.config/alot/ — symlink for email client config ║ +# ║ ~/.config/yazi/ — symlink for file manager config ║ +# ║ ~/Pictures/fflogo.svg — logo asset ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ + set -euo pipefail +# -e: any failed package install or symlink operation aborts the module +# -u: unset variable references are errors +# -o pipefail: catch pipe failures + +# Load shared logging helpers (log, skip, warn, err functions) source "$(dirname "${BASH_SOURCE[0]}")/lib/logging.sh" +# ── System update ────────────────────────────────────────────────────────────── +# WHY: Ensure the package database is current before installing to avoid +# dependency conflicts on a rolling-release distro like Arch. log "Updating system..." sudo pacman -Syu --noconfirm +# ── Base shell packages ──────────────────────────────────────────────────────── +# WHY: Install package-by-package with idempotency checks rather than one big +# pacman call. This lets us skip already-installed packages individually +# and produce useful "already installed" log messages. +# Each package in PACKAGES is checked with `pacman -Qi` (query installed). +# +# Package notes: +# zsh — primary shell (replaces bash as default) +# neovim — primary editor (configured via dotfiles/nvim/) +# pyright — Python LSP server for neovim (coc/LSP plugin) +# bash-language-server — bash LSP for neovim +# btop — interactive resource monitor +# clang — C/C++ toolchain; needed by treesitter + some nvim plugins +# fastfetch — system info display on terminal open +# fzf — fuzzy finder; used by shell history, yazi, neovim +# hyfetch — pride-themed neofetch variant +# lua-language-server — Lua LSP for Hyprland config editing in neovim +# micro — beginner-friendly terminal text editor (alternative to nano) +# pulsemixer — TUI PulseAudio/PipeWire mixer +# yazi — terminal file manager with image preview +# z — smart directory jumper (learns frequent paths) +# qrencode — QR code generator for terminal +# distrobox — run other Linux distros in containers +# dysk — disk usage summary (prettier df alternative) +# glow — render markdown in the terminal +# notmuch — fast email indexer (used by alot mail client) +# alot — TUI email client built on notmuch log "Installing base shell packages..." PACKAGES=(zsh neovim curl pyright bash atftp bash-language-server btop clang fastfetch fzf hyfetch lua-language-server micro nano pulsemixer yazi z qrencode distrobox dysk python python-pip glow notmuch alot) for pkg in "${PACKAGES[@]}"; do + # -Qi queries the local package database; if it returns non-zero, pkg is not installed if ! pacman -Qi "$pkg" &>/dev/null; then log "Installing $pkg..." + # --needed: skip if already at latest version (double-safety with -Qi check) sudo pacman -S --noconfirm --needed "$pkg" else skip "$pkg already installed." fi done -# abook (AUR) +# ── abook (AUR) ─────────────────────────────────────────────────────────────── +# WHY: abook is an address book app that integrates with mail clients like alot. +# It is only available in the AUR (not in official repos), so we use yay. +# IDEMPOTENCY: Check if `abook` binary exists before installing. if ! command -v abook &>/dev/null; then log "Installing abook (AUR)..." yay -S --noconfirm --needed abook @@ -24,7 +86,11 @@ else skip "abook already installed." fi -# yay +# ── yay fallback ────────────────────────────────────────────────────────────── +# WHY: shell-setup.sh can run standalone (e.g. on an existing system that didn't +# go through package-managers.sh first). This guards against yay being absent. +# HOW: Build yay from the AUR using its PKGBUILD. /tmp/yay is ephemeral but fine +# for a build that completes synchronously. if ! command -v yay &>/dev/null; then log "Installing yay..." sudo pacman -S --noconfirm --needed git base-devel @@ -35,66 +101,116 @@ else skip "yay already installed." fi -# Rust / Cargo +# ── Rust / Cargo ────────────────────────────────────────────────────────────── +# WHY: Some shell tools are installed via `cargo install`. This provides the +# Rust toolchain in the user's home directory (~/.cargo/) separate from +# any system-wide Rust installation. +# NOTE: Uses the official rustup installer script rather than the pacman package +# because it installs to ~/.cargo which is user-owned (no sudo needed for +# cargo install later). The -y flag disables interactive prompts. if ! command -v cargo &>/dev/null; then log "Installing Rust & Cargo..." + # --proto: restrict to HTTPS only for security + # --tlsv1.2: require TLS 1.2 minimum + # -s -- -y: pass -y to the installer script (non-interactive) curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + # Source cargo env so `cargo` is available in this shell session immediately . "$HOME/.cargo/env" else skip "Rust & Cargo already installed." fi -# nvm + Node.js +# ── nvm + Node.js ───────────────────────────────────────────────────────────── +# WHY: Same rationale as in package-managers.sh. shell-setup.sh can run +# independently so nvm is bootstrapped here as a fallback. if ! command -v node &>/dev/null; then log "Installing nvm and Node.js..." if [ ! -d "$HOME/.nvm" ]; then curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash fi + # Source nvm into current shell to make the `nvm` command available immediately . "$HOME/.nvm/nvm.sh" nvm install 22 else skip "Node.js already installed." fi +# ── Git global configuration ─────────────────────────────────────────────────── +# WHY: Set neovim as the commit message editor so that `git commit` without -m +# opens neovim instead of vi/nano. This is user-global config (not per-repo). log "Configuring git..." git config --global core.editor nvim +# ── Dotfile deployment ──────────────────────────────────────────────────────── +# WHY: We use symlinks (ln -sf) rather than copies wherever possible so that +# edits to files in ~/Dotfiles/ are immediately reflected in the live config +# without needing to re-run this script. +# EXCEPTION: micro/ is copied because micro stores plugin state files in its +# config directory — symlinks would write plugin data back to the repo. log "Deploying dotfiles..." mkdir -p ~/.config ~/Pictures +# Shell init files — symlink so edits in the repo apply immediately ln -sf ~/Dotfiles/.bashrc ~/.bashrc ln -sf ~/Dotfiles/.zshrc ~/.zshrc + +# Starship prompt config — symlink into ~/.config/ ln -sf ~/Dotfiles/starship.toml ~/.config/starship.toml +# Micro editor — copy (not symlink) because micro writes plugin/state data into +# its config directory and we don't want those writes going into the git repo rm -rf ~/.config/micro cp -r ~/Dotfiles/micro ~/.config/ +# Neovim — symlink the entire config directory to the repo rm -rf ~/.config/nvim ln -sf ~/Dotfiles/nvim ~/.config/nvim +# ── Neovim plugin sync ──────────────────────────────────────────────────────── +# WHY: lazy.nvim (the Neovim plugin manager) needs to download and compile plugins +# on first launch. We run it headlessly here so the first interactive launch +# is fast and plugins are ready. +# HOW: --headless runs neovim without a UI; +Lazy! sync triggers lazy.nvim's +# sync command; +qa quits after sync completes. +# || true: a failed sync is non-fatal — user can run :Lazy sync manually later. log "Syncing neovim plugins (lazy.nvim)..." nvim --headless "+Lazy! sync" +qa 2>/dev/null || true +# alot email client config — symlink rm -rf ~/.config/alot ln -sf ~/Dotfiles/alot ~/.config/alot +# yazi file manager config — symlink rm -rf ~/.config/yazi ln -sf ~/Dotfiles/yazi ~/.config/yazi +# spotify-tui config — symlink (for the TUI Spotify client) rm -rf ~/.config/spotify-tui ln -sf ~/Dotfiles/spotify-tui ~/.config/spotify-tui +# Copy FF (Fastfetch) logo SVG to ~/Pictures for the fastfetch config to reference cp -f ~/Dotfiles/resources/fflogo.svg ~/Pictures/fflogo.svg -# Starship +# ── Starship prompt ──────────────────────────────────────────────────────────── +# WHY: Starship is a fast, customizable cross-shell prompt. The config is in +# dotfiles/starship.toml (symlinked above). The binary itself must be +# installed separately — the official install script handles arch/platform +# detection automatically. +# IDEMPOTENCY: Check for the `starship` binary before running the installer. if ! command -v starship &>/dev/null; then log "Installing Starship..." + # -sS: silent but show errors; --yes: non-interactive curl -sS https://starship.rs/install.sh | sh -s -- --yes else skip "Starship already installed." fi -# oh-my-zsh +# ── oh-my-zsh ───────────────────────────────────────────────────────────────── +# WHY: oh-my-zsh provides the plugin framework, completion system, and themes +# that our .zshrc configuration depends on. It installs to ~/.oh-my-zsh/. +# HOW: Download and run the official install script. +# RUNZSH=no: don't switch to zsh and start a new shell during install +# CHSH=no: don't automatically change the default shell (we do that explicitly below) if [ ! -d "$HOME/.oh-my-zsh" ]; then log "Installing oh-my-zsh..." RUNZSH=no CHSH=no sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" @@ -102,8 +218,13 @@ else skip "oh-my-zsh already installed." fi -# oh-my-zsh plugins +# ── oh-my-zsh plugins ───────────────────────────────────────────────────────── +# WHY: These two plugins are referenced in .zshrc and must exist in ZSH_CUSTOM. +# They are not bundled with oh-my-zsh and must be cloned separately. +# ZSH_CUSTOM defaults to ~/.oh-my-zsh/custom/ — the standard plugin install location. ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" + +# zsh-syntax-highlighting: colors commands as you type (green=valid, red=invalid) if [ ! -d "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting" ]; then log "Installing zsh-syntax-highlighting..." git clone https://github.com/zsh-users/zsh-syntax-highlighting.git \ @@ -111,6 +232,8 @@ if [ ! -d "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting" ]; then else skip "zsh-syntax-highlighting already installed." fi + +# zsh-autosuggestions: suggests previously-typed commands in gray as you type if [ ! -d "$ZSH_CUSTOM/plugins/zsh-autosuggestions" ]; then log "Installing zsh-autosuggestions..." git clone https://github.com/zsh-users/zsh-autosuggestions \ @@ -119,7 +242,10 @@ else skip "zsh-autosuggestions already installed." fi -# Default shell +# ── Default shell change ─────────────────────────────────────────────────────── +# WHY: New login shells still default to bash unless explicitly changed. +# `chsh` writes the new shell to /etc/passwd for this user. +# HOW: Compare current $SHELL to /usr/bin/zsh and change if different. if [ "$SHELL" != "/usr/bin/zsh" ]; then log "Setting zsh as default shell..." chsh -s /usr/bin/zsh diff --git a/setup/reset-arch.sh b/setup/reset-arch.sh index 2d73dc5..a9e4bfc 100755 --- a/setup/reset-arch.sh +++ b/setup/reset-arch.sh @@ -1,4 +1,31 @@ #!/usr/bin/env bash +# ╔══════════════════════════════════════════════════════════════════════════════╗ +# ║ setup/reset-arch.sh — Wipe and reinstall the OS while keeping user data ║ +# ║ ║ +# ║ PURPOSE: ║ +# ║ Performs a clean OS reinstall on an existing btrfs-on-LUKS system ║ +# ║ WITHOUT destroying user home directories or passwords. Useful when ║ +# ║ the system is broken, corrupted, or needs a clean slate. ║ +# ║ ║ +# ║ WHAT THIS DOES: ║ +# ║ 1. Detects LUKS encryption; unlocks via FIDO2 token and/or passphrase ║ +# ║ 2. Saves user credentials and system config from the old @ subvolume ║ +# ║ 3. Clears app configs (~/.config) from @home, preserving auth keys ║ +# ║ (Yubico/ is kept so FIDO2 login works after the reset) ║ +# ║ 4. Deletes and recreates the @ (root) btrfs subvolume ║ +# ║ 5. Reinstalls the base system via pacstrap ║ +# ║ 6. Restores credentials, PAM, fstab, mkinitcpio, GRUB config ║ +# ║ 7. Regenerates initramfs and GRUB menu from chroot ║ +# ║ ║ +# ║ REQUIREMENTS: ║ +# ║ - Run from an Arch Linux live environment (archiso USB) ║ +# ║ - Target disk uses btrfs with @ and @home subvolumes ║ +# ║ - Optional: LUKS2 encryption on the root partition ║ +# ║ ║ +# ║ WARNING: The root subvolume (@ i.e. /etc, /usr, /var) is DESTROYED. ║ +# ║ User home data (@home) is preserved. Run with extreme caution. ║ +# ╚══════════════════════════════════════════════════════════════════════════════╝ + # reset-arch.sh — Reset the root btrfs subvolume while preserving user home data. # # What this does: @@ -11,12 +38,20 @@ # 7. Regenerates initramfs and GRUB menu from chroot so the system boots cleanly set -euo pipefail +# -e: abort immediately on any error — wrong moves here could corrupt the system +# -u: error on unset variable references +# -o pipefail: catch failures in pipes +# ── Temporary working directory ─────────────────────────────────────────────── +# All temporary files (saved configs, mount points) go here. +# Cleaned up automatically on EXIT, INT, or error via trap. TMPDIR=$(mktemp -d /tmp/arch-reset.XXXXXX) trap 'rm -rf "$TMPDIR"' EXIT +# Simple "pause and wait for user" helper pause() { read -rp "Press ENTER to continue..."; } +# ── Banner ──────────────────────────────────────────────────────────────────── echo "=======================================" echo " M-Archy System Reset" echo "=======================================" @@ -28,12 +63,25 @@ echo " • Preserve home directories, passwords, and FIDO2 login keys" echo "" # ── Required tools in live environment ────────────────────────────────────── +# WHY: The archiso live environment has minimal packages. We need these specific +# tools that may not be pre-installed on the ISO: +# - cryptsetup: to detect and unlock LUKS partitions +# - btrfs-progs: to manage btrfs subvolumes +# - jq: used by some sub-scripts +# - libfido2: for FIDO2/token-based LUKS unlock +# -yd: sync database, download-only skip (don't install dependencies blindly) pacman -Syd --noconfirm cryptsetup btrfs-progs jq libfido2 # ── Drive selection ────────────────────────────────────────────────────────── +# List all block devices so the user can see what's available lsblk echo "" read -rp "Enter drive to reset (e.g., /dev/sda): " DRIVE + +# Assume standard partition layout from arch-autoinstall.sh: +# partition 1 = EFI/boot +# partition 2 = root (possibly LUKS) +# NOTE: This assumes a non-NVMe drive. NVMe would need ${DRIVE}p2 etc. ROOT_PART="${DRIVE}2" EFI_PART="${DRIVE}1" @@ -42,12 +90,17 @@ echo "WARNING: The root subvolume on $ROOT_PART will be DELETED and reinstalled. echo " User home directories will be preserved." echo " App configs (~/.config) will be wiped (Yubico auth keys excepted)." echo "" + +# Require explicit "YES" (uppercase) to prevent accidental data loss read -rp "Type YES to continue: " _CONFIRM [[ "$_CONFIRM" == "YES" ]] || { echo "Aborted."; exit 1; } # ── LUKS detection and unlock ──────────────────────────────────────────────── +# Default assumption: root partition is NOT encrypted — MAPPER_DEV points to it directly. +# If LUKS is detected, MAPPER_DEV is updated to /dev/mapper/cryptroot after unlock. MAPPER_DEV="$ROOT_PART" +# cryptsetup isLuks: exits 0 if the partition has a LUKS header, non-zero if not. if cryptsetup isLuks "$ROOT_PART" 2>/dev/null; then echo "" echo "Partition $ROOT_PART is LUKS2-encrypted." @@ -56,21 +109,27 @@ if cryptsetup isLuks "$ROOT_PART" 2>/dev/null; then echo " 2) Passphrase only" echo " 3) Enrolled token only (FIDO2/TPM2)" read -rp "Choice [1]: " _UNLOCK - _UNLOCK="${_UNLOCK:-1}" + _UNLOCK="${_UNLOCK:-1}" # Default to option 1 if user just presses Enter case "$_UNLOCK" in 1) + # Best of both worlds: try hardware token first, fall back to passphrase. + # This works whether or not the user has their FIDO2 key with them. echo "Insert FIDO2 key if using one, then press ENTER..." pause + # --token-only: use enrolled tokens (FIDO2, TPM2) without prompting passphrase + # If token unlock fails (key not present / not enrolled), fall back to password if ! cryptsetup open --token-only "$ROOT_PART" cryptroot 2>/dev/null; then echo "Token unlock failed — enter passphrase..." cryptsetup open "$ROOT_PART" cryptroot fi ;; 2) + # Traditional passphrase-only unlock — no hardware key needed cryptsetup open "$ROOT_PART" cryptroot ;; 3) + # Token-only mode — will fail if key isn't present/enrolled echo "Insert FIDO2 key and press ENTER..." pause cryptsetup open --token-only "$ROOT_PART" cryptroot @@ -80,97 +139,143 @@ if cryptsetup isLuks "$ROOT_PART" 2>/dev/null; then cryptsetup open "$ROOT_PART" cryptroot ;; esac + # After unlock, the decrypted device is available at /dev/mapper/cryptroot MAPPER_DEV="/dev/mapper/cryptroot" echo "Partition unlocked." fi # ── Detect installed kernel from EFI partition ─────────────────────────────── +# WHY: We need to know which kernel to reinstall (linux, linux-lts, linux-zen, etc.). +# HOW: Mount the EFI partition temporarily and look for vmlinuz-* kernel images. +# The filename suffix is the package name (e.g. vmlinuz-linux-lts → linux-lts). TMPBOOT=$(mktemp -d /tmp/arch-reset-boot.XXXXXX) mount "$EFI_PART" "$TMPBOOT" -KERNEL_PKG="linux" +KERNEL_PKG="linux" # Default to mainline kernel for _img in "$TMPBOOT"/vmlinuz-*; do + # Loop over glob; first match wins (strip the vmlinuz- prefix) [[ -f "$_img" ]] && KERNEL_PKG=$(basename "$_img" | sed 's/^vmlinuz-//') && break done umount "$TMPBOOT"; rmdir "$TMPBOOT" echo "Detected kernel: $KERNEL_PKG" # ── Detect GPU from live hardware ──────────────────────────────────────────── +# WHY: Different GPU vendors need different Xorg/DRM driver packages. +# lspci reads the PCI device list; grep filters for display-related devices +# (VGA=traditional GPU, 3D=compute GPU/integrated graphics). +# GPU_PKGS will be passed to pacstrap as an extra package. GPU_INFO=$(lspci 2>/dev/null | grep -E "VGA|3D" || true) GPU_PKGS="" -if echo "$GPU_INFO" | grep -qi nvidia; then GPU_PKGS="nvidia-open" -elif echo "$GPU_INFO" | grep -qi amd; then GPU_PKGS="xf86-video-amdgpu" -elif echo "$GPU_INFO" | grep -qi intel; then GPU_PKGS="xf86-video-intel" +if echo "$GPU_INFO" | grep -qi nvidia; then GPU_PKGS="nvidia-open" # NVIDIA open-source driver +elif echo "$GPU_INFO" | grep -qi amd; then GPU_PKGS="xf86-video-amdgpu" # AMD open-source driver +elif echo "$GPU_INFO" | grep -qi intel; then GPU_PKGS="xf86-video-intel" # Intel driver fi +# If no GPU detected, GPU_PKGS remains empty and the kernel's DRM will handle it # ── Mount btrfs top-level ──────────────────────────────────────────────────── +# WHY: We need to access the raw btrfs filesystem (not via a subvolume) so we +# can manipulate subvolumes directly. +# HOW: subvolid=5 is always the btrfs top-level (root of all subvolumes). BTRFS_MNT="$TMPDIR/btrfs" mkdir -p "$BTRFS_MNT" mount -o subvolid=5 "$MAPPER_DEV" "$BTRFS_MNT" # ── Save critical configuration from @ ────────────────────────────────────── +# WHY: We're about to destroy the @ subvolume, which contains /etc. +# We need to preserve critical system files so the reinstalled system +# has the same users, passwords, locales, hostname, and boot config. echo "Saving system configuration..." SAVED="$TMPDIR/saved" +# _save: helper to copy a file/dir from the old @ subvolume to our save area. +# Skips silently if the source doesn't exist (not all systems have all configs). _save() { local src="$BTRFS_MNT/@/etc/$1" local dst="$SAVED/etc/$1" - [[ -e "$src" ]] || return 0 + [[ -e "$src" ]] || return 0 # Skip if source doesn't exist mkdir -p "$(dirname "$dst")" - cp -a "$src" "$dst" + cp -a "$src" "$dst" # -a: archive mode (preserves permissions, timestamps, symlinks) } +# User and group databases — contain usernames, UIDs, group memberships _save passwd -_save shadow +_save shadow # Hashed passwords (mode 000 — only root can read) _save group -_save gshadow +_save gshadow # Group passwords (mode 000) + +# Privilege escalation config _save sudoers _save sudoers.d + +# PAM (Pluggable Authentication Modules) — contains FIDO2 PAM config +# WHY: Must preserve this so FIDO2 login works after reset _save pam.d + +# System identity and locale _save hostname _save locale.conf _save locale.gen -_save vconsole.conf -_save fstab -_save mkinitcpio.conf -_save mkinitcpio.conf.d -_save default/grub -_save NetworkManager +_save vconsole.conf # Console keymap and font settings -# Save timezone symlink target as plain text (the symlink itself can't cross roots) +# Boot configuration +_save fstab # Mount point definitions (UUIDs etc.) +_save mkinitcpio.conf # Initramfs hooks config (encryption hooks must be preserved) +_save mkinitcpio.conf.d +_save default/grub # GRUB cmdline (has cryptdevice= or rd.luks.name= parameters) + +# Network connection profiles +_save NetworkManager # Contains /etc/NetworkManager/system-connections/ WiFi passwords + +# Save timezone symlink target as plain text because symlinks can't cross roots. +# The symlink /etc/localtime → /usr/share/zoneinfo/Europe/Vienna would be broken +# if we copied the symlink itself into a different root. { readlink "$BTRFS_MNT/@/etc/localtime" 2>/dev/null || echo "/usr/share/zoneinfo/UTC"; } \ > "$SAVED/timezone" # ── Clear ~/.config in @home (preserve auth-critical subdirs) ──────────────── +# WHY: After a system reset, old app configs may be incompatible with the +# freshly installed versions. Clearing them gives apps a clean start. +# EXCEPTION: Yubico/ and pam-u2f/ contain FIDO2/U2F PAM key files. +# Deleting these would break FIDO2 login for all users after the reset. echo "Clearing user app configs..." -# Yubico/ holds U2F/FIDO2 PAM keys — deleting these would break FIDO2 login PRESERVED_CONFIG_DIRS=("Yubico" "pam-u2f") +# Iterate over all user home directories in the @home subvolume for _homedir in "$BTRFS_MNT/@home"/*/; do - [[ -d "$_homedir" ]] || continue + [[ -d "$_homedir" ]] || continue # Skip if glob matched nothing _user=$(basename "$_homedir") _cfgdir="$_homedir/.config" - [[ -d "$_cfgdir" ]] || continue + [[ -d "$_cfgdir" ]] || continue # Skip users with no .config echo " Clearing ~/.config for: $_user" - # Top-level files in .config + + # Delete all top-level files in .config (not directories — handled separately) + # -mindepth 1: don't delete .config itself + # -maxdepth 1: only direct children + # ! -type d: only files, not directories find "$_cfgdir" -mindepth 1 -maxdepth 1 ! -type d -delete - # Subdirectories, skipping preserved ones + + # Delete subdirectories, except preserved ones + # Using process substitution + read with NUL delimiter for paths with spaces while IFS= read -r -d '' _subdir; do _dname=$(basename "$_subdir") _skip=false + # Check if this directory is in the preserved list for _keep in "${PRESERVED_CONFIG_DIRS[@]}"; do [[ "$_dname" == "$_keep" ]] && _skip=true && break done - $_skip || rm -rf "$_subdir" + $_skip || rm -rf "$_subdir" # Delete if not in preserved list done < <(find "$_cfgdir" -mindepth 1 -maxdepth 1 -type d -print0) done # ── Delete @ and recreate fresh ────────────────────────────────────────────── +# WHY: btrfs subvolume delete removes the entire @ filesystem tree instantly. +# Creating a new empty @ gives us a clean slate for pacstrap. echo "Deleting root subvolume @..." btrfs subvolume delete "$BTRFS_MNT/@" echo "Creating fresh root subvolume @..." btrfs subvolume create "$BTRFS_MNT/@" # ── Mount for installation ─────────────────────────────────────────────────── +# Mount the new empty @ subvolume and the existing @home subvolume for pacstrap. umount "$BTRFS_MNT" mount -o subvol=@ "$MAPPER_DEV" /mnt mkdir -p /mnt/home @@ -179,58 +284,90 @@ mkdir -p /mnt/boot mount "$EFI_PART" /mnt/boot # ── Pacstrap base system ───────────────────────────────────────────────────── +# WHY: pacstrap installs packages into the new root at /mnt. +# We install only the minimal base needed to boot and manage the system. +# The full package set is reinstalled via tui-install.sh after first boot. echo "Reinstalling base system (this will take a while)..." -# shellcheck disable=SC2086 +# shellcheck disable=SC2086 — GPU_PKGS is intentionally word-split here pacstrap /mnt \ base base-devel "$KERNEL_PKG" linux-firmware vim zsh git networkmanager grub efibootmgr \ btrfs-progs cryptsetup libfido2 pam-u2f sudo less jq $GPU_PKGS # ── Restore saved configuration ────────────────────────────────────────────── +# WHY: The fresh @ has default /etc files from pacstrap. We overwrite them +# with the saved versions to restore users, passwords, hostname, etc. echo "Restoring system configuration..." +# _restore: copy from save area into the new /mnt/etc _restore() { local src="$SAVED/etc/$1" local dst="/mnt/etc/$1" - [[ -e "$src" ]] || return 0 + [[ -e "$src" ]] || return 0 # Skip if we didn't save this file mkdir -p "$(dirname "$dst")" cp -a "$src" "$dst" } -# Auth files — explicit permissions +# Auth files get explicit permission modes with `install` rather than `cp -a` +# because cp -a preserves the original permissions which may be wrong after +# copying across filesystem boundaries. +# passwd/group: world-readable (644), shadow/gshadow: inaccessible to non-root (000) [[ -f "$SAVED/etc/passwd" ]] && install -m 644 "$SAVED/etc/passwd" /mnt/etc/passwd [[ -f "$SAVED/etc/group" ]] && install -m 644 "$SAVED/etc/group" /mnt/etc/group [[ -f "$SAVED/etc/shadow" ]] && install -m 000 "$SAVED/etc/shadow" /mnt/etc/shadow [[ -f "$SAVED/etc/gshadow" ]] && install -m 000 "$SAVED/etc/gshadow" /mnt/etc/gshadow +# Restore everything else using the _restore helper _restore sudoers _restore sudoers.d -_restore pam.d +_restore pam.d # FIDO2 PAM rules restored here _restore hostname _restore locale.conf _restore locale.gen _restore vconsole.conf -_restore fstab -_restore mkinitcpio.conf +_restore fstab # CRITICAL: without fstab the system won't mount properly on boot +_restore mkinitcpio.conf # CRITICAL: must have correct encryption hooks _restore mkinitcpio.conf.d -_restore default/grub +_restore default/grub # CRITICAL: must have correct cryptdevice= kernel parameters _restore NetworkManager +# Restore the timezone symlink using the saved target path TZ_TARGET=$(cat "$SAVED/timezone") ln -sf "$TZ_TARGET" /mnt/etc/localtime # ── Chroot: regenerate initramfs, GRUB menu, services ─────────────────────── +# WHY: mkinitcpio and grub-mkconfig must run from inside the new root because +# they reference files and kernel modules specific to that root. +# Running them from outside would produce incorrect initramfs/GRUB configs. echo "Finalizing inside chroot..." arch-chroot /mnt /bin/bash <<'CHROOT_EOF' set -euo pipefail +# Regenerate locale files (locale.gen was restored, re-run locale-gen to apply) locale-gen + +# Sync hardware clock from system time hwclock --systohc + +# Enable NetworkManager so the system has network on first boot systemctl enable NetworkManager + +# Regenerate initramfs for ALL installed kernels (-P = all presets) +# WHY: The restored mkinitcpio.conf has the correct hooks (encrypt, btrfs, etc.) +# but the initramfs itself was in the old @ and is now gone. mkinitcpio -P + +# Regenerate GRUB boot menu +# WHY: GRUB needs a fresh grub.cfg pointing to the new kernel images. +# The restored /etc/default/grub already has the correct cmdline. grub-mkconfig -o /boot/grub/grub.cfg -# Re-apply correct ownership for user home directories using restored UIDs +# Fix home directory ownership using the restored UID/GID from /etc/passwd +# WHY: @home was preserved but the new @ has a fresh uid/gid namespace until +# /etc/passwd is restored. Now that passwd is in place, we re-apply +# ownership to ensure home dirs are accessible by the right users. +# IFS=: splits /etc/passwd fields by colon; fields: name:pass:uid:gid:gecos:home:shell while IFS=: read -r _uname _ _uid _gid _ _home _; do + # Only fix user accounts (UID >= 1000), not system accounts (( _uid >= 1000 )) && [[ -d "$_home" ]] && chown -R "${_uid}:${_gid}" "$_home" || true done < /etc/passwd CHROOT_EOF diff --git a/setup/simple-install.sh b/setup/simple-install.sh index 248d696..6ad7049 100755 --- a/setup/simple-install.sh +++ b/setup/simple-install.sh @@ -1,36 +1,65 @@ #!/bin/bash # simple-install.sh — TUI installer for the_miro's Arch dotfiles + +# -u: error on unset variables; -o pipefail: a pipe fails if any stage fails. +# Intentionally omits -e so individual module failures can be handled explicitly. set -uo pipefail # ── Paths ───────────────────────────────────────────────────────────────────── +# Resolve the installer's own directory regardless of where it was invoked from. +# BASH_SOURCE[0] remains correct even when the script is sourced. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Dotfiles root is one level above setup/. DOTFILES_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" MODULES="$DOTFILES_DIR/setup/modules" APPS="$MODULES/optional-Modules/apps" LOG="$HOME/dotfiles-install.log" +# Private scratch space for temporary files (e.g. a transient colors.conf). +# mktemp -d creates a unique directory visible only to this process. TMP_D="$(mktemp -d)" +# Cleanup trap: remove scratch dir and restore terminal state on any exit signal. +# tput reset issues a full terminal reset; stty sane is the bare-console fallback. +# '|| true' ensures the trap never masks the real process exit code. trap 'rm -rf "$TMP_D"; tput reset 2>/dev/null || stty sane 2>/dev/null || true' EXIT INT TERM HUP +# Allow callers to inject an alternative answerfile via the ANSWERFILE env var. +# /answerfile.json is a conventional location for CI/PXE unattended installs. ANSWERFILE="${ANSWERFILE:-/answerfile.json}" ANSWERFILE_MODE=false +# Activate unattended mode only if the answerfile is present on disk. [[ -f "$ANSWERFILE" ]] && ANSWERFILE_MODE=true TITLE="the_miro's Arch Dotfiles" # ── Terminal dimensions ─────────────────────────────────────────────────────── +# tput may fail on a bare TTY with no TERM set; fall back to safe 80×24 defaults. TERM_H=$(tput lines 2>/dev/null || echo 24) TERM_W=$(tput cols 2>/dev/null || echo 80) +# Cap the TUI width at 72 columns but shrink to fit narrow terminals. +# Leaving a 2-column margin (TERM_W - 2) prevents line wrapping on small screens. TUI_W=$(( TERM_W < 74 ? TERM_W - 2 : 72 )) +# Build a horizontal rule string of exactly TUI_W '─' characters. +# printf -v assigns the result to TUI_BAR without printing it. +# '%.0s─' prints zero characters from the argument then appends '─', repeated +# once per number from seq, producing TUI_W copies of the character. printf -v TUI_BAR '%.0s─' $(seq 1 "$TUI_W") # ── ANSI color codes ────────────────────────────────────────────────────────── +# Short variable names to keep printf format strings readable. +# R=reset, B=bold, D=dim, M=magenta+bold, CY=cyan+bold. +# $'\e[...' is the ANSI CSI escape sequence; 0m resets all attributes. R=$'\e[0m'; B=$'\e[1m'; D=$'\e[2m'; M=$'\e[1;35m'; CY=$'\e[1;36m' # ── TUI primitives ──────────────────────────────────────────────────────────── # All display goes to /dev/tty so functions work correctly inside $() captures. +# If output went to stdout, the calling $() subshell would swallow the UI text +# along with the return value, making the screen appear blank. _hdr() { + # \e[2J clears the screen; \e[H moves the cursor to the top-left. + # The box is drawn using TUI_BAR (pre-built ─ string) and box-drawing chars. + # %-*s with width (TUI_W - 4) left-pads the title to fill the box exactly. printf '\e[2J\e[H' >/dev/tty printf "${M} ┌%s┐${R}\n ${M}│ ${B}%-*s${R} ${M}│${R}\n ${M}└%s┘${R}\n\n" \ "$TUI_BAR" $(( TUI_W - 4 )) "$TITLE" "$TUI_BAR" >/dev/tty @@ -42,7 +71,10 @@ _sep() { printf " ${D}─────────────────── tui_msg() { _hdr printf " ${M}${B}%s${R}\n" "$1" >/dev/tty; _sep + # %b interprets escape sequences in MSG so callers can embed \n for newlines. printf "%b\n\n" "$2" >/dev/tty + # Blocks until Enter; 'read -r </dev/tty' reads from the terminal even when + # the function is called inside a $() subshell that has stdin redirected. printf " ${D}Press Enter to continue...${R}" >/dev/tty; read -r </dev/tty } @@ -54,6 +86,8 @@ tui_yesno() { local a while true; do printf " ${CY}[Y/n]${R} > " >/dev/tty; read -r a </dev/tty + # ${a,,} lowercases the input; empty input (bare Enter) defaults to "yes" + # to follow the conventional [Y/n] prompt behaviour. case "${a,,}" in y|yes|"") return 0 ;; n|no) return 1 ;; @@ -69,6 +103,8 @@ tui_input() { printf " %b\n" "$2" >/dev/tty [[ -n "${3:-}" ]] && printf " ${D}(default: %s)${R}\n" "$3" >/dev/tty printf "\n > " >/dev/tty; local v; read -r v </dev/tty + # If the user pressed Enter without typing, fall back to the DEFAULT argument. + # The nested :- handles both an empty input and a missing DEFAULT gracefully. printf '%s' "${v:-${3:-}}" } @@ -78,25 +114,33 @@ tui_input() { # Arrow keys navigate · Space toggles · a selects all · Enter/n confirms tui_checklist() { local title="$1" prompt="$2"; shift 2 + # Parse triplets (tag, description, state) into three parallel arrays. + # Using parallel arrays avoids nested data structures (unavailable in bash 4). local -a _T _D _S while [[ $# -ge 3 ]]; do _T+=("$1"); _D+=("$2"); _S+=("$3"); shift 3; done local n=${#_T[@]} local _tv _vp _cur _scr _VIS _k _k2 _k3 _k4 _i _j _f _l _chk _res _toti _posi i + # _vp = number of visible list rows; reserve 11 rows for header, prompt, and status bar. _tv=$(tput lines 2>/dev/null || echo 24) _vp=$(( _tv - 11 )) (( _vp < 4 )) && _vp=4 + # Pre-count selectable (non-header) items to display the [N/total] counter. _toti=0 for (( i=0; i<n; i++ )); do [[ "${_S[$i]}" != "header" ]] && (( _toti++ )); done + # Initialise cursor to the first non-header item so the cursor is never + # parked on an unselectable section label. _cur=0 for (( i=0; i<n; i++ )); do [[ "${_S[$i]}" != "header" ]] && { _cur=$i; break; } done _scr=0; _VIS=0 - # Count visual lines from index _s up to (not including) _e; result in _VIS + # Count visual lines from index _s up to (not including) _e; result in _VIS. + # Headers take 1 line when at the top of the viewport, 2 lines otherwise + # (a blank separator line is printed before each non-first header). __cl_vis() { local _s=$1 _e=$2; _VIS=0 for (( i=_s; i<_e && i<n; i++ )); do @@ -108,7 +152,8 @@ tui_checklist() { done } - # Ensure _cur is within the visible viewport; adjust _scr if needed + # Ensure _cur is within the visible viewport; adjust _scr (scroll offset) if needed. + # Scrolls _scr forward one step at a time until _cur fits within _vp visual lines. __cl_sync() { (( _cur < _scr )) && _scr=$_cur while true; do @@ -129,18 +174,22 @@ tui_checklist() { for (( i=_scr; i<n; i++ )); do if [[ "${_S[$i]}" == "header" ]]; then if (( i == _scr )); then + # First visible item is a header: print it without a leading blank line. (( _l + 1 > _vp )) && break printf " ${M}── %s${R}\n" "${_D[$i]}" >/dev/tty (( _l++ )) else + # Non-first header: add a blank separator line before it (costs 2 rows). (( _l + 2 > _vp )) && break printf "\n ${M}── %s${R}\n" "${_D[$i]}" >/dev/tty (( _l+=2 )) fi else (( _l + 1 > _vp )) && break + # _chk is the checkbox fill: '*' when selected, ' ' when not. _chk=" "; [[ "${_S[$i]}" == "on" ]] && _chk="*" if (( i == _cur )); then + # Highlight the cursor row with cyan arrow and colour. printf " ${CY}▶ [%s] %-22s${R} %s\n" "$_chk" "${_T[$i]}" "${_D[$i]}" >/dev/tty else printf " [%s] %-22s %s\n" "$_chk" "${_T[$i]}" "${_D[$i]}" >/dev/tty @@ -149,6 +198,8 @@ tui_checklist() { fi done + # Compute the 1-based position of the cursor among selectable items for + # the status bar counter, e.g. "[7/42]". _posi=0 for (( i=0; i<_cur; i++ )); do [[ "${_S[$i]}" != "header" ]] && (( _posi++ )); done (( _posi++ )) @@ -160,15 +211,22 @@ tui_checklist() { __cl_sync __cl_draw + # Read a keypress: -r disables backslash interpretation, -s suppresses echo, + # -n1 reads exactly one byte at a time. IFS= prevents whitespace trimming. IFS= read -rsn1 _k </dev/tty if [[ "$_k" == $'\e' ]]; then + # Escape is the lead byte of multi-byte arrow/page key sequences (ESC [ X). + # Read up to three more bytes with a short timeout (-t 0.05 s) so a bare + # Escape key press doesn't hang waiting for bytes that will never arrive. IFS= read -rsn1 -t 0.05 _k2 </dev/tty if [[ "$_k2" == '[' ]]; then IFS= read -rsn1 -t 0.05 _k3 </dev/tty if [[ "$_k3" =~ [0-9] ]]; then + # Four-byte sequence (e.g. Page Up ESC[5~, Page Down ESC[6~). IFS= read -rsn1 -t 0.05 _k4 </dev/tty _k="${_k}${_k2}${_k3}${_k4}" else + # Three-byte sequence (e.g. Up ESC[A, Down ESC[B, Home ESC[H, End ESC[F). _k="${_k}${_k2}${_k3}" fi else @@ -185,15 +243,16 @@ tui_checklist() { for (( _i=_cur+1; _i<n; _i++ )); do [[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; } done ;; - $'\e[5~') # Page Up + $'\e[5~') # Page Up — jump half a viewport upward for (( _j=0; _j < _vp/2; _j++ )); do _f=0 for (( _i=_cur-1; _i>=0; _i-- )); do [[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; _f=1; break; } done + # _f=0 means we hit the top; stop the outer loop early. (( _f == 0 )) && break done ;; - $'\e[6~') # Page Down + $'\e[6~') # Page Down — jump half a viewport downward for (( _j=0; _j < _vp/2; _j++ )); do _f=0 for (( _i=_cur+1; _i<n; _i++ )); do @@ -201,24 +260,26 @@ tui_checklist() { done (( _f == 0 )) && break done ;; - $'\e[H') # Home + $'\e[H') # Home — jump to first selectable item; also reset scroll for (( _i=0; _i<n; _i++ )); do [[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; } done; _scr=0 ;; - $'\e[F') # End + $'\e[F') # End — jump to last selectable item for (( _i=n-1; _i>=0; _i-- )); do [[ "${_S[$_i]}" != "header" ]] && { _cur=$_i; break; } done ;; - ' ') # Space — toggle current item + ' ') # Space — toggle current item between on/off [[ "${_S[$_cur]}" == "on" ]] && _S[$_cur]="off" || _S[$_cur]="on" ;; - 'a') # Select all + 'a') # Select all selectable items at once for (( _i=0; _i<n; _i++ )); do [[ "${_S[$_i]}" != "header" ]] && _S[$_i]="on" done ;; - ''|'n') break ;; # Enter or n — confirm + ''|'n') break ;; # Enter or n — confirm and exit the loop esac done + # Collect tags of all "on" items into a space-separated string. + # "${_res% }" trims the trailing space added by the loop. _res="" for (( i=0; i<n; i++ )); do [[ "${_S[$i]}" == "on" ]] && _res+="${_T[$i]} "; done printf '%s' "${_res% }" diff --git a/setup/tui-install.sh b/setup/tui-install.sh index 566c72d..a505da6 100755 --- a/setup/tui-install.sh +++ b/setup/tui-install.sh @@ -1,29 +1,50 @@ #!/bin/bash # tui-install.sh — TUI installer for the_miro's Arch dotfiles + +# -u: treat unset variables as errors; -o pipefail: a pipe fails if any stage fails. +# Intentionally omits -e so individual module failures can be handled explicitly. set -uo pipefail # ── Paths ───────────────────────────────────────────────────────────────────── +# Resolve the installer's own directory regardless of where the script was called from. +# BASH_SOURCE[0] stays correct even when the script is sourced rather than executed directly. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Dotfiles root is one level up from setup/. DOTFILES_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" MODULES="$DOTFILES_DIR/setup/modules" APPS="$MODULES/optional-Modules/apps" LOG="$HOME/dotfiles-install.log" +# Private scratch space for the custom DIALOGRC theme file and a temporary colors.conf. +# mktemp -d creates a uniquely named directory that only this process owns. TMP_D="$(mktemp -d)" +# Cleanup trap: remove the scratch dir and restore the terminal state on any exit signal. +# tput reset issues a full terminal reset sequence; stty sane is the bare-console fallback. +# '|| true' ensures the trap never returns a non-zero code, which would mask the real exit status. trap 'rm -rf "$TMP_D"; tput reset 2>/dev/null || stty sane 2>/dev/null || true' EXIT INT TERM HUP +# Allow the caller to inject a different answerfile path via the ANSWERFILE env var. +# The default /answerfile.json is a conventional location for CI/PXE boot images. ANSWERFILE="${ANSWERFILE:-/answerfile.json}" ANSWERFILE_MODE=false +# Enable unattended mode only when the answerfile is actually present on disk. [[ -f "$ANSWERFILE" ]] && ANSWERFILE_MODE=true BACKTITLE="the_miro's Arch Dotfiles" # ── Terminal dimensions (bare console safe) ─────────────────────────────────── +# tput may fail on a bare TTY with no TERM set; fall back to safe 80×24 defaults +# so dialog sizing arithmetic never operates on empty strings. TERM_H=$(tput lines 2>/dev/null || echo 24) TERM_W=$(tput cols 2>/dev/null || echo 80) # ── Cyberqueer dialog theme ─────────────────────────────────────────────────── +# dialog reads its color scheme from $DIALOGRC at startup. +# Writing it to TMP_D (rather than ~/.dialogrc) avoids permanently altering +# the user's existing dialog configuration on the live system. export DIALOGRC="$TMP_D/dialogrc" +# The heredoc uses single-quoted 'EOF' so no variable expansion happens inside; +# every value is a literal dialog color tuple: (FG, BG, BOLD). cat > "$DIALOGRC" <<'EOF' use_shadow = ON use_colors = ON @@ -54,29 +75,39 @@ darrow_color = (MAGENTA,BLACK,ON) EOF # ── State ───────────────────────────────────────────────────────────────────── +# STEP counts completed modules; TOTAL is pre-computed by count_steps() before +# installation begins so run_module() can print accurate [N/TOTAL] progress. STEP=0 TOTAL=0 # ── Helpers ─────────────────────────────────────────────────────────────────── require_dialog() { + # Short-circuit if dialog is already present; otherwise bootstrap it with pacman. + # This allows the script to be run on a fresh Arch install that hasn't yet installed dialog. command -v dialog &>/dev/null && return echo "dialog not found — installing..." sudo pacman -S --noconfirm dialog || { echo "Failed to install dialog."; exit 1; } } require_jq() { + # Same bootstrap pattern as require_dialog: jq is only needed in answerfile mode + # but we install it on demand to avoid a hard dependency for interactive use. command -v jq &>/dev/null && return echo "jq not found — installing..." sudo pacman -S --noconfirm jq || { echo "Failed to install jq."; exit 1; } } die() { + # Fatal error helper: clear the dialog overlay before printing so the message + # is readable, then exit 1. Uses stderr to keep it out of any $() capture. clear printf "\n Error: %s\n\n" "$1" >&2 exit 1 } log_sep() { + # Write a visible separator plus a timestamp to the log before each module run. + # Appended with >> so earlier log entries are never overwritten mid-install. printf "\n══════════════════════════════════\n %s\n %s\n" "$1" "$(date)" >> "$LOG" } @@ -85,17 +116,26 @@ run_module() { STEP=$(( STEP + 1 )) log_sep "[$STEP/$TOTAL] $label" + # Clear the dialog overlay so module output scrolls cleanly on the raw terminal. clear printf "\n\033[1;35m [$STEP/$TOTAL] %s\033[0m\n" "$label" printf "\033[35m ─────────────────────────────────────────────\033[0m\n\n" + # Run the module script, merging stderr into stdout, and tee to the log. + # PIPESTATUS[0] captures the exit code of 'bash "$script"' even though + # 'tee' is the last command in the pipe (and would always succeed). local rc=0 bash "$script" 2>&1 | tee -a "$LOG" || rc=${PIPESTATUS[0]} if [[ $rc -ne 0 ]]; then if [[ $ANSWERFILE_MODE == true ]]; then + # Unattended mode: log the failure and keep going; aborting mid-CI + # would require a full manual re-run. printf "\n Warning: %s exited with code %d — continuing.\n" "$label" "$rc" | tee -a "$LOG" else + # Interactive mode: ask the user whether to abort or continue. + # 'dialog --yesno' returns 0 for Yes and 1 for No; the '|| { ...; exit 1; }' + # fires on No, aborting the install. dialog --backtitle "$BACKTITLE" \ --title " Module Failed " \ --yesno "$label exited with code $rc.\n\nContinue anyway?" 8 54 \ @@ -105,13 +145,22 @@ run_module() { } count_steps() { + # Pre-count the total number of modules that will actually run so that + # run_module() can display accurate [N/TOTAL] progress from the first step. + # Each glob test mirrors the corresponding run_module() call below; they + # must stay in sync whenever a new module is added or removed. local c="$1" de="$2" a="${3:-}" TOTAL=0 + # Base components: each keyword maps to exactly one module script. [[ "$c" == *"pkg"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$c" == *"core"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$c" == *"svc"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$c" == *"shell"* ]] && TOTAL=$(( TOTAL + 1 )) + # A non-"none" DE selection always installs exactly one DE module. [[ "$de" != "none" ]] && TOTAL=$(( TOTAL + 1 )) + # Optional app modules: one glob check per app, one increment per match. + # Glob syntax *"tag"* matches if the space-separated SELECTED_APPS string + # contains the tag anywhere — works because tags never share substrings. [[ "$a" == *"ollama"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"llama-cpp"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"open-webui"* ]] && TOTAL=$(( TOTAL + 1 )) @@ -190,6 +239,9 @@ count_steps() { } # ── Answerfile ──────────────────────────────────────────────────────────────── +# AF_* variables hold values parsed from the JSON answerfile. +# They are initialised to safe defaults so the rest of the script can reference +# them unconditionally whether or not answerfile mode is active. AF_HOSTNAME="" AF_COMPONENTS="" AF_DE="none" @@ -202,10 +254,16 @@ AF_COLOR_RED="" load_answerfile() { require_jq + # -r outputs raw strings without JSON quoting. + # '// ""' / '// "none"' are jq's null-coalescing operator: if the key is + # absent from the file the expression yields the fallback instead. AF_HOSTNAME=$(jq -r '.hostname // ""' "$ANSWERFILE") + # JSON arrays are flattened to a space-separated string to match the format + # that COMPONENTS and SELECTED_APPS expect throughout the rest of the script. AF_COMPONENTS=$(jq -r '(.components // []) | join(" ")' "$ANSWERFILE") AF_DE=$(jq -r '.desktop_environment // "none"' "$ANSWERFILE") AF_APPS=$(jq -r '(.apps // []) | join(" ")' "$ANSWERFILE") + # Color values are optional; an empty string means "keep the repo default". AF_COLOR_TEXT=$(jq -r '.colors.COLOR_TEXT // ""' "$ANSWERFILE") AF_COLOR_BG=$(jq -r '.colors.COLOR_BG // ""' "$ANSWERFILE") AF_COLOR_HIGHLIGHT=$(jq -r '.colors.COLOR_HIGHLIGHT // ""' "$ANSWERFILE") @@ -215,22 +273,35 @@ load_answerfile() { # ── MAC address helper ──────────────────────────────────────────────────────── get_mac_suffix() { + # Returns the MAC address of the first non-loopback interface, colons stripped. + # Used to make answerfile-derived hostnames unique per machine (e.g. "myhost-aabbccddee"). local mac + # awk state machine: sets iface=1 when it sees an interface line whose name + # does NOT start with "lo" ([^l][^o] skips "lo"), then captures the + # 'link/ether XX:XX:XX:XX:XX:XX' line and exits immediately after the first hit. mac=$(ip link show 2>/dev/null \ | awk '/^[0-9]+: [^l][^o]/{iface=1} iface && /link\/ether/{print $2; iface=0; exit}') + # Strip colons from the MAC so it is safe to embed in a hostname (only [a-z0-9-] allowed). printf '%s' "${mac//:/}" } # ── Preflight ───────────────────────────────────────────────────────────────── if [[ $EUID -eq 0 ]]; then # Root context (e.g. archiso chroot): shim sudo as a passthrough + # Module scripts call 'sudo' for privilege operations; when we are already root + # there is no sudo binary (or it may be absent), so inject a fake one that is + # just 'exec "$@"' — transparently forwarding all arguments. mkdir -p "$TMP_D/bin" printf '#!/bin/bash\nexec "$@"\n' > "$TMP_D/bin/sudo" chmod +x "$TMP_D/bin/sudo" + # Prepend to PATH so this shim takes precedence over any real sudo. export PATH="$TMP_D/bin:$PATH" fi +# Hard guard: pacman is the package manager used by every module; without it +# the installer cannot function on this OS. command -v pacman &>/dev/null || die "pacman not found — Arch Linux required." +# Ensure the dialog binary is available before we try to use it. require_dialog if $ANSWERFILE_MODE; then @@ -238,10 +309,17 @@ if $ANSWERFILE_MODE; then printf "Answerfile mode: %s\n" "$ANSWERFILE" | tee -a "$LOG" fi +# Network check: -c1 sends one ICMP echo, -W3 waits up to 3 seconds for reply. +# We test against archlinux.org because that's where pacman will fetch packages. if ! ping -c1 -W3 archlinux.org &>/dev/null; then if $ANSWERFILE_MODE; then + # In unattended mode just warn; the caller decides whether the image + # is expected to be offline (e.g. local mirror pre-seeded in pacman). printf "Warning: no internet connection detected.\n" | tee -a "$LOG" else + # Give the user a chance to plug in a cable or run iwctl before we + # re-test. A second failed ping offers a soft-abort rather than a + # hard failure, in case the user wants to proceed with a local mirror. dialog --backtitle "$BACKTITLE" \ --title " No Network Detected " \ --msgbox "\n No internet connection found.\n\n Wired: ensure the cable is plugged in.\n WiFi: switch to another TTY (Alt+F2)\n and run: iwctl\n\n Press OK once connected.\n" 13 58 @@ -254,6 +332,8 @@ if ! ping -c1 -W3 archlinux.org &>/dev/null; then fi fi +# Truncate the log file now that preflight passed; all prior output was to stdout. +# Entries appended from this point on are the authoritative install log. > "$LOG" printf "Dotfiles install: %s\nDotfiles dir: %s\n" "$(date)" "$DOTFILES_DIR" >> "$LOG" @@ -276,11 +356,18 @@ fi HOSTNAME_SET="" if $ANSWERFILE_MODE; then if [[ -n "$AF_HOSTNAME" ]]; then + # Append the stripped MAC address to the base name from the answerfile. + # This makes each cloned machine's hostname unique without needing per-host + # answerfiles (useful for mass-provisioning identical images). MAC=$(get_mac_suffix) HOSTNAME_SET="${AF_HOSTNAME}-${MAC}" printf "Hostname (from answerfile + MAC): %s\n" "$HOSTNAME_SET" | tee -a "$LOG" fi else + # dialog --inputbox: 3>&1 1>&2 2>&3 swaps fd1 and fd2 so the user's typed + # value (written to stdout by dialog) is captured by the $() subshell, while + # dialog's UI (which needs stderr for the terminal) flows to the actual terminal. + # The '|| HOSTNAME_INPUT=""' handles the user pressing Esc (dialog returns 1). HOSTNAME_INPUT=$(dialog --backtitle "$BACKTITLE" \ --title " Hostname " \ --inputbox "\n Hostname for this machine (leave blank to keep default).\n" 9 54 "" \ @@ -289,6 +376,8 @@ else fi if [[ -n "$HOSTNAME_SET" ]]; then + # hostnamectl is the preferred method on systemd systems; fall back to writing + # /etc/hostname directly for minimal chroot environments that lack systemd. sudo hostnamectl set-hostname "$HOSTNAME_SET" 2>/dev/null \ || echo "$HOSTNAME_SET" | sudo tee /etc/hostname > /dev/null printf "Hostname set: %s\n" "$HOSTNAME_SET" >> "$LOG" @@ -298,6 +387,10 @@ fi if $ANSWERFILE_MODE; then COMPONENTS="$AF_COMPONENTS" else + # dialog --checklist args: height width list-height, then triplets of tag desc state. + # All four components are pre-selected ("on") because they form the expected base. + # The 3>&1 1>&2 2>&3 fd swap captures dialog's output (the selected tags) via $(). + # Esc / Cancel returns exit code 1; the '|| { ...; exit 0; }' treats that as a clean abort. COMPONENTS=$(dialog --backtitle "$BACKTITLE" \ --title " Select Components " \ --checklist "Space toggles · Enter confirms · Esc quits" 15 68 4 \ @@ -312,6 +405,9 @@ fi if $ANSWERFILE_MODE; then DE="$AF_DE" else + # dialog --menu is a single-choice list; it outputs the selected tag. + # Esc returns exit code 1 — '|| DE="none"' defaults to skipping the DE, + # preserving whatever DE the user already has installed. DE=$(dialog --backtitle "$BACKTITLE" \ --title " Desktop Environment " \ --menu "Select a desktop environment · Esc / none to skip:" 24 72 11 \ @@ -332,6 +428,9 @@ fi if $ANSWERFILE_MODE; then SELECTED_APPS="$AF_APPS" else + # Cap the dialog box at 40 rows but shrink to fit smaller terminals. + # The list height is the dialog height minus 8 rows of chrome (title, borders, + # prompt, buttons), with a minimum of 4 to remain usable on tiny screens. _APP_H=$(( TERM_H - 2 < 40 ? TERM_H - 2 : 40 )) _APP_LIST_H=$(( _APP_H - 8 < 4 ? 4 : _APP_H - 8 )) SELECTED_APPS=$(dialog --backtitle "$BACKTITLE" \ @@ -415,11 +514,15 @@ else "rdp-client" "RDP Client Remmina + FreeRDP + VNC plugins" off \ "lamco-rdp-server" "Lamco RDP Server native Wayland RDP server (AUR, Rust)" off \ "qemu" "QEMU/KVM full virt stack + virt-manager GUI" off \ + # Esc / Cancel yields exit code 1; treat it as "no optional apps selected" + # rather than a hard abort so the user can still install base components. 3>&1 1>&2 2>&3) || SELECTED_APPS="" fi # ── Confirmation (interactive mode only) ────────────────────────────────────── if ! $ANSWERFILE_MODE; then + # Build a human-readable summary of everything that will be installed so the + # user can review the full list before any changes are made to the system. SUMMARY="" [[ -n "$HOSTNAME_SET" ]] && SUMMARY+=" ✦ Hostname: $HOSTNAME_SET\n" [[ "$COMPONENTS" == *"pkg"* ]] && SUMMARY+=" ✦ Package managers (yay, nvm, rust)\n" @@ -505,6 +608,7 @@ if ! $ANSWERFILE_MODE; then [[ "$SELECTED_APPS" == *"qemu"* ]] && SUMMARY+=" ✦ QEMU/KVM + virt-manager\n" fi + # Size the confirmation dialog to the terminal, capped at 24 rows. _CONF_H=$(( TERM_H - 2 < 24 ? TERM_H - 2 : 24 )) dialog --backtitle "$BACKTITLE" \ --title " Confirm Installation " \ @@ -512,14 +616,20 @@ if ! $ANSWERFILE_MODE; then "$_CONF_H" 62 || { clear; echo "Aborted."; exit 0; } fi +# Pre-count all selected steps before installation starts so run_module() can +# display [N/TOTAL] with an accurate denominator from the very first module. count_steps "$COMPONENTS" "$DE" "$SELECTED_APPS" # ── Installation: base components ───────────────────────────────────────────── +# Each guard uses glob matching against the space-separated COMPONENTS string. +# Order matters: package managers must be installed before packages that need yay/rust. [[ "$COMPONENTS" == *"pkg"* ]] && run_module "Package Managers" "$MODULES/package-managers.sh" [[ "$COMPONENTS" == *"core"* ]] && run_module "Core Packages" "$MODULES/core-packages.sh" [[ "$COMPONENTS" == *"svc"* ]] && run_module "Core Services" "$MODULES/core.sh" [[ "$COMPONENTS" == *"shell"* ]] && run_module "Shell Setup" "$MODULES/shell-setup.sh" +# Route the single selected DE value to its corresponding install script. +# "none" is the skip sentinel — no case branch matches it intentionally. if [[ "$DE" != "none" ]]; then case "$DE" in hyprlua) run_module "HyprLua" "$MODULES/Desktop-Environments/hyprlua.sh" ;; @@ -535,6 +645,9 @@ if [[ "$DE" != "none" ]]; then fi # ── Installation: applications ──────────────────────────────────────────────── +# Same guard pattern as base components. Each line is independent; a missing +# module script will be caught by run_module()'s error handling rather than +# silently skipped — 'bash "$script"' will exit non-zero if the file is absent. [[ "$SELECTED_APPS" == *"ollama"* ]] && run_module "Ollama" "$APPS/ollama.sh" [[ "$SELECTED_APPS" == *"llama-cpp"* ]] && run_module "llama.cpp" "$APPS/llama-cpp.sh" [[ "$SELECTED_APPS" == *"open-webui"* ]] && run_module "Open WebUI" "$APPS/open-webui.sh" @@ -612,9 +725,15 @@ fi [[ "$SELECTED_APPS" == *"qemu"* ]] && run_module "QEMU/KVM" "$APPS/qemu.sh" # ── Colorway (final step) ───────────────────────────────────────────────────── -# Read defaults from repo colors.conf for pre-population +# Read defaults from repo colors.conf for pre-population. +# Running after all modules ensures apply-theme.sh can re-process configs +# that were just symlinked or written by the DE/app modules. declare -A _cdef if [[ -f "$DOTFILES_DIR/colors.conf" ]]; then + # Parse 'KEY=VALUE' lines, skipping comments and blank lines. + # k="${k%%[[:space:]]*}" strips any trailing whitespace from the key. + # v="${v%%#*}" strips inline comments; v="${v//[[:space:]]/}" removes all spaces; + # v="${v^^}" uppercases the hex value so comparisons are case-insensitive. while IFS='=' read -r k v; do k="${k%%[[:space:]]*}" [[ "$k" =~ ^[[:space:]]*# || -z "$k" ]] && continue @@ -622,6 +741,7 @@ if [[ -f "$DOTFILES_DIR/colors.conf" ]]; then _cdef[$k]="$v" done < "$DOTFILES_DIR/colors.conf" fi +# Fall back to hard-coded defaults if colors.conf is absent or missing a key. DEF_TEXT="${_cdef[COLOR_TEXT]:-D6ABAB}" DEF_BG="${_cdef[COLOR_BG]:-1A1A1A}" DEF_HIGHLIGHT="${_cdef[COLOR_HIGHLIGHT]:-E40046}" @@ -629,15 +749,22 @@ DEF_DARK="${_cdef[COLOR_DARK]:-5018DD}" DEF_RED="${_cdef[COLOR_RED]:-F50505}" _write_colors_conf() { + # Write a normalized colors.conf to a temporary path. + # ${t^^} uppercases each hex value so apply-theme.sh always sees consistent casing. + # The file is written to TMP_D so it never clobbers the repo's own colors.conf. local out="$1" t="$2" b="$3" h="$4" d="$5" r="$6" printf 'COLOR_TEXT=%s\nCOLOR_BG=%s\nCOLOR_HIGHLIGHT=%s\nCOLOR_DARK=%s\nCOLOR_RED=%s\n' \ "${t^^}" "${b^^}" "${h^^}" "${d^^}" "${r^^}" > "$out" } if $ANSWERFILE_MODE; then - # Apply colors from answerfile if any are set + # Apply colors from answerfile if any are set. + # The concatenated string test is a concise way to check if at least one + # color field is non-empty without needing five separate [[ -n ]] guards. if [[ -n "$AF_COLOR_TEXT$AF_COLOR_BG$AF_COLOR_HIGHLIGHT$AF_COLOR_DARK$AF_COLOR_RED" ]]; then TMP_COLORS="$TMP_D/colors.conf" + # Per-field fallback: if the answerfile omits a color, use the repo default + # so apply-theme.sh always receives a complete, five-color config file. _write_colors_conf "$TMP_COLORS" \ "${AF_COLOR_TEXT:-$DEF_TEXT}" \ "${AF_COLOR_BG:-$DEF_BG}" \ @@ -645,10 +772,15 @@ if $ANSWERFILE_MODE; then "${AF_COLOR_DARK:-$DEF_DARK}" \ "${AF_COLOR_RED:-$DEF_RED}" printf "Applying colorway from answerfile...\n" | tee -a "$LOG" + # '|| true' suppresses a non-zero exit from apply-theme.sh so a colorway + # failure never aborts an otherwise successful unattended install. bash "$DOTFILES_DIR/apply-theme.sh" "$TMP_COLORS" 2>&1 | tee -a "$LOG" || true fi else # Interactive: show color form dialog + # dialog --form: each field is specified as label row col default frow fcol flen max. + # The fd swap (3>&1 1>&2 2>&3) captures the form output (one value per line) via $(). + # Esc / Cancel sets COLORWAY_RAW="" so the colorway step is simply skipped. COLORWAY_RAW=$(dialog --backtitle "$BACKTITLE" \ --title " Colorway (optional) " \ --form "\n Customize theme colors — bare 6-digit hex, no #.\n Leave unchanged to skip colorway setup.\n" \ @@ -661,6 +793,8 @@ else 3>&1 1>&2 2>&3) || COLORWAY_RAW="" if [[ -n "$COLORWAY_RAW" ]]; then + # mapfile reads the newline-separated form output into an array. + # _cv[0] = COLOR_TEXT line, _cv[1] = COLOR_BG, ... through _cv[4]. mapfile -t _cv <<< "$COLORWAY_RAW" N_TEXT="${_cv[0]:-$DEF_TEXT}" N_BG="${_cv[1]:-$DEF_BG}" @@ -668,6 +802,10 @@ else N_DARK="${_cv[3]:-$DEF_DARK}" N_RED="${_cv[4]:-$DEF_RED}" + # Only invoke apply-theme.sh if at least one color actually changed. + # ${VAR^^} uppercases before comparison to treat 'e40046' == 'E40046'. + # This avoids a redundant theme rebuild when the user just pressed Enter + # on every field without changing anything. if [[ "${N_TEXT^^}" != "$DEF_TEXT" || \ "${N_BG^^}" != "$DEF_BG" || \ "${N_HIGHLIGHT^^}" != "$DEF_HIGHLIGHT" || \ @@ -683,12 +821,17 @@ else fi # ── Sync user config to /etc/skel ───────────────────────────────────────────── -# Captures everything installed by modules so future users start with the same setup. +# /etc/skel is the skeleton directory Linux copies to new user home dirs on creation. +# Propagating the post-install config here means any new user added to this machine +# automatically gets the full dotfiles environment without a manual copy step. if [[ -d "$HOME/.config" ]]; then printf "\n Syncing ~/.config to /etc/skel...\n" sudo mkdir -p /etc/skel/.config + # '.' suffix on the source copies directory contents rather than the directory + # itself, making the merge non-destructive if /etc/skel/.config already exists. sudo cp -r "$HOME/.config/." /etc/skel/.config/ fi +# Copy themes, shell rc files, and vimrc only if they were actually created during install. [[ -d "$HOME/.themes" ]] && { sudo mkdir -p /etc/skel/.themes; sudo cp -r "$HOME/.themes/." /etc/skel/.themes/; } [[ -f "$HOME/.zshrc" ]] && sudo cp "$HOME/.zshrc" /etc/skel/.zshrc [[ -f "$HOME/.bashrc" ]] && sudo cp "$HOME/.bashrc" /etc/skel/.bashrc diff --git a/starship.toml b/starship.toml index 113fd58..f6961bc 100644 --- a/starship.toml +++ b/starship.toml @@ -1,85 +1,140 @@ -#format = "[](#f50505)$os$username[](bg:#E40046 fg:#f50505)$directory[](fg:#E40046 bg:#5018dd)$git_branch$git_status[](fg:#5018dd bg:#5018dd)$c$elixir$elm$golang$gradle$haskell$java$julia$nodejs$nim$rust$scala[](fg:#5018dd bg:#f50505)$docker_context[](fg:#f50505 bg:#5018dd)$time[ ](fg:#5018dd)\n [╚═](#f50505)" -format = "[](#f50505)$os$username[](bg:#E40046 fg:#f50505)$directory[](fg:#E40046 bg:#5018dd)$git_branch$git_status[](fg:#5018dd bg:#5018dd)$c$elixir$elm$golang$gradle$haskell$java$julia$nodejs$nim$rust$scala[](fg:#5018dd bg:#f50505)$docker_context[](fg:#f50505 bg:#5018dd)$time[ ](fg:#5018dd)" -#format = "[](#f50505)$os$username[](bg:#E40046 fg:#f50505)$directory[](fg:#E40046 bg:#5018dd)$git_branch$git_status[](fg:#5018dd bg:#5018dd)$c$elixir$elm$golang$gradle$haskell$java$julia$nodejs$nim$rust$scala[](fg:#5018dd bg:#f50505)$docker_context[](fg:#f50505 bg:#5018dd)$time[ ](fg:#5018dd)" +# starship.toml — Starship cross-shell prompt configuration +# +# Starship reads this file (by default at ~/.config/starship.toml) and renders +# the shell prompt. This config implements a powerline-style segmented prompt +# using the CyberQueer color palette: +# +# #f50505 — Red Hi-vis (segment borders, username background) +# #E40046 — Hot Pink Accent (directory segment background) +# #5018dd — Electric Violet (git/language/time segment background) +# +# Prompt layout (left to right on one line): +# [red border] [OS icon + username] [pink separator] [path] +# [violet separator] [git branch + status] [language versions (if detected)] +# [red separator] [docker context] [violet separator] [time] +# +# The format string uses Starship's powerline "arrow" glyphs () as segment +# separators: each changes both the foreground and background color to create +# the filled-triangle transition between segments. +# -continuation_prompt = "\n [](#E40046 )[](fg:#E40046 bg:#5018dd)[](#5018dd)" +# ─── Top-level format ────────────────────────────────────────────────────────── + +# Commented-out alternative that included a second line with '╚═' continuation. +# Currently using the single-line version below for a compact prompt. +#format = "[](#f50505)$os$username[](bg:#E40046 fg:#f50505)$directory[](fg:#E40046 bg:#5018dd)$git_branch$git_status[](fg:#5018dd bg:#5018dd)$c$elixir$elm$golang$gradle$haskell$java$julia$nodejs$nim$rust$scala[](fg:#5018dd bg:#f50505)$docker_context[](fg:#f50505 bg:#5018dd)$time[ ](fg:#5018dd)\n [╚═](#f50505)" + +# Active single-line format. +# Each segment is a bracketed block with inline styling: [content](style). +# The bare characters are powerline separator glyphs that create colored triangles. +format = "[](#f50505)$os$username[](bg:#E40046 fg:#f50505)$directory[](fg:#E40046 bg:#5018dd)$git_branch$git_status[](fg:#5018dd bg:#5018dd)$c$elixir$elm$golang$gradle$haskell$java$julia$nodejs$nim$rust$scala[](fg:#5018dd bg:#f50505)$docker_context[](fg:#f50505 bg:#5018dd)$time[ ](fg:#5018dd)" + +# Another commented alternative — kept for reference when experimenting with layout. +#format = "[](#f50505)$os$username[](bg:#E40046 fg:#f50505)$directory[](fg:#E40046 bg:#5018dd)$git_branch$git_status[](fg:#5018dd bg:#5018dd)$c$elixir$elm$golang$gradle$haskell$java$julia$nodejs$nim$rust$scala[](fg:#5018dd bg:#f50505)$docker_context[](fg:#f50505 bg:#5018dd)$time[ ](fg:#5018dd)" + +# continuation_prompt: shown on the second and subsequent lines of a multi-line command. +# The glyphs create a mini powerline row indicating the shell is waiting for more input. +continuation_prompt = "\n [](#E40046 )[](fg:#E40046 bg:#5018dd)[](#5018dd)" # Disable the blank line at the start of the prompt # add_newline = false -# You can also replace your username with a neat symbol like  or disable this +# You can also replace your username with a neat symbol like or disable this # and use the os module below + +# ─── Username segment ────────────────────────────────────────────────────────── +# Always shows the current username — useful as a reminder when switching accounts. +# Sits on the red (#f50505) segment immediately after the OS icon. [username] -show_always = true -style_user = "bg:#f50505" -style_root = "bg:#f50505" -format = "[$user ]($style)" +show_always = true # Show even when not in SSH or root context +style_user = "bg:#f50505" # Regular user: red background (matches the border color) +style_root = "bg:#f50505" # Root: same red background (no color distinction by design) +format = "[$user ]($style)" # Trailing space before the next separator glyph disabled = false # An alternative to the username module which displays a symbol that # represents the current operating system + +# ─── OS icon segment ─────────────────────────────────────────────────────────── +# Displays a Nerd Font icon for the detected OS (e.g., for Arch Linux). +# Placed before the username on the same red background segment. [os] style = "bg:#f50505" disabled = false # Disabled by default +# ─── Directory segment ───────────────────────────────────────────────────────── +# Shows the current path on a hot-pink (#E40046) background. [directory] style = "bg:#E40046" format = "[ $path ]($style)" -truncation_length = 3 -truncation_symbol = "…/" +truncation_length = 3 # Show at most 3 path components before truncating +truncation_symbol = "…/" # Prefix shown when the path is truncated # Here is how you can shorten some long paths by text replacement # similar to mapped_locations in Oh My Posh: +# ─── Directory substitutions ─────────────────────────────────────────────────── +# Replaces long directory names with compact Nerd Font icons for common folders. +# This keeps the prompt short without losing context. [directory.substitutions] -"Documents" = "󰈙 " -"Downloads" = " " -"Music" = " " -"music" = " " -"Musik" = " " -"musik" = " " -"Pictures" = " " +"Documents" = "󰈙 " # Document/paper icon +"Downloads" = " " # Download/arrow icon +"Music" = " " # Music note icon (English) +"music" = " " # lowercase variant +"Musik" = " " # German 'Musik' +"musik" = " " # German lowercase +"Pictures" = " " # Image/photo icon # Keep in mind that the order matters. For example: -# "Important Documents" = " 󰈙 " +# "Important Documents" = " 󰈙 " # will not be replaced, because "Documents" was already substituted before. # So either put "Important Documents" before "Documents" or use the substituted version: -# "Important 󰈙 " = " 󰈙 " +# "Important 󰈙 " = " 󰈙 " + +# ─── Language / tool version segments ───────────────────────────────────────── +# Each of these segments auto-detects project files and shows the active +# language/runtime version. All share the violet (#5018dd) background to form +# a unified "toolchain" segment block. They only appear when relevant files +# are detected in the current or parent directories (e.g., *.c for C, go.mod for Go). [c] -symbol = " " +symbol = " " # C language Nerd Font icon style = "bg:#5018dd" format = '[ $symbol ($version) ]($style)' [docker_context] -symbol = " " -style = "bg:#f50505 fg:#5018dd" +symbol = " " # Docker whale icon +style = "bg:#f50505 fg:#5018dd" # Stands out: red background, violet text — highlights active Docker context format = '[ $symbol $context ]($style)' [elixir] -symbol = " " +symbol = " " # Elixir drop icon style = "bg:#5018dd" format = '[ $symbol ($version) ]($style)' [elm] -symbol = " " +symbol = " " # Elm triangle icon style = "bg:#5018dd" format = '[ $symbol ($version) ]($style)' +# ─── Git segments ────────────────────────────────────────────────────────────── + [git_branch] -symbol = "" +symbol = "" # Git branch icon (nerd font) style = "bg:#5018dd" -format = '[ $symbol $branch ]($style)' +format = '[ $symbol $branch ]($style)' # Shows current branch name [git_status] style = "bg:#5018dd" +# $all_status: compact status codes (M=modified, ?=untracked, !=staged, etc.) +# $ahead_behind: shows ↑N/↓N when ahead/behind the remote branch format = '[$all_status $ahead_behind]($style)' [golang] -symbol = " " +symbol = " " # Go gopher icon style = "bg:#5018dd" format = '[ $symbol ($version) ]($style)' @@ -88,175 +143,184 @@ style = "bg:#5018dd" format = '[ $symbol ($version) ]($style)' [haskell] -symbol = " " +symbol = " " # Haskell lambda icon style = "bg:#5018dd" format = '[ $symbol ($version) ]($style)' [java] -symbol = " " +symbol = " " # Java coffee cup icon style = "bg:#5018dd" format = '[ $symbol ($version) ]($style)' [julia] -symbol = " " +symbol = " " # Julia dots icon style = "bg:#5018dd" format = '[ $symbol ($version) ]($style)' [nodejs] -symbol = "" +symbol = "" # Node.js hexagon icon style = "bg:#5018dd" format = '[ $symbol ($version) ]($style)' [nim] -symbol = "󰆥 " +symbol = "󰆥 " # Nim crown icon style = "bg:#5018dd" format = '[ $symbol ($version) ]($style)' [rust] -symbol = "" -style = "bg:#5018dd" +symbol = "" # Rust gear icon +style = "bg:#5018dd" format = '[ $symbol ($version) ]($style)' [scala] -symbol = " " +symbol = " " # Scala 'S' icon style = "bg:#5018dd" format = '[ $symbol ($version) ]($style)' +# ─── Time segment ────────────────────────────────────────────────────────────── +# Always-on clock at the right end of the prompt. +# Helps track how long commands take and time awareness during deep work sessions. [time] disabled = false -time_format = "%R" # Hour:Minute Format +time_format = "%R" # Hour:Minute Format (24h, e.g. "14:35") — no seconds to save space style = "bg:#5018dd" -format = "[  $time ]($style)" +format = "[ $time ]($style)" # Clock icon + time on violet background +# ─── Symbol-only overrides ───────────────────────────────────────────────────── +# The following sections only override the icon symbol for modules that aren't +# actively displayed in the main format string. Starship uses these symbols +# in the `help` output and when the module is added to format later. + [aws] -symbol = " " +symbol = " " [buf] -symbol = " " +symbol = " " [cmake] -symbol = " " +symbol = " " [conda] -symbol = " " +symbol = " " [crystal] -symbol = " " +symbol = " " [dart] -symbol = " " +symbol = " " [fennel] -symbol = " " +symbol = " " [fossil_branch] -symbol = " " +symbol = " " [git_commit] -tag_symbol = '  ' +tag_symbol = ' ' # Tag icon shown next to tagged commits [guix_shell] -symbol = " " +symbol = " " [haxe] -symbol = " " +symbol = " " [hg_branch] -symbol = " " +symbol = " " # Mercurial branch icon [hostname] -ssh_symbol = " " - +ssh_symbol = " " # Server/rack icon shown when connected via SSH [kotlin] -symbol = " " +symbol = " " [lua] -symbol = " " +symbol = " " [memory_usage] -symbol = "󰍛 " +symbol = "󰍛 " # RAM chip icon [meson] -symbol = "󰔷 " - +symbol = "󰔷 " # Build system icon [nix_shell] -symbol = " " +symbol = " " # Snowflake icon for Nix environments [ocaml] -symbol = " " +symbol = " " +# ─── OS icon symbol table ────────────────────────────────────────────────────── +# Maps detected OS names to their Nerd Font icons. +# These are displayed by the [os] module defined above. +# All icons require a Nerd Font (or compatible patched font) to render correctly. [os.symbols] -Alpaquita = " " -Alpine = " " -AlmaLinux = " " -Amazon = " " -Android = " " -Arch = " " -Artix = " " -CentOS = " " -Debian = " " -DragonFly = " " -Emscripten = " " -EndeavourOS = " " -Fedora = " " -FreeBSD = " " +Alpaquita = " " +Alpine = " " +AlmaLinux = " " +Amazon = " " +Android = " " +Arch = " " # Arch Linux pacman-ghost icon +Artix = " " +CentOS = " " +Debian = " " +DragonFly = " " +Emscripten = " " +EndeavourOS = " " +Fedora = " " +FreeBSD = " " Garuda = "󰛓 " -Gentoo = " " +Gentoo = " " HardenedBSD = "󰞌 " Illumos = "󰈸 " -Kali = " " -Linux = " " -Mabox = " " -Macos = " " -Manjaro = " " -Mariner = " " -MidnightBSD = " " -Mint = " " -NetBSD = " " -NixOS = " " +Kali = " " +Linux = " " # Generic Linux penguin icon +Mabox = " " +Macos = " " # Apple icon +Manjaro = " " +Mariner = " " +MidnightBSD = " " +Mint = " " +NetBSD = " " +NixOS = " " OpenBSD = "󰈺 " -openSUSE = " " +openSUSE = " " OracleLinux = "󰌷 " -Pop = " " -Raspbian = " " -Redhat = " " -RedHatEnterprise = " " -RockyLinux = " " +Pop = " " +Raspbian = " " +Redhat = " " +RedHatEnterprise = " " +RockyLinux = " " Redox = "󰀘 " Solus = "󰠳 " -SUSE = " " -Ubuntu = " " -Unknown = " " -Void = " " -Windows = "󰍲 " +SUSE = " " +Ubuntu = " " +Unknown = " " +Void = " " +Windows = "󰍲 " # Windows logo icon (for WSL sessions) [package] -symbol = "󰏗 " +symbol = "󰏗 " # Package/box icon for package.json / Cargo.toml version display [perl] -symbol = " " +symbol = " " [php] -symbol = " " +symbol = " " [pijul_channel] -symbol = " " +symbol = " " [python] -symbol = " " +symbol = " " # Python snake icon [rlang] symbol = "󰟔 " [ruby] -symbol = " " +symbol = " " [swift] -symbol = " " +symbol = " " [zig] -symbol = " " - +symbol = " " diff --git a/update-aur-onebyone.sh b/update-aur-onebyone.sh index c1af9fc..0289ff5 100755 --- a/update-aur-onebyone.sh +++ b/update-aur-onebyone.sh @@ -1,7 +1,14 @@ #!/bin/bash +# Update every out-of-date AUR package individually so a single broken build +# doesn't abort the entire upgrade run. Failed packages are collected and +# reported at the end; the script exits 1 if any package failed. +# set -u: treat unset variables as errors to avoid silent empty-string expansion set -u +# yay -Qua: list installed AUR packages that have an available update. +# awk '{print $1}' strips the version columns, leaving just the package names. +# mapfile -t reads those names into the indexed array pkgs (one entry per line). mapfile -t pkgs < <(yay -Qua | awk '{print $1}') if [ ${#pkgs[@]} -eq 0 ]; then @@ -13,9 +20,14 @@ echo "Found ${#pkgs[@]} AUR package(s) with updates:" printf ' %s\n' "${pkgs[@]}" echo +# Accumulate failures so later packages still get a chance to update. failed=() for pkg in "${pkgs[@]}"; do echo "==> Updating $pkg" + # -S <pkg> : install/upgrade the specific package (not all AUR) + # --answerdiff None : skip PKGBUILD diff review (non-interactive) + # --answerclean All : always clean the build directory before compiling + # --removemake : remove make-only deps after the build (keeps system lean) if yay -S --noconfirm --answerdiff None --answerclean All --removemake "$pkg"; then echo "==> $pkg: OK" else @@ -25,6 +37,7 @@ for pkg in "${pkgs[@]}"; do echo done +# Report any failures and signal a non-zero exit so callers can react. if [ ${#failed[@]} -gt 0 ]; then echo "Failed packages:" printf ' %s\n' "${failed[@]}" diff --git a/update.sh b/update.sh index 808f5b2..941ed6b 100755 --- a/update.sh +++ b/update.sh @@ -1,5 +1,15 @@ #!/bin/bash +# Batch full-system upgrade — runs both official repos and AUR in one shot. +# Prefer sysupdate.sh for interactive, per-package control; use this script +# only when a silent, non-interactive sweep is acceptable (e.g. in CI or cron). +# -Syu: sync package databases (-y) then upgrade all installed packages (-u); +# --noconfirm skips all "proceed? [Y/n]" prompts pacman -Syu --noconfirm -yay -Syu --noconfirm --answerdiff None --answerclean All --removemake + +# yay wraps pacman and also handles AUR packages in a single pass. +# --answerdiff None : never show diffs for PKGBUILDs (avoids interactive pause) +# --answerclean All : always clean build directories before rebuilding +# --removemake : discard make-only dependencies after the build completes +yay -Syu --noconfirm --answerdiff None --answerclean All --removemake diff --git a/zshplugins.sh b/zshplugins.sh index 962423b..b2d47c7 100755 --- a/zshplugins.sh +++ b/zshplugins.sh @@ -1,4 +1,31 @@ +#!/bin/bash +# zshplugins.sh — Install third-party Oh My Zsh plugins +# +# Run this script once after a fresh install to pull plugins that are not +# bundled with Oh My Zsh itself. These plugins are cloned directly into +# the OMZ custom plugins directory so OMZ picks them up automatically. +# +# Plugins installed: +# - zsh-syntax-highlighting: colors valid commands green and errors red +# as you type — immediate feedback before pressing Enter. +# - zsh-autosuggestions: shows ghost-text completions derived from command +# history; accept with the right-arrow key. +# +# After cloning, add both to the plugins=(...) array in ~/.zshrc and restart +# your shell (or run: source ~/.zshrc). +# +# Usage: bash ~/Dotfiles/zshplugins.sh +# + #ohmyzsh plugins echo "Installing oh my zsh plugins" + +# Clone zsh-syntax-highlighting into the OMZ custom plugins directory. +# ${ZSH_CUSTOM:-~/.oh-my-zsh/custom} resolves to $ZSH_CUSTOM if set (non-standard +# OMZ install location), or falls back to the default path. git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting + +# Clone zsh-autosuggestions into the same directory. +# Requires zsh-syntax-highlighting to already be loaded for optimal display, +# though both are independent and can be loaded in any order. git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions