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 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..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@ {}%" @@ -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..6a13f02 100644 --- a/desktopenvs/hyprlua/hypr/hypridle.conf +++ b/desktopenvs/hyprlua/hypr/hypridle.conf @@ -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 } diff --git a/desktopenvs/hyprlua/hypr/usr/binds.lua b/desktopenvs/hyprlua/hypr/usr/binds.lua index 359d046..09c38b5 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 ------ @@ -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/hypr/usr/monitors.lua b/desktopenvs/hyprlua/hypr/usr/monitors.lua index 259f532..38a74ab 100644 --- a/desktopenvs/hyprlua/hypr/usr/monitors.lua +++ b/desktopenvs/hyprlua/hypr/usr/monitors.lua @@ -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({ 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 \ diff --git a/desktopenvs/hyprlua/scripts/monitor-manager b/desktopenvs/hyprlua/scripts/monitor-manager new file mode 100755 index 0000000..69110f5 --- /dev/null +++ b/desktopenvs/hyprlua/scripts/monitor-manager @@ -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() 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/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/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/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." diff --git a/setup/simple-install.sh b/setup/simple-install.sh index f52bcfe..248d696 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" @@ -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" diff --git a/setup/tui-install.sh b/setup/tui-install.sh index e3c9d1c..566c72d 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" @@ -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" 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" }