Merge branch 'main' of https://git.abdelbaki.eu/The_miro/Dotfiles
commit
ce32d645e1
|
|
@ -56,5 +56,9 @@ Thumbs.db
|
||||||
# Temporary files
|
# Temporary files
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|
||||||
|
# Python bytecode
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
# thunar settings
|
# thunar settings
|
||||||
*desktopenvs/hyprland/xfce4/xfconf/xfce-perchannel-xml/thunar.xml
|
*desktopenvs/hyprland/xfce4/xfconf/xfce-perchannel-xml/thunar.xml
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
:value {round((1 - (EWW_DISK["/"].free / EWW_DISK["/"].total)) * 100, 0)}
|
:value {round((1 - (EWW_DISK["/"].free / EWW_DISK["/"].total)) * 100, 0)}
|
||||||
:onchange ""
|
:onchange ""
|
||||||
:onclick ""))
|
:onclick ""))
|
||||||
|
(caffeine)
|
||||||
(clock)
|
(clock)
|
||||||
(systray :class "music" :orientation "h" :spacing 2 :space-evenly true)
|
(systray :class "music" :orientation "h" :spacing 2 :space-evenly true)
|
||||||
))
|
))
|
||||||
|
|
@ -112,3 +113,12 @@
|
||||||
(defpoll disks :interval "600s"
|
(defpoll disks :interval "600s"
|
||||||
"~/Dotfiles/desktopenvs/hyprland/scripts/dysk-phydisks.sh")
|
"~/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" ? "☕" : ""}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
(box :orientation "h" :space-evenly false :halign "start"
|
(box :orientation "h" :space-evenly false :halign "start"
|
||||||
(osk)
|
(osk)
|
||||||
(box :class "music" {"${battery}"})
|
(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 " "
|
(metric :label " "
|
||||||
:value volume
|
:value volume
|
||||||
:onchange "pactl set-sink-volume @DEFAULT_SINK@ {}%"
|
:onchange "pactl set-sink-volume @DEFAULT_SINK@ {}%"
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
|
|
||||||
(defwidget sidestuff []
|
(defwidget sidestuff []
|
||||||
(box :class "sidestuff" :orientation "h" :space-evenly false :halign "end"
|
(box :class "sidestuff" :orientation "h" :space-evenly false :halign "end"
|
||||||
|
(caffeine)
|
||||||
(clock)
|
(clock)
|
||||||
(systray :class "music" :orientation "h" :spacing 2 :space-evenly true)
|
(systray :class "music" :orientation "h" :spacing 2 :space-evenly true)
|
||||||
))
|
))
|
||||||
|
|
@ -159,3 +160,13 @@
|
||||||
|
|
||||||
(defpoll calender :interval "600s"
|
(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" ? "☕" : ""}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@
|
||||||
:onchange ""
|
:onchange ""
|
||||||
:onclick ""))
|
:onclick ""))
|
||||||
|
|
||||||
|
(caffeine)
|
||||||
(clock)
|
(clock)
|
||||||
(systray :class "music" :orientation "h" :spacing 2 :space-evenly true)
|
(systray :class "music" :orientation "h" :spacing 2 :space-evenly true)
|
||||||
))
|
))
|
||||||
|
|
@ -117,3 +118,12 @@
|
||||||
(defpoll disks :interval "600s"
|
(defpoll disks :interval "600s"
|
||||||
"~/Dotfiles/desktopenvs/hyprland/scripts/dysk-phydisks.sh")
|
"~/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" ? "☕" : ""}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@ general {
|
||||||
lock_cmd = pidof hyprlock || hyprlock
|
lock_cmd = pidof hyprlock || hyprlock
|
||||||
before_sleep_cmd = loginctl lock-session
|
before_sleep_cmd = loginctl lock-session
|
||||||
# fprintd restart ensures fingerprint sensor is ready after resume
|
# 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)
|
ignore_dbus_inhibit = false # respect systemd-inhibit locks (presence-detect, caffeine)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Presence detection resets the idle timer every 2 minutes while you're visible,
|
# Presence detection resets the idle timer every 2 minutes while you're visible,
|
||||||
# so these timeouts only run when you've actually stepped away.
|
# so these timeouts only run when you've actually stepped away.
|
||||||
listener {
|
listener {
|
||||||
timeout = 180 # 3 min — lock screen
|
timeout = 150 # 2.5 min — lock screen
|
||||||
on-timeout = loginctl lock-session
|
on-timeout = loginctl lock-session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ local winswitch = "" -- TODO: define your window switcher command
|
||||||
---- LID SWITCH ----
|
---- LID SWITCH ----
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
hl.bind("switch:on:Lid Switch", hl.dsp.exec_cmd("bash -c 'pidof hypridle > /dev/null && 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 exec hyprlock"), { locked = true })
|
hl.bind("switch:off:Lid Switch", hl.dsp.exec_cmd("hyprctl dispatch dpms on"), { locked = true })
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
---- GESTURES ------
|
---- 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 .. " + S", hl.dsp.exec_cmd("[tag +mixer] pavucontrol"))
|
||||||
hl.bind(mainMod .. " + U", hl.dsp.exec_cmd("[tag +centered-L] kitty btop"))
|
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 .. " + 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 .. " + 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 .. " + 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"))
|
hl.bind(mainMod .. " + CTRL + T", hl.dsp.exec_cmd("[tag +centered-S] kitty bash ~/.config/scripts/timer-pick"))
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
-- https://wiki.hypr.land/Configuring/Basics/Monitors/
|
-- generated by monitor-manager -- do not edit by hand
|
||||||
hl.monitor({
|
hl.monitor({
|
||||||
output = "",
|
output = "eDP-1",
|
||||||
mode = "highres",
|
mode = "1920x1200@60",
|
||||||
position = "auto",
|
position = "0x0",
|
||||||
scale = 2,
|
scale = 1.5,
|
||||||
|
transform = 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
hl.config({
|
hl.config({
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -7,7 +7,7 @@ if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
|
||||||
rm -f "$PID_FILE"
|
rm -f "$PID_FILE"
|
||||||
notify-send -t 2000 "Caffeine" "Idle inhibit OFF"
|
notify-send -t 2000 "Caffeine" "Idle inhibit OFF"
|
||||||
else
|
else
|
||||||
systemd-inhibit --what=idle:sleep \
|
systemd-inhibit --what=idle \
|
||||||
--who="caffeine" \
|
--who="caffeine" \
|
||||||
--why="Caffeine mode active" \
|
--why="Caffeine mode active" \
|
||||||
--mode=block \
|
--mode=block \
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -382,6 +382,18 @@ echo "$HOSTNAME" > /etc/hostname
|
||||||
# NetworkManager
|
# NetworkManager
|
||||||
systemctl enable 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
|
# User
|
||||||
useradd -m -G wheel -s /bin/zsh "$USERNAME"
|
useradd -m -G wheel -s /bin/zsh "$USERNAME"
|
||||||
echo "$USERNAME:$USERPASS" | chpasswd
|
echo "$USERNAME:$USERPASS" | chpasswd
|
||||||
|
|
@ -433,10 +445,15 @@ fi
|
||||||
###################################################
|
###################################################
|
||||||
# CLONE DOTFILES
|
# CLONE DOTFILES
|
||||||
###################################################
|
###################################################
|
||||||
echo "Cloning dotfiles..."
|
if [[ -d "/home/$USERNAME/Dotfiles" ]]; then
|
||||||
git clone https://git.abdelbaki.eu/The_miro/Dotfiles.git "/home/$USERNAME/Dotfiles" \
|
echo "Dotfiles already in home via skel — fixing ownership."
|
||||||
&& chown -R "$USERNAME":"$USERNAME" "/home/$USERNAME/Dotfiles" \
|
chown -R "$USERNAME:$USERNAME" "/home/$USERNAME"
|
||||||
|| echo "Warning: dotfiles clone failed — clone manually after first boot."
|
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
|
CHROOT_EOF
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,14 @@ echo "Cloning dotfiles into /etc/skel..."
|
||||||
git clone https://git.abdelbaki.eu/The_miro/Dotfiles.git /etc/skel/Dotfiles \
|
git clone https://git.abdelbaki.eu/The_miro/Dotfiles.git /etc/skel/Dotfiles \
|
||||||
|| echo "Warning: dotfiles clone failed — clone manually after first boot."
|
|| 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}
|
mkdir -p /etc/skel/{Desktop,Documents,Downloads,Music,Pictures,Public,Templates,Videos}
|
||||||
|
|
||||||
useradd -m -G wheel -s /bin/zsh "$USERNAME"
|
useradd -m -G wheel -s /bin/zsh "$USERNAME"
|
||||||
|
|
|
||||||
|
|
@ -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 "========================================================================="
|
||||||
|
|
@ -224,6 +224,8 @@ if [[ "$AF_RUN_TUI" == "true" ]]; then
|
||||||
"networking-cli" "Networking CLI nmap · nethogs · mitmproxy · httpie" off \
|
"networking-cli" "Networking CLI nmap · nethogs · mitmproxy · httpie" off \
|
||||||
"disk-recovery" "Disk Recovery ddrescue · f3" off \
|
"disk-recovery" "Disk Recovery ddrescue · f3" off \
|
||||||
"himalaya" "Himalaya terminal email client (AUR)" 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 \
|
"gnuplot" "Gnuplot scientific plotting" off \
|
||||||
"blender-povray" "Blender + POV-Ray 3D modelling & ray-tracing" off \
|
"blender-povray" "Blender + POV-Ray 3D modelling & ray-tracing" off \
|
||||||
"toot" "toot Mastodon CLI client (AUR)" off \
|
"toot" "toot Mastodon CLI client (AUR)" off \
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,30 @@ log() { printf "${GREEN}[+] %s${RESET}\n" "$*"; }
|
||||||
skip() { printf "${YELLOW}[~] %s${RESET}\n" "$*"; }
|
skip() { printf "${YELLOW}[~] %s${RESET}\n" "$*"; }
|
||||||
warn() { printf "${YELLOW}[!] %s${RESET}\n" "$*" >&2; }
|
warn() { printf "${YELLOW}[!] %s${RESET}\n" "$*" >&2; }
|
||||||
err() { printf "${RED}[✖] %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."
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Ardour (professional DAW)..."
|
log "Installing Ardour (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed ardour
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.ardour.Ardour
|
||||||
|
apply_flatpak_theme "org.ardour.Ardour"
|
||||||
log "Ardour installed."
|
log "Ardour installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Audacity (audio editor)..."
|
log "Installing Audacity (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed audacity
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.audacityteam.Audacity
|
||||||
|
apply_flatpak_theme "org.audacityteam.Audacity"
|
||||||
log "Audacity installed."
|
log "Audacity installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Blender and POV-Ray..."
|
log "Installing Blender (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed blender povray
|
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."
|
log "Blender and POV-Ray installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Chromium..."
|
log "Installing Chromium (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed chromium
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.chromium.Chromium
|
||||||
|
apply_flatpak_theme "org.chromium.Chromium"
|
||||||
log "Chromium installed."
|
log "Chromium installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Firefox..."
|
log "Installing Firefox (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed firefox
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.mozilla.firefox
|
||||||
|
apply_flatpak_theme "org.mozilla.firefox"
|
||||||
log "Firefox installed."
|
log "Firefox installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Geany and plugins..."
|
log "Installing Geany (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed geany geany-plugins
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.geany.Geany
|
||||||
|
apply_flatpak_theme "org.geany.Geany"
|
||||||
log "Geany installed."
|
log "Geany installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing GIMP..."
|
log "Installing GIMP (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed gimp
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.gimp.GIMP
|
||||||
|
apply_flatpak_theme "org.gimp.GIMP"
|
||||||
log "GIMP installed."
|
log "GIMP installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Inkscape..."
|
log "Installing Inkscape (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed inkscape
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.inkscape.Inkscape
|
||||||
|
apply_flatpak_theme "org.inkscape.Inkscape"
|
||||||
log "Inkscape installed."
|
log "Inkscape installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Kubernetes tools (kubectl, podman-desktop)..."
|
log "Installing kubectl (pacman)..."
|
||||||
sudo pacman -S --noconfirm --needed kubectl podman-desktop
|
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."
|
log "Kubernetes tools installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Kate..."
|
log "Installing Kate (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed kate
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.kde.kate
|
||||||
|
apply_flatpak_theme "org.kde.kate"
|
||||||
log "Kate installed."
|
log "Kate installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Kdenlive..."
|
log "Installing Kdenlive (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed kdenlive
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.kde.kdenlive
|
||||||
|
apply_flatpak_theme "org.kde.kdenlive"
|
||||||
log "Kdenlive installed."
|
log "Kdenlive installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Krita..."
|
log "Installing Krita (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed krita
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.kde.krita
|
||||||
|
apply_flatpak_theme "org.kde.krita"
|
||||||
log "Krita installed."
|
log "Krita installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing LibreWolf (AUR)..."
|
log "Installing LibreWolf (Flatpak)..."
|
||||||
yay -S --answerdiff None --answerclean All --noconfirm librewolf-bin
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub io.gitlab.librewolf-community.librewolf
|
||||||
|
apply_flatpak_theme "io.gitlab.librewolf-community.librewolf"
|
||||||
log "LibreWolf installed."
|
log "LibreWolf installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing LMMS..."
|
log "Installing LMMS (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed lmms
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub io.lmms.LMMS
|
||||||
|
apply_flatpak_theme "io.lmms.LMMS"
|
||||||
log "LMMS installed."
|
log "LMMS installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing LocalSend (AUR)..."
|
log "Installing LocalSend (Flatpak)..."
|
||||||
yay -S --answerdiff None --answerclean All --noconfirm localsend
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.localsend.localsend
|
||||||
|
apply_flatpak_theme "org.localsend.localsend"
|
||||||
log "LocalSend installed."
|
log "LocalSend installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Min browser (AUR)..."
|
log "Installing Min browser (Flatpak)..."
|
||||||
yay -S --answerdiff None --answerclean All --noconfirm min
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub com.github.minbrowser.min
|
||||||
|
apply_flatpak_theme "com.github.minbrowser.min"
|
||||||
log "Min browser installed."
|
log "Min browser installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Mixxx (DJ software)..."
|
log "Installing Mixxx (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed mixxx
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.mixxx.Mixxx
|
||||||
|
apply_flatpak_theme "org.mixxx.Mixxx"
|
||||||
log "Mixxx installed."
|
log "Mixxx installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing OnlyOffice (AUR)..."
|
log "Installing OnlyOffice (Flatpak)..."
|
||||||
yay -S --answerdiff None --answerclean All --noconfirm onlyoffice-bin
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.onlyoffice.desktopeditors
|
||||||
|
apply_flatpak_theme "org.onlyoffice.desktopeditors"
|
||||||
log "OnlyOffice installed."
|
log "OnlyOffice installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing OpenShot..."
|
log "Installing OpenShot (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed openshot
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.openshot.OpenShot
|
||||||
|
apply_flatpak_theme "org.openshot.OpenShot"
|
||||||
log "OpenShot installed."
|
log "OpenShot installed."
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,7 @@ set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing PrismLauncher (Flatpak)..."
|
log "Installing PrismLauncher (Flatpak)..."
|
||||||
|
ensure_flatpak
|
||||||
flatpak install -y flathub org.prismlauncher.PrismLauncher
|
flatpak install -y flathub org.prismlauncher.PrismLauncher
|
||||||
|
apply_flatpak_theme "org.prismlauncher.PrismLauncher"
|
||||||
log "PrismLauncher installed."
|
log "PrismLauncher installed."
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Remmina RDP client with FreeRDP and VNC support..."
|
log "Installing Remmina (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed \
|
ensure_flatpak
|
||||||
remmina \
|
flatpak install -y flathub org.remmina.Remmina
|
||||||
freerdp \
|
apply_flatpak_theme "org.remmina.Remmina"
|
||||||
libvncserver
|
log "Remmina installed."
|
||||||
log "Remmina installed with RDP (freerdp) and VNC support."
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Shotcut..."
|
log "Installing Shotcut (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed shotcut
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.shotcut.Shotcut
|
||||||
|
apply_flatpak_theme "org.shotcut.Shotcut"
|
||||||
log "Shotcut installed."
|
log "Shotcut installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Steam..."
|
log "Installing Steam (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed steam
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub com.valvesoftware.Steam
|
||||||
|
apply_flatpak_theme "com.valvesoftware.Steam"
|
||||||
log "Steam installed."
|
log "Steam installed."
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,7 @@ set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Stunt Rally (Flatpak)..."
|
log "Installing Stunt Rally (Flatpak)..."
|
||||||
|
ensure_flatpak
|
||||||
flatpak install -y flathub io.github.stuntrally.StuntRally3
|
flatpak install -y flathub io.github.stuntrally.StuntRally3
|
||||||
|
apply_flatpak_theme "io.github.stuntrally.StuntRally3"
|
||||||
log "Stunt Rally installed."
|
log "Stunt Rally installed."
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,19 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Vesktop (AUR)..."
|
log "Installing Vesktop (Flatpak)..."
|
||||||
yay -S --answerdiff None --answerclean All --noconfirm vesktop
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub dev.vencord.Vesktop
|
||||||
|
apply_flatpak_theme "dev.vencord.Vesktop"
|
||||||
|
|
||||||
log "Deploying Vencord config..."
|
log "Deploying Vencord config..."
|
||||||
rm -rf ~/.config/Vencord ~/.config/vesktop
|
FLATPAK_CFG="$HOME/.var/app/dev.vencord.Vesktop/config"
|
||||||
cp -r ~/Dotfiles/desktopenvs/hyprland/Vencord ~/.config/
|
mkdir -p "$FLATPAK_CFG"
|
||||||
cp -r ~/Dotfiles/desktopenvs/hyprland/Vencord ~/.config/vesktop
|
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."
|
log "Vesktop installed with Vencord theme."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing VSCodium (AUR)..."
|
log "Installing VSCodium (Flatpak)..."
|
||||||
yay -S --answerdiff None --answerclean All --noconfirm vscodium-bin
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub com.vscodium.codium
|
||||||
|
apply_flatpak_theme "com.vscodium.codium"
|
||||||
log "VSCodium installed."
|
log "VSCodium installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Wireshark..."
|
log "Installing Wireshark (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed wireshark-qt
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub org.wireshark.Wireshark
|
||||||
|
apply_flatpak_theme "org.wireshark.Wireshark"
|
||||||
log "Wireshark installed."
|
log "Wireshark installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Xournal++ (PDF annotator)..."
|
log "Installing Xournal++ (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed xournalpp
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub com.github.xournalpp.xournalpp
|
||||||
|
apply_flatpak_theme "com.github.xournalpp.xournalpp"
|
||||||
log "Xournal++ installed."
|
log "Xournal++ installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Zed editor..."
|
log "Installing Zed editor (Flatpak)..."
|
||||||
sudo pacman -S --noconfirm --needed zed
|
ensure_flatpak
|
||||||
log "Zed installed."
|
flatpak install -y flathub dev.zed.Zed
|
||||||
|
apply_flatpak_theme "dev.zed.Zed"
|
||||||
|
log "Zed editor installed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh"
|
||||||
|
|
||||||
log "Installing Zen Browser (AUR)..."
|
log "Installing Zen Browser (Flatpak)..."
|
||||||
yay -S --answerdiff None --answerclean All --noconfirm zen-browser-bin
|
ensure_flatpak
|
||||||
|
flatpak install -y flathub io.github.zen_browser.zen
|
||||||
|
apply_flatpak_theme "io.github.zen_browser.zen"
|
||||||
log "Zen Browser installed."
|
log "Zen Browser installed."
|
||||||
|
|
|
||||||
|
|
@ -360,6 +360,21 @@ count_steps() {
|
||||||
[[ "$a" == *"lamco-rdp-server"* ]] && TOTAL=$(( TOTAL + 1 ))
|
[[ "$a" == *"lamco-rdp-server"* ]] && TOTAL=$(( TOTAL + 1 ))
|
||||||
[[ "$a" == *"qemu"* ]] && TOTAL=$(( TOTAL + 1 ))
|
[[ "$a" == *"qemu"* ]] && TOTAL=$(( TOTAL + 1 ))
|
||||||
[[ "$a" == *"xournal"* ]] && 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 ────────────────────────────────────────────────────────────────
|
# ── Answerfile ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -501,6 +516,8 @@ else
|
||||||
\
|
\
|
||||||
"" "CLI Tools" header \
|
"" "CLI Tools" header \
|
||||||
"himalaya" "Himalaya terminal email client (AUR)" 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 \
|
"gnuplot" "Gnuplot scientific plotting" off \
|
||||||
"blender-povray" "Blender + POV-Ray 3D modelling & ray-tracing" off \
|
"blender-povray" "Blender + POV-Ray 3D modelling & ray-tracing" off \
|
||||||
"toot" "toot Mastodon CLI client (AUR)" 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" == *"networking-cli"* ]] && run_module "Networking CLI" "$APPS/networking-cli.sh"
|
||||||
[[ "$SELECTED_APPS" == *"disk-recovery"* ]] && run_module "Disk Recovery" "$APPS/disk-recovery.sh"
|
[[ "$SELECTED_APPS" == *"disk-recovery"* ]] && run_module "Disk Recovery" "$APPS/disk-recovery.sh"
|
||||||
[[ "$SELECTED_APPS" == *"himalaya"* ]] && run_module "Himalaya" "$APPS/himalaya.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" == *"gnuplot"* ]] && run_module "Gnuplot" "$APPS/gnuplot.sh"
|
||||||
[[ "$SELECTED_APPS" == *"blender-povray"* ]] && run_module "Blender + POV-Ray" "$APPS/blender-povray.sh"
|
[[ "$SELECTED_APPS" == *"blender-povray"* ]] && run_module "Blender + POV-Ray" "$APPS/blender-povray.sh"
|
||||||
[[ "$SELECTED_APPS" == *"toot"* ]] && run_module "toot" "$APPS/toot.sh"
|
[[ "$SELECTED_APPS" == *"toot"* ]] && run_module "toot" "$APPS/toot.sh"
|
||||||
|
|
@ -778,6 +797,18 @@ else
|
||||||
fi
|
fi
|
||||||
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 ──────────────────────────────────────────────────────────────────────
|
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||||
if $ANSWERFILE_MODE; then
|
if $ANSWERFILE_MODE; then
|
||||||
printf "\nDone. Log: %s\n" "$LOG"
|
printf "\nDone. Log: %s\n" "$LOG"
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,8 @@ count_steps() {
|
||||||
[[ "$a" == *"shotcut"* ]] && TOTAL=$(( TOTAL + 1 ))
|
[[ "$a" == *"shotcut"* ]] && TOTAL=$(( TOTAL + 1 ))
|
||||||
[[ "$a" == *"anti-malware"* ]] && TOTAL=$(( TOTAL + 1 ))
|
[[ "$a" == *"anti-malware"* ]] && TOTAL=$(( TOTAL + 1 ))
|
||||||
[[ "$a" == *"timeshift"* ]] && TOTAL=$(( TOTAL + 1 ))
|
[[ "$a" == *"timeshift"* ]] && TOTAL=$(( TOTAL + 1 ))
|
||||||
|
[[ "$a" == *"mail-notmuch"* ]] && TOTAL=$(( TOTAL + 1 ))
|
||||||
|
[[ "$a" == *"caldav-sync"* ]] && TOTAL=$(( TOTAL + 1 ))
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Answerfile ────────────────────────────────────────────────────────────────
|
# ── Answerfile ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -342,6 +344,8 @@ else
|
||||||
"networking-cli" "Networking CLI nmap · nethogs · mitmproxy · httpie" off \
|
"networking-cli" "Networking CLI nmap · nethogs · mitmproxy · httpie" off \
|
||||||
"disk-recovery" "Disk Recovery ddrescue · f3" off \
|
"disk-recovery" "Disk Recovery ddrescue · f3" off \
|
||||||
"himalaya" "Himalaya terminal email client (AUR)" 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 \
|
"gnuplot" "Gnuplot scientific plotting" off \
|
||||||
"blender-povray" "Blender + POV-Ray 3D modelling & ray-tracing" off \
|
"blender-povray" "Blender + POV-Ray 3D modelling & ray-tracing" off \
|
||||||
"toot" "toot Mastodon CLI client (AUR)" 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" == *"networking-cli"* ]] && run_module "Networking CLI" "$APPS/networking-cli.sh"
|
||||||
[[ "$SELECTED_APPS" == *"disk-recovery"* ]] && run_module "Disk Recovery" "$APPS/disk-recovery.sh"
|
[[ "$SELECTED_APPS" == *"disk-recovery"* ]] && run_module "Disk Recovery" "$APPS/disk-recovery.sh"
|
||||||
[[ "$SELECTED_APPS" == *"himalaya"* ]] && run_module "Himalaya" "$APPS/himalaya.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" == *"gnuplot"* ]] && run_module "Gnuplot" "$APPS/gnuplot.sh"
|
||||||
[[ "$SELECTED_APPS" == *"blender-povray"* ]] && run_module "Blender + POV-Ray" "$APPS/blender-povray.sh"
|
[[ "$SELECTED_APPS" == *"blender-povray"* ]] && run_module "Blender + POV-Ray" "$APPS/blender-povray.sh"
|
||||||
[[ "$SELECTED_APPS" == *"toot"* ]] && run_module "toot" "$APPS/toot.sh"
|
[[ "$SELECTED_APPS" == *"toot"* ]] && run_module "toot" "$APPS/toot.sh"
|
||||||
|
|
@ -676,6 +682,18 @@ else
|
||||||
fi
|
fi
|
||||||
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 ──────────────────────────────────────────────────────────────────────
|
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||||
if $ANSWERFILE_MODE; then
|
if $ANSWERFILE_MODE; then
|
||||||
printf "\nDone. Log: %s\n" "$LOG"
|
printf "\nDone. Log: %s\n" "$LOG"
|
||||||
|
|
|
||||||
51
sysupdate.sh
51
sysupdate.sh
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# sysupdate — Arch Linux System Update TUI
|
# 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
|
# Deps: yay, pacman-contrib (checkupdates), curl, python3, dialog
|
||||||
# flatpak (optional) | claude CLI (required for --AI)
|
# flatpak (optional) | claude CLI (required for --AI)
|
||||||
# State: /updatestate — ISO timestamp of last completed update
|
# 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)}"
|
DOTFILES="${DOTFILES:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
|
||||||
|
|
||||||
AI_MODE=false
|
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
|
# TERMINAL / COLOR SETUP
|
||||||
|
|
@ -568,11 +576,23 @@ _migrate_hypr_usr() {
|
||||||
fi
|
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() {
|
sync_configs() {
|
||||||
section "Config Sync"
|
section "Config Sync"
|
||||||
|
|
||||||
# ── Migration: old flat layout → hypr/usr/ ───────────────────────────────
|
# ── Migration: old flat layout → hypr/usr/ ───────────────────────────────
|
||||||
_migrate_hypr_usr
|
_migrate_hypr_usr
|
||||||
|
_fix_hypr_usr
|
||||||
|
|
||||||
# ── Preferred: use the installed update-configs.sh ───────────────────────
|
# ── Preferred: use the installed update-configs.sh ───────────────────────
|
||||||
local cfg_script
|
local cfg_script
|
||||||
|
|
@ -675,11 +695,27 @@ declare -a FLATPAK_PKGS=()
|
||||||
main() {
|
main() {
|
||||||
header
|
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)
|
local last_update; last_update=$(read_state)
|
||||||
log "Last recorded update: ${BO}${last_update}${RS}"
|
log "Last recorded update: ${BO}${last_update}${RS}"
|
||||||
echo
|
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 ────────────────────────────────────────────
|
# ── Collect available updates ────────────────────────────────────────────
|
||||||
section "Collecting Updates"
|
section "Collecting Updates"
|
||||||
|
|
||||||
|
|
@ -705,10 +741,6 @@ main() {
|
||||||
echo
|
echo
|
||||||
ok "${BO}System is up to date.${RS}"
|
ok "${BO}System is up to date.${RS}"
|
||||||
write_state
|
write_state
|
||||||
echo
|
|
||||||
if ask "Sync dotfiles configs to ~/.config?"; then
|
|
||||||
sync_configs
|
|
||||||
fi
|
|
||||||
rm -f "$_NEWS_PY"
|
rm -f "$_NEWS_PY"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
@ -861,11 +893,6 @@ main() {
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# ── Config sync ──────────────────────────────────────────────────────────
|
|
||||||
if ask "Sync dotfiles configs to ~/.config?"; then
|
|
||||||
sync_configs
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$_NEWS_PY"
|
rm -f "$_NEWS_PY"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue