Amir Alexander Abdelbaki 2026-06-24 17:52:16 +02:00
commit ce32d645e1
48 changed files with 1305 additions and 92 deletions

4
.gitignore vendored
View File

@ -56,5 +56,9 @@ Thumbs.db
# Temporary files
*.tmp
# Python bytecode
__pycache__/
*.pyc
# thunar settings
*desktopenvs/hyprland/xfce4/xfconf/xfce-perchannel-xml/thunar.xml

View File

@ -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" ? "☕" : "󰅺"}))

View File

@ -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@ {}%"
@ -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" ? "☕" : "󰅺"}))

View File

@ -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" ? "☕" : "󰅺"}))

View File

@ -2,14 +2,14 @@ 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)
}
# 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
}

View File

@ -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 ------
@ -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"))

View File

@ -1,9 +1,10 @@
-- 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",
mode = "1920x1200@60",
position = "0x0",
scale = 1.5,
transform = 0,
})
hl.config({

View File

@ -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"

View File

@ -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 \

View File

@ -0,0 +1,716 @@
#!/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
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
Enter save & quit
q / Esc quit (prompts if unsaved changes)
Mirror-pick mode:
Tab / Shift+Tab cycle target
Enter confirm
Esc cancel
"""
import curses
import json
import math
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_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
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)
# ---------------------------------------------------------------------------
# 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
# ---------------------------------------------------------------------------
@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="" if d.get("mirrorOf", "none") in ("none", "") else d["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)
# Scale
elif ch == ord("t"):
self.scale_monitor(+1)
elif ch == ord("g"):
self.scale_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()
# 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:
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 scale_monitor(self, direction: int):
mon = self.monitors[self.selected_idx]
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 → {new_scale}x"
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}"
subprocess.run(["hyprctl", "reload"], capture_output=True, check=False)
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 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))
# ---------------------------------------------------------------------------
# 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()

View File

@ -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

View File

@ -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"

257
setup/archiso/wds-deploy.sh Executable file
View File

@ -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 <URL> 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
# <tftp-prefix>/arch/boot/x86_64/vmlinuz-linux
# <tftp-prefix>/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" <<PXECFG
DEFAULT menu.c32
PROMPT 0
TIMEOUT 150
ONTIMEOUT m-archy
MENU TITLE M-Archy Arch Linux Installer
LABEL m-archy
MENU LABEL M-Archy Arch Linux Installer
KERNEL ${KERNEL_PATH}
APPEND ${APPEND_BASE}
LABEL m-archy-console
MENU LABEL M-Archy Arch Linux Installer (console only, no auto-launch)
KERNEL ${KERNEL_PATH}
APPEND ${APPEND_BASE} systemd.unit=multi-user.target
LABEL m-archy-ssh
MENU LABEL M-Archy Arch Linux Installer (SSH access enabled)
KERNEL ${KERNEL_PATH}
APPEND ${APPEND_BASE} systemd.unit=multi-user.target sshd=1
LABEL local
MENU LABEL Boot from local disk
LOCALBOOT 0
PXECFG
# ── Zip TFTP directory for easy transfer to Windows Server ────────────────────
ZIP_FILE="$WDS_DIR/wds-tftp.zip"
echo "Creating wds-tftp.zip..."
if command -v zip &>/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) = <WDS server IP>"
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 "========================================================================="

View File

@ -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 \

View File

@ -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."
}

6
setup/modules/optional-Modules/apps/ardour.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/audacity.sh Normal file → Executable file
View File

@ -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."

10
setup/modules/optional-Modules/apps/blender-povray.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/chromium.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/firefox.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/geany.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/gimp.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/inkscape.sh Normal file → Executable file
View File

@ -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."

9
setup/modules/optional-Modules/apps/k8s.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/kate.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/kdenlive.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/krita.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/librewolf.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/lmms.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/localsend.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/min-browser.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/mixxx.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/onlyoffice.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/openshot.sh Normal file → Executable file
View File

@ -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."

2
setup/modules/optional-Modules/apps/prismlauncher.sh Normal file → Executable file
View File

@ -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."

11
setup/modules/optional-Modules/apps/rdp-client.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/shotcut.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/steam.sh Normal file → Executable file
View File

@ -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."

2
setup/modules/optional-Modules/apps/stuntrally.sh Normal file → Executable file
View File

@ -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."

18
setup/modules/optional-Modules/apps/vesktop.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/vscodium.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/wireshark.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/xournal.sh Normal file → Executable file
View File

@ -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."

8
setup/modules/optional-Modules/apps/zed.sh Normal file → Executable file
View File

@ -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."

6
setup/modules/optional-Modules/apps/zen-browser.sh Normal file → Executable file
View File

@ -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."

View File

@ -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"
@ -778,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"

View File

@ -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"
@ -676,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"

View File

@ -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"
}