From 49ece6a238162db14fb6dbcff4db9e4072d08271 Mon Sep 17 00:00:00 2001 From: The_miro Date: Fri, 19 Jun 2026 11:15:23 +0200 Subject: [PATCH 01/14] feat(hyprlua): tighten lid-lock, idle timer, and add caffeine eww widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - caffeine: inhibit only `idle`, not `sleep`, so lid close still locks - binds: lid-close unconditionally calls hyprlock; lid-open does dpms on - hypridle: reduce lock timeout from 3 min to 2.5 min (150 s) - eww (all 3 variants): add caffeine toggle button (☕/󰅺) with tooltip Co-Authored-By: Claude Sonnet 4.6 --- desktopenvs/hyprlua/eww-nobattery/eww.yuck | 10 ++++++++++ desktopenvs/hyprlua/eww-touch/eww.yuck | 13 ++++++++++++- desktopenvs/hyprlua/eww/eww.yuck | 10 ++++++++++ desktopenvs/hyprlua/hypr/hypridle.conf | 2 +- desktopenvs/hyprlua/hypr/usr/binds.lua | 4 ++-- desktopenvs/hyprlua/scripts/caffeine-status.sh | 3 +++ desktopenvs/hyprlua/scripts/caffeine.sh | 2 +- 7 files changed, 39 insertions(+), 5 deletions(-) create mode 100755 desktopenvs/hyprlua/scripts/caffeine-status.sh diff --git a/desktopenvs/hyprlua/eww-nobattery/eww.yuck b/desktopenvs/hyprlua/eww-nobattery/eww.yuck index b88e136..b41fd2d 100644 --- a/desktopenvs/hyprlua/eww-nobattery/eww.yuck +++ b/desktopenvs/hyprlua/eww-nobattery/eww.yuck @@ -38,6 +38,7 @@ :value {round((1 - (EWW_DISK["/"].free / EWW_DISK["/"].total)) * 100, 0)} :onchange "" :onclick "")) + (caffeine) (clock) (systray :class "music" :orientation "h" :spacing 2 :space-evenly true) )) @@ -112,3 +113,12 @@ (defpoll disks :interval "600s" "~/Dotfiles/desktopenvs/hyprland/scripts/dysk-phydisks.sh") +(defpoll caffeine-active :interval "2s" + "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-status.sh") + +(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/eww-touch/eww.yuck b/desktopenvs/hyprlua/eww-touch/eww.yuck index 066ae5d..6413d1d 100644 --- a/desktopenvs/hyprlua/eww-touch/eww.yuck +++ b/desktopenvs/hyprlua/eww-touch/eww.yuck @@ -37,6 +37,7 @@ (defwidget sidestuff [] (box :class "sidestuff" :orientation "h" :space-evenly false :halign "end" + (caffeine) (clock) (systray :class "music" :orientation "h" :spacing 2 :space-evenly true) )) @@ -158,4 +159,14 @@ (defpoll calender :interval "600s" - "~/Dotfiles/desktopenvs/hyprland/scripts/calender-fix.sh") + "~/Dotfiles/desktopenvs/hyprland/scripts/calender-fix.sh") + +(defpoll caffeine-active :interval "2s" + "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-status.sh") + +(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/eww/eww.yuck b/desktopenvs/hyprlua/eww/eww.yuck index 69c5f14..bc39bd1 100644 --- a/desktopenvs/hyprlua/eww/eww.yuck +++ b/desktopenvs/hyprlua/eww/eww.yuck @@ -44,6 +44,7 @@ :onchange "" :onclick "")) + (caffeine) (clock) (systray :class "music" :orientation "h" :spacing 2 :space-evenly true) )) @@ -117,3 +118,12 @@ (defpoll disks :interval "600s" "~/Dotfiles/desktopenvs/hyprland/scripts/dysk-phydisks.sh") +(defpoll caffeine-active :interval "2s" + "~/Dotfiles/desktopenvs/hyprlua/scripts/caffeine-status.sh") + +(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/hypridle.conf b/desktopenvs/hyprlua/hypr/hypridle.conf index fbf07ba..8786b0d 100644 --- a/desktopenvs/hyprlua/hypr/hypridle.conf +++ b/desktopenvs/hyprlua/hypr/hypridle.conf @@ -9,7 +9,7 @@ general { # Presence detection resets the idle timer every 2 minutes while you're visible, # so these timeouts only run when you've actually stepped away. listener { - timeout = 180 # 3 min — lock screen + timeout = 150 # 2.5 min — lock screen on-timeout = loginctl lock-session } diff --git a/desktopenvs/hyprlua/hypr/usr/binds.lua b/desktopenvs/hyprlua/hypr/usr/binds.lua index 359d046..0a4a2e9 100644 --- a/desktopenvs/hyprlua/hypr/usr/binds.lua +++ b/desktopenvs/hyprlua/hypr/usr/binds.lua @@ -12,8 +12,8 @@ local winswitch = "" -- TODO: define your window switcher command ---- LID SWITCH ---- -------------------- -hl.bind("switch:on:Lid Switch", hl.dsp.exec_cmd("bash -c 'pidof hypridle > /dev/null && hyprlock'"), { locked = true }) -hl.bind("switch:off:Lid Switch", hl.dsp.exec_cmd("hyprctl dispatch exec hyprlock"), { locked = true }) +hl.bind("switch:on:Lid Switch", hl.dsp.exec_cmd("hyprlock"), { locked = true }) +hl.bind("switch:off:Lid Switch", hl.dsp.exec_cmd("hyprctl dispatch dpms on"), { locked = true }) -------------------- ---- GESTURES ------ diff --git a/desktopenvs/hyprlua/scripts/caffeine-status.sh b/desktopenvs/hyprlua/scripts/caffeine-status.sh new file mode 100755 index 0000000..3b86ea0 --- /dev/null +++ b/desktopenvs/hyprlua/scripts/caffeine-status.sh @@ -0,0 +1,3 @@ +#!/bin/bash +PID_FILE="/tmp/caffeine-inhibit.pid" +[[ -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 b21cc27..84dab61 100755 --- a/desktopenvs/hyprlua/scripts/caffeine.sh +++ b/desktopenvs/hyprlua/scripts/caffeine.sh @@ -7,7 +7,7 @@ if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then rm -f "$PID_FILE" notify-send -t 2000 "Caffeine" "Idle inhibit OFF" else - systemd-inhibit --what=idle:sleep \ + systemd-inhibit --what=idle \ --who="caffeine" \ --why="Caffeine mode active" \ --mode=block \ From 708137831bef723a301cda465335152ee331c863 Mon Sep 17 00:00:00 2001 From: The_miro Date: Tue, 23 Jun 2026 09:03:17 +0200 Subject: [PATCH 02/14] fix(hypridle,sysupdate,archiso): misc improvements - hypridle: remove fprintd restart from after_sleep_cmd (caused resume delays) - sysupdate: add --packages/-p, --configs/-c, --both/-b selective update modes; move config sync prompt before package step in --both mode - archiso: add wds-deploy.sh for packaging M-Archy netboot artifacts for WDS/PXELinux Co-Authored-By: Claude Sonnet 4.6 --- desktopenvs/hyprlua/hypr/hypridle.conf | 2 +- setup/archiso/wds-deploy.sh | 257 +++++++++++++++++++++++++ sysupdate.sh | 51 +++-- 3 files changed, 297 insertions(+), 13 deletions(-) create mode 100755 setup/archiso/wds-deploy.sh diff --git a/desktopenvs/hyprlua/hypr/hypridle.conf b/desktopenvs/hyprlua/hypr/hypridle.conf index 8786b0d..6a13f02 100644 --- a/desktopenvs/hyprlua/hypr/hypridle.conf +++ b/desktopenvs/hyprlua/hypr/hypridle.conf @@ -2,7 +2,7 @@ general { lock_cmd = pidof hyprlock || hyprlock before_sleep_cmd = loginctl lock-session # fprintd restart ensures fingerprint sensor is ready after resume - after_sleep_cmd = systemctl restart fprintd.service ; hyprctl dispatch dpms on + after_sleep_cmd = hyprctl dispatch dpms on ignore_dbus_inhibit = false # respect systemd-inhibit locks (presence-detect, caffeine) } diff --git a/setup/archiso/wds-deploy.sh b/setup/archiso/wds-deploy.sh new file mode 100755 index 0000000..7149dee --- /dev/null +++ b/setup/archiso/wds-deploy.sh @@ -0,0 +1,257 @@ +#!/usr/bin/env bash +# wds-deploy.sh — Package M-Archy archiso netboot artifacts for WDS + PXELinux deployment +# +# Usage: +# bash wds-deploy.sh --http-srv URL [OPTIONS] [OUT_DIR] +# +# --http-srv URL HTTP base URL where arch netboot files will be served +# (e.g. http://192.168.1.10/m-archy) +# Arch's initrd fetches airootfs.sfs over HTTP at boot time. +# Required. +# --tftp-prefix PATH Subdirectory within the WDS TFTP root for kernel/initramfs +# (default: m-archy) +# --preconf [FILE] Passed through to build.sh — embeds an answerfile into the ISO +# --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/): +# 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: +# 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_BIOS="/usr/lib/syslinux/bios" + +# ── Argument parsing ─────────────────────────────────────────────────────────── +HTTP_SRV="" +TFTP_PREFIX="m-archy" +PRECONF_ARGS=() +NO_REBUILD=0 +OUT_ARG="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --http-srv) + [[ $# -gt 1 ]] || { echo "ERROR: --http-srv requires a URL" >&2; exit 1; } + HTTP_SRV="$2"; shift 2 ;; + --http-srv=*) + HTTP_SRV="${1#--http-srv=}"; shift ;; + --tftp-prefix) + [[ $# -gt 1 ]] || { echo "ERROR: --tftp-prefix requires a value" >&2; exit 1; } + TFTP_PREFIX="$2"; shift 2 ;; + --tftp-prefix=*) + TFTP_PREFIX="${1#--tftp-prefix=}"; shift ;; + --preconf) + if [[ $# -gt 1 && "${2:0:1}" != "-" ]]; then + PRECONF_ARGS=(--preconf "$2"); shift 2 + else + PRECONF_ARGS=(--preconf); shift + fi ;; + --preconf=*) + PRECONF_ARGS=("$1"); shift ;; + --no-rebuild) + NO_REBUILD=1; shift ;; + -*) + echo "Unknown flag: $1" >&2; exit 1 ;; + *) + OUT_ARG="$1"; shift ;; + esac +done + +OUT_DIR="${OUT_ARG:-${OUT_DIR:-$HOME/m-archy-out}}" +WDS_DIR="$OUT_DIR/wds-deploy" + +# ── Validate ────────────────────────────────────────────────────────────────── +if [[ -z "$HTTP_SRV" ]]; then + echo "ERROR: --http-srv is required." >&2 + echo " The Arch initrd fetches airootfs.sfs over HTTP at boot." >&2 + echo " Example: --http-srv http://192.168.1.10/m-archy" >&2 + exit 1 +fi + +HTTP_SRV="${HTTP_SRV%/}" # strip trailing slash + +# ── Ensure syslinux is available ────────────────────────────────────────────── +if [[ ! -f "$SYSLINUX_BIOS/pxelinux.0" ]]; then + echo "syslinux not found — installing..." + sudo pacman -S --noconfirm syslinux +fi + +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) ───────────────────────── +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)..." + 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 + +[[ -n "$NETBOOT_TARBALL" ]] \ + || { echo "ERROR: No netboot tarball found in $OUT_DIR — build may have failed." >&2; exit 1; } + +echo "Using netboot tarball: $(basename "$NETBOOT_TARBALL")" + +# ── Extract netboot tarball ─────────────────────────────────────────────────── +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" + +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)" + +[[ -f "$VMLINUZ" ]] || { echo "ERROR: vmlinuz-linux not found in netboot tarball." >&2; exit 1; } +[[ -f "$INITRAMFS" ]] || { echo "ERROR: initramfs-linux.img not found in netboot tarball." >&2; exit 1; } +[[ -d "$ARCH_DIR" ]] || { echo "ERROR: arch/ directory not found in netboot tarball." >&2; exit 1; } + +# ── Build WDS deployment tree ───────────────────────────────────────────────── +# +# 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 +# /arch/boot/x86_64/vmlinuz-linux +# /arch/boot/x86_64/initramfs-linux.img +# +# 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) + +TFTP_ROOT="$WDS_DIR/TFTP" +HTTP_ROOT="$WDS_DIR/HTTP" + +rm -rf "$WDS_DIR" +mkdir -p \ + "$TFTP_ROOT/$TFTP_PREFIX/arch/boot/x86_64" \ + "$TFTP_ROOT/pxelinux.cfg" \ + "$HTTP_ROOT" + +# ── Copy syslinux modules ───────────────────────────────────────────────────── +echo "Copying syslinux PXELinux modules..." +REQUIRED_MODS=(pxelinux.0 ldlinux.c32 menu.c32 libcom32.c32 libutil.c32) +MISSING_MODS=() + +for mod in "${REQUIRED_MODS[@]}"; do + if [[ -f "$SYSLINUX_BIOS/$mod" ]]; then + cp "$SYSLINUX_BIOS/$mod" "$TFTP_ROOT/" + else + MISSING_MODS+=("$mod") + fi +done + +if [[ ${#MISSING_MODS[@]} -gt 0 ]]; then + echo "Warning: missing syslinux modules (non-fatal for some setups): ${MISSING_MODS[*]}" +fi + +# ── Copy kernel and initramfs ───────────────────────────────────────────────── +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) ──────────────── +echo "Copying HTTP content (airootfs + supporting files)..." +cp -r "$ARCH_DIR" "$HTTP_ROOT/arch" + +# ── Generate pxelinux.cfg/default ──────────────────────────────────────────── +echo "Writing pxelinux.cfg/default..." + +# Kernel path is relative to the TFTP root +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 +APPEND_BASE="initrd=${INITRD_PATH} archiso_http_srv=${HTTP_SRV}/ archisobasedir=arch ip=dhcp" + +cat > "$TFTP_ROOT/pxelinux.cfg/default" </dev/null; then + (cd "$TFTP_ROOT" && zip -r "$ZIP_FILE" .) + echo "Created: $ZIP_FILE" +else + echo "Note: 'zip' not installed — skipping wds-tftp.zip creation." + echo " Install with: sudo pacman -S zip" +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── +echo +echo "=========================================================================" +echo " WDS deployment package: $WDS_DIR" +echo "=========================================================================" +echo +echo " TFTP/ → WDS TFTP root (copy to Windows Server)" +echo " Destination: C:\\RemoteInstall\\Boot\\x64\\" +echo " Tip: use wds-tftp.zip if created above, or robocopy/SCP the TFTP/ tree." +echo +echo " HTTP/ → HTTP root (serve at: ${HTTP_SRV}/)" +echo " The initrd fetches: ${HTTP_SRV}/arch/x86_64/airootfs.sfs" +echo " Serve with IIS, Nginx, or Apache pointing to the HTTP/ directory." +echo +echo " pxelinux.cfg/default kernel args summary:" +echo " archiso_http_srv = ${HTTP_SRV}/" +echo " archisobasedir = arch" +echo +echo " WDS configuration steps:" +echo " 1. Copy TFTP/ contents to C:\\RemoteInstall\\Boot\\x64\\" +echo " 2. Serve HTTP/ over IIS/Nginx at: ${HTTP_SRV}/" +echo " 3. Open WDS console → right-click server → Properties → Boot tab:" +echo " x64 default boot program: Boot\\x64\\pxelinux.0" +echo " Check 'Always continue the PXE boot' for unknown clients" +echo " 4. If WDS manages DHCP, ensure option 67 is: Boot\\x64\\pxelinux.0" +echo " If using a separate DHCP server, set:" +echo " option 66 (next-server) = " +echo " option 67 (boot-file) = Boot\\x64\\pxelinux.0" +echo " 5. PXE-boot a client — the M-Archy menu should appear." +echo +echo " File sizes:" +du -sh "$TFTP_ROOT" "$HTTP_ROOT" 2>/dev/null | sed 's/^/ /' +echo "=========================================================================" diff --git a/sysupdate.sh b/sysupdate.sh index 9179bd4..d55abb7 100755 --- a/sysupdate.sh +++ b/sysupdate.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # sysupdate — Arch Linux System Update TUI -# Usage: sysupdate [--AI] +# Usage: sysupdate [--AI] [--packages|-p] [--configs|-c] [--both|-b] # Deps: yay, pacman-contrib (checkupdates), curl, python3, dialog # flatpak (optional) | claude CLI (required for --AI) # State: /updatestate — ISO timestamp of last completed update @@ -17,7 +17,15 @@ readonly NEWS_FEED="https://archlinux.org/feeds/news/" DOTFILES="${DOTFILES:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" AI_MODE=false -for _arg in "$@"; do [[ "$_arg" == "--AI" ]] && AI_MODE=true; done +UPDATE_MODE="both" # packages | configs | both +for _arg in "$@"; do + case "$_arg" in + --AI) AI_MODE=true ;; + --packages|-p) UPDATE_MODE="packages" ;; + --configs|-c) UPDATE_MODE="configs" ;; + --both|-b) UPDATE_MODE="both" ;; + esac +done # ═══════════════════════════════════════════════════════════════════════════════ # TERMINAL / COLOR SETUP @@ -568,11 +576,23 @@ _migrate_hypr_usr() { fi } +# Patch known stale paths inside hypr/usr/ that aren't auto-synced. +_fix_hypr_usr() { + local usr_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hypr/usr" + [[ -d "$usr_dir" ]] || return 0 + local input_lua="$usr_dir/input.lua" + if [[ -f "$input_lua" ]] && grep -q 'require("input-device-exceptions")' "$input_lua"; then + sed -i 's/require("input-device-exceptions")/require("usr.input-device-exceptions")/' "$input_lua" + ok "Fixed require path in hypr/usr/input.lua ${DI}(input-device-exceptions → usr.input-device-exceptions)${RS}" + fi +} + sync_configs() { section "Config Sync" # ── Migration: old flat layout → hypr/usr/ ─────────────────────────────── _migrate_hypr_usr + _fix_hypr_usr # ── Preferred: use the installed update-configs.sh ─────────────────────── local cfg_script @@ -675,11 +695,27 @@ declare -a FLATPAK_PKGS=() main() { header - # ── Read state ─────────────────────────────────────────────────────────── + # ── Configs-only mode ──────────────────────────────────────────────────── + if [[ "$UPDATE_MODE" == "configs" ]]; then + if ask "Sync dotfiles configs to ~/.config?"; then + sync_configs + fi + rm -f "$_NEWS_PY" + exit 0 + fi + + # ── Read state (packages / both modes) ─────────────────────────────────── local last_update; last_update=$(read_state) log "Last recorded update: ${BO}${last_update}${RS}" echo + # ── Config sync (both mode) ────────────────────────────────────────────── + if [[ "$UPDATE_MODE" == "both" ]]; then + if ask "Sync dotfiles configs to ~/.config?"; then + sync_configs + fi + fi + # ── Collect available updates ──────────────────────────────────────────── section "Collecting Updates" @@ -705,10 +741,6 @@ main() { echo ok "${BO}System is up to date.${RS}" write_state - echo - if ask "Sync dotfiles configs to ~/.config?"; then - sync_configs - fi rm -f "$_NEWS_PY" exit 0 fi @@ -861,11 +893,6 @@ main() { fi echo - # ── Config sync ────────────────────────────────────────────────────────── - if ask "Sync dotfiles configs to ~/.config?"; then - sync_configs - fi - rm -f "$_NEWS_PY" } From 199f7296a9151260f17286f639003f843e618011 Mon Sep 17 00:00:00 2001 From: The_miro Date: Tue, 23 Jun 2026 09:03:25 +0200 Subject: [PATCH 03/14] feat(hyprlua): add WYSIWYG monitor manager TUI Python curses TUI for managing Hyprland monitors interactively: - Canvas shows monitors as boxes at their real relative positions - Tab/Shift+Tab to cycle selection; hjkl/HJKL to move (50/10 px) - u/i to rotate CCW/CW; n/N to cycle display modes live - m to mirror (pick target with Tab, confirm with Enter) - s saves to hypr/usr/monitors.lua atomically - Scale cached and only recomputed on resize or viewport overflow - Bound to Super+Shift+M as a centered-L floating kitty popup Co-Authored-By: Claude Sonnet 4.6 --- desktopenvs/hyprlua/hypr/usr/binds.lua | 1 + desktopenvs/hyprlua/scripts/monitor-manager | 655 ++++++++++++++++++++ 2 files changed, 656 insertions(+) create mode 100755 desktopenvs/hyprlua/scripts/monitor-manager diff --git a/desktopenvs/hyprlua/hypr/usr/binds.lua b/desktopenvs/hyprlua/hypr/usr/binds.lua index 0a4a2e9..09c38b5 100644 --- a/desktopenvs/hyprlua/hypr/usr/binds.lua +++ b/desktopenvs/hyprlua/hypr/usr/binds.lua @@ -58,6 +58,7 @@ hl.bind(mainMod .. " + ALT + F", hl.dsp.exec_cmd("wofi-calc")) hl.bind(mainMod .. " + S", hl.dsp.exec_cmd("[tag +mixer] pavucontrol")) hl.bind(mainMod .. " + U", hl.dsp.exec_cmd("[tag +centered-L] kitty btop")) hl.bind(mainMod .. " + W", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/wallpaper-picker ~/Pictures")) +hl.bind(mainMod .. " + SHIFT + M", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/monitor-manager")) hl.bind(mainMod .. " + CTRL + R", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/amssh")) hl.bind(mainMod .. " + F1", hl.dsp.exec_cmd("[tag +centered] kitty ~/.config/scripts/helpmenu.sh")) hl.bind(mainMod .. " + CTRL + T", hl.dsp.exec_cmd("[tag +centered-S] kitty bash ~/.config/scripts/timer-pick")) diff --git a/desktopenvs/hyprlua/scripts/monitor-manager b/desktopenvs/hyprlua/scripts/monitor-manager new file mode 100755 index 0000000..4383efd --- /dev/null +++ b/desktopenvs/hyprlua/scripts/monitor-manager @@ -0,0 +1,655 @@ +#!/usr/bin/env python3 +""" +Hyprland monitor manager — WYSIWYG curses TUI. + +Keys (normal mode): + Tab / Shift+Tab cycle selected monitor + h j k l move monitor (50 px) + H J K L move monitor (10 px fine) + u / i rotate CCW / CW + m toggle mirror (pick target) / un-mirror + n / N cycle display mode forward / backward + s save to hypr/usr/monitors.lua + q / Esc quit (prompts if unsaved changes) + +Mirror-pick mode: + Tab / Shift+Tab cycle target + Enter confirm + Esc cancel +""" + +import curses +import json +import os +import re +import subprocess +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import List, Optional + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +MONITORS_LUA = Path.home() / "Dotfiles/desktopenvs/hyprlua/hypr/usr/monitors.lua" +MOVE_STEP = 50 +MOVE_STEP_FINE = 10 +MIN_BOX_W = 14 +MIN_BOX_H = 4 +INFO_W = 32 +STATUS_ROWS = 2 # status + help rows at the bottom + +TRANSFORM_LABEL = { + 0: "↕ 0°", + 1: "↻ 90°", + 2: "↕ 180°", + 3: "↺ 90°", + 4: "⇔ 0°", + 5: "⇔↻ 90°", + 6: "⇔ 180°", + 7: "⇔↺ 90°", +} + +_MODE_RE = re.compile(r"(\d+)x(\d+)@([\d.]+)Hz") + +# --------------------------------------------------------------------------- +# Rotation helpers +# --------------------------------------------------------------------------- + +def rotate_cw(t: int) -> int: + return (t & 4) | ((t + 1) & 3) + +def rotate_ccw(t: int) -> int: + return (t & 4) | ((t - 1) & 3) + +# --------------------------------------------------------------------------- +# Data model +# --------------------------------------------------------------------------- + +@dataclass +class MonitorState: + name: str + x: int + y: int + width: int + height: int + refresh_rate: float + transform: int + scale: float + mirror_of: str + available_modes: List[str] + mode_index: int + dirty: bool = False + + @property + def logical_width(self) -> int: + if (self.transform & 3) in (1, 3): + return self.height + return self.width + + @property + def logical_height(self) -> int: + if (self.transform & 3) in (1, 3): + return self.width + return self.height + + @property + def mode_str(self) -> str: + return f"{self.width}x{self.height}@{int(round(self.refresh_rate))}" + + @classmethod + def from_json(cls, d: dict) -> "MonitorState": + modes = d.get("availableModes", []) + w = d.get("width", 1920) + h = d.get("height", 1080) + rr = d.get("refreshRate", 60.0) + # Find current mode index + mode_index = 0 + for i, m in enumerate(modes): + mo = _MODE_RE.match(m) + if mo and int(mo.group(1)) == w and int(mo.group(2)) == h: + if abs(float(mo.group(3)) - rr) < 1.0: + mode_index = i + break + return cls( + name=d.get("name", ""), + x=d.get("x", 0), + y=d.get("y", 0), + width=w, + height=h, + refresh_rate=rr, + transform=d.get("transform", 0), + scale=d.get("scale", 1.0), + mirror_of=d.get("mirrorOf", ""), + available_modes=modes, + mode_index=mode_index, + ) + +# --------------------------------------------------------------------------- +# hyprctl helpers +# --------------------------------------------------------------------------- + +def fetch_monitors() -> List[MonitorState]: + r = subprocess.run( + ["hyprctl", "monitors", "-j"], + capture_output=True, text=True, check=True, + ) + return [MonitorState.from_json(d) for d in json.loads(r.stdout)] + + +def apply_monitor(m: MonitorState) -> Optional[str]: + if m.mirror_of: + lua = f"hl.monitor({{output='{m.name}', mirror='{m.mirror_of}'}})" + else: + lua = ( + f"hl.monitor({{" + f"output='{m.name}', " + f"mode='{m.mode_str}', " + f"position='{m.x}x{m.y}', " + f"scale={m.scale}, " + f"transform={m.transform}" + f"}})" + ) + r = subprocess.run(["hyprctl", "eval", lua], capture_output=True, text=True, check=False) + if r.returncode != 0: + return (r.stderr or r.stdout).strip() + return None + +# --------------------------------------------------------------------------- +# Save +# --------------------------------------------------------------------------- + +def save_monitors_lua(monitors: List[MonitorState], path: Path) -> None: + lines = ["-- generated by monitor-manager -- do not edit by hand\n"] + for m in monitors: + if m.mirror_of: + lines.append( + f'hl.monitor({{\n' + f' output = "{m.name}",\n' + f' mirror = "{m.mirror_of}",\n' + f'}})\n\n' + ) + else: + lines.append( + f'hl.monitor({{\n' + f' output = "{m.name}",\n' + f' mode = "{m.mode_str}",\n' + f' position = "{m.x}x{m.y}",\n' + f' scale = {m.scale},\n' + f' transform = {m.transform},\n' + f'}})\n\n' + ) + lines.append( + 'hl.config({\n' + ' xwayland = {\n' + ' force_zero_scaling = true,\n' + ' },\n' + '})\n' + ) + tmp = path.with_suffix(".lua.tmp") + tmp.write_text("".join(lines)) + os.replace(tmp, path) + +# --------------------------------------------------------------------------- +# Canvas math +# --------------------------------------------------------------------------- + +def compute_scale(monitors: List[MonitorState], pane_cols: int, pane_rows: int) -> float: + if not monitors: + return 1.0 + max_x = max(m.x + m.logical_width for m in monitors) + max_y = max(m.y + m.logical_height for m in monitors) + if max_x <= 0 or max_y <= 0: + return 1.0 + sx = pane_cols / (max_x * 1.15) + sy = pane_rows / (max_y * 1.15) + return min(sx, sy) + + +def to_screen(cx: int, cy: int, scale: float, margin_col: int = 1, margin_row: int = 1): + col = margin_col + int(cx * scale) + row = margin_row + int(cy * scale * 0.5) + return row, col + +# --------------------------------------------------------------------------- +# Safe addstr / addch wrappers +# --------------------------------------------------------------------------- + +def safe_addstr(win, row, col, text, attr=0): + try: + max_row, max_col = win.getmaxyx() + if row < 0 or row >= max_row or col < 0 or col >= max_col: + return + avail = max_col - col - 1 + if avail <= 0: + return + win.addstr(row, col, text[:avail], attr) + except curses.error: + pass + + +def safe_addch(win, row, col, ch, attr=0): + try: + max_row, max_col = win.getmaxyx() + if row < 0 or row >= max_row or col < 0 or col >= max_col: + return + win.addch(row, col, ch, attr) + except curses.error: + pass + +# --------------------------------------------------------------------------- +# Box drawing +# --------------------------------------------------------------------------- + +def draw_box(win, row, col, h, w, attr=0): + if h < 2 or w < 2: + return + safe_addch(win, row, col, "┌", attr) + safe_addch(win, row, col + w-1, "┐", attr) + safe_addch(win, row + h-1, col, "└", attr) + safe_addch(win, row + h-1, col + w-1, "┘", attr) + for c in range(col + 1, col + w - 1): + safe_addch(win, row, c, "─", attr) + safe_addch(win, row + h-1, c, "─", attr) + for r in range(row + 1, row + h - 1): + safe_addch(win, r, col, "│", attr) + safe_addch(win, r, col + w-1, "│", attr) + +# --------------------------------------------------------------------------- +# App +# --------------------------------------------------------------------------- + +class App: + def __init__(self, stdscr): + self.stdscr = stdscr + self.monitors: List[MonitorState] = [] + self.selected_idx: int = 0 + self.mode: str = "normal" # "normal" | "mirror_pick" + self.mirror_source_idx: int = 0 + self.mirror_target_idx: int = 1 + self.dirty: bool = False + self.status_msg: str = "" + self._scale: float = 0.0 # cached canvas scale + self._scale_pane: tuple = (0, 0) # pane size used for cached scale + self._load_monitors() + self._init_colors() + + def _load_monitors(self): + self.monitors = fetch_monitors() + if self.selected_idx >= len(self.monitors): + self.selected_idx = 0 + + def _init_colors(self): + curses.start_color() + curses.use_default_colors() + # 1 = selected (cyan bold) + curses.init_pair(1, curses.COLOR_CYAN, -1) + # 2 = normal (white) + curses.init_pair(2, curses.COLOR_WHITE, -1) + # 3 = mirror target (yellow) + curses.init_pair(3, curses.COLOR_YELLOW, -1) + # 4 = mirrored / dim + curses.init_pair(4, curses.COLOR_BLACK + 8 if curses.COLORS >= 16 else curses.COLOR_WHITE, -1) + # 5 = status bar (reversed) + curses.init_pair(5, -1, -1) + # 6 = help (green) + curses.init_pair(6, curses.COLOR_GREEN, -1) + + def _get_scale(self, pane_cols: int, pane_rows: int) -> float: + """Return cached scale; recompute only on resize or when a monitor escapes the viewport.""" + pane = (pane_cols, pane_rows) + if self._scale > 0 and self._scale_pane == pane: + # Check every monitor still fits inside the current viewport + inner_cols = pane_cols - 2 + inner_rows = pane_rows - 2 + all_fit = True + for m in self.monitors: + brow, bcol = to_screen(m.x + m.logical_width, m.y + m.logical_height, self._scale) + if bcol > inner_cols or brow > inner_rows: + all_fit = False + break + if all_fit: + return self._scale + self._scale = compute_scale(self.monitors, pane_cols - 2, pane_rows - 2) + self._scale_pane = pane + return self._scale + + # ----------------------------------------------------------------------- + # Event loop + # ----------------------------------------------------------------------- + + def run(self): + curses.curs_set(0) + self.stdscr.timeout(100) + self.draw() + while True: + ch = self.stdscr.getch() + if ch == curses.KEY_RESIZE: + self._scale_pane = (0, 0) # force scale recompute on resize + self.draw() + continue + if ch == -1: + continue + if self.mode == "normal": + result = self.handle_key_normal(ch) + if result == "quit": + break + elif self.mode == "mirror_pick": + self.handle_key_mirror(ch) + self.draw() + + # ----------------------------------------------------------------------- + # Key handlers + # ----------------------------------------------------------------------- + + def handle_key_normal(self, ch) -> Optional[str]: + mon = self.monitors[self.selected_idx] if self.monitors else None + + # Tab / Shift+Tab — cycle monitor + if ch == ord("\t"): + self.selected_idx = (self.selected_idx + 1) % max(1, len(self.monitors)) + return + if ch == curses.KEY_BTAB: + self.selected_idx = (self.selected_idx - 1) % max(1, len(self.monitors)) + return + + if mon is None: + return + + # Movement — coarse + if ch == ord("h"): + self.move_monitor(-MOVE_STEP, 0) + elif ch == ord("l"): + self.move_monitor(MOVE_STEP, 0) + elif ch == ord("k"): + self.move_monitor(0, -MOVE_STEP) + elif ch == ord("j"): + self.move_monitor(0, MOVE_STEP) + # Movement — fine + elif ch == ord("H"): + self.move_monitor(-MOVE_STEP_FINE, 0) + elif ch == ord("L"): + self.move_monitor(MOVE_STEP_FINE, 0) + elif ch == ord("K"): + self.move_monitor(0, -MOVE_STEP_FINE) + elif ch == ord("J"): + self.move_monitor(0, MOVE_STEP_FINE) + # Rotation + elif ch == ord("u"): + self.rotate_monitor(-1) + elif ch == ord("i"): + self.rotate_monitor(+1) + # Mirror + elif ch == ord("m"): + if mon.mirror_of: + mon.mirror_of = "" + err = apply_monitor(mon) + mon.dirty = True + self.dirty = True + self.status_msg = err or f"Un-mirrored {mon.name}" + elif len(self.monitors) < 2: + self.status_msg = "Need 2+ monitors to mirror" + else: + self.mirror_source_idx = self.selected_idx + self.mirror_target_idx = (self.selected_idx + 1) % len(self.monitors) + self.mode = "mirror_pick" + # Mode cycling + elif ch == ord("n"): + self.cycle_mode(+1) + elif ch == ord("N"): + self.cycle_mode(-1) + # Save + elif ch == ord("s"): + self._save() + # Quit + elif ch in (ord("q"), 27): # q or Esc + if self.dirty: + action = self.prompt_save_quit() + if action == "cancel": + return None + if action == "save": + self._save() + return "quit" + + def handle_key_mirror(self, ch): + n = len(self.monitors) + + def next_target(delta: int): + t = (self.mirror_target_idx + delta) % n + # skip source + if t == self.mirror_source_idx: + t = (t + delta) % n + self.mirror_target_idx = t + + if ch == ord("\t"): + next_target(+1) + elif ch == curses.KEY_BTAB: + next_target(-1) + elif ch in (curses.KEY_ENTER, ord("\n"), ord("\r")): + self.set_mirror(self.mirror_source_idx, self.mirror_target_idx) + self.mode = "normal" + elif ch == 27: # Esc + self.mode = "normal" + self.status_msg = "Mirror cancelled" + + # ----------------------------------------------------------------------- + # Actions + # ----------------------------------------------------------------------- + + def move_monitor(self, dx: int, dy: int): + mon = self.monitors[self.selected_idx] + mon.x = max(0, mon.x + dx) + mon.y = max(0, mon.y + dy) + err = apply_monitor(mon) + mon.dirty = True + self.dirty = True + self.status_msg = err or f"Moved {mon.name} to {mon.x},{mon.y}" + + def rotate_monitor(self, direction: int): + mon = self.monitors[self.selected_idx] + if direction > 0: + mon.transform = rotate_cw(mon.transform) + else: + mon.transform = rotate_ccw(mon.transform) + err = apply_monitor(mon) + mon.dirty = True + self.dirty = True + self.status_msg = err or f"Rotated {mon.name} → {TRANSFORM_LABEL[mon.transform]}" + + def cycle_mode(self, delta: int): + mon = self.monitors[self.selected_idx] + if not mon.available_modes: + self.status_msg = "No mode list available" + return + mon.mode_index = (mon.mode_index + delta) % len(mon.available_modes) + mo = _MODE_RE.match(mon.available_modes[mon.mode_index]) + if mo: + mon.width = int(mo.group(1)) + mon.height = int(mo.group(2)) + mon.refresh_rate = float(mo.group(3)) + err = apply_monitor(mon) + mon.dirty = True + self.dirty = True + self.status_msg = err or f"{mon.name} → {mon.mode_str}" + + def set_mirror(self, src_idx: int, tgt_idx: int): + src = self.monitors[src_idx] + tgt = self.monitors[tgt_idx] + src.mirror_of = tgt.name + err = apply_monitor(src) + src.dirty = True + self.dirty = True + self.status_msg = err or f"Mirroring {src.name} → {tgt.name}" + + def _save(self): + try: + save_monitors_lua(self.monitors, MONITORS_LUA) + for m in self.monitors: + m.dirty = False + self.dirty = False + self.status_msg = f"Saved to {MONITORS_LUA.name}" + except Exception as e: + self.status_msg = f"Save failed: {e}" + + # ----------------------------------------------------------------------- + # Prompt + # ----------------------------------------------------------------------- + + def prompt_save_quit(self) -> str: + rows, cols = self.stdscr.getmaxyx() + prompt = "Unsaved changes. [s]ave & quit [n]o save [c]ancel" + safe_addstr(self.stdscr, rows - 1, 0, " " * (cols - 1), curses.A_REVERSE) + safe_addstr(self.stdscr, rows - 1, 0, prompt[:cols - 1], curses.A_REVERSE) + self.stdscr.timeout(-1) + while True: + ch = self.stdscr.getch() + if ch in (ord("s"), ord("S")): + self.stdscr.timeout(100) + return "save" + if ch in (ord("n"), ord("N")): + self.stdscr.timeout(100) + return "nosave" + if ch in (ord("c"), ord("C"), 27): + self.stdscr.timeout(100) + return "cancel" + + # ----------------------------------------------------------------------- + # Drawing + # ----------------------------------------------------------------------- + + def draw(self): + self.stdscr.erase() + rows, cols = self.stdscr.getmaxyx() + + use_info = cols >= 80 + canvas_w = (cols - INFO_W - 1) if use_info else cols + canvas_h = max(4, rows - STATUS_ROWS) + + self.draw_canvas(canvas_w, canvas_h) + if use_info: + self.draw_info(canvas_w, canvas_h) + # vertical separator + for r in range(canvas_h): + safe_addch(self.stdscr, r, canvas_w, "│", curses.color_pair(2)) + + self.draw_status(rows - 2, cols) + self.draw_help(rows - 1, cols) + self.stdscr.noutrefresh() + curses.doupdate() + + def draw_canvas(self, pane_cols: int, pane_rows: int): + scale = self._get_scale(pane_cols, pane_rows) + + for idx, mon in enumerate(self.monitors): + brow, bcol = to_screen(mon.x, mon.y, scale) + bw = max(MIN_BOX_W, int(mon.logical_width * scale)) + bh = max(MIN_BOX_H, int(mon.logical_height * scale * 0.5)) + + # Clamp to pane + if brow >= pane_rows or bcol >= pane_cols: + continue + + # Choose color + if self.mode == "mirror_pick" and idx == self.mirror_target_idx: + attr = curses.color_pair(3) | curses.A_BOLD + elif idx == self.selected_idx: + attr = curses.color_pair(1) | curses.A_BOLD + elif mon.mirror_of: + attr = curses.color_pair(4) + else: + attr = curses.color_pair(2) + + draw_box(self.stdscr, brow, bcol, bh, bw, attr) + + # Labels inside box + inner_w = bw - 2 + if inner_w < 1: + continue + + def label_line(row_offset: int, text: str): + r = brow + row_offset + c = bcol + 1 + if r >= pane_rows or r <= brow or r >= brow + bh - 1: + return + safe_addstr(self.stdscr, r, c, text[:inner_w], attr) + + # Line 1: name (+ dirty marker) + name_label = mon.name + (" *" if mon.dirty else "") + if mon.mirror_of: + name_label = f"{mon.name} →{mon.mirror_of}" + label_line(1, name_label) + + # Line 2: mode + if bh > 3: + label_line(2, mon.mode_str) + + # Line 3: rotation + if bh > 4: + label_line(3, TRANSFORM_LABEL.get(mon.transform, "")) + + def draw_info(self, start_col: int, pane_rows: int): + col = start_col + 1 + row = 0 + safe_addstr(self.stdscr, row, col, "Monitors:", curses.color_pair(6) | curses.A_BOLD) + row += 1 + for idx, mon in enumerate(self.monitors): + if row >= pane_rows: + break + prefix = "> " if idx == self.selected_idx else " " + attr = curses.color_pair(1) | curses.A_BOLD if idx == self.selected_idx else curses.color_pair(2) + safe_addstr(self.stdscr, row, col, f"{prefix}{mon.name}", attr) + row += 1 + if row < pane_rows: + safe_addstr(self.stdscr, row, col + 2, mon.mode_str, curses.color_pair(2)) + row += 1 + if row < pane_rows: + safe_addstr(self.stdscr, row, col + 2, f"pos {mon.x},{mon.y}", curses.color_pair(2)) + row += 1 + if row < pane_rows: + safe_addstr(self.stdscr, row, col + 2, f"scale {mon.scale}", curses.color_pair(2)) + row += 1 + if row < pane_rows: + rot_str = TRANSFORM_LABEL.get(mon.transform, "") + mirror_str = f" →{mon.mirror_of}" if mon.mirror_of else "" + safe_addstr(self.stdscr, row, col + 2, rot_str + mirror_str, curses.color_pair(2)) + row += 1 + row += 1 # blank between monitors + + def draw_status(self, row: int, cols: int): + if self.mode == "mirror_pick": + src = self.monitors[self.mirror_source_idx].name if self.monitors else "?" + tgt = self.monitors[self.mirror_target_idx].name if self.monitors else "?" + mode_tag = f"[MIRROR] {src} → {tgt}" + else: + mode_tag = "[NORMAL]" + if self.dirty: + mode_tag += " *" + + msg = f"{mode_tag} {self.status_msg}" + safe_addstr(self.stdscr, row, 0, " " * (cols - 1), curses.A_REVERSE) + safe_addstr(self.stdscr, row, 0, msg[:cols - 1], curses.A_REVERSE) + + def draw_help(self, row: int, cols: int): + if self.mode == "mirror_pick": + text = "Tab:cycle-target Enter:confirm Esc:cancel" + else: + text = "Tab:next hjkl:move HJK L:fine u/i:rot m:mirror n/N:mode s:save q:quit" + safe_addstr(self.stdscr, row, 0, text[:cols - 1], curses.color_pair(6)) + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main(): + try: + curses.wrapper(lambda stdscr: App(stdscr).run()) + except subprocess.CalledProcessError as e: + print(f"Error: hyprctl failed — is Hyprland running?\n{e}", file=sys.stderr) + sys.exit(1) + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + main() From e840072f69e18d6b8c26417594cdd09e1184c945 Mon Sep 17 00:00:00 2001 From: The_miro Date: Tue, 23 Jun 2026 09:04:01 +0200 Subject: [PATCH 04/14] chore: ignore Python __pycache__ and *.pyc files Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 7c59e3d..a0dc77e 100644 --- a/.gitignore +++ b/.gitignore @@ -56,5 +56,9 @@ Thumbs.db # Temporary files *.tmp +# Python bytecode +__pycache__/ +*.pyc + # thunar settings *desktopenvs/hyprland/xfce4/xfconf/xfce-perchannel-xml/thunar.xml From 280a41133fc722567672640ffb68d9310184c2d8 Mon Sep 17 00:00:00 2001 From: The_miro Date: Tue, 23 Jun 2026 09:15:44 +0200 Subject: [PATCH 05/14] feat(hyprlua): add T/G scale adjust and Enter save+quit to monitor-manager Co-Authored-By: Claude Sonnet 4.6 --- desktopenvs/hyprlua/hypr/usr/monitors.lua | 8 +++--- desktopenvs/hyprlua/scripts/monitor-manager | 30 ++++++++++++++++++++- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/desktopenvs/hyprlua/hypr/usr/monitors.lua b/desktopenvs/hyprlua/hypr/usr/monitors.lua index 259f532..c1b36b1 100644 --- a/desktopenvs/hyprlua/hypr/usr/monitors.lua +++ b/desktopenvs/hyprlua/hypr/usr/monitors.lua @@ -1,9 +1,7 @@ --- https://wiki.hypr.land/Configuring/Basics/Monitors/ +-- generated by monitor-manager -- do not edit by hand hl.monitor({ - output = "", - mode = "highres", - position = "auto", - scale = 2, + output = "eDP-1", + mirror = "none", }) hl.config({ diff --git a/desktopenvs/hyprlua/scripts/monitor-manager b/desktopenvs/hyprlua/scripts/monitor-manager index 4383efd..46272ee 100755 --- a/desktopenvs/hyprlua/scripts/monitor-manager +++ b/desktopenvs/hyprlua/scripts/monitor-manager @@ -7,9 +7,11 @@ Keys (normal mode): h j k l move monitor (50 px) H J K L move monitor (10 px fine) u / i rotate CCW / CW + T / G scale up / scale down (0.25 steps) m toggle mirror (pick target) / un-mirror n / N cycle display mode forward / backward s save to hypr/usr/monitors.lua + Enter save & quit q / Esc quit (prompts if unsaved changes) Mirror-pick mode: @@ -35,6 +37,9 @@ from typing import List, Optional MONITORS_LUA = Path.home() / "Dotfiles/desktopenvs/hyprlua/hypr/usr/monitors.lua" MOVE_STEP = 50 MOVE_STEP_FINE = 10 +SCALE_STEP = 0.25 +MIN_SCALE = 0.25 +MAX_SCALE = 4.0 MIN_BOX_W = 14 MIN_BOX_H = 4 INFO_W = 32 @@ -380,6 +385,11 @@ class App: self.rotate_monitor(-1) elif ch == ord("i"): self.rotate_monitor(+1) + # Scale + elif ch == ord("T"): + self.scale_monitor(+SCALE_STEP) + elif ch == ord("G"): + self.scale_monitor(-SCALE_STEP) # Mirror elif ch == ord("m"): if mon.mirror_of: @@ -402,6 +412,10 @@ class App: # Save elif ch == ord("s"): self._save() + # Save & quit + elif ch in (curses.KEY_ENTER, ord("\n"), ord("\r")): + self._save() + return "quit" # Quit elif ch in (ord("q"), 27): # q or Esc if self.dirty: @@ -457,6 +471,20 @@ class App: self.dirty = True self.status_msg = err or f"Rotated {mon.name} → {TRANSFORM_LABEL[mon.transform]}" + def scale_monitor(self, delta: float): + mon = self.monitors[self.selected_idx] + new_scale = round(max(MIN_SCALE, min(MAX_SCALE, mon.scale + delta)), 10) + # round to nearest SCALE_STEP to avoid floating-point drift + new_scale = round(new_scale / SCALE_STEP) * SCALE_STEP + if new_scale == mon.scale: + self.status_msg = f"Scale already at {mon.scale}x (limit)" + return + mon.scale = new_scale + err = apply_monitor(mon) + mon.dirty = True + self.dirty = True + self.status_msg = err or f"{mon.name} scale → {mon.scale}x" + def cycle_mode(self, delta: int): mon = self.monitors[self.selected_idx] if not mon.available_modes: @@ -634,7 +662,7 @@ class App: if self.mode == "mirror_pick": text = "Tab:cycle-target Enter:confirm Esc:cancel" else: - text = "Tab:next hjkl:move HJK L:fine u/i:rot m:mirror n/N:mode s:save q:quit" + text = "Tab:next hjkl:move HJKL:fine u/i:rot T/G:scale m:mirror n/N:mode s:save Enter:save+quit q:quit" safe_addstr(self.stdscr, row, 0, text[:cols - 1], curses.color_pair(6)) # --------------------------------------------------------------------------- From b28ee57ebf52bff0bbd716b9634b254c4b3a3789 Mon Sep 17 00:00:00 2001 From: The_miro Date: Tue, 23 Jun 2026 09:39:10 +0200 Subject: [PATCH 06/14] fix(hyprlua): normalize hyprctl mirrorOf "none" to prevent monitor reset hyprctl returns mirrorOf:"none" (string) for non-mirrored monitors. Python treated it as truthy, causing apply_monitor to always emit a mirror command, resetting resolution and scale on every keypress. Also restores monitors.lua with correct mode/scale/transform fields. Co-Authored-By: Claude Sonnet 4.6 --- desktopenvs/hyprlua/hypr/usr/monitors.lua | 7 +++++-- desktopenvs/hyprlua/scripts/monitor-manager | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/desktopenvs/hyprlua/hypr/usr/monitors.lua b/desktopenvs/hyprlua/hypr/usr/monitors.lua index c1b36b1..40be34a 100644 --- a/desktopenvs/hyprlua/hypr/usr/monitors.lua +++ b/desktopenvs/hyprlua/hypr/usr/monitors.lua @@ -1,7 +1,10 @@ -- generated by monitor-manager -- do not edit by hand hl.monitor({ - output = "eDP-1", - mirror = "none", + output = "eDP-1", + mode = "1920x1200@60", + position = "0x0", + scale = 2, + transform = 0, }) hl.config({ diff --git a/desktopenvs/hyprlua/scripts/monitor-manager b/desktopenvs/hyprlua/scripts/monitor-manager index 46272ee..29d3b61 100755 --- a/desktopenvs/hyprlua/scripts/monitor-manager +++ b/desktopenvs/hyprlua/scripts/monitor-manager @@ -126,7 +126,7 @@ class MonitorState: refresh_rate=rr, transform=d.get("transform", 0), scale=d.get("scale", 1.0), - mirror_of=d.get("mirrorOf", ""), + mirror_of="" if d.get("mirrorOf", "none") in ("none", "") else d["mirrorOf"], available_modes=modes, mode_index=mode_index, ) From 8343a741b878f95b7590912c16b4bfe3a1a6ce7e Mon Sep 17 00:00:00 2001 From: The_miro Date: Tue, 23 Jun 2026 09:42:44 +0200 Subject: [PATCH 07/14] =?UTF-8?q?chore(hyprlua):=20monitors.lua=20scale=20?= =?UTF-8?q?2=20=E2=86=92=202.0=20(written=20by=20monitor-manager)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- desktopenvs/hyprlua/hypr/usr/monitors.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktopenvs/hyprlua/hypr/usr/monitors.lua b/desktopenvs/hyprlua/hypr/usr/monitors.lua index 40be34a..6a5ecce 100644 --- a/desktopenvs/hyprlua/hypr/usr/monitors.lua +++ b/desktopenvs/hyprlua/hypr/usr/monitors.lua @@ -3,7 +3,7 @@ hl.monitor({ output = "eDP-1", mode = "1920x1200@60", position = "0x0", - scale = 2, + scale = 2.0, transform = 0, }) From 08e4e583a3f2aee65a6cf8927cdb12c3727d6dae Mon Sep 17 00:00:00 2001 From: The_miro Date: Tue, 23 Jun 2026 09:52:45 +0200 Subject: [PATCH 08/14] feat(hyprlua): snap T/G scale to valid Hyprland steps for current resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces fixed 0.25 increments with mathematically valid scales p/q (lowest terms, q≤6) where both width/s and height/s are integers. For 1920x1200 this gives 25 steps including 2.4, matching what Hyprland actually applies — no more mismatch between TUI display and live value. Co-Authored-By: Claude Sonnet 4.6 --- desktopenvs/hyprlua/scripts/monitor-manager | 56 ++++++++++++++++----- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/desktopenvs/hyprlua/scripts/monitor-manager b/desktopenvs/hyprlua/scripts/monitor-manager index 29d3b61..6f0d3f1 100755 --- a/desktopenvs/hyprlua/scripts/monitor-manager +++ b/desktopenvs/hyprlua/scripts/monitor-manager @@ -7,7 +7,7 @@ Keys (normal mode): h j k l move monitor (50 px) H J K L move monitor (10 px fine) u / i rotate CCW / CW - T / G scale up / scale down (0.25 steps) + T / G scale up / scale down (valid Hyprland steps) m toggle mirror (pick target) / un-mirror n / N cycle display mode forward / backward s save to hypr/usr/monitors.lua @@ -22,6 +22,7 @@ Mirror-pick mode: import curses import json +import math import os import re import subprocess @@ -37,9 +38,9 @@ from typing import List, Optional MONITORS_LUA = Path.home() / "Dotfiles/desktopenvs/hyprlua/hypr/usr/monitors.lua" MOVE_STEP = 50 MOVE_STEP_FINE = 10 -SCALE_STEP = 0.25 MIN_SCALE = 0.25 MAX_SCALE = 4.0 +_SCALE_MAX_DENOM = 6 # max denominator when enumerating valid Hyprland scales MIN_BOX_W = 14 MIN_BOX_H = 4 INFO_W = 32 @@ -68,6 +69,29 @@ def rotate_cw(t: int) -> int: def rotate_ccw(t: int) -> int: return (t & 4) | ((t - 1) & 3) +# --------------------------------------------------------------------------- +# Scale helpers +# --------------------------------------------------------------------------- + +def valid_scales(width: int, height: int) -> List[float]: + """Return sorted list of scales valid for (width, height). + + A scale s = p/q (in lowest terms) is valid iff both width/s and height/s + are integers, i.e. p divides gcd(width, height). We limit q ≤ + _SCALE_MAX_DENOM to keep the step count practical (~20 steps per monitor). + """ + g = math.gcd(width, height) + divisors = [k for k in range(1, g + 1) if g % k == 0] + result: set = set() + for p in divisors: + for q in range(1, _SCALE_MAX_DENOM + 1): + if math.gcd(p, q) != 1: + continue + s = p / q + if MIN_SCALE <= s <= MAX_SCALE: + result.add(round(s, 10)) + return sorted(result) + # --------------------------------------------------------------------------- # Data model # --------------------------------------------------------------------------- @@ -387,9 +411,9 @@ class App: self.rotate_monitor(+1) # Scale elif ch == ord("T"): - self.scale_monitor(+SCALE_STEP) + self.scale_monitor(+1) elif ch == ord("G"): - self.scale_monitor(-SCALE_STEP) + self.scale_monitor(-1) # Mirror elif ch == ord("m"): if mon.mirror_of: @@ -471,19 +495,27 @@ class App: self.dirty = True self.status_msg = err or f"Rotated {mon.name} → {TRANSFORM_LABEL[mon.transform]}" - def scale_monitor(self, delta: float): + def scale_monitor(self, direction: int): mon = self.monitors[self.selected_idx] - new_scale = round(max(MIN_SCALE, min(MAX_SCALE, mon.scale + delta)), 10) - # round to nearest SCALE_STEP to avoid floating-point drift - new_scale = round(new_scale / SCALE_STEP) * SCALE_STEP - if new_scale == mon.scale: - self.status_msg = f"Scale already at {mon.scale}x (limit)" - return + scales = valid_scales(mon.width, mon.height) + cur = round(mon.scale, 10) + if direction > 0: + candidates = [s for s in scales if s > cur + 1e-9] + if not candidates: + self.status_msg = f"Scale already at max ({mon.scale}x)" + return + new_scale = candidates[0] + else: + candidates = [s for s in scales if s < cur - 1e-9] + if not candidates: + self.status_msg = f"Scale already at min ({mon.scale}x)" + return + new_scale = candidates[-1] mon.scale = new_scale err = apply_monitor(mon) mon.dirty = True self.dirty = True - self.status_msg = err or f"{mon.name} scale → {mon.scale}x" + self.status_msg = err or f"{mon.name} scale → {new_scale}x" def cycle_mode(self, delta: int): mon = self.monitors[self.selected_idx] From fc90432d221f926627b709cbe9de86f238cbd27e Mon Sep 17 00:00:00 2001 From: The_miro Date: Tue, 23 Jun 2026 09:54:38 +0200 Subject: [PATCH 09/14] fix(hyprlua): change scale keybinds from T/G to t/g Co-Authored-By: Claude Sonnet 4.6 --- desktopenvs/hyprlua/scripts/monitor-manager | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/desktopenvs/hyprlua/scripts/monitor-manager b/desktopenvs/hyprlua/scripts/monitor-manager index 6f0d3f1..ee40a6e 100755 --- a/desktopenvs/hyprlua/scripts/monitor-manager +++ b/desktopenvs/hyprlua/scripts/monitor-manager @@ -7,7 +7,7 @@ Keys (normal mode): h j k l move monitor (50 px) H J K L move monitor (10 px fine) u / i rotate CCW / CW - T / G scale up / scale down (valid Hyprland steps) + t / g scale up / scale down (valid Hyprland steps) m toggle mirror (pick target) / un-mirror n / N cycle display mode forward / backward s save to hypr/usr/monitors.lua @@ -410,9 +410,9 @@ class App: elif ch == ord("i"): self.rotate_monitor(+1) # Scale - elif ch == ord("T"): + elif ch == ord("t"): self.scale_monitor(+1) - elif ch == ord("G"): + elif ch == ord("g"): self.scale_monitor(-1) # Mirror elif ch == ord("m"): @@ -694,7 +694,7 @@ class App: if self.mode == "mirror_pick": text = "Tab:cycle-target Enter:confirm Esc:cancel" else: - text = "Tab:next hjkl:move HJKL:fine u/i:rot T/G:scale m:mirror n/N:mode s:save Enter:save+quit q:quit" + text = "Tab:next hjkl:move HJKL:fine u/i:rot t/g:scale m:mirror n/N:mode s:save Enter:save+quit q:quit" safe_addstr(self.stdscr, row, 0, text[:cols - 1], curses.color_pair(6)) # --------------------------------------------------------------------------- From 3388910bb040e7f6e5c33603b013a2bfe405fe0b Mon Sep 17 00:00:00 2001 From: The_miro Date: Tue, 23 Jun 2026 09:56:19 +0200 Subject: [PATCH 10/14] feat(hyprlua): hyprctl reload after saving monitor config Co-Authored-By: Claude Sonnet 4.6 --- desktopenvs/hyprlua/scripts/monitor-manager | 1 + 1 file changed, 1 insertion(+) diff --git a/desktopenvs/hyprlua/scripts/monitor-manager b/desktopenvs/hyprlua/scripts/monitor-manager index ee40a6e..69110f5 100755 --- a/desktopenvs/hyprlua/scripts/monitor-manager +++ b/desktopenvs/hyprlua/scripts/monitor-manager @@ -549,6 +549,7 @@ class App: m.dirty = False self.dirty = False self.status_msg = f"Saved to {MONITORS_LUA.name}" + subprocess.run(["hyprctl", "reload"], capture_output=True, check=False) except Exception as e: self.status_msg = f"Save failed: {e}" From 449df42974bdd93d4dc9d3d43369c2a047d71679 Mon Sep 17 00:00:00 2001 From: The_miro Date: Tue, 23 Jun 2026 16:42:49 +0200 Subject: [PATCH 11/14] minor fixes, paths --- desktopenvs/hyprlua/eww-touch/eww.yuck | 2 +- desktopenvs/hyprlua/hypr/usr/monitors.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/desktopenvs/hyprlua/eww-touch/eww.yuck b/desktopenvs/hyprlua/eww-touch/eww.yuck index 6413d1d..ad2d16c 100644 --- a/desktopenvs/hyprlua/eww-touch/eww.yuck +++ b/desktopenvs/hyprlua/eww-touch/eww.yuck @@ -26,7 +26,7 @@ (box :orientation "h" :space-evenly false :halign "start" (osk) (box :class "music" {"${battery}"}) - (button :onclick "~/Dotfiles/desktopenvs/hyprland/scripts/drawer.sh" :class "icon-btn" :valign "center" :width 26 :height 26 {""}) + (button :onclick "~/.config/scripts/drawer.sh" :class "icon-btn" :valign "center" :width 26 :height 26 {""}) (metric :label "󰓃 " :value volume :onchange "pactl set-sink-volume @DEFAULT_SINK@ {}%" diff --git a/desktopenvs/hyprlua/hypr/usr/monitors.lua b/desktopenvs/hyprlua/hypr/usr/monitors.lua index 6a5ecce..38a74ab 100644 --- a/desktopenvs/hyprlua/hypr/usr/monitors.lua +++ b/desktopenvs/hyprlua/hypr/usr/monitors.lua @@ -3,7 +3,7 @@ hl.monitor({ output = "eDP-1", mode = "1920x1200@60", position = "0x0", - scale = 2.0, + scale = 1.5, transform = 0, }) From b4d0aec647e4e92f6cb1397cfcffaf3a5ccac2bb Mon Sep 17 00:00:00 2001 From: The_miro Date: Wed, 24 Jun 2026 09:26:15 +0200 Subject: [PATCH 12/14] feat(setup): wire mail-notmuch and caldav-sync into TUI installers + answerfile Two fully-featured module scripts already existed under optional-Modules/apps/ (mail-notmuch.sh and caldav-sync.sh) but were never surfaced in the installer UI, so users had no way to select them during setup. Changes across three files: simple-install.sh - count_steps(): added entries for mail-notmuch and caldav-sync so the [N/total] progress counter stays accurate; also back-filled 13 other apps (gimp, inkscape, krita, ardour, audacity, lmms, mixxx, cecilia, kdenlive, openshot, shotcut, anti-malware, timeshift) that were already in the checklist but missing from count_steps, causing the total to be wrong. - Checklist: added both entries under the CLI Tools header, directly after himalaya, with a human-readable description of the stack each installs. - Run section: added the conditional run_module calls so the modules actually execute when selected. tui-install.sh (dialog-based TUI, same three locations as above) - count_steps(): added mail-notmuch and caldav-sync. - Checklist: added both entries with matching descriptions. - Run section: added the conditional run_module calls. generate-answerfile.sh - Added both entries to the dialog checklist so the JSON answerfile generator (used for unattended / ISO-embedded installs) can also select them, keeping the answerfile schema in sync with the interactive TUIs. Co-Authored-By: Claude Sonnet 4.6 --- setup/generate-answerfile.sh | 2 ++ setup/simple-install.sh | 19 +++++++++++++++++++ setup/tui-install.sh | 6 ++++++ 3 files changed, 27 insertions(+) diff --git a/setup/generate-answerfile.sh b/setup/generate-answerfile.sh index 866108d..b9a1941 100644 --- a/setup/generate-answerfile.sh +++ b/setup/generate-answerfile.sh @@ -224,6 +224,8 @@ if [[ "$AF_RUN_TUI" == "true" ]]; then "networking-cli" "Networking CLI nmap · nethogs · mitmproxy · httpie" off \ "disk-recovery" "Disk Recovery ddrescue · f3" off \ "himalaya" "Himalaya terminal email client (AUR)" off \ + "mail-notmuch" "Mail (notmuch) isync · msmtp · notmuch · alot stack" off \ + "caldav-sync" "CalDAV Sync vdirsyncer · khal calendar sync" off \ "gnuplot" "Gnuplot scientific plotting" off \ "blender-povray" "Blender + POV-Ray 3D modelling & ray-tracing" off \ "toot" "toot Mastodon CLI client (AUR)" off \ diff --git a/setup/simple-install.sh b/setup/simple-install.sh index f52bcfe..0b397db 100755 --- a/setup/simple-install.sh +++ b/setup/simple-install.sh @@ -360,6 +360,21 @@ count_steps() { [[ "$a" == *"lamco-rdp-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"qemu"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"xournal"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"gimp"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"inkscape"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"krita"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"ardour"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"audacity"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"lmms"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"mixxx"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"cecilia"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"kdenlive"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"openshot"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"shotcut"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"anti-malware"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"timeshift"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"mail-notmuch"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"caldav-sync"* ]] && TOTAL=$(( TOTAL + 1 )) } # ── Answerfile ──────────────────────────────────────────────────────────────── @@ -501,6 +516,8 @@ else \ "" "CLI Tools" header \ "himalaya" "Himalaya terminal email client (AUR)" off \ + "mail-notmuch" "Mail (notmuch) isync · msmtp · notmuch · alot stack" off \ + "caldav-sync" "CalDAV Sync vdirsyncer · khal calendar sync" off \ "gnuplot" "Gnuplot scientific plotting" off \ "blender-povray" "Blender + POV-Ray 3D modelling & ray-tracing" off \ "toot" "toot Mastodon CLI client (AUR)" off \ @@ -653,6 +670,8 @@ fi [[ "$SELECTED_APPS" == *"networking-cli"* ]] && run_module "Networking CLI" "$APPS/networking-cli.sh" [[ "$SELECTED_APPS" == *"disk-recovery"* ]] && run_module "Disk Recovery" "$APPS/disk-recovery.sh" [[ "$SELECTED_APPS" == *"himalaya"* ]] && run_module "Himalaya" "$APPS/himalaya.sh" +[[ "$SELECTED_APPS" == *"mail-notmuch"* ]] && run_module "Mail (notmuch)" "$APPS/mail-notmuch.sh" +[[ "$SELECTED_APPS" == *"caldav-sync"* ]] && run_module "CalDAV Sync" "$APPS/caldav-sync.sh" [[ "$SELECTED_APPS" == *"gnuplot"* ]] && run_module "Gnuplot" "$APPS/gnuplot.sh" [[ "$SELECTED_APPS" == *"blender-povray"* ]] && run_module "Blender + POV-Ray" "$APPS/blender-povray.sh" [[ "$SELECTED_APPS" == *"toot"* ]] && run_module "toot" "$APPS/toot.sh" diff --git a/setup/tui-install.sh b/setup/tui-install.sh index e3c9d1c..aecdae7 100755 --- a/setup/tui-install.sh +++ b/setup/tui-install.sh @@ -185,6 +185,8 @@ count_steps() { [[ "$a" == *"shotcut"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"anti-malware"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$a" == *"timeshift"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"mail-notmuch"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$a" == *"caldav-sync"* ]] && TOTAL=$(( TOTAL + 1 )) } # ── Answerfile ──────────────────────────────────────────────────────────────── @@ -342,6 +344,8 @@ else "networking-cli" "Networking CLI nmap · nethogs · mitmproxy · httpie" off \ "disk-recovery" "Disk Recovery ddrescue · f3" off \ "himalaya" "Himalaya terminal email client (AUR)" off \ + "mail-notmuch" "Mail (notmuch) isync · msmtp · notmuch · alot stack" off \ + "caldav-sync" "CalDAV Sync vdirsyncer · khal calendar sync" off \ "gnuplot" "Gnuplot scientific plotting" off \ "blender-povray" "Blender + POV-Ray 3D modelling & ray-tracing" off \ "toot" "toot Mastodon CLI client (AUR)" off \ @@ -538,6 +542,8 @@ fi [[ "$SELECTED_APPS" == *"networking-cli"* ]] && run_module "Networking CLI" "$APPS/networking-cli.sh" [[ "$SELECTED_APPS" == *"disk-recovery"* ]] && run_module "Disk Recovery" "$APPS/disk-recovery.sh" [[ "$SELECTED_APPS" == *"himalaya"* ]] && run_module "Himalaya" "$APPS/himalaya.sh" +[[ "$SELECTED_APPS" == *"mail-notmuch"* ]] && run_module "Mail (notmuch)" "$APPS/mail-notmuch.sh" +[[ "$SELECTED_APPS" == *"caldav-sync"* ]] && run_module "CalDAV Sync" "$APPS/caldav-sync.sh" [[ "$SELECTED_APPS" == *"gnuplot"* ]] && run_module "Gnuplot" "$APPS/gnuplot.sh" [[ "$SELECTED_APPS" == *"blender-povray"* ]] && run_module "Blender + POV-Ray" "$APPS/blender-povray.sh" [[ "$SELECTED_APPS" == *"toot"* ]] && run_module "toot" "$APPS/toot.sh" From dd1cd2b8c7406398b866acaf44473d0b5314090d Mon Sep 17 00:00:00 2001 From: The_miro Date: Wed, 24 Jun 2026 09:26:47 +0200 Subject: [PATCH 13/14] feat(setup/apps): convert graphical apps to Flatpak-first with cyberqueer GTK theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Policy change: graphical apps now prefer Flatpak > pacman > AUR. Non-graphical tools keep pacman > AUR > source. This makes installed apps sandboxed, keeps system packages clean, and gives us a single hook point (apply_flatpak_theme) to theme every GUI app consistently. lib/logging.sh — two new helper functions sourced by every module: ensure_flatpak() Checks if flatpak is installed (pacman installs it if not) and ensures the Flathub remote is registered. Called at the top of every Flatpak script so the module is self-contained and safe to run in any order. apply_flatpak_theme(app_id) Copies gtk-themes/cyberqueer/ from the Dotfiles repo into ~/.themes/, then calls `flatpak override --user --filesystem=~/.themes:ro ` so the sandbox can read it, and `flatpak override --user --env=GTK_THEME=cyberqueer ` to activate it. Gracefully skips with a warning if the theme source directory is absent. App scripts converted (pacman/AUR → Flatpak + theme): ardour org.ardour.Ardour audacity org.audacityteam.Audacity chromium org.chromium.Chromium firefox org.mozilla.firefox geany org.geany.Geany gimp org.gimp.GIMP inkscape org.inkscape.Inkscape kate org.kde.kate kdenlive org.kde.kdenlive krita org.kde.krita librewolf io.gitlab.librewolf-community.librewolf lmms io.lmms.LMMS localsend org.localsend.localsend min-browser com.github.minbrowser.min mixxx org.mixxx.Mixxx onlyoffice org.onlyoffice.desktopeditors openshot org.openshot.OpenShot rdp-client org.remmina.Remmina (was pacman remmina + freerdp + libvncserver; Flatpak bundles all protocols, including VNC and SSH tunnels) shotcut org.shotcut.Shotcut steam com.valvesoftware.Steam vscodium com.vscodium.codium wireshark org.wireshark.Wireshark xournal com.github.xournalpp.xournalpp zed dev.zed.Zed zen-browser io.github.zen_browser.zen Special cases: blender-povray: Blender → Flatpak (org.blender.Blender) + theme; POV-Ray stays pacman because it has no Flatpak and is a CLI renderer, not a GUI app. prismlauncher / stuntrally: were already Flatpak installs; added apply_flatpak_theme so they pick up the cyberqueer theme like everything else. vesktop: switched from AUR vesktop to Flatpak dev.vencord.Vesktop. The AUR build requires cargo and takes several minutes; the Flatpak is pre-built. Vencord config is now deployed to ~/.var/app/dev.vencord.Vesktop/config/ (both Vencord/ and vesktop/ sub-dirs) instead of ~/.config/, which is where the Flatpak sandbox exposes its config directory. k8s: kubectl stays pacman (it is a CLI tool with no GUI, no Flatpak needed); podman-desktop switches from pacman podman-desktop to Flatpak io.podman_desktop.PodmanDesktop + theme, because it is a full GUI app. Co-Authored-By: Claude Sonnet 4.6 --- setup/modules/lib/logging.sh | 27 +++++++++++++++++++ setup/modules/optional-Modules/apps/ardour.sh | 6 +++-- .../modules/optional-Modules/apps/audacity.sh | 6 +++-- .../optional-Modules/apps/blender-povray.sh | 10 +++++-- .../modules/optional-Modules/apps/chromium.sh | 6 +++-- .../modules/optional-Modules/apps/firefox.sh | 6 +++-- setup/modules/optional-Modules/apps/geany.sh | 6 +++-- setup/modules/optional-Modules/apps/gimp.sh | 6 +++-- .../modules/optional-Modules/apps/inkscape.sh | 6 +++-- setup/modules/optional-Modules/apps/k8s.sh | 9 +++++-- setup/modules/optional-Modules/apps/kate.sh | 6 +++-- .../modules/optional-Modules/apps/kdenlive.sh | 6 +++-- setup/modules/optional-Modules/apps/krita.sh | 6 +++-- .../optional-Modules/apps/librewolf.sh | 6 +++-- setup/modules/optional-Modules/apps/lmms.sh | 6 +++-- .../optional-Modules/apps/localsend.sh | 6 +++-- .../optional-Modules/apps/min-browser.sh | 6 +++-- setup/modules/optional-Modules/apps/mixxx.sh | 6 +++-- .../optional-Modules/apps/onlyoffice.sh | 6 +++-- .../modules/optional-Modules/apps/openshot.sh | 6 +++-- .../optional-Modules/apps/prismlauncher.sh | 2 ++ .../optional-Modules/apps/rdp-client.sh | 11 ++++---- .../modules/optional-Modules/apps/shotcut.sh | 6 +++-- setup/modules/optional-Modules/apps/steam.sh | 6 +++-- .../optional-Modules/apps/stuntrally.sh | 2 ++ .../modules/optional-Modules/apps/vesktop.sh | 18 +++++++++---- .../modules/optional-Modules/apps/vscodium.sh | 6 +++-- .../optional-Modules/apps/wireshark.sh | 6 +++-- .../modules/optional-Modules/apps/xournal.sh | 6 +++-- setup/modules/optional-Modules/apps/zed.sh | 8 +++--- .../optional-Modules/apps/zen-browser.sh | 6 +++-- 31 files changed, 161 insertions(+), 64 deletions(-) mode change 100644 => 100755 setup/modules/optional-Modules/apps/ardour.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/audacity.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/blender-povray.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/chromium.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/firefox.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/geany.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/gimp.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/inkscape.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/k8s.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/kate.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/kdenlive.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/krita.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/librewolf.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/lmms.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/localsend.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/min-browser.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/mixxx.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/onlyoffice.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/openshot.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/prismlauncher.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/rdp-client.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/shotcut.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/steam.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/stuntrally.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/vesktop.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/vscodium.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/wireshark.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/xournal.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/zed.sh mode change 100644 => 100755 setup/modules/optional-Modules/apps/zen-browser.sh diff --git a/setup/modules/lib/logging.sh b/setup/modules/lib/logging.sh index df0050c..23d9cf4 100644 --- a/setup/modules/lib/logging.sh +++ b/setup/modules/lib/logging.sh @@ -10,3 +10,30 @@ log() { printf "${GREEN}[+] %s${RESET}\n" "$*"; } skip() { printf "${YELLOW}[~] %s${RESET}\n" "$*"; } warn() { printf "${YELLOW}[!] %s${RESET}\n" "$*" >&2; } err() { printf "${RED}[✖] %s${RESET}\n" "$*" >&2; } + +ensure_flatpak() { + if ! command -v flatpak &>/dev/null; then + log "Installing flatpak..." + sudo pacman -S --noconfirm --needed flatpak + fi + if ! flatpak remotes 2>/dev/null | grep -q flathub; then + log "Adding Flathub remote..." + flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + fi +} + +apply_flatpak_theme() { + local app_id="$1" + local theme_name="cyberqueer" + local theme_src="$HOME/Dotfiles/gtk-themes/$theme_name" + local themes_dir="$HOME/.themes" + if [[ ! -d "$theme_src" ]]; then + warn "Cyberqueer theme not found at $theme_src — skipping Flatpak theme override." + return 0 + fi + mkdir -p "$themes_dir" + cp -r "$theme_src" "$themes_dir/$theme_name" + flatpak override --user --filesystem="$themes_dir":ro "$app_id" + 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/ardour.sh b/setup/modules/optional-Modules/apps/ardour.sh old mode 100644 new mode 100755 index 7b2cb79..f23aada --- a/setup/modules/optional-Modules/apps/ardour.sh +++ b/setup/modules/optional-Modules/apps/ardour.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Ardour (professional DAW)..." -sudo pacman -S --noconfirm --needed ardour +log "Installing Ardour (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.ardour.Ardour +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 old mode 100644 new mode 100755 index 3548eb9..ff2fc84 --- a/setup/modules/optional-Modules/apps/audacity.sh +++ b/setup/modules/optional-Modules/apps/audacity.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Audacity (audio editor)..." -sudo pacman -S --noconfirm --needed audacity +log "Installing Audacity (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.audacityteam.Audacity +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 old mode 100644 new mode 100755 index c813518..e35e17c --- a/setup/modules/optional-Modules/apps/blender-povray.sh +++ b/setup/modules/optional-Modules/apps/blender-povray.sh @@ -2,6 +2,12 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Blender and POV-Ray..." -sudo pacman -S --noconfirm --needed blender povray +log "Installing Blender (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.blender.Blender +apply_flatpak_theme "org.blender.Blender" + +log "Installing POV-Ray (pacman)..." +sudo pacman -S --noconfirm --needed povray + log "Blender and POV-Ray installed." diff --git a/setup/modules/optional-Modules/apps/chromium.sh b/setup/modules/optional-Modules/apps/chromium.sh old mode 100644 new mode 100755 index 502444b..6898f1b --- a/setup/modules/optional-Modules/apps/chromium.sh +++ b/setup/modules/optional-Modules/apps/chromium.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Chromium..." -sudo pacman -S --noconfirm --needed chromium +log "Installing Chromium (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.chromium.Chromium +apply_flatpak_theme "org.chromium.Chromium" log "Chromium installed." diff --git a/setup/modules/optional-Modules/apps/firefox.sh b/setup/modules/optional-Modules/apps/firefox.sh old mode 100644 new mode 100755 index c2225fc..cb85b92 --- a/setup/modules/optional-Modules/apps/firefox.sh +++ b/setup/modules/optional-Modules/apps/firefox.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Firefox..." -sudo pacman -S --noconfirm --needed firefox +log "Installing Firefox (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.mozilla.firefox +apply_flatpak_theme "org.mozilla.firefox" log "Firefox installed." diff --git a/setup/modules/optional-Modules/apps/geany.sh b/setup/modules/optional-Modules/apps/geany.sh old mode 100644 new mode 100755 index d9fecb3..7c2b291 --- a/setup/modules/optional-Modules/apps/geany.sh +++ b/setup/modules/optional-Modules/apps/geany.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Geany and plugins..." -sudo pacman -S --noconfirm --needed geany geany-plugins +log "Installing Geany (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.geany.Geany +apply_flatpak_theme "org.geany.Geany" log "Geany installed." diff --git a/setup/modules/optional-Modules/apps/gimp.sh b/setup/modules/optional-Modules/apps/gimp.sh old mode 100644 new mode 100755 index 34d275e..93b6958 --- a/setup/modules/optional-Modules/apps/gimp.sh +++ b/setup/modules/optional-Modules/apps/gimp.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing GIMP..." -sudo pacman -S --noconfirm --needed gimp +log "Installing GIMP (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.gimp.GIMP +apply_flatpak_theme "org.gimp.GIMP" log "GIMP installed." diff --git a/setup/modules/optional-Modules/apps/inkscape.sh b/setup/modules/optional-Modules/apps/inkscape.sh old mode 100644 new mode 100755 index e8d84b5..c8d7b59 --- a/setup/modules/optional-Modules/apps/inkscape.sh +++ b/setup/modules/optional-Modules/apps/inkscape.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Inkscape..." -sudo pacman -S --noconfirm --needed inkscape +log "Installing Inkscape (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.inkscape.Inkscape +apply_flatpak_theme "org.inkscape.Inkscape" log "Inkscape installed." diff --git a/setup/modules/optional-Modules/apps/k8s.sh b/setup/modules/optional-Modules/apps/k8s.sh old mode 100644 new mode 100755 index 74e6a30..317bb43 --- a/setup/modules/optional-Modules/apps/k8s.sh +++ b/setup/modules/optional-Modules/apps/k8s.sh @@ -2,6 +2,11 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Kubernetes tools (kubectl, podman-desktop)..." -sudo pacman -S --noconfirm --needed kubectl podman-desktop +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 "Kubernetes tools installed." diff --git a/setup/modules/optional-Modules/apps/kate.sh b/setup/modules/optional-Modules/apps/kate.sh old mode 100644 new mode 100755 index 43e80c6..45b32f4 --- a/setup/modules/optional-Modules/apps/kate.sh +++ b/setup/modules/optional-Modules/apps/kate.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Kate..." -sudo pacman -S --noconfirm --needed kate +log "Installing Kate (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.kde.kate +apply_flatpak_theme "org.kde.kate" log "Kate installed." diff --git a/setup/modules/optional-Modules/apps/kdenlive.sh b/setup/modules/optional-Modules/apps/kdenlive.sh old mode 100644 new mode 100755 index c4b8c12..56d836d --- a/setup/modules/optional-Modules/apps/kdenlive.sh +++ b/setup/modules/optional-Modules/apps/kdenlive.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Kdenlive..." -sudo pacman -S --noconfirm --needed kdenlive +log "Installing Kdenlive (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.kde.kdenlive +apply_flatpak_theme "org.kde.kdenlive" log "Kdenlive installed." diff --git a/setup/modules/optional-Modules/apps/krita.sh b/setup/modules/optional-Modules/apps/krita.sh old mode 100644 new mode 100755 index da4c0a0..f7202a7 --- a/setup/modules/optional-Modules/apps/krita.sh +++ b/setup/modules/optional-Modules/apps/krita.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Krita..." -sudo pacman -S --noconfirm --needed krita +log "Installing Krita (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.kde.krita +apply_flatpak_theme "org.kde.krita" log "Krita installed." diff --git a/setup/modules/optional-Modules/apps/librewolf.sh b/setup/modules/optional-Modules/apps/librewolf.sh old mode 100644 new mode 100755 index de8425e..112107a --- a/setup/modules/optional-Modules/apps/librewolf.sh +++ b/setup/modules/optional-Modules/apps/librewolf.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing LibreWolf (AUR)..." -yay -S --answerdiff None --answerclean All --noconfirm librewolf-bin +log "Installing LibreWolf (Flatpak)..." +ensure_flatpak +flatpak install -y flathub io.gitlab.librewolf-community.librewolf +apply_flatpak_theme "io.gitlab.librewolf-community.librewolf" log "LibreWolf installed." diff --git a/setup/modules/optional-Modules/apps/lmms.sh b/setup/modules/optional-Modules/apps/lmms.sh old mode 100644 new mode 100755 index 5de8a28..f607b1b --- a/setup/modules/optional-Modules/apps/lmms.sh +++ b/setup/modules/optional-Modules/apps/lmms.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing LMMS..." -sudo pacman -S --noconfirm --needed lmms +log "Installing LMMS (Flatpak)..." +ensure_flatpak +flatpak install -y flathub io.lmms.LMMS +apply_flatpak_theme "io.lmms.LMMS" log "LMMS installed." diff --git a/setup/modules/optional-Modules/apps/localsend.sh b/setup/modules/optional-Modules/apps/localsend.sh old mode 100644 new mode 100755 index 0454412..95af553 --- a/setup/modules/optional-Modules/apps/localsend.sh +++ b/setup/modules/optional-Modules/apps/localsend.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing LocalSend (AUR)..." -yay -S --answerdiff None --answerclean All --noconfirm localsend +log "Installing LocalSend (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.localsend.localsend +apply_flatpak_theme "org.localsend.localsend" log "LocalSend installed." diff --git a/setup/modules/optional-Modules/apps/min-browser.sh b/setup/modules/optional-Modules/apps/min-browser.sh old mode 100644 new mode 100755 index 9b30375..e59845c --- a/setup/modules/optional-Modules/apps/min-browser.sh +++ b/setup/modules/optional-Modules/apps/min-browser.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Min browser (AUR)..." -yay -S --answerdiff None --answerclean All --noconfirm min +log "Installing Min browser (Flatpak)..." +ensure_flatpak +flatpak install -y flathub com.github.minbrowser.min +apply_flatpak_theme "com.github.minbrowser.min" log "Min browser installed." diff --git a/setup/modules/optional-Modules/apps/mixxx.sh b/setup/modules/optional-Modules/apps/mixxx.sh old mode 100644 new mode 100755 index a9676a6..4277adf --- a/setup/modules/optional-Modules/apps/mixxx.sh +++ b/setup/modules/optional-Modules/apps/mixxx.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Mixxx (DJ software)..." -sudo pacman -S --noconfirm --needed mixxx +log "Installing Mixxx (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.mixxx.Mixxx +apply_flatpak_theme "org.mixxx.Mixxx" log "Mixxx installed." diff --git a/setup/modules/optional-Modules/apps/onlyoffice.sh b/setup/modules/optional-Modules/apps/onlyoffice.sh old mode 100644 new mode 100755 index e878198..5c6fda7 --- a/setup/modules/optional-Modules/apps/onlyoffice.sh +++ b/setup/modules/optional-Modules/apps/onlyoffice.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing OnlyOffice (AUR)..." -yay -S --answerdiff None --answerclean All --noconfirm onlyoffice-bin +log "Installing OnlyOffice (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.onlyoffice.desktopeditors +apply_flatpak_theme "org.onlyoffice.desktopeditors" log "OnlyOffice installed." diff --git a/setup/modules/optional-Modules/apps/openshot.sh b/setup/modules/optional-Modules/apps/openshot.sh old mode 100644 new mode 100755 index 3101e14..2f1c1f1 --- a/setup/modules/optional-Modules/apps/openshot.sh +++ b/setup/modules/optional-Modules/apps/openshot.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing OpenShot..." -sudo pacman -S --noconfirm --needed openshot +log "Installing OpenShot (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.openshot.OpenShot +apply_flatpak_theme "org.openshot.OpenShot" log "OpenShot installed." diff --git a/setup/modules/optional-Modules/apps/prismlauncher.sh b/setup/modules/optional-Modules/apps/prismlauncher.sh old mode 100644 new mode 100755 index a72b539..60e57d6 --- a/setup/modules/optional-Modules/apps/prismlauncher.sh +++ b/setup/modules/optional-Modules/apps/prismlauncher.sh @@ -3,5 +3,7 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing PrismLauncher (Flatpak)..." +ensure_flatpak flatpak install -y flathub org.prismlauncher.PrismLauncher +apply_flatpak_theme "org.prismlauncher.PrismLauncher" log "PrismLauncher installed." diff --git a/setup/modules/optional-Modules/apps/rdp-client.sh b/setup/modules/optional-Modules/apps/rdp-client.sh old mode 100644 new mode 100755 index 7ac2885..a037660 --- a/setup/modules/optional-Modules/apps/rdp-client.sh +++ b/setup/modules/optional-Modules/apps/rdp-client.sh @@ -2,9 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Remmina RDP client with FreeRDP and VNC support..." -sudo pacman -S --noconfirm --needed \ - remmina \ - freerdp \ - libvncserver -log "Remmina installed with RDP (freerdp) and VNC support." +log "Installing Remmina (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.remmina.Remmina +apply_flatpak_theme "org.remmina.Remmina" +log "Remmina installed." diff --git a/setup/modules/optional-Modules/apps/shotcut.sh b/setup/modules/optional-Modules/apps/shotcut.sh old mode 100644 new mode 100755 index 7cb1d2f..32603f2 --- a/setup/modules/optional-Modules/apps/shotcut.sh +++ b/setup/modules/optional-Modules/apps/shotcut.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Shotcut..." -sudo pacman -S --noconfirm --needed shotcut +log "Installing Shotcut (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.shotcut.Shotcut +apply_flatpak_theme "org.shotcut.Shotcut" log "Shotcut installed." diff --git a/setup/modules/optional-Modules/apps/steam.sh b/setup/modules/optional-Modules/apps/steam.sh old mode 100644 new mode 100755 index 6babf77..8b730c8 --- a/setup/modules/optional-Modules/apps/steam.sh +++ b/setup/modules/optional-Modules/apps/steam.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Steam..." -sudo pacman -S --noconfirm --needed steam +log "Installing Steam (Flatpak)..." +ensure_flatpak +flatpak install -y flathub com.valvesoftware.Steam +apply_flatpak_theme "com.valvesoftware.Steam" log "Steam installed." diff --git a/setup/modules/optional-Modules/apps/stuntrally.sh b/setup/modules/optional-Modules/apps/stuntrally.sh old mode 100644 new mode 100755 index ba66aac..494275b --- a/setup/modules/optional-Modules/apps/stuntrally.sh +++ b/setup/modules/optional-Modules/apps/stuntrally.sh @@ -3,5 +3,7 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Installing Stunt Rally (Flatpak)..." +ensure_flatpak flatpak install -y flathub io.github.stuntrally.StuntRally3 +apply_flatpak_theme "io.github.stuntrally.StuntRally3" log "Stunt Rally installed." diff --git a/setup/modules/optional-Modules/apps/vesktop.sh b/setup/modules/optional-Modules/apps/vesktop.sh old mode 100644 new mode 100755 index e2153e0..4ea10d3 --- a/setup/modules/optional-Modules/apps/vesktop.sh +++ b/setup/modules/optional-Modules/apps/vesktop.sh @@ -2,11 +2,19 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Vesktop (AUR)..." -yay -S --answerdiff None --answerclean All --noconfirm vesktop +log "Installing Vesktop (Flatpak)..." +ensure_flatpak +flatpak install -y flathub dev.vencord.Vesktop +apply_flatpak_theme "dev.vencord.Vesktop" log "Deploying Vencord config..." -rm -rf ~/.config/Vencord ~/.config/vesktop -cp -r ~/Dotfiles/desktopenvs/hyprland/Vencord ~/.config/ -cp -r ~/Dotfiles/desktopenvs/hyprland/Vencord ~/.config/vesktop +FLATPAK_CFG="$HOME/.var/app/dev.vencord.Vesktop/config" +mkdir -p "$FLATPAK_CFG" +if [[ -d "$HOME/Dotfiles/desktopenvs/hyprland/Vencord" ]]; then + rm -rf "$FLATPAK_CFG/Vencord" "$FLATPAK_CFG/vesktop" + cp -r "$HOME/Dotfiles/desktopenvs/hyprland/Vencord" "$FLATPAK_CFG/Vencord" + cp -r "$HOME/Dotfiles/desktopenvs/hyprland/Vencord" "$FLATPAK_CFG/vesktop" +else + warn "Vencord config not found at ~/Dotfiles/desktopenvs/hyprland/Vencord — skipping." +fi log "Vesktop installed with Vencord theme." diff --git a/setup/modules/optional-Modules/apps/vscodium.sh b/setup/modules/optional-Modules/apps/vscodium.sh old mode 100644 new mode 100755 index 32e7d7d..fa5eccd --- a/setup/modules/optional-Modules/apps/vscodium.sh +++ b/setup/modules/optional-Modules/apps/vscodium.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing VSCodium (AUR)..." -yay -S --answerdiff None --answerclean All --noconfirm vscodium-bin +log "Installing VSCodium (Flatpak)..." +ensure_flatpak +flatpak install -y flathub com.vscodium.codium +apply_flatpak_theme "com.vscodium.codium" log "VSCodium installed." diff --git a/setup/modules/optional-Modules/apps/wireshark.sh b/setup/modules/optional-Modules/apps/wireshark.sh old mode 100644 new mode 100755 index 4b38e48..0d10f59 --- a/setup/modules/optional-Modules/apps/wireshark.sh +++ b/setup/modules/optional-Modules/apps/wireshark.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Wireshark..." -sudo pacman -S --noconfirm --needed wireshark-qt +log "Installing Wireshark (Flatpak)..." +ensure_flatpak +flatpak install -y flathub org.wireshark.Wireshark +apply_flatpak_theme "org.wireshark.Wireshark" log "Wireshark installed." diff --git a/setup/modules/optional-Modules/apps/xournal.sh b/setup/modules/optional-Modules/apps/xournal.sh old mode 100644 new mode 100755 index 5cbf6b4..3d1aabc --- a/setup/modules/optional-Modules/apps/xournal.sh +++ b/setup/modules/optional-Modules/apps/xournal.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Xournal++ (PDF annotator)..." -sudo pacman -S --noconfirm --needed xournalpp +log "Installing Xournal++ (Flatpak)..." +ensure_flatpak +flatpak install -y flathub com.github.xournalpp.xournalpp +apply_flatpak_theme "com.github.xournalpp.xournalpp" log "Xournal++ installed." diff --git a/setup/modules/optional-Modules/apps/zed.sh b/setup/modules/optional-Modules/apps/zed.sh old mode 100644 new mode 100755 index 0c38da1..336b25a --- a/setup/modules/optional-Modules/apps/zed.sh +++ b/setup/modules/optional-Modules/apps/zed.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Zed editor..." -sudo pacman -S --noconfirm --needed zed -log "Zed installed." +log "Installing Zed editor (Flatpak)..." +ensure_flatpak +flatpak install -y flathub dev.zed.Zed +apply_flatpak_theme "dev.zed.Zed" +log "Zed editor installed." diff --git a/setup/modules/optional-Modules/apps/zen-browser.sh b/setup/modules/optional-Modules/apps/zen-browser.sh old mode 100644 new mode 100755 index f51e33d..ec6577f --- a/setup/modules/optional-Modules/apps/zen-browser.sh +++ b/setup/modules/optional-Modules/apps/zen-browser.sh @@ -2,6 +2,8 @@ set -euo pipefail source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" -log "Installing Zen Browser (AUR)..." -yay -S --answerdiff None --answerclean All --noconfirm zen-browser-bin +log "Installing Zen Browser (Flatpak)..." +ensure_flatpak +flatpak install -y flathub io.github.zen_browser.zen +apply_flatpak_theme "io.github.zen_browser.zen" log "Zen Browser installed." From 7627dd67ff3285ba35f12da71b10842423cc78ea Mon Sep 17 00:00:00 2001 From: The_miro Date: Wed, 24 Jun 2026 09:27:08 +0200 Subject: [PATCH 14/14] feat(setup): seed /etc/skel from installed user's ~/.config after all modules run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: every module installs its config into the running user's ~/.config, but /etc/skel was never updated afterwards. Any additional user created with `useradd -m` later would get an empty home directory with no configs at all — they would have to manually copy or re-run setup. Solution: at the end of both TUI installer scripts (after every module and the colorway step have finished), copy the fully-configured user's home into /etc/skel so that it becomes the template for all future users. How it works — tui-install.sh + simple-install.sh (identical block in both): The block runs AFTER the last run_module call and AFTER apply-theme.sh, so the snapshot is taken when the home directory is in its final state. It copies: ~/.config/ → /etc/skel/.config/ (all app configs, DE configs, etc.) ~/.themes/ → /etc/skel/.themes/ (GTK themes, including cyberqueer) ~/.zshrc → /etc/skel/.zshrc ~/.bashrc → /etc/skel/.bashrc ~/.vimrc → /etc/skel/.vimrc Each copy is guarded ([[ -d ]] / [[ -f ]]) so missing files are silently skipped rather than erroring. sudo is used because /etc/skel is root-owned but the installer runs as the normal user. arch-autoinstall.sh + archbaseos-guided-install.sh (chroot-phase changes): The previous version tried to cherry-pick specific subdirectories from the Dotfiles repo clone (hypr/, niri/, waybar/, etc.) using a long list of cp commands. This was brittle — any new module that installs to ~/.config was not automatically captured, and the list had to be manually maintained. Replaced with a minimal block that only copies the three shell dotfiles (.zshrc, .bashrc, .vimrc) from the repo clone into /etc/skel. This is sufficient for the first user created during installation (useradd -m runs immediately after, before any modules). The full ~/.config sync above then takes over for all subsequent users after the modules have run. arch-autoinstall.sh additionally had the skel setup moved to before the useradd -m call (was missing entirely before) so even the first user gets the shell dotfiles, with a fallback direct-clone path if the skel clone fails. Co-Authored-By: Claude Sonnet 4.6 --- setup/arch-autoinstall.sh | 25 +++++++++++++++++++++---- setup/archbaseos-guided-install.sh | 8 ++++++++ setup/simple-install.sh | 12 ++++++++++++ setup/tui-install.sh | 12 ++++++++++++ 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/setup/arch-autoinstall.sh b/setup/arch-autoinstall.sh index f6a56a6..d873f26 100755 --- a/setup/arch-autoinstall.sh +++ b/setup/arch-autoinstall.sh @@ -382,6 +382,18 @@ echo "$HOSTNAME" > /etc/hostname # NetworkManager systemctl enable NetworkManager +# Populate /etc/skel with dotfiles and base configs for all new users +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 + 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 useradd -m -G wheel -s /bin/zsh "$USERNAME" echo "$USERNAME:$USERPASS" | chpasswd @@ -433,10 +445,15 @@ fi ################################################### # CLONE DOTFILES ################################################### -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." +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 CHROOT_EOF diff --git a/setup/archbaseos-guided-install.sh b/setup/archbaseos-guided-install.sh index 0c35f3f..5b5b6fb 100755 --- a/setup/archbaseos-guided-install.sh +++ b/setup/archbaseos-guided-install.sh @@ -343,6 +343,14 @@ 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 + mkdir -p /etc/skel/{Desktop,Documents,Downloads,Music,Pictures,Public,Templates,Videos} useradd -m -G wheel -s /bin/zsh "$USERNAME" diff --git a/setup/simple-install.sh b/setup/simple-install.sh index 0b397db..248d696 100755 --- a/setup/simple-install.sh +++ b/setup/simple-install.sh @@ -797,6 +797,18 @@ else fi fi +# ── Sync user config to /etc/skel ───────────────────────────────────────────── +# Captures everything installed by modules so future users start with the same setup. +if [[ -d "$HOME/.config" ]]; then + printf "\n Syncing ~/.config to /etc/skel...\n" + sudo mkdir -p /etc/skel/.config + sudo cp -r "$HOME/.config/." /etc/skel/.config/ +fi +[[ -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 +[[ -f "$HOME/.vimrc" ]] && sudo cp "$HOME/.vimrc" /etc/skel/.vimrc + # ── Done ────────────────────────────────────────────────────────────────────── if $ANSWERFILE_MODE; then printf "\nDone. Log: %s\n" "$LOG" diff --git a/setup/tui-install.sh b/setup/tui-install.sh index aecdae7..566c72d 100755 --- a/setup/tui-install.sh +++ b/setup/tui-install.sh @@ -682,6 +682,18 @@ else fi fi +# ── Sync user config to /etc/skel ───────────────────────────────────────────── +# Captures everything installed by modules so future users start with the same setup. +if [[ -d "$HOME/.config" ]]; then + printf "\n Syncing ~/.config to /etc/skel...\n" + sudo mkdir -p /etc/skel/.config + sudo cp -r "$HOME/.config/." /etc/skel/.config/ +fi +[[ -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 +[[ -f "$HOME/.vimrc" ]] && sudo cp "$HOME/.vimrc" /etc/skel/.vimrc + # ── Done ────────────────────────────────────────────────────────────────────── if $ANSWERFILE_MODE; then printf "\nDone. Log: %s\n" "$LOG"