commit be7bffc1e55b82463cd50ae7774e9b496911a5cd Author: Pepe44DEV Date: Wed May 27 20:51:58 2026 +0200 Initial commit: Omeron modular Hyprland setup framework - Modular installer with gum-based TUI - Fresh-install detection with auto GPU driver selection - Preflight module for system detection (Intel/AMD/NVIDIA) - Core modules: packages, dotfiles, services, SDDM - Optional software installer (Obsidian, Neovim, VS Code, etc.) - Homelab config module with dynamic AGS integration - Two complete themes: Forest Neon and Rose Night - 19 Hyprland control scripts + 4 AGS widgets - Idempotent dotfile deployment with automatic backup - YAML-based configuration, extensible module system - Full logging to ~/.local/share/omeron/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f6ed8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# OS +.DS_Store +Thumbs.db + +# Editor +*.swp +*.swo +*~ +*.bak + +# Python +__pycache__/ +*.pyc +.cache/ + +# Omeron specific +*.before-theme-switcher diff --git a/README.md b/README.md new file mode 100644 index 0000000..75eb336 --- /dev/null +++ b/README.md @@ -0,0 +1,219 @@ +# ᎾᎷᎬᏒᎾᏁ + +**Modular System Setup Framework** für Arch/Hyprland + +Omeron ist ein modulares, interaktives Setup-Framework, das Dotfiles-Konsolidierung, +Paketverwaltung und Systemkonfiguration in einem sauberen, wartbaren Projekt vereint. + +--- + +## Features + +- **Fresh-Install Detection** — Erkennt automatisch ein neues System und installiert Hyprland, GPU-Treiber + alle Dependencies +- **GPU-Erkennung** — Intel/AMD/NVIDIA via PCI Vendor-ID, installiert passende Treiber +- **Modularer Installer** — Jede Komponente ist ein eigenständiges Modul +- **Interaktive TUI** — Basierend auf `gum`, mit fallback auf einfaches CLI +- **Idempotent** — Beliebig oft ausführbar, Backup vor jeder Änderung +- **Dotfiles-Management** — Automatische Sicherung und Deployment +- **Homelab-Integration** — Unraid-Server-Konfiguration für das AGS Control Center +- **Theme-Engine** — Zwei vollständige Themes (Forest Neon, Rose Night) +- **Optionale Software** — Interaktive Auswahl via TUI +- **Logging** — Alle Schritte werden protokolliert +- **Erweiterbar** — Einfaches Plugin-System für eigene Module + +--- + +## Projektstruktur + +``` +Omeron/ +├── install.sh # Main Installer (Einstiegspunkt) +├── README.md +├── config/ +│ ├── omeron.yaml # Installer-Konfiguration +│ └── homelab.yaml # Homelab Standard-Konfiguration +├── lib/ +│ ├── log.sh # Logging-Framework +│ ├── tui.sh # TUI-Wrapper (gum/basic) +│ ├── config.sh # YAML/Shell-Konfiguration +│ ├── utils.sh # Utility-Funktionen +│ └── modules.sh # Modul-Manager +├── modules/ +│ ├── core/ # Core-Module (obligatorisch) +│ │ ├── preflight.sh # System-Detection (GPU, Fresh-Install, AUR-Helper) +│ │ ├── packages.sh # System-Pakete inkl. GPU-Treiber installieren +│ │ ├── dotfiles.sh # Dotfiles deployen +│ │ ├── services.sh # Systemd-Services aktivieren +│ │ └── sddm.sh # SDDM-Theme installieren +│ ├── optional/ +│ │ ├── install.sh # Optionale Software-Auswahl +│ │ └── packages/ # Einzelpaket-Installer +│ ├── homelab/ +│ │ └── setup.sh # Homelab-Konfiguration +│ └── post/ +│ └── apply-theme.sh # Theme nach Installation anwenden +├── dotfiles/ # Konsolidierte Dotfiles +│ ├── hypr/ # Hyprland + AGS Widgets +│ ├── waybar/ # Waybar Status Bar +│ ├── wofi/ # Application Launcher +│ ├── swaync/ # Notification Center +│ ├── kitty/ # Terminal +│ ├── gtk-3.0/ # GTK3-Theme +│ ├── gtk-4.0/ # GTK4-Theme +│ ├── qt5ct/ # Qt5-Theme +│ ├── qt6ct/ # Qt6-Theme +│ ├── starship.toml # Shell Prompt +│ └── wallpapers/ # Mitgelieferte Wallpaper +└── templates/ + └── homelab/ # Config-Templates +``` + +--- + +## Installation + +### Voraussetzungen + +- **Arch Linux / CachyOS** (oder andere Arch-Derivate) +- `bash` >= 4.0 +- `gum` (optional, aber empfohlen) — `sudo pacman -S gum` +- `git` + +### Schnellstart + +```bash +git clone /Omeron.git +cd Omeron +./install.sh +``` + +### Optionen + +### Autodetect (Fresh Install) + +Wird `./install.sh` auf einem frischen Arch-System ohne Hyprland ausgeführt, erkennt der Installer dies automatisch und installiert: + +- **Hyprland** + alle Komponenten (Hyprlock, Hyprpaper, Hypridle) +- **GPU-Treiber** (Intel/AMD/NVIDIA automatisch erkannt) +- **Audio** (PipeWire + WirePlumber) +- **Netzwerk** (NetworkManager + Bluetooth) +- **Fonts & Themes** (Nerd Fonts, Papirus Icons) +- **Dotfiles** inkl. Theme und Services + +Ohne `--fresh` und ohne `--modules` startet der interaktive Modus mit Step-by-Step Auswahl. + +### Optionen + +| Flag | Beschreibung | +|------|-------------| +| `--fresh` | Full System Setup (Hyprland + GPU + alle Dependencies) | +| `--modules m1,m2` | Nur bestimmte Module ausführen | +| `--skip m1,m2` | Bestimmte Module überspringen | +| `--skip-packages` | Paketinstallation überspringen | +| `--with-sddm` | SDDM-Theme inkludieren | +| `--list-modules` | Verfügbare Module anzeigen | +| `--help` | Hilfe anzeigen | + +### Beispiele + +```bash +# Automatisch: erkennt Fresh-Install oder interaktiv +./install.sh + +# Fresh Install auf neuem System (oder forcieren) +./install.sh --fresh + +# Nur Dotfiles und Theme deployen +./install.sh --modules core/dotfiles,post/apply-theme + +# Komplette Installation ohne Pakete +./install.sh --skip-packages + +# Mit SDDM-Theme +./install.sh --with-sddm +``` + +--- + +## Module Erstellen + +Jedes Modul ist eine Bash-Datei in `modules/` mit folgenden Funktionen: + +```bash +#!/usr/bin/env bash + +module_description() { printf "My Module - does something\n"; } +module_required() { return 1; } # 0 = immer ausführen +module_should_skip() { return 1; } # 0 = überspringen +module_prereqs() { require mycmd mypackage; } + +module_main() { + log_section "My Module" + # Your code here +} +``` + +--- + +## Homelab-Konfiguration + +Der Installer fragt beim Homelab-Modul ab: + +- **Server-Adresse** (IP oder Domain) +- **SSH-Benutzername** + +Gespeichert in `~/.config/homelab/config.yaml`: + +```yaml +server: + address: "192.168.1.100" + username: "root" + port: 22 +``` + +--- + +## Themes + +| Theme | Akzent | Hintergrund | Wallpaper | +|-------|--------|-------------|-----------| +| **Forest Neon** | `#00ff9c` (grün) | `#14141e` | `forest.jpg` | +| **Rose Night** | `#f38ba8` (pink) | `#18141f` | `rose-pink.jpg` | + +Theme-Wechsel via: + +```bash +~/.config/hypr/Scripts/theme-menu.sh +``` + +--- + +## Verzeichnis-Layout + +| Pfad | Zweck | +|------|-------| +| `Omeron/.gitignore` | Git-Ignore-Regeln | +| `Omeron/install.sh` | Main Installer (Einstiegspunkt) | +| `Omeron/config/omeron.yaml` | Installer-Konfiguration | +| `Omeron/lib/` | Framework-Bibliotheken (Log, TUI, Config, Utils, Module) | +| `Omeron/modules/core/` | Core-Module (Preflight, Packages, Dotfiles, Services, SDDM) | +| `Omeron/modules/optional/` | Optionale Software-Auswahl | +| `Omeron/modules/homelab/` | Homelab-Konfiguration | +| `Omeron/dotfiles/` | Alle Konfigurationsdateien | +| `~/.config/hypr/` | Hyprland-Konfiguration + Scripts | +| `~/.config/hypr/Scripts/` | Alle Steuerungsskripte (19 Stück) | +| `~/.config/hypr/ags/` | AGS Widgets (Panel, Switcher, Package Manager, Homelab) | +| `~/.config/hypr/Themes/` | Theme-Definitionen | +| `~/.config/waybar/` | Status Bar | +| `~/.config/wofi/` | Application Launcher | +| `~/.config/swaync/` | Notification Center | +| `~/.config/starship.toml` | Shell Prompt | +| `~/.dotfiles-backup/` | Automatische Backups | +| `~/.config/homelab/config.yaml` | Homelab-Server-Konfiguration | +| `~/.local/share/omeron/` | Installer-Logs | + +--- + +## Lizenz + +MIT diff --git a/config/homelab.yaml b/config/homelab.yaml new file mode 100644 index 0000000..29e77d1 --- /dev/null +++ b/config/homelab.yaml @@ -0,0 +1,18 @@ +# Homelab Configuration +# Server connection details for the Homelab Control Center + +server: + address: "" + username: "root" + port: 22 + +control_center: + refresh_interval: 5 + theme: "dark" + +features: + docker: true + services: true + storage: true + network: true + monitoring: true diff --git a/config/omeron.yaml b/config/omeron.yaml new file mode 100644 index 0000000..cb0e2d2 --- /dev/null +++ b/config/omeron.yaml @@ -0,0 +1,130 @@ +# Omeron System Setup Framework +# Main Configuration + +installer: + title: "Omeron System Setup" + style: "gum" + log_level: "INFO" + backup_dir: "${HOME}/.dotfiles-backup" + + fresh_install_defaults: true + + steps: + - id: "packages" + name: "System Packages" + description: "Install core system packages" + module: "core/packages" + required: true + default: true + + - id: "dotfiles" + name: "Dotfiles" + description: "Deploy configuration files" + module: "core/dotfiles" + required: true + default: true + + - id: "services" + name: "System Services" + description: "Enable and start system services" + module: "core/services" + required: false + default: true + + - id: "sddm" + name: "SDDM Theme" + description: "Install SDDM login theme" + module: "core/sddm" + required: false + default: false + + - id: "homelab" + name: "Homelab Configuration" + description: "Configure Homelab server access" + module: "homelab/setup" + required: false + default: false + + - id: "optional" + name: "Optional Software" + description: "Select and install optional packages" + module: "optional/install" + required: false + default: false + + - id: "theme" + name: "Apply Theme" + description: "Apply default theme" + module: "post/apply-theme" + required: false + default: true + +detection: + gpu_auto: true + fresh_install_auto: true + +packages: + core: + - hyprland + - hyprpaper + - hyprlock + - waybar + - wofi + - swaync + - kitty + - nautilus + - brightnessctl + - playerctl + - wireplumber + - pipewire + - pipewire-pulse + - networkmanager + - bluez + - bluez-utils + - hyprshot + - grim + - slurp + - swappy + - wl-clipboard + - libnotify + - sshpass + - papirus-icon-theme + - qt5ct + - qt6ct + - starship + - python-gobject + - gtk3 + - gtk4 + - noto-fonts + - noto-fonts-emoji + - ttf-jetbrains-mono-nerd + + aur: [] + +services: + - networkmanager + - bluetooth + +theme: + default: "forest-neon" + themes_dir: "${HOME}/.config/hypr/Themes" + +dotfiles: + items: + - hypr + - waybar + - wofi + - swaync + - kitty + - gtk-3.0 + - gtk-4.0 + - qt5ct + - qt6ct + + extra: + - source: "starship.toml" + target: "${HOME}/.config/starship.toml" + + wallpapers: + source: "wallpapers" + target: "${HOME}/Bilder/Wallpaper" diff --git a/dotfiles/gtk-3.0/bookmarks b/dotfiles/gtk-3.0/bookmarks new file mode 100644 index 0000000..a6b264b --- /dev/null +++ b/dotfiles/gtk-3.0/bookmarks @@ -0,0 +1,7 @@ +file:///home/pascal/Schreibtisch Schreibtisch +file:///home/pascal/Dokumente Dokumente +file:///home/pascal/Downloads Downloads +file:///home/pascal/Videos Videos +file:///home/pascal/Musik Musik +file:///home/pascal/Bilder Bilder +file:///home/pascal/Projekte Projekte diff --git a/dotfiles/gtk-3.0/gtk.css b/dotfiles/gtk-3.0/gtk.css new file mode 100644 index 0000000..e8b810f --- /dev/null +++ b/dotfiles/gtk-3.0/gtk.css @@ -0,0 +1,18 @@ +@define-color accent_color #00ff9c; +@define-color accent_bg_color #00ff9c; +@define-color accent_fg_color #000000; +@define-color window_bg_color #14141e; +@define-color window_fg_color #cdd6f4; +@define-color view_bg_color #14141e; +@define-color view_fg_color #cdd6f4; +@define-color card_bg_color #282837; +@define-color card_fg_color #cdd6f4; +@define-color popover_bg_color #282837; +@define-color popover_fg_color #cdd6f4; +@define-color headerbar_bg_color #282837; +@define-color headerbar_fg_color #cdd6f4; +@define-color sidebar_bg_color #14141e; +@define-color sidebar_fg_color #cdd6f4; +@define-color destructive_color #f38ba8; +@define-color warning_color #f9e2af; +@define-color success_color #a6e3a1; diff --git a/dotfiles/gtk-3.0/settings.ini b/dotfiles/gtk-3.0/settings.ini new file mode 100644 index 0000000..279b5e6 --- /dev/null +++ b/dotfiles/gtk-3.0/settings.ini @@ -0,0 +1,4 @@ +[Settings] +gtk-theme-name=Adwaita +gtk-icon-theme-name=Papirus-ForestNeon +gtk-application-prefer-dark-theme=1 diff --git a/dotfiles/gtk-4.0/gtk.css b/dotfiles/gtk-4.0/gtk.css new file mode 100644 index 0000000..e8b810f --- /dev/null +++ b/dotfiles/gtk-4.0/gtk.css @@ -0,0 +1,18 @@ +@define-color accent_color #00ff9c; +@define-color accent_bg_color #00ff9c; +@define-color accent_fg_color #000000; +@define-color window_bg_color #14141e; +@define-color window_fg_color #cdd6f4; +@define-color view_bg_color #14141e; +@define-color view_fg_color #cdd6f4; +@define-color card_bg_color #282837; +@define-color card_fg_color #cdd6f4; +@define-color popover_bg_color #282837; +@define-color popover_fg_color #cdd6f4; +@define-color headerbar_bg_color #282837; +@define-color headerbar_fg_color #cdd6f4; +@define-color sidebar_bg_color #14141e; +@define-color sidebar_fg_color #cdd6f4; +@define-color destructive_color #f38ba8; +@define-color warning_color #f9e2af; +@define-color success_color #a6e3a1; diff --git a/dotfiles/gtk-4.0/settings.ini b/dotfiles/gtk-4.0/settings.ini new file mode 100644 index 0000000..279b5e6 --- /dev/null +++ b/dotfiles/gtk-4.0/settings.ini @@ -0,0 +1,4 @@ +[Settings] +gtk-theme-name=Adwaita +gtk-icon-theme-name=Papirus-ForestNeon +gtk-application-prefer-dark-theme=1 diff --git a/dotfiles/hypr/Scripts/ags-package-runner.py b/dotfiles/hypr/Scripts/ags-package-runner.py new file mode 100755 index 0000000..312a558 --- /dev/null +++ b/dotfiles/hypr/Scripts/ags-package-runner.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +import json +import os +import pty +import select +import sys + + +def emit(event_type, **payload): + print(json.dumps({"type": event_type, **payload}), flush=True) + + +def handle_message(pid, fd, raw_line): + try: + message = json.loads(raw_line.decode("utf-8", "replace")) + except json.JSONDecodeError: + return + + if message.get("type") == "input": + os.write(fd, str(message.get("data", "")).encode("utf-8", "replace")) + elif message.get("type") == "signal": + os.kill(pid, int(message.get("signal", 15))) + + +def main(): + if "--" not in sys.argv: + emit("exit", code=2, signaled=False) + return 2 + + command = sys.argv[sys.argv.index("--") + 1 :] + if not command: + emit("exit", code=2, signaled=False) + return 2 + + pid, fd = pty.fork() + if pid == 0: + os.execvp(command[0], command) + + emit("start", pid=pid) + stdin_fd = sys.stdin.fileno() + open_fds = [fd, stdin_fd] + stdin_buffer = b"" + + while open_fds: + readable, _, _ = select.select(open_fds, [], [], 0.2) + + if fd in readable: + try: + data = os.read(fd, 4096) + except OSError: + data = b"" + + if data: + emit("out", data=data.decode("utf-8", "replace")) + else: + open_fds.remove(fd) + + if stdin_fd in readable: + try: + chunk = os.read(stdin_fd, 4096) + except OSError: + chunk = b"" + + if not chunk: + open_fds.remove(stdin_fd) + continue + + stdin_buffer += chunk + while b"\n" in stdin_buffer: + line, stdin_buffer = stdin_buffer.split(b"\n", 1) + handle_message(pid, fd, line) + + try: + finished_pid, status = os.waitpid(pid, os.WNOHANG) + except ChildProcessError: + break + + if finished_pid == pid: + if os.WIFSIGNALED(status): + emit("exit", code=os.WTERMSIG(status), signaled=True) + return 128 + os.WTERMSIG(status) + + code = os.WEXITSTATUS(status) + emit("exit", code=code, signaled=False) + return code + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/dotfiles/hypr/Scripts/ags-switcher.sh b/dotfiles/hypr/Scripts/ags-switcher.sh new file mode 100755 index 0000000..d610268 --- /dev/null +++ b/dotfiles/hypr/Scripts/ags-switcher.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +HYPR_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" +MODE="${1:-wallpaper}" +export HYPR_DIR +export HYPR_SWITCHER_THEME_DIR="$HYPR_DIR/Themes" +export HYPR_SWITCHER_WALLPAPER_DIR="${WALLPAPER_DIR:-$HOME/Bilder/Wallpaper}" + +notify() { + notify-send "AGS Switcher" "$1" >/dev/null 2>&1 || true +} + +if ! command -v ags >/dev/null 2>&1; then + notify "ags ist nicht installiert." + exit 1 +fi + +cd "$HYPR_DIR" +ags quit --instance hypr-switcher >/dev/null 2>&1 || true +exec ags run "$HYPR_DIR/ags/switcher.tsx" "$MODE" diff --git a/dotfiles/hypr/Scripts/appearance-menu.sh b/dotfiles/hypr/Scripts/appearance-menu.sh new file mode 100755 index 0000000..af800e7 --- /dev/null +++ b/dotfiles/hypr/Scripts/appearance-menu.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +choice="$( + printf '%s\n' \ + "󰌪 Theme wechseln" \ + "󰸉 Wallpaper wechseln" | + wofi --dmenu --prompt "󰉼 Aussehen" --insensitive +)" + +case "$choice" in + *"Theme wechseln"*) + "$SCRIPT_DIR/ags-switcher.sh" theme + ;; + *"Wallpaper wechseln"*) + "$SCRIPT_DIR/ags-switcher.sh" wallpaper + ;; +esac diff --git a/dotfiles/hypr/Scripts/audio-menu.sh b/dotfiles/hypr/Scripts/audio-menu.sh new file mode 100755 index 0000000..2b2ba39 --- /dev/null +++ b/dotfiles/hypr/Scripts/audio-menu.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +notify() { + notify-send "󰕾 Audio" "$1" +} + +require_wpctl() { + if ! command -v wpctl >/dev/null 2>&1; then + notify "wpctl ist nicht installiert." + exit 1 + fi +} + +choose_sink() { + wpctl status | + awk ' + /Sinks:/ {in_sinks=1; next} + /Sources:/ {in_sinks=0} + in_sinks && /\*/ {gsub(/^[[:space:]]*[│├└─* ]*/, ""); print "󰓃 " $0} + in_sinks && /^[[:space:]]*[│├└─ ]*[0-9]+\./ {gsub(/^[[:space:]]*[│├└─ ]*/, ""); print "󰓃 " $0} + ' | + wofi --dmenu --prompt "󰕾 Audioausgabe" --insensitive +} + +require_wpctl + +choice="$( + printf '%s\n' \ + "󰝝 Lauter" \ + "󰝞 Leiser" \ + "󰖁 Stumm schalten" \ + "󰓃 Ausgabe wechseln" \ + "󰍬 Mikrofon stumm" \ + "󰎆 Play/Pause" \ + "󰒮 Naechster Titel" \ + "󰒭 Vorheriger Titel" \ + "󰩟 Status" | + wofi --dmenu --prompt "󰕾 Audio" --insensitive +)" + +case "$choice" in + *"Lauter"*) + wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ + ;; + *"Leiser"*) + wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- + ;; + *"Stumm schalten"*) + wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle + ;; + *"Ausgabe wechseln"*) + selection="$(choose_sink)" + [ -n "$selection" ] || exit 0 + sink_id="$(printf '%s\n' "$selection" | sed -n 's/.* \([0-9][0-9]*\)\..*/\1/p')" + [ -n "$sink_id" ] || exit 0 + wpctl set-default "$sink_id" && notify "Audioausgabe gewechselt." + ;; + *"Mikrofon stumm"*) + wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle + ;; + *"Play/Pause"*) + playerctl play-pause + ;; + *"Naechster Titel"*) + playerctl next + ;; + *"Vorheriger Titel"*) + playerctl previous + ;; + *"Status"*) + wpctl status | wofi --dmenu --prompt "󰩟 Audiostatus" + ;; +esac diff --git a/dotfiles/hypr/Scripts/bluetooth-menu.sh b/dotfiles/hypr/Scripts/bluetooth-menu.sh new file mode 100755 index 0000000..5bb758e --- /dev/null +++ b/dotfiles/hypr/Scripts/bluetooth-menu.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +set -euo pipefail + +notify() { + notify-send "󰂯 Bluetooth" "$1" +} + +require_bluetoothctl() { + if ! command -v bluetoothctl >/dev/null 2>&1; then + notify "bluetoothctl ist nicht installiert." + exit 1 + fi +} + +wofi_pick() { + wofi --dmenu --prompt "$1" --insensitive +} + +adapter_powered() { + { bluetoothctl show 2>/dev/null || true; } | awk -F': ' '/Powered/ {print $2; exit}' +} + +device_name() { + local mac="$1" + bluetoothctl info "$mac" 2>/dev/null | awk -F': ' '/Name/ {print $2; exit}' +} + +pick_device() { + local prompt="$1" + bluetoothctl devices | + awk '{mac=$2; $1=""; $2=""; sub(/^ */, ""); printf "󰂯 %s [%s]\n", $0, mac}' | + wofi_pick "$prompt" +} + +selected_mac() { + sed -n 's/.*\[\([0-9A-Fa-f:]\{17\}\)\].*/\1/p' +} + +scan_devices() { + notify "Scan laeuft fuer 8 Sekunden." + timeout 8 bluetoothctl scan on >/dev/null 2>&1 || true + bluetoothctl scan off >/dev/null 2>&1 || true + + local selection mac name + selection="$(pick_device "󰂯 Geraet verbinden")" + [ -n "$selection" ] || exit 0 + mac="$(printf '%s\n' "$selection" | selected_mac)" + name="$(device_name "$mac")" + [ -n "$name" ] || name="$mac" + + bluetoothctl pair "$mac" >/dev/null 2>&1 || true + bluetoothctl trust "$mac" >/dev/null 2>&1 || true + bluetoothctl connect "$mac" && notify "Verbunden mit $name." +} + +connect_saved_device() { + local selection mac name + selection="$(pick_device "󰛳 Gekoppeltes Geraet verbinden")" + [ -n "$selection" ] || exit 0 + mac="$(printf '%s\n' "$selection" | selected_mac)" + name="$(device_name "$mac")" + [ -n "$name" ] || name="$mac" + bluetoothctl connect "$mac" && notify "Verbunden mit $name." +} + +disconnect_device() { + local selection mac name + selection="$( + bluetoothctl devices Connected | + awk '{mac=$2; $1=""; $2=""; sub(/^ */, ""); printf "󰂯 %s [%s]\n", $0, mac}' | + wofi_pick "󰅖 Bluetooth trennen" + )" + + [ -n "$selection" ] || exit 0 + mac="$(printf '%s\n' "$selection" | selected_mac)" + name="$(device_name "$mac")" + [ -n "$name" ] || name="$mac" + bluetoothctl disconnect "$mac" && notify "$name getrennt." +} + +remove_device() { + local selection mac name + selection="$(pick_device "󰆴 Geraet entfernen")" + [ -n "$selection" ] || exit 0 + mac="$(printf '%s\n' "$selection" | selected_mac)" + name="$(device_name "$mac")" + [ -n "$name" ] || name="$mac" + bluetoothctl remove "$mac" && notify "$name entfernt." +} + +require_bluetoothctl + +power="$(adapter_powered)" +[ -n "$power" ] || power="unknown" + +choice="$( + printf '%s\n' \ + "󰂯 Geraet suchen und verbinden" \ + "󰂲 Bluetooth ein/aus" \ + "󰛳 Gekoppelte Geraete" \ + "󰅖 Verbundenes Geraet trennen" \ + "󰆴 Geraet entfernen" \ + "󰩟 Status: $power" | + wofi_pick "󰂯 Bluetooth" +)" + +case "$choice" in + *"Geraet suchen"*) + bluetoothctl power on >/dev/null + bluetoothctl agent on >/dev/null 2>&1 || true + bluetoothctl default-agent >/dev/null 2>&1 || true + scan_devices + ;; + *"Bluetooth ein/aus"*) + if [ "$power" = "yes" ]; then + bluetoothctl power off && notify "Bluetooth ausgeschaltet." + else + bluetoothctl power on && notify "Bluetooth eingeschaltet." + fi + ;; + *"Gekoppelte Geraete"*) + connect_saved_device + ;; + *"Verbundenes Geraet trennen"*) + disconnect_device + ;; + *"Geraet entfernen"*) + remove_device + ;; + *"Status:"*) + bluetoothctl show | wofi --dmenu --prompt "󰩟 Bluetoothstatus" + ;; +esac diff --git a/dotfiles/hypr/Scripts/dev-menu.sh b/dotfiles/hypr/Scripts/dev-menu.sh new file mode 100755 index 0000000..341f591 --- /dev/null +++ b/dotfiles/hypr/Scripts/dev-menu.sh @@ -0,0 +1,405 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +PROJECT_ROOTS=( + "$HOME/Projekte" + "$HOME/Projects" + "$HOME/Code" + "$HOME/Developer" + "$HOME/dev" + "$HOME/test" +) + +notify() { + notify-send "󰅩 Dev Menue" "$1" >/dev/null 2>&1 || true +} + +pick() { + wofi --dmenu --prompt "$1" --insensitive +} + +terminal_hold() { + local title="$1" + shift + + if command -v kitty >/dev/null 2>&1; then + kitty --title "$title" sh -lc "$*; printf '\n'; read -r -p 'Enter zum Schliessen... ' _" + else + notify "kitty ist nicht installiert." + fi +} + +shell_quote() { + printf '%q' "$1" +} + +code_cmd() { + if command -v code >/dev/null 2>&1; then + printf '%s\n' code + elif command -v codium >/dev/null 2>&1; then + printf '%s\n' codium + else + return 1 + fi +} + +docker_available() { + if ! command -v docker >/dev/null 2>&1; then + notify "Docker ist nicht installiert." + return 1 + fi + + if ! docker info >/dev/null 2>&1; then + notify "Docker laeuft nicht oder ist nicht erreichbar." + return 1 + fi +} + +has_compose() { + local project="$1" + + [[ -f "$project/compose.yml" || + -f "$project/compose.yaml" || + -f "$project/docker-compose.yml" || + -f "$project/docker-compose.yaml" ]] +} + +project_label() { + local project="$1" + local marker="" + + if [[ -d "$project/.git" ]]; then + marker+=" " + fi + + if has_compose "$project"; then + marker+="󰡨 " + fi + + printf '%s%s' "$marker" "${project#$HOME/}" +} + +pick_project() { + local roots=() + local projects=() + local labels=() + local root project choice + + for root in "${PROJECT_ROOTS[@]}"; do + [[ -d "$root" ]] && roots+=("$root") + done + + if ((${#roots[@]} == 0)); then + notify "Keine Projektordner gefunden." + return 1 + fi + + mapfile -t projects < <( + { + find "${roots[@]}" -maxdepth 4 -type d -name .git -printf '%h\n' 2>/dev/null + find "${roots[@]}" -maxdepth 3 -type d \( -name node_modules -o -name vendor \) -prune -o \ + -type f \( -name compose.yml -o -name compose.yaml -o -name docker-compose.yml -o -name docker-compose.yaml -o -name package.json -o -name Cargo.toml -o -name pyproject.toml -o -name go.mod \) \ + -printf '%h\n' 2>/dev/null + } | + sort -u + ) + + if ((${#projects[@]} == 0)); then + notify "Keine Projekte gefunden." + return 1 + fi + + for project in "${projects[@]}"; do + labels+=("$(project_label "$project")") + done + + choice="$(printf '%s\n' "${labels[@]}" | pick "📁 Projekt")" + [[ -z "${choice:-}" ]] && return 1 + + for i in "${!labels[@]}"; do + if [[ "$choice" == "${labels[$i]}" ]]; then + printf '%s\n' "${projects[$i]}" + return 0 + fi + done + + return 1 +} + +open_project_browser() { + local project="$1" + local choice url + + choice="$( + printf '%s\n' \ + "🌐 http://localhost:3000" \ + "🌐 http://localhost:5173" \ + "🌐 http://localhost:8000" \ + "🌐 http://localhost:8080" \ + "🌐 http://localhost:5000" \ + "🌐 http://localhost:4200" \ + "📂 Projektordner im Browser" | + pick "🌐 Projekt im Browser" + )" + + case "$choice" in + *"Projektordner"*) + xdg-open "$project" >/dev/null 2>&1 & + ;; + *"http"*) + url="${choice#* }" + xdg-open "$url" >/dev/null 2>&1 & + ;; + esac +} + +project_menu() { + local project project_q choice editor + + project="$(pick_project)" || return 0 + project_q="$(shell_quote "$project")" + + choice="$( + printf '%s\n' \ + "📂 Projekt oeffnen (Code)" \ + "🌐 Projekt im Browser oeffnen" \ + "🐳 Docker Compose starten" \ + "🛑 Docker stoppen" \ + "🔄 Container neu starten" \ + "📜 Logs anzeigen" \ + "📦 Git Repo Status anzeigen" | + pick "📁 $(basename "$project")" + )" + + case "$choice" in + *"Projekt oeffnen"*) + if editor="$(code_cmd)"; then + "$editor" "$project" >/dev/null 2>&1 & + else + notify "VS Code/Codium ist nicht installiert." + fi + ;; + *"Browser"*) + open_project_browser "$project" + ;; + *"Docker Compose starten"*) + docker_available || return 0 + if has_compose "$project"; then + terminal_hold "Docker Compose: $(basename "$project")" "cd $project_q && docker compose up -d && docker compose ps" + else + notify "Kein Docker-Compose File im Projekt." + fi + ;; + *"Docker stoppen"*) + docker_available || return 0 + if has_compose "$project"; then + terminal_hold "Docker Stop: $(basename "$project")" "cd $project_q && docker compose stop && docker compose ps" + else + notify "Kein Docker-Compose File im Projekt." + fi + ;; + *"Container neu starten"*) + docker_available || return 0 + if has_compose "$project"; then + terminal_hold "Docker Restart: $(basename "$project")" "cd $project_q && docker compose restart && docker compose ps" + else + notify "Kein Docker-Compose File im Projekt." + fi + ;; + *"Logs anzeigen"*) + docker_available || return 0 + if has_compose "$project"; then + if command -v kitty >/dev/null 2>&1; then + kitty --title "Logs: $(basename "$project")" sh -lc "cd $project_q && docker compose logs -f --tail=200" + else + notify "kitty ist nicht installiert." + fi + else + notify "Kein Docker-Compose File im Projekt." + fi + ;; + *"Git Repo Status"*) + if [[ -d "$project/.git" ]]; then + terminal_hold "Git Status: $(basename "$project")" "cd $project_q && git status --short --branch" + else + notify "Kein Git Repository." + fi + ;; + esac +} + +pick_container() { + local containers=() + local labels=() + local line name status choice + + docker_available || return 1 + + mapfile -t containers < <(docker ps -a --format '{{.Names}}|{{.Status}}' | sort) + if ((${#containers[@]} == 0)); then + notify "Keine Docker Container gefunden." + return 1 + fi + + for line in "${containers[@]}"; do + name="${line%%|*}" + status="${line#*|}" + labels+=("🐳 $name · $status") + done + + choice="$(printf '%s\n' "${labels[@]}" | pick "🐳 Container")" + [[ -z "${choice:-}" ]] && return 1 + + for i in "${!labels[@]}"; do + if [[ "$choice" == "${labels[$i]}" ]]; then + printf '%s\n' "${containers[$i]%%|*}" + return 0 + fi + done + + return 1 +} + +docker_menu() { + local choice container running_ids all_ids + + choice="$( + printf '%s\n' \ + "▶️ Alle Container starten" \ + "⏹️ Alle stoppen" \ + "🔄 Einzelnen Container neu starten" \ + "📊 docker stats" \ + "📜 Logs anzeigen" \ + "🧹 Cleanup dangling images" | + pick "🐳 Docker Control" + )" + + case "$choice" in + *"Alle Container starten"*) + docker_available || return 0 + all_ids="$(docker ps -aq)" + if [[ -n "$all_ids" ]]; then + terminal_hold "Docker Start" "docker start $all_ids && docker ps" + else + notify "Keine Container gefunden." + fi + ;; + *"Alle stoppen"*) + docker_available || return 0 + running_ids="$(docker ps -q)" + if [[ -n "$running_ids" ]]; then + terminal_hold "Docker Stop" "docker stop $running_ids && docker ps -a" + else + notify "Keine laufenden Container." + fi + ;; + *"Einzelnen Container neu starten"*) + container="$(pick_container)" || return 0 + terminal_hold "Docker Restart: $container" "docker restart '$container' && docker ps -a --filter name='^/$container$'" + ;; + *"docker stats"*) + docker_available || return 0 + if command -v kitty >/dev/null 2>&1; then + kitty --title "docker stats" sh -lc "docker stats" + else + notify "kitty ist nicht installiert." + fi + ;; + *"Logs anzeigen"*) + container="$(pick_container)" || return 0 + if command -v kitty >/dev/null 2>&1; then + kitty --title "Logs: $container" sh -lc "docker logs -f --tail=200 '$container'" + else + notify "kitty ist nicht installiert." + fi + ;; + *"Cleanup dangling images"*) + docker_available || return 0 + terminal_hold "Docker Cleanup" "docker image prune -f" + ;; + esac +} + +launch_codex() { + if ! command -v kitty >/dev/null 2>&1; then + notify "kitty ist nicht installiert." + return 0 + fi + + if ! command -v codex >/dev/null 2>&1; then + notify "codex ist nicht installiert." + return 0 + fi + + kitty --title "Codex" sh -lc 'cd "$HOME" && exec codex' >/dev/null 2>&1 & +} + +launch_opencode() { + if ! command -v kitty >/dev/null 2>&1; then + notify "kitty ist nicht installiert." + return 0 + fi + + if ! command -v opencode >/dev/null 2>&1; then + notify "opencode ist nicht installiert." + return 0 + fi + + kitty --title "opencode" sh -lc 'cd "$HOME" && exec opencode' >/dev/null 2>&1 & +} + +choice="$( + printf '%s\n' \ + "📁 Projekt Management" \ + "󰌘 Homelab Controlcenter" \ + "🐳 Docker Control" \ + "󰚩 Codex" \ + "󰚩 opencode" \ + " Terminal" \ + " Projektordner" \ + " VS Code / Codium" \ + "󰊢 Git GUI" | + pick "󰅩 Dev Menue" +)" + +case "$choice" in + *"Projekt Management"*) + project_menu + ;; + *"Homelab Controlcenter"*) + "$SCRIPT_DIR/homelab-control.sh" + ;; + *"Docker Control"*) + docker_menu + ;; + *"Codex"*) + launch_codex + ;; + *"opencode"*) + launch_opencode + ;; + *"Terminal"*) + kitty + ;; + *"Projektordner"*) + nautilus + ;; + *"VS Code"*|*"Codium"*) + if editor="$(code_cmd)"; then + "$editor" >/dev/null 2>&1 & + else + notify "VS Code/Codium ist nicht installiert." + fi + ;; + *"Git GUI"*) + if command -v gitg >/dev/null 2>&1; then + gitg + elif command -v git-cola >/dev/null 2>&1; then + git-cola + else + notify "gitg oder git-cola ist nicht installiert." + fi + ;; +esac diff --git a/dotfiles/hypr/Scripts/display-menu.sh b/dotfiles/hypr/Scripts/display-menu.sh new file mode 100755 index 0000000..0136665 --- /dev/null +++ b/dotfiles/hypr/Scripts/display-menu.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +set -euo pipefail + +notify() { + notify-send "󰍹 Display" "$1" +} + +wofi_pick() { + wofi --dmenu --prompt "$1" --insensitive +} + +connected_monitors() { + hyprctl monitors all 2>/dev/null | awk ' + /^Monitor / { + name=$2 + disabled=0 + } + /disabled: true/ { + disabled=1 + } + /^$/ && name != "" { + if (!disabled) print name + name="" + } + END { + if (name != "" && !disabled) print name + } + ' +} + +all_monitors() { + hyprctl monitors all 2>/dev/null | awk '/^Monitor / {print $2}' +} + +first_monitor() { + connected_monitors | head -n 1 +} + +second_monitor() { + connected_monitors | sed -n '2p' +} + +show_status() { + hyprctl monitors all | wofi --dmenu --prompt "󰩟 Displaystatus" +} + +choose_monitor() { + local monitors + monitors="$(all_monitors)" + + if [ -z "$monitors" ]; then + notify "Keine Monitore ueber hyprctl gefunden." + exit 0 + fi + + printf '%s\n' "$monitors" | awk '{print "󰍹 " $0}' | wofi_pick "$1" +} + +disable_monitor() { + local selection monitor + selection="$(choose_monitor "󰍹 Monitor deaktivieren")" + [ -n "$selection" ] || exit 0 + monitor="${selection#* }" + hyprctl keyword monitor "$monitor,disable" && notify "$monitor deaktiviert." +} + +enable_preferred() { + local selection monitor + selection="$(choose_monitor "󰍹 Monitor aktivieren")" + [ -n "$selection" ] || exit 0 + monitor="${selection#* }" + hyprctl keyword monitor "$monitor,preferred,auto,1" && notify "$monitor aktiviert." +} + +extend_right() { + local primary secondary + primary="$(first_monitor)" + secondary="$(second_monitor)" + + if [ -z "$primary" ] || [ -z "$secondary" ]; then + notify "Dafuer muessen mindestens zwei aktive Monitore vorhanden sein." + exit 0 + fi + + hyprctl keyword monitor "$primary,preferred,0x0,1" + hyprctl keyword monitor "$secondary,preferred,auto-right,1" + notify "Displays erweitert." +} + +mirror_displays() { + local primary secondary + primary="$(first_monitor)" + secondary="$(second_monitor)" + + if [ -z "$primary" ] || [ -z "$secondary" ]; then + notify "Dafuer muessen mindestens zwei aktive Monitore vorhanden sein." + exit 0 + fi + + hyprctl keyword monitor "$primary,preferred,0x0,1" + hyprctl keyword monitor "$secondary,preferred,0x0,1,mirror,$primary" + notify "Displays gespiegelt." +} + +choice="$( + printf '%s\n' \ + "󰍹 Status anzeigen" \ + "󰑓 Display-Konfig neu laden" \ + "󰹑 Monitor aktivieren" \ + "󰶐 Monitor deaktivieren" \ + "󰹑 Displays erweitern" \ + "󰹑 Displays spiegeln" \ + "󰍹 Grafisches Display-Tool" | + wofi_pick "󰍹 Display" +)" + +case "$choice" in + *"Status anzeigen"*) + show_status + ;; + *"Display-Konfig neu laden"*) + hyprctl reload + ;; + *"Monitor aktivieren"*) + enable_preferred + ;; + *"Monitor deaktivieren"*) + disable_monitor + ;; + *"Displays erweitern"*) + extend_right + ;; + *"Displays spiegeln"*) + mirror_displays + ;; + *"Grafisches Display-Tool"*) + if command -v nwg-displays >/dev/null 2>&1; then + nwg-displays + elif command -v wdisplays >/dev/null 2>&1; then + wdisplays + else + notify "Installiere nwg-displays oder wdisplays fuer ein grafisches Display-Tool." + fi + ;; +esac diff --git a/dotfiles/hypr/Scripts/homelab-control.sh b/dotfiles/hypr/Scripts/homelab-control.sh new file mode 100755 index 0000000..f3a1c44 --- /dev/null +++ b/dotfiles/hypr/Scripts/homelab-control.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +HYPR_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" +HOMELAB_CONFIG="${HOMELAB_CONFIG:-$HOME/.config/homelab/config.yaml}" + +notify() { + notify-send "Homelab" "$1" >/dev/null 2>&1 || true +} + +if ! command -v ags >/dev/null 2>&1; then + notify "ags ist nicht installiert." + exit 1 +fi + +if ! command -v sshpass >/dev/null 2>&1; then + notify "sshpass ist nicht installiert." + exit 1 +fi + +if [[ -f "$HOMELAB_CONFIG" ]]; then + export HOMELAB_CONFIG +fi + +cd "$HYPR_DIR" +ags quit --instance homelab-control >/dev/null 2>&1 || true +exec ags run "$HYPR_DIR/ags/homelab.tsx" diff --git a/dotfiles/hypr/Scripts/main-menu.sh b/dotfiles/hypr/Scripts/main-menu.sh new file mode 100755 index 0000000..193f89c --- /dev/null +++ b/dotfiles/hypr/Scripts/main-menu.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +if pgrep -x wofi >/dev/null; then + pkill -x wofi + exit 0 +fi + +choice="$( + printf '%s\n' \ + "󰀻 Apps" \ + "󰅩 Dev Menue" \ + "󰒓 Einstellungen" \ + " Terminal" \ + " Dateien" \ + "󰑓 Hyprland neu laden" \ + "󰍃 Session beenden" | + wofi --dmenu --prompt "󰣇 Hauptmenue" --insensitive +)" + +case "$choice" in + *"Apps"*) + wofi --show drun + ;; + *"Dev Menue"*) + "$SCRIPT_DIR/dev-menu.sh" + ;; + *"Einstellungen"*) + "$SCRIPT_DIR/settings-menu.sh" + ;; + *"Terminal"*) + kitty + ;; + *"Dateien"*) + nautilus + ;; + *"Hyprland neu laden"*) + hyprctl reload + ;; + *"Session beenden"*) + if command -v hyprshutdown >/dev/null 2>&1; then + hyprshutdown + else + hyprctl dispatch exit + fi + ;; +esac diff --git a/dotfiles/hypr/Scripts/network-menu.sh b/dotfiles/hypr/Scripts/network-menu.sh new file mode 100755 index 0000000..bcacfdf --- /dev/null +++ b/dotfiles/hypr/Scripts/network-menu.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +set -euo pipefail + +notify() { + notify-send "󰤨 Netzwerk" "$1" +} + +require_nmcli() { + if ! command -v nmcli >/dev/null 2>&1; then + notify "nmcli ist nicht installiert." + exit 1 + fi +} + +wofi_pick() { + wofi --dmenu --prompt "$1" --insensitive +} + +wifi_status() { + nmcli -t -f WIFI general | head -n 1 +} + +active_connection() { + nmcli -t -f NAME,TYPE connection show --active | awk -F: '$2 ~ /wireless|ethernet/ {print $1; exit}' +} + +connect_wifi() { + nmcli radio wifi on + nmcli device wifi rescan >/dev/null 2>&1 || true + + local selection ssid security password + selection="$( + nmcli -t -f IN-USE,SSID,SECURITY,SIGNAL device wifi list | + awk -F: ' + $2 != "" { + icon = $1 == "*" ? "󰁥" : "󰤨" + lock = $3 == "" ? "offen" : "gesichert" + printf "%s %s [%s, %s%%]\n", icon, $2, lock, $4 + } + ' | + wofi_pick "󰤨 WLAN verbinden" + )" + + [ -n "$selection" ] || exit 0 + ssid="$(printf '%s\n' "$selection" | sed -E 's/^[^ ]+ //; s/ \[.*$//')" + security="$(nmcli -t -f SSID,SECURITY device wifi list | awk -F: -v ssid="$ssid" '$1 == ssid {print $2; exit}')" + + if [ -n "$security" ]; then + password="$(printf '' | wofi --dmenu --password --prompt "󰌾 Passwort fuer $ssid")" + [ -n "$password" ] || exit 0 + nmcli device wifi connect "$ssid" password "$password" && notify "Verbunden mit $ssid." + else + nmcli device wifi connect "$ssid" && notify "Verbunden mit $ssid." + fi +} + +saved_connections() { + local selection name + selection="$( + nmcli -t -f NAME,TYPE connection show | + awk -F: '$2 ~ /wireless|ethernet/ {printf "󰛳 %s\n", $1}' | + wofi_pick "󰛳 Gespeicherte Verbindungen" + )" + + [ -n "$selection" ] || exit 0 + name="${selection#* }" + nmcli connection up "$name" && notify "Verbindung $name gestartet." +} + +disconnect_network() { + local conn + conn="$(active_connection)" + + if [ -z "$conn" ]; then + notify "Keine aktive Netzwerkverbindung gefunden." + exit 0 + fi + + nmcli connection down "$conn" && notify "Verbindung $conn getrennt." +} + +require_nmcli + +current="$(active_connection)" +[ -n "$current" ] || current="Nicht verbunden" + +choice="$( + printf '%s\n' \ + "󰤨 WLAN verbinden" \ + "󰖩 WLAN ein/aus" \ + "󰛳 Gespeicherte Verbindungen" \ + "󰅖 Aktive Verbindung trennen" \ + "󰑓 Netzwerk neu scannen" \ + "󰩟 Status: $current" | + wofi_pick "󰤨 Netzwerk" +)" + +case "$choice" in + *"WLAN verbinden"*) + connect_wifi + ;; + *"WLAN ein/aus"*) + if [ "$(wifi_status)" = "enabled" ]; then + nmcli radio wifi off && notify "WLAN ausgeschaltet." + else + nmcli radio wifi on && notify "WLAN eingeschaltet." + fi + ;; + *"Gespeicherte Verbindungen"*) + saved_connections + ;; + *"Aktive Verbindung trennen"*) + disconnect_network + ;; + *"Netzwerk neu scannen"*) + nmcli device wifi rescan && notify "WLAN-Scan gestartet." + ;; + *"Status:"*) + nmcli general status | wofi --dmenu --prompt "󰩟 Netzwerkstatus" + ;; +esac diff --git a/dotfiles/hypr/Scripts/package-manager.sh b/dotfiles/hypr/Scripts/package-manager.sh new file mode 100755 index 0000000..9e51134 --- /dev/null +++ b/dotfiles/hypr/Scripts/package-manager.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +HYPR_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" + +notify() { + notify-send "Pakete" "$1" >/dev/null 2>&1 || true +} + +if ! command -v ags >/dev/null 2>&1; then + notify "ags ist nicht installiert." + exit 1 +fi + +cd "$HYPR_DIR" +ags quit --instance package-manager >/dev/null 2>&1 || true +exec ags run "$HYPR_DIR/ags/package-manager.tsx" diff --git a/dotfiles/hypr/Scripts/power-menu.py b/dotfiles/hypr/Scripts/power-menu.py new file mode 100755 index 0000000..b8fb838 --- /dev/null +++ b/dotfiles/hypr/Scripts/power-menu.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +import os +import shlex +import subprocess +from pathlib import Path + +import gi + +gi.require_version("Gtk", "3.0") +gi.require_version("Gdk", "3.0") +from gi.repository import Gdk, Gtk # noqa: E402 + + +HYPR_DIR = Path.home() / ".config" / "hypr" +THEME_DIR = HYPR_DIR / "Themes" +CURRENT_WALLPAPER = HYPR_DIR / "current-wallpaper" + + +DEFAULT_THEME = { + "NAME": "Power", + "ACCENT": "#f38ba8", + "ACCENT_2": "#cba6f7", + "BACKGROUND_HEX": "#18141f", + "PANEL_HEX": "#313244", + "FOREGROUND": "#f5e0dc", + "MUTED": "#a6adc8", + "SELECTED_TEXT": "#11111b", +} + + +def parse_theme_file(path): + theme = {} + + for line in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + + key, value = line.split("=", 1) + key = key.strip() + value = value.strip() + + try: + parsed = shlex.split(value) + theme[key] = parsed[0] if parsed else "" + except ValueError: + theme[key] = value.strip("\"'") + + return theme + + +def load_current_theme(): + current_wallpaper = "" + if CURRENT_WALLPAPER.exists(): + current_wallpaper = CURRENT_WALLPAPER.read_text(encoding="utf-8", errors="ignore").strip() + + for theme_file in sorted(THEME_DIR.glob("*.theme")): + theme = parse_theme_file(theme_file) + if theme.get("WALLPAPER") == current_wallpaper: + return {**DEFAULT_THEME, **theme} + + rose = THEME_DIR / "rose-night.theme" + if rose.exists(): + return {**DEFAULT_THEME, **parse_theme_file(rose)} + + return DEFAULT_THEME + + +def run(command): + subprocess.Popen(command, start_new_session=True) + Gtk.main_quit() + + +class PowerMenu(Gtk.Window): + def __init__(self): + super().__init__(title="Power") + + self.theme = load_current_theme() + self.set_decorated(False) + self.set_resizable(False) + self.set_keep_above(True) + self.set_position(Gtk.WindowPosition.CENTER) + self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + self.set_skip_taskbar_hint(True) + self.set_skip_pager_hint(True) + self.set_app_paintable(True) + self.connect("key-press-event", self.on_key_press) + self.connect("focus-out-event", lambda *_: Gtk.main_quit()) + + overlay = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=18) + overlay.get_style_context().add_class("power-shell") + + title = Gtk.Label(label=self.theme.get("NAME", "Power")) + title.get_style_context().add_class("power-title") + overlay.pack_start(title, False, False, 0) + + grid = Gtk.Grid() + grid.set_column_spacing(14) + grid.set_row_spacing(14) + grid.set_halign(Gtk.Align.CENTER) + + actions = [ + ("", "Sperren", ["hyprlock"]), + ("󰤄", "Ruhezustand", ["systemctl", "suspend"]), + ("󰗽", "Abmelden", ["hyprctl", "dispatch", "exit"]), + ("󰜉", "Neustart", ["systemctl", "reboot"]), + ("⏻", "Ausschalten", ["systemctl", "poweroff"]), + ("󰗼", "Abbrechen", None), + ] + + for index, (icon, label, command) in enumerate(actions): + button = self.make_button(icon, label, command) + grid.attach(button, index % 3, index // 3, 1, 1) + + overlay.pack_start(grid, False, False, 0) + self.add(overlay) + self.apply_css() + + def make_button(self, icon, label, command): + button = Gtk.Button() + button.get_style_context().add_class("power-button") + button.set_size_request(150, 118) + button.set_relief(Gtk.ReliefStyle.NONE) + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) + box.set_halign(Gtk.Align.CENTER) + box.set_valign(Gtk.Align.CENTER) + + icon_label = Gtk.Label(label=icon) + icon_label.get_style_context().add_class("power-icon") + text_label = Gtk.Label(label=label) + text_label.get_style_context().add_class("power-label") + + box.pack_start(icon_label, False, False, 0) + box.pack_start(text_label, False, False, 0) + button.add(box) + + if command: + button.connect("clicked", lambda *_: run(command)) + else: + button.connect("clicked", lambda *_: Gtk.main_quit()) + + return button + + def apply_css(self): + css = f""" + window {{ + background: transparent; + }} + + .power-shell {{ + margin: 0; + padding: 24px; + border-radius: 26px; + border: 1px solid {self.theme["ACCENT"]}; + background: alpha({self.theme["BACKGROUND_HEX"]}, 0.94); + box-shadow: 0 22px 70px alpha(#000000, 0.55); + }} + + .power-title {{ + color: {self.theme["FOREGROUND"]}; + font-family: "JetBrainsMono Nerd Font", "JetBrains Mono", sans-serif; + font-size: 18px; + font-weight: 800; + }} + + .power-button {{ + color: {self.theme["FOREGROUND"]}; + background: alpha({self.theme["PANEL_HEX"]}, 0.82); + border: 1px solid alpha({self.theme["ACCENT_2"]}, 0.42); + border-radius: 18px; + transition: 160ms ease; + }} + + .power-button:hover, + .power-button:focus {{ + color: {self.theme["SELECTED_TEXT"]}; + background: linear-gradient(135deg, {self.theme["ACCENT"]}, {self.theme["ACCENT_2"]}); + border-color: {self.theme["ACCENT"]}; + }} + + .power-icon {{ + font-family: "JetBrainsMono Nerd Font", "JetBrains Mono", sans-serif; + font-size: 34px; + font-weight: 800; + }} + + .power-label {{ + font-family: "JetBrainsMono Nerd Font", "JetBrains Mono", sans-serif; + font-size: 13px; + font-weight: 800; + }} + """ + + provider = Gtk.CssProvider() + provider.load_from_data(css.encode("utf-8")) + Gtk.StyleContext.add_provider_for_screen( + Gdk.Screen.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, + ) + + def on_key_press(self, _widget, event): + if event.keyval in (Gdk.KEY_Escape, Gdk.KEY_q): + Gtk.main_quit() + + +if __name__ == "__main__": + window = PowerMenu() + window.show_all() + Gtk.main() diff --git a/dotfiles/hypr/Scripts/screenshot-menu.sh b/dotfiles/hypr/Scripts/screenshot-menu.sh new file mode 100755 index 0000000..f1c6d9d --- /dev/null +++ b/dotfiles/hypr/Scripts/screenshot-menu.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +set -euo pipefail + +screenshot_dir="${XDG_PICTURES_DIR:-$HOME/Pictures}/Screenshots" + +notify() { + notify-send "󰸉 Screenshot" "$1" +} + +require_cmd() { + local cmd="$1" + + if ! command -v "$cmd" >/dev/null 2>&1; then + notify "$cmd ist nicht installiert." + exit 1 + fi +} + +filename() { + date +%Y-%m-%d_%H-%M-%S.png +} + +take_hyprshot() { + local mode="$1" + local name="$2" + + require_cmd hyprshot + mkdir -p "$screenshot_dir" + hyprshot -m "$mode" -o "$screenshot_dir" -f "$name" -s + printf '%s/%s\n' "$screenshot_dir" "$name" +} + +annotate_region() { + local name path + + require_cmd satty + name="$(filename)" + path="$(take_hyprshot region "$name")" + satty --filename "$path" --output-filename "$path" +} + +quick_region() { + local name + + name="$(filename)" + take_hyprshot region "$name" >/dev/null + notify "Bereich gespeichert." +} + +quick_window() { + local name + + name="$(filename)" + take_hyprshot window "$name" >/dev/null + notify "Fenster gespeichert." +} + +quick_output() { + local name + + name="$(filename)" + take_hyprshot output "$name" >/dev/null + notify "Bildschirm gespeichert." +} + +copy_region() { + require_cmd hyprshot + hyprshot -m region --clipboard-only -s + notify "Bereich in die Zwischenablage kopiert." +} + +show_menu() { + require_cmd wofi + + local choice + choice="$( + printf '%s\n' \ + "󰹑 Bereich markieren" \ + "󰸉 Bereich speichern" \ + "󰍹 Fenster speichern" \ + "󰹑 Bildschirm speichern" \ + "󰅌 Bereich kopieren" | + wofi --dmenu --prompt "󰸉 Screenshot" --insensitive + )" + + case "$choice" in + *"Bereich markieren"*) + annotate_region + ;; + *"Bereich speichern"*) + quick_region + ;; + *"Fenster speichern"*) + quick_window + ;; + *"Bildschirm speichern"*) + quick_output + ;; + *"Bereich kopieren"*) + copy_region + ;; + esac +} + +case "${1:-menu}" in + annotate-region) + annotate_region + ;; + region) + quick_region + ;; + window) + quick_window + ;; + output) + quick_output + ;; + copy-region) + copy_region + ;; + menu) + show_menu + ;; + *) + notify "Unbekannter Screenshot-Modus: $1" + exit 2 + ;; +esac diff --git a/dotfiles/hypr/Scripts/settings-menu.sh b/dotfiles/hypr/Scripts/settings-menu.sh new file mode 100755 index 0000000..c9acbe7 --- /dev/null +++ b/dotfiles/hypr/Scripts/settings-menu.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +choice="$( + printf '%s\n' \ + "󰉼 Aussehen" \ + "󰕾 Audio" \ + "󰂯 Bluetooth" \ + "󰍹 Display" \ + "󰤨 Netzwerk" \ + "󰸉 Screenshot" \ + "󰒓 System" | + wofi --dmenu --prompt "󰒓 Einstellungen" --insensitive +)" + +case "$choice" in + *"Aussehen"*) + "$SCRIPT_DIR/appearance-menu.sh" + ;; + *"Audio"*) + "$SCRIPT_DIR/audio-menu.sh" + ;; + *"Netzwerk"*) + "$SCRIPT_DIR/network-menu.sh" + ;; + *"Bluetooth"*) + "$SCRIPT_DIR/bluetooth-menu.sh" + ;; + *"Display"*) + "$SCRIPT_DIR/display-menu.sh" + ;; + *"Screenshot"*) + "$SCRIPT_DIR/screenshot-menu.sh" + ;; + *"System"*) + "$SCRIPT_DIR/system-menu.sh" + ;; +esac diff --git a/dotfiles/hypr/Scripts/system-menu.sh b/dotfiles/hypr/Scripts/system-menu.sh new file mode 100755 index 0000000..660988b --- /dev/null +++ b/dotfiles/hypr/Scripts/system-menu.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +notify() { + notify-send "󰒓 System" "$1" +} + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" + +start_detached() { + local command_name="$1" + local unit_name="${command_name//[^[:alnum:]_.-]/-}" + + if command -v systemd-run >/dev/null 2>&1; then + systemd-run --user --unit="$unit_name" --collect "$@" >/dev/null 2>&1 && return + fi + + if command -v setsid >/dev/null 2>&1; then + setsid -f "$@" >/dev/null 2>&1 + else + "$@" >/dev/null 2>&1 & + fi +} + +restart_waybar() { + local i + + pkill -x waybar >/dev/null 2>&1 || true + + for i in {1..20}; do + pgrep -x waybar >/dev/null 2>&1 || break + sleep 0.05 + done + + pgrep -x waybar >/dev/null 2>&1 && pkill -9 -x waybar >/dev/null 2>&1 || true + start_detached waybar +} + +choice="$( + printf '%s\n' \ + "󰑓 Hyprland neu laden" \ + "󰌢 Waybar neu starten" \ + "󰏖 Paket Installation / Updates" \ + "󰗽 Bildschirm heller" \ + "󰗾 Bildschirm dunkler" \ + "󰍃 Session beenden" | + wofi --dmenu --prompt "󰒓 System" --insensitive +)" + +case "$choice" in + *"Hyprland neu laden"*) + hyprctl reload + ;; + *"Waybar neu starten"*) + restart_waybar + ;; + *"Paket Installation / Updates"*) + "$SCRIPT_DIR/package-manager.sh" + ;; + *"Bildschirm heller"*) + if command -v brightnessctl >/dev/null 2>&1; then + brightnessctl -e4 -n2 set 5%+ + else + notify "brightnessctl ist nicht installiert." + fi + ;; + *"Bildschirm dunkler"*) + if command -v brightnessctl >/dev/null 2>&1; then + brightnessctl -e4 -n2 set 5%- + else + notify "brightnessctl ist nicht installiert." + fi + ;; + *"Session beenden"*) + if command -v hyprshutdown >/dev/null 2>&1; then + hyprshutdown + else + hyprctl dispatch exit + fi + ;; +esac diff --git a/dotfiles/hypr/Scripts/theme-menu.sh b/dotfiles/hypr/Scripts/theme-menu.sh new file mode 100755 index 0000000..b1fa5e3 --- /dev/null +++ b/dotfiles/hypr/Scripts/theme-menu.sh @@ -0,0 +1,1204 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +HYPR_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" +THEME_DIR="$HYPR_DIR/Themes" +CURRENT_THEME="$HYPR_DIR/current-theme.conf" +HYPRLOCK_CONF="$HYPR_DIR/hyprlock.conf" +HYPRPAPER_CONF="$HYPR_DIR/hyprpaper.conf" +CURRENT_WALLPAPER="$HYPR_DIR/current-wallpaper" +WOFI_STYLE="${XDG_CONFIG_HOME:-$HOME/.config}/wofi/style.css" +SWAYNC_STYLE="${XDG_CONFIG_HOME:-$HOME/.config}/swaync/style.css" +WAYBAR_STYLE="${XDG_CONFIG_HOME:-$HOME/.config}/waybar/style.css" +GTK3_SETTINGS="${XDG_CONFIG_HOME:-$HOME/.config}/gtk-3.0/settings.ini" +GTK4_SETTINGS="${XDG_CONFIG_HOME:-$HOME/.config}/gtk-4.0/settings.ini" +GTK3_CSS="${XDG_CONFIG_HOME:-$HOME/.config}/gtk-3.0/gtk.css" +GTK4_CSS="${XDG_CONFIG_HOME:-$HOME/.config}/gtk-4.0/gtk.css" +KDEGLOBALS="${XDG_CONFIG_HOME:-$HOME/.config}/kdeglobals" +KDE_COLOR_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/color-schemes" +LOCAL_ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons" +QT6CT_CONF="${XDG_CONFIG_HOME:-$HOME/.config}/qt6ct/qt6ct.conf" +QT5CT_CONF="${XDG_CONFIG_HOME:-$HOME/.config}/qt5ct/qt5ct.conf" +STARSHIP_CONF="${XDG_CONFIG_HOME:-$HOME/.config}/starship.toml" +SDDM_STATE_DIR="/var/lib/pascal-sddm-theme" + +notify() { + notify-send "󰌪 Theme" "$1" >/dev/null 2>&1 || true +} + +load_theme() { + local theme_file="$1" + + NAME="" + ICON="󰌪" + WALLPAPER="" + TRANSITION_TYPE="grow" + TRANSITION_DURATION="1.0" + TRANSITION_FPS="60" + TRANSITION_POS="center" + ACTIVE_BORDER="" + INACTIVE_BORDER="" + ACCENT="" + ACCENT_2="" + BACKGROUND="" + BACKGROUND_SOFT="" + BACKGROUND_HEX="" + PANEL_HEX="" + FOREGROUND="" + MUTED="" + SELECTED_TEXT="" + WAYBAR_PANEL="" + WAYBAR_ISLAND="" + WAYBAR_MUTED="" + WAYBAR_SUCCESS="" + WAYBAR_WARNING="" + WAYBAR_DANGER="" + WAYBAR_BLUE="" + WAYBAR_CYAN="" + WAYBAR_ORANGE="" + WAYBAR_PURPLE="" + APP_THEME_MODE="prefer-dark" + GNOME_ACCENT_COLOR="blue" + GTK_THEME_NAME="Adwaita" + ICON_THEME_NAME="Adwaita" + PAPIRUS_FOLDER_COLOR="" + KDE_COLOR_SCHEME="" + + # shellcheck source=/dev/null + source "$theme_file" + + if [[ -z "$NAME" || -z "$WALLPAPER" || -z "$ACTIVE_BORDER" || -z "$INACTIVE_BORDER" || + -z "$ACCENT" || -z "$ACCENT_2" || -z "$BACKGROUND" || -z "$BACKGROUND_SOFT" || + -z "$FOREGROUND" || -z "$MUTED" || -z "$SELECTED_TEXT" ]]; then + notify "Theme ist unvollstaendig: $(basename "$theme_file")" + exit 1 + fi + + if [[ ! -f "$WALLPAPER" ]]; then + notify "Wallpaper nicht gefunden: $WALLPAPER" + exit 1 + fi + + WAYBAR_PANEL="${WAYBAR_PANEL:-$BACKGROUND_SOFT}" + WAYBAR_ISLAND="${WAYBAR_ISLAND:-$BACKGROUND}" + WAYBAR_MUTED="${WAYBAR_MUTED:-$MUTED}" + WAYBAR_SUCCESS="${WAYBAR_SUCCESS:-$ACCENT_2}" + WAYBAR_WARNING="${WAYBAR_WARNING:-#f9e2af}" + WAYBAR_DANGER="${WAYBAR_DANGER:-#f38ba8}" + WAYBAR_BLUE="${WAYBAR_BLUE:-$ACCENT}" + WAYBAR_CYAN="${WAYBAR_CYAN:-$ACCENT_2}" + WAYBAR_ORANGE="${WAYBAR_ORANGE:-#fab387}" + WAYBAR_PURPLE="${WAYBAR_PURPLE:-$ACCENT_2}" + BACKGROUND_HEX="${BACKGROUND_HEX:-#18141f}" + PANEL_HEX="${PANEL_HEX:-#313244}" + KDE_COLOR_SCHEME="${KDE_COLOR_SCHEME:-${NAME//[^[:alnum:]]/}}" +} + +backup_once() { + local file="$1" + local backup="$file.before-theme-switcher" + + if [[ -f "$file" && ! -f "$backup" ]]; then + cp "$file" "$backup" + fi +} + +start_detached() { + local command_name="$1" + local unit_name="${command_name//[^[:alnum:]_.-]/-}" + + if command -v systemd-run >/dev/null 2>&1; then + systemd-run --user --unit="$unit_name" --collect "$@" >/dev/null 2>&1 && return + fi + + if command -v setsid >/dev/null 2>&1; then + setsid -f "$@" >/dev/null 2>&1 + else + "$@" >/dev/null 2>&1 & + fi +} + +restart_waybar() { + local i + + pkill -x waybar >/dev/null 2>&1 || true + + for i in {1..20}; do + pgrep -x waybar >/dev/null 2>&1 || break + sleep 0.05 + done + + pgrep -x waybar >/dev/null 2>&1 && pkill -9 -x waybar >/dev/null 2>&1 || true + start_detached waybar +} + +hex_to_rgb() { + local hex="${1#'#'}" + + if [[ ! "$hex" =~ ^[0-9a-fA-F]{6}$ ]]; then + printf '0,0,0\n' + return + fi + + printf '%d,%d,%d\n' "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}" +} + +hex_to_argb() { + local hex="${1#'#'}" + + if [[ ! "$hex" =~ ^[0-9a-fA-F]{6}$ ]]; then + printf '#ff000000\n' + return + fi + + printf '#ff%s\n' "${hex,,}" +} + +write_gtk_settings() { + mkdir -p "$(dirname "$GTK3_SETTINGS")" "$(dirname "$GTK4_SETTINGS")" + backup_once "$GTK3_SETTINGS" + backup_once "$GTK4_SETTINGS" + backup_once "$GTK3_CSS" + backup_once "$GTK4_CSS" + + cat >"$GTK3_SETTINGS" <"$GTK4_SETTINGS" <"$gtk_css" </dev/null 2>&1; then + gsettings set org.gnome.desktop.interface color-scheme "$APP_THEME_MODE" >/dev/null 2>&1 || true + gsettings set org.gnome.desktop.interface accent-color "$GNOME_ACCENT_COLOR" >/dev/null 2>&1 || true + gsettings set org.gnome.desktop.interface gtk-theme "$GTK_THEME_NAME" >/dev/null 2>&1 || true + gsettings set org.gnome.desktop.interface icon-theme "$ICON_THEME_NAME" >/dev/null 2>&1 || true + fi +} + +link_icon_if_present() { + local source="$1" + local target="$2" + + if [[ -e "$source" ]]; then + ln -sfn "$source" "$target" + fi +} + +write_papirus_icon_theme() { + local source_theme="/usr/share/icons/Papirus-Dark" + local target_theme="$LOCAL_ICON_DIR/$ICON_THEME_NAME" + local source_places relative_dir target_places source_size size scale suffix + local folder_color="${PAPIRUS_FOLDER_COLOR:-}" + local stamp_file="$target_theme/.theme-switcher-stamp" + local stamp_value="$NAME|$ICON_THEME_NAME|$folder_color" + local place_dirs=() + local directories + + [[ -d "$source_theme" && -n "$folder_color" && "$ICON_THEME_NAME" == Papirus-* ]] || return 0 + + if [[ -f "$target_theme/index.theme" && -f "$stamp_file" && "$(cat "$stamp_file")" == "$stamp_value" ]]; then + return 0 + fi + + mkdir -p "$target_theme" + mapfile -t place_dirs < <(find -L "$source_theme" -path '*/places' -type d | sort) + directories="$( + for source_places in "${place_dirs[@]}"; do + printf '%s,' "${source_places#"$source_theme"/}" + done + )" + directories="${directories%,}" + + cat >"$target_theme/index.theme" <>"$target_theme/index.theme" </dev/null 2>&1; then + gtk-update-icon-cache -q "$target_theme" >/dev/null 2>&1 || true + fi + + printf '%s\n' "$stamp_value" >"$stamp_file" +} + +write_qtct_theme() { + local qt_dir conf_file color_file + local foreground_argb muted_argb background_argb panel_argb accent_argb accent_2_argb selected_argb danger_argb placeholder_argb + local active_colors disabled_colors inactive_colors + + foreground_argb="$(hex_to_argb "$FOREGROUND")" + muted_argb="$(hex_to_argb "$MUTED")" + background_argb="$(hex_to_argb "${BACKGROUND_HEX:-#18141f}")" + panel_argb="$(hex_to_argb "${PANEL_HEX:-#313244}")" + accent_argb="$(hex_to_argb "$ACCENT")" + accent_2_argb="$(hex_to_argb "$ACCENT_2")" + selected_argb="$(hex_to_argb "$SELECTED_TEXT")" + danger_argb="$(hex_to_argb "$WAYBAR_DANGER")" + placeholder_argb="#80${MUTED#'#'}" + + active_colors="$foreground_argb, $panel_argb, #ffffffff, $muted_argb, $background_argb, $panel_argb, $foreground_argb, #ffffffff, $foreground_argb, $background_argb, $background_argb, #ff000000, $accent_argb, $selected_argb, $accent_2_argb, $danger_argb, $panel_argb, $foreground_argb, $panel_argb, $foreground_argb, $placeholder_argb" + disabled_colors="$muted_argb, $panel_argb, #ffffffff, $muted_argb, $background_argb, $panel_argb, $muted_argb, #ffffffff, $muted_argb, $background_argb, $background_argb, #ff000000, $panel_argb, $muted_argb, $accent_2_argb, $danger_argb, $panel_argb, $muted_argb, $panel_argb, $muted_argb, $placeholder_argb" + inactive_colors="$foreground_argb, $panel_argb, #ffffffff, $muted_argb, $background_argb, $panel_argb, $foreground_argb, #ffffffff, $foreground_argb, $background_argb, $background_argb, #ff000000, $accent_argb, $selected_argb, $accent_2_argb, $danger_argb, $panel_argb, $foreground_argb, $panel_argb, $foreground_argb, $placeholder_argb" + + for qt_dir in "${XDG_CONFIG_HOME:-$HOME/.config}/qt6ct" "${XDG_CONFIG_HOME:-$HOME/.config}/qt5ct"; do + mkdir -p "$qt_dir/colors" + color_file="$qt_dir/colors/$KDE_COLOR_SCHEME.conf" + + cat >"$color_file" <"$conf_file" <"$scheme_file" </dev/null 2>&1; then + kwriteconfig6 --file kdeglobals --group General --key ColorScheme "$KDE_COLOR_SCHEME" >/dev/null 2>&1 || true + kwriteconfig6 --file kdeglobals --group General --key Name "$NAME" >/dev/null 2>&1 || true + kwriteconfig6 --file kdeglobals --group Icons --key Theme "$ICON_THEME_NAME" >/dev/null 2>&1 || true + kwriteconfig6 --file kdeglobals --group KDE --key widgetStyle Fusion >/dev/null 2>&1 || true + else + mkdir -p "$(dirname "$KDEGLOBALS")" + cat >"$KDEGLOBALS" <"$WOFI_STYLE" <"$SWAYNC_STYLE" <"$WAYBAR_STYLE" <"$HYPRLOCK_CONF" <Passwort oder Fingerprint + + check_color = rgba($accent_2_rgb, 0.88) + fail_color = rgba($danger_rgb, 0.95) + capslock_color = rgba($warning_rgb, 0.95) + + position = 0, -20 + halign = center + valign = center +} + +label { + monitor = + text = cmd[update:1000] echo " $(whoami)@$(hostname)" + color = rgba($muted_rgb, 0.70) + font_size = 16 + font_family = JetBrainsMono Nerd Font + position = 0, -92 + halign = center + valign = center +} +EOF +} + +write_starship_theme() { + mkdir -p "$(dirname "$STARSHIP_CONF")" + backup_once "$STARSHIP_CONF" + + cat >"$STARSHIP_CONF" <"$SDDM_STATE_DIR/theme.conf" <"$HYPRPAPER_CONF" <"$CURRENT_WALLPAPER" + + if command -v awww >/dev/null 2>&1; then + if ! pgrep -x awww-daemon >/dev/null 2>&1; then + start_detached awww-daemon + sleep 0.2 + fi + + awww img "$WALLPAPER" \ + --transition-type "$TRANSITION_TYPE" \ + --transition-duration "$TRANSITION_DURATION" \ + --transition-fps "$TRANSITION_FPS" \ + --transition-pos "$TRANSITION_POS" >/dev/null 2>&1 || true + return + fi + + if command -v swww >/dev/null 2>&1; then + if ! pgrep -x swww-daemon >/dev/null 2>&1; then + start_detached swww-daemon + sleep 0.2 + fi + + swww img "$WALLPAPER" \ + --transition-type "$TRANSITION_TYPE" \ + --transition-duration "$TRANSITION_DURATION" \ + --transition-fps "$TRANSITION_FPS" \ + --transition-pos "$TRANSITION_POS" >/dev/null 2>&1 || true + return + fi + + if command -v hyprctl >/dev/null 2>&1; then + if ! pgrep -x hyprpaper >/dev/null 2>&1; then + start_detached hyprpaper + sleep 0.2 + fi + + hyprctl hyprpaper preload "$WALLPAPER" >/dev/null || true + hyprctl hyprpaper wallpaper ",$WALLPAPER" >/dev/null || true + fi +} + +apply_theme() { + local theme_file="$1" + load_theme "$theme_file" + + cat >"$CURRENT_THEME" </dev/null 2>&1; then + hyprctl keyword general:col.active_border "$ACTIVE_BORDER" >/dev/null || true + hyprctl keyword general:col.inactive_border "$INACTIVE_BORDER" >/dev/null || true + fi + + if command -v swaync-client >/dev/null 2>&1; then + swaync-client -rs >/dev/null 2>&1 || true + fi + + if command -v waybar >/dev/null 2>&1; then + restart_waybar + fi + + if pgrep -x nautilus >/dev/null 2>&1; then + nautilus -q >/dev/null 2>&1 || pkill -x nautilus >/dev/null 2>&1 || true + fi + + notify "$NAME aktiviert." +} + +if [[ "${1:-}" == "--apply" ]]; then + theme_arg="${2:-}" + + if [[ -z "$theme_arg" ]]; then + notify "Kein Theme angegeben." + exit 1 + fi + + if [[ -f "$theme_arg" ]]; then + apply_theme "$theme_arg" + elif [[ -f "$THEME_DIR/$theme_arg.theme" ]]; then + apply_theme "$THEME_DIR/$theme_arg.theme" + else + notify "Theme nicht gefunden: $theme_arg" + exit 1 + fi + + exit 0 +fi + +mkdir -p "$THEME_DIR" + +mapfile -t theme_files < <(find "$THEME_DIR" -maxdepth 1 -type f -name '*.theme' | sort) + +if ((${#theme_files[@]} == 0)); then + notify "Keine Themes in $THEME_DIR gefunden." + exit 1 +fi + +menu_entries=() +for theme_file in "${theme_files[@]}"; do + load_theme "$theme_file" + menu_entries+=("$ICON $NAME") +done + +choice="$( + printf '%s\n' "${menu_entries[@]}" | + wofi --dmenu --prompt "󰌪 Theme wechseln" --insensitive +)" + +[[ -z "${choice:-}" ]] && exit 0 + +for i in "${!menu_entries[@]}"; do + if [[ "$choice" == "${menu_entries[$i]}" ]]; then + apply_theme "${theme_files[$i]}" + exit 0 + fi +done diff --git a/dotfiles/hypr/Scripts/toggle-wofi.sh b/dotfiles/hypr/Scripts/toggle-wofi.sh new file mode 100755 index 0000000..09d1b6a --- /dev/null +++ b/dotfiles/hypr/Scripts/toggle-wofi.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +if pgrep -x wofi >/dev/null; then + pkill -x wofi +else + wofi --show drun +fi diff --git a/dotfiles/hypr/Scripts/wallpaper-menu.sh b/dotfiles/hypr/Scripts/wallpaper-menu.sh new file mode 100755 index 0000000..125b25c --- /dev/null +++ b/dotfiles/hypr/Scripts/wallpaper-menu.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +HYPR_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" +WALLPAPER_DIR="${WALLPAPER_DIR:-$HOME/Bilder/Wallpaper}" +HYPRPAPER_CONF="$HYPR_DIR/hyprpaper.conf" +CURRENT_WALLPAPER="$HYPR_DIR/current-wallpaper" + +TRANSITION_TYPE="${TRANSITION_TYPE:-grow}" +TRANSITION_DURATION="${TRANSITION_DURATION:-1.0}" +TRANSITION_FPS="${TRANSITION_FPS:-60}" +TRANSITION_POS="${TRANSITION_POS:-center}" + +notify() { + notify-send "󰸉 Wallpaper" "$1" >/dev/null 2>&1 || true +} + +start_detached() { + local command_name="$1" + local unit_name="${command_name//[^[:alnum:]_.-]/-}" + + if command -v systemd-run >/dev/null 2>&1; then + systemd-run --user --unit="$unit_name" --collect "$@" >/dev/null 2>&1 && return + fi + + if command -v setsid >/dev/null 2>&1; then + setsid -f "$@" >/dev/null 2>&1 + else + "$@" >/dev/null 2>&1 & + fi +} + +apply_wallpaper() { + local wallpaper="$1" + + cat >"$HYPRPAPER_CONF" <"$CURRENT_WALLPAPER" + + if command -v awww >/dev/null 2>&1; then + if ! pgrep -x awww-daemon >/dev/null 2>&1; then + start_detached awww-daemon + sleep 0.2 + fi + + awww img "$wallpaper" \ + --transition-type "$TRANSITION_TYPE" \ + --transition-duration "$TRANSITION_DURATION" \ + --transition-fps "$TRANSITION_FPS" \ + --transition-pos "$TRANSITION_POS" >/dev/null 2>&1 || true + return + fi + + if command -v swww >/dev/null 2>&1; then + if ! pgrep -x swww-daemon >/dev/null 2>&1; then + start_detached swww-daemon + sleep 0.2 + fi + + swww img "$wallpaper" \ + --transition-type "$TRANSITION_TYPE" \ + --transition-duration "$TRANSITION_DURATION" \ + --transition-fps "$TRANSITION_FPS" \ + --transition-pos "$TRANSITION_POS" >/dev/null 2>&1 || true + return + fi + + if command -v hyprctl >/dev/null 2>&1; then + if ! pgrep -x hyprpaper >/dev/null 2>&1; then + start_detached hyprpaper + sleep 0.2 + fi + + hyprctl hyprpaper preload "$wallpaper" >/dev/null || true + hyprctl hyprpaper wallpaper ",$wallpaper" >/dev/null || true + fi +} + +preview_wallpaper() { + local wallpaper="$1" + + if command -v swayimg >/dev/null 2>&1; then + start_detached swayimg "$wallpaper" + return + fi + + if command -v imv >/dev/null 2>&1; then + start_detached imv "$wallpaper" + return + fi + + if command -v kitty >/dev/null 2>&1; then + start_detached kitty \ + --class wallpaper-preview \ + --title "Wallpaper Vorschau" \ + sh -c 'clear; printf "%s\n\n" "$1"; kitty +kitten icat --fit both --align center "$1"; printf "\nEnter schliesst die Vorschau."; read -r _' \ + sh "$wallpaper" + return + fi + + notify "Kein Vorschauprogramm gefunden." +} + +if [[ "${1:-}" == "--apply" ]]; then + wallpaper="${2:-}" + + if [[ -z "$wallpaper" || ! -f "$wallpaper" ]]; then + notify "Wallpaper nicht gefunden." + exit 1 + fi + + apply_wallpaper "$wallpaper" + notify "$(basename "$wallpaper") angewendet." + exit 0 +fi + +if [[ "${1:-}" == "--preview" ]]; then + wallpaper="${2:-}" + + if [[ -z "$wallpaper" || ! -f "$wallpaper" ]]; then + notify "Wallpaper nicht gefunden." + exit 1 + fi + + preview_wallpaper "$wallpaper" + exit 0 +fi + +pick_wallpaper() { + local entries=() + local wallpaper + + mapfile -t wallpapers < <( + find "$WALLPAPER_DIR" -maxdepth 1 -type f \ + \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' -o -iname '*.gif' \) | + sort + ) + + if ((${#wallpapers[@]} == 0)); then + notify "Keine Bilder in $WALLPAPER_DIR gefunden." + exit 1 + fi + + for wallpaper in "${wallpapers[@]}"; do + entries+=("󰸉 $(basename "$wallpaper")") + done + + choice="$( + printf '%s\n' "${entries[@]}" | + wofi --dmenu --prompt "󰸉 Wallpaper" --insensitive + )" + + [[ -z "${choice:-}" ]] && exit 0 + + for i in "${!entries[@]}"; do + if [[ "$choice" == "${entries[$i]}" ]]; then + printf '%s\n' "${wallpapers[$i]}" + return + fi + done +} + +while true; do + wallpaper="$(pick_wallpaper)" + [[ -z "${wallpaper:-}" ]] && exit 0 + + action="$( + printf '%s\n' \ + "󰋩 Vorschau" \ + "󰄬 Anwenden" \ + "󰁍 Zurueck" | + wofi --dmenu --prompt "󰸉 $(basename "$wallpaper")" --insensitive + )" + + case "$action" in + *"Vorschau"*) + preview_wallpaper "$wallpaper" + ;; + *"Anwenden"*) + apply_wallpaper "$wallpaper" + notify "$(basename "$wallpaper") angewendet." + exit 0 + ;; + *"Zurueck"*) + ;; + *) + exit 0 + ;; + esac +done diff --git a/dotfiles/hypr/Scripts/widget-panel.sh b/dotfiles/hypr/Scripts/widget-panel.sh new file mode 100755 index 0000000..a38c6b8 --- /dev/null +++ b/dotfiles/hypr/Scripts/widget-panel.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +HYPR_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" +export HYPR_DIR + +notify() { + notify-send "Widgetbereich" "$1" >/dev/null 2>&1 || true +} + +if ! command -v ags >/dev/null 2>&1; then + notify "ags ist nicht installiert." + exit 1 +fi + +mapfile -t AGS_INSTANCES < <(ags list 2>/dev/null || true) +for INSTANCE in "${AGS_INSTANCES[@]}"; do + if [[ "$INSTANCE" == "widget-panel" ]]; then + ags toggle widget-panel --instance widget-panel >/dev/null 2>&1 || true + exit 0 + fi +done + +cd "$HYPR_DIR" +nohup ags run "$HYPR_DIR/ags/widget-panel.tsx" >/dev/null 2>&1 & diff --git a/dotfiles/hypr/Themes/forest-neon.theme b/dotfiles/hypr/Themes/forest-neon.theme new file mode 100644 index 0000000..44a4109 --- /dev/null +++ b/dotfiles/hypr/Themes/forest-neon.theme @@ -0,0 +1,34 @@ +NAME="Forest Neon" +ICON="󰌪" +WALLPAPER="/home/pascal/Bilder/Wallpaper/forest.jpg" +TRANSITION_TYPE="grow" +TRANSITION_DURATION="1.0" +TRANSITION_FPS="60" +TRANSITION_POS="center" +ACTIVE_BORDER="rgba(00ff9cee) rgba(00cc88ee) 45deg" +INACTIVE_BORDER="rgba(2a2a2aaa)" +ACCENT="#00ff9c" +ACCENT_2="#00cc88" +BACKGROUND="rgba(20, 20, 30, 0.95)" +BACKGROUND_SOFT="rgba(40, 40, 55, 0.8)" +BACKGROUND_HEX="#14141e" +PANEL_HEX="#282837" +FOREGROUND="#cdd6f4" +MUTED="#cccccc" +SELECTED_TEXT="#000000" +WAYBAR_PANEL="rgba(24, 24, 37, 0.72)" +WAYBAR_ISLAND="rgba(5, 5, 9, 0.88)" +WAYBAR_MUTED="#7f849c" +WAYBAR_SUCCESS="#a6e3a1" +WAYBAR_WARNING="#f9e2af" +WAYBAR_DANGER="#f38ba8" +WAYBAR_BLUE="#89b4fa" +WAYBAR_CYAN="#89dceb" +WAYBAR_ORANGE="#fab387" +WAYBAR_PURPLE="#cba6f7" +APP_THEME_MODE="prefer-dark" +GNOME_ACCENT_COLOR="green" +GTK_THEME_NAME="Adwaita" +ICON_THEME_NAME="Papirus-ForestNeon" +PAPIRUS_FOLDER_COLOR="green" +KDE_COLOR_SCHEME="ForestNeon" diff --git a/dotfiles/hypr/Themes/rose-night.theme b/dotfiles/hypr/Themes/rose-night.theme new file mode 100644 index 0000000..3966fd1 --- /dev/null +++ b/dotfiles/hypr/Themes/rose-night.theme @@ -0,0 +1,34 @@ +NAME="Rose Night" +ICON="󰌪" +WALLPAPER="/home/pascal/Bilder/Wallpaper/rose-pink.jpg" +TRANSITION_TYPE="wipe" +TRANSITION_DURATION="1.0" +TRANSITION_FPS="60" +TRANSITION_POS="center" +ACTIVE_BORDER="rgba(f38ba8ee) rgba(cba6f7ee) 45deg" +INACTIVE_BORDER="rgba(313244aa)" +ACCENT="#f38ba8" +ACCENT_2="#cba6f7" +BACKGROUND="rgba(24, 20, 31, 0.95)" +BACKGROUND_SOFT="rgba(49, 50, 68, 0.82)" +BACKGROUND_HEX="#18141f" +PANEL_HEX="#313244" +FOREGROUND="#f5e0dc" +MUTED="#cdd6f4" +SELECTED_TEXT="#11111b" +WAYBAR_PANEL="rgba(49, 50, 68, 0.76)" +WAYBAR_ISLAND="rgba(24, 20, 31, 0.90)" +WAYBAR_MUTED="#a6adc8" +WAYBAR_SUCCESS="#f5c2e7" +WAYBAR_WARNING="#f9e2af" +WAYBAR_DANGER="#f38ba8" +WAYBAR_BLUE="#cba6f7" +WAYBAR_CYAN="#f5c2e7" +WAYBAR_ORANGE="#fab387" +WAYBAR_PURPLE="#cba6f7" +APP_THEME_MODE="prefer-dark" +GNOME_ACCENT_COLOR="pink" +GTK_THEME_NAME="Adwaita" +ICON_THEME_NAME="Papirus-RoseNight" +PAPIRUS_FOLDER_COLOR="pink" +KDE_COLOR_SCHEME="RoseNight" diff --git a/dotfiles/hypr/ags/homelab.css b/dotfiles/hypr/ags/homelab.css new file mode 100644 index 0000000..4a3faf5 --- /dev/null +++ b/dotfiles/hypr/ags/homelab.css @@ -0,0 +1,312 @@ +* { + all: unset; + font-family: "JetBrainsMono Nerd Font", "Noto Sans", sans-serif; + font-size: 13px; +} + +.homelab-window { + background: transparent; +} + +.login-panel, +.shell { + border: 1px solid rgba(205, 214, 244, 0.16); + border-radius: 16px; + background: rgba(20, 20, 30, 0.97); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45); + color: #cdd6f4; +} + +.login-panel { + min-width: 520px; + padding: 18px; +} + +.shell { + min-width: 0; + min-height: 0; +} + +.sidebar { + min-width: 218px; + padding: 14px 10px; + border-right: 1px solid rgba(205, 214, 244, 0.10); + background: rgba(14, 16, 24, 0.72); +} + +.brand { + padding: 8px 8px 14px; +} + +.brand-title { + color: #00ff9c; + font-size: 19px; + font-weight: 800; +} + +.brand-subtitle, +.subtitle, +.row-subtitle { + color: #a6adc8; + font-size: 12px; +} + +.nav-button { + min-height: 34px; + padding: 7px 8px; + border-radius: 8px; + color: #cdd6f4; +} + +.nav-button:hover, +.nav-button:focus { + background: rgba(0, 255, 156, 0.14); +} + +.nav-button.active { + background: rgba(0, 255, 156, 0.22); + color: #00ff9c; +} + +.nav-icon { + min-width: 24px; +} + +.nav-label { + font-size: 12px; +} + +.content { + padding: 18px; +} + +.page-scroll { + min-width: 0; + min-height: 0; +} + +.header { + min-height: 38px; +} + +.title { + font-size: 20px; + font-weight: 750; + color: #00ff9c; +} + +.subtitle { + margin-top: 3px; +} + +entry { + padding: 10px 12px; + border: 1px solid rgba(205, 214, 244, 0.14); + border-radius: 10px; + background: rgba(40, 40, 55, 0.82); + color: #cdd6f4; +} + +entry:focus { + border-color: rgba(0, 255, 156, 0.78); +} + +.grid, +.live-strip, +.service-grid { + min-height: 92px; +} + +.card, +.chart-card, +.service-card, +.table-row { + border: 1px solid rgba(205, 214, 244, 0.10); + border-radius: 8px; + background: rgba(40, 40, 55, 0.62); +} + +.card { + min-width: 220px; + padding: 12px; +} + +.wide-card { + min-width: 0; + min-height: 220px; +} + +.log-card { + min-width: 0; + min-height: 560px; +} + +.stat-card { + min-height: 80px; +} + +.chart-card { + min-width: 260px; + min-height: 98px; + padding: 12px; +} + +.chart-card.cpu { + border-color: rgba(0, 255, 156, 0.22); +} + +.chart-card.memory { + border-color: rgba(137, 180, 250, 0.26); +} + +.chart-card.load { + border-color: rgba(249, 226, 175, 0.24); +} + +.chart-card.docker, +.chart-card.network { + border-color: rgba(116, 199, 236, 0.24); +} + +.card-title { + color: #00cc88; + font-size: 12px; + font-weight: 750; +} + +.card-value { + color: #cdd6f4; +} + +.compact, +.row-subtitle { + font-size: 12px; +} + +.percent-label, +.chart-value { + color: #f9e2af; + font-size: 12px; + font-weight: 750; +} + +.sparkline { + color: #00ff9c; + font-size: 25px; + letter-spacing: 0; +} + +.meter { + min-height: 8px; + border-radius: 6px; + background: rgba(205, 214, 244, 0.10); +} + +.meter-fill { + min-height: 8px; + border-radius: 6px; + background: linear-gradient(to right, #00ff9c, #89b4fa); +} + +.alerts { + min-height: 32px; +} + +.alert, +.pill { + padding: 4px 8px; + border-radius: 999px; + background: rgba(205, 214, 244, 0.10); + color: #cdd6f4; + font-size: 11px; +} + +.alert.ok, +.pill.ok { + background: rgba(0, 255, 156, 0.16); + color: #00ff9c; +} + +.alert.warn, +.pill.warn { + background: rgba(249, 226, 175, 0.16); + color: #f9e2af; +} + +.table-row { + min-height: 72px; + padding: 10px; +} + +.container-main { + min-width: 260px; +} + +.container-stats { + min-width: 112px; +} + +.row-actions { + min-width: 250px; +} + +.row-title { + color: #cdd6f4; + font-weight: 750; +} + +.service-card { + min-width: 250px; + min-height: 112px; + padding: 12px; +} + +.actions { + margin-top: 2px; +} + +.button, +.mini-button, +.icon-button { + padding: 8px 10px; + border-radius: 8px; + background: rgba(40, 40, 55, 0.82); + color: #cdd6f4; +} + +.mini-button { + padding: 6px 8px; + font-size: 12px; +} + +.button:hover, +.button:focus, +.mini-button:hover, +.mini-button:focus, +.icon-button:hover, +.icon-button:focus { + background: rgba(0, 255, 156, 0.22); +} + +.primary { + background: rgba(0, 255, 156, 0.24); + color: #00ff9c; +} + +.danger { + color: #f38ba8; +} + +.close { + color: #f38ba8; +} + +.icon-button { + min-width: 34px; + min-height: 34px; + padding: 0; +} + +.error { + color: #f38ba8; +} diff --git a/dotfiles/hypr/ags/homelab.tsx b/dotfiles/hypr/ags/homelab.tsx new file mode 100644 index 0000000..078e965 --- /dev/null +++ b/dotfiles/hypr/ags/homelab.tsx @@ -0,0 +1,1156 @@ +import app from "ags/gtk4/app"; +import { Astal, Gtk } from "ags/gtk4"; +import { execAsync } from "ags/process"; +import css from "./homelab.css"; +import GLib from "gi://GLib"; + +function loadConfig() { + const configPath = GLib.getenv("HOMELAB_CONFIG") || `${GLib.getenv("HOME")}/.config/homelab/config.yaml`; + const defaults = { host: "10.0.0.15", user: "root", port: 22 }; + + try { + const [ok, contents] = GLib.file_get_contents(configPath); + if (!ok || !contents) return defaults; + + const text = new TextDecoder().decode(contents); + const lines = text.split("\n"); + let host = defaults.host; + let user = defaults.user; + let inServer = false; + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith("#") || trimmed.length === 0) continue; + if (trimmed === "server:") { inServer = true; continue; } + if (inServer && /^\w+:/.test(trimmed) && !trimmed.startsWith(" ")) inServer = false; + if (!inServer) continue; + const addrMatch = trimmed.match(/address:\s*["']?(.+?)["']?$/); + if (addrMatch) host = addrMatch[1]; + const userMatch = trimmed.match(/username:\s*["']?(.+?)["']?$/); + if (userMatch) user = userMatch[1]; + } + return { host, user, port: defaults.port }; + } catch { + return defaults; + } +} + +const CONFIG = loadConfig(); +const UNRAID_HOST = CONFIG.host; +const UNRAID_USER = CONFIG.user; +const SSH_OPTS = [ + "-o", "StrictHostKeyChecking=accept-new", + "-o", "ConnectTimeout=5", + "-o", "ServerAliveInterval=5", + "-o", "ServerAliveCountMax=1", +]; +const REFRESH_INTERVAL_SECONDS = 30; +const HISTORY_LIMIT = 24; +const WINDOW_MARGIN_TOP = 48; +const WINDOW_EDGE_GAP = 48; +const ESC_KEYVAL = 65307; + +type PageId = "dashboard" | "docker" | "services" | "storage" | "network" | "system" | "logs" | "automations" | "auth" | "integrations" | "ai" | "monitoring"; + +type DockerContainer = { + id: string; + name: string; + image: string; + status: string; + state: string; + health: string; + ports: string; + restart: string; + cpu: number; + memory: string; +}; + +type DiskInfo = { + filesystem: string; + size: string; + used: string; + available: string; + percent: number; + mount: string; +}; + +type ServerStatus = { + host: string; + uptime: string; + kernel: string; + load: string; + load1: number; + cpuPercent: number; + memory: string; + memoryPercent: number; + rootDisk: string; + rootDiskPercent: number; + userDisk: string; + userDiskPercent: number; + array: string; + parity: string; + temps: string; + docker: string; + dockerRunning: number; + dockerStopped: number; + dockerUnhealthy: number; + dockerContainers: DockerContainer[]; + disks: DiskInfo[]; + smart: string; + network: string; + networkRxBytes: number; + networkTxBytes: number; + dockerNetworks: string; + ports: string; + updates: string; + serviceHealth: Record; + alerts: string[]; + updated: string; +}; + +type MetricSample = { + time: string; + load1: number; + cpuPercent: number; + memoryPercent: number; + rootDiskPercent: number; + userDiskPercent: number; + dockerRunning: number; + networkRxRate: number; + networkTxRate: number; + rxBytes: number; + txBytes: number; + timestamp: number; +}; + +const navItems: { id: PageId; icon: string; label: string }[] = [ + { id: "dashboard", icon: "🏠", label: "Dashboard" }, + { id: "docker", icon: "🐳", label: "Docker" }, + { id: "services", icon: "📦", label: "Apps / Services" }, + { id: "storage", icon: "💾", label: "Storage / Disks" }, + { id: "network", icon: "🌐", label: "Netzwerk" }, + { id: "system", icon: "⚙️", label: "System" }, + { id: "logs", icon: "📜", label: "Logs" }, + { id: "automations", icon: "🤖", label: "Automationen" }, + { id: "auth", icon: "🔐", label: "Auth / User" }, + { id: "integrations", icon: "🔌", label: "Integrationen" }, + { id: "ai", icon: "🧠", label: "AI / Agent" }, + { id: "monitoring", icon: "📊", label: "Monitoring Advanced" }, +]; + +const serviceChecks = [ + { name: "Unraid", url: `http://${UNRAID_HOST}`, localUrl: "http://127.0.0.1", hint: "Core Web UI", query: "" }, + { name: "Navidrome", url: `http://${UNRAID_HOST}:4533`, localUrl: "http://127.0.0.1:4533", hint: "Music", query: "navidrome" }, + { name: "Paperless", url: `http://${UNRAID_HOST}:8000`, localUrl: "http://127.0.0.1:8000", hint: "Docs", query: "paperless" }, + { name: "Nextcloud", url: `http://${UNRAID_HOST}:8080`, localUrl: "http://127.0.0.1:8080", hint: "Cloud", query: "nextcloud" }, + { name: "Ollama", url: `http://${UNRAID_HOST}:11434`, localUrl: "http://127.0.0.1:11434", hint: "AI API", query: "ollama" }, + { name: "n8n", url: `http://${UNRAID_HOST}:5678`, localUrl: "http://127.0.0.1:5678", hint: "Automation", query: "n8n" }, + { name: "Home Assistant", url: `http://${UNRAID_HOST}:8123`, localUrl: "http://127.0.0.1:8123", hint: "Smart Home", query: "homeassistant" }, + { name: "Authentik", url: `http://${UNRAID_HOST}:9000`, localUrl: "http://127.0.0.1:9000", hint: "SSO", query: "authentik" }, +]; + +let password = ""; +let authed = false; +let busy = false; +let status: ServerStatus | null = null; +let errorMessage = ""; +let history: MetricSample[] = []; +let nextRefreshAt = 0; +let timerStarted = false; +let armedAction = ""; +let activePage: PageId = "dashboard"; +let logOutput = "Noch keine Logs geladen."; +let logTitle = "Logs"; + +function shQuote(value: string) { + return `'${value.replace(/'/g, `'\\''`)}'`; +} + +function notify(message: string) { + execAsync(["notify-send", "Homelab", message]).catch(console.error); +} + +function sshCommand(remoteCommand: string) { + const opts = SSH_OPTS.map(shQuote).join(" "); + return `SSHPASS=${shQuote(password)} sshpass -e ssh ${opts} ${shQuote(`${UNRAID_USER}@${UNRAID_HOST}`)} ${shQuote(remoteCommand)}`; +} + +function runRemote(remoteCommand: string) { + return execAsync(["bash", "-lc", sshCommand(remoteCommand)]); +} + +function parsePercent(value: string) { + return clampNumber(Number.parseFloat(value.replace("%", "").trim() || "0"), 0, 100); +} + +function parseSections(output: string) { + const sections: Record = {}; + let current = ""; + + for (const line of output.split("\n")) { + const match = line.match(/^===([A-Z_]+)===$/); + if (match) { + current = match[1]; + sections[current] = ""; + continue; + } + + if (current) { + sections[current] += `${line}\n`; + } + } + + return sections; +} + +function parseDocker(sections: Record) { + const stats: Record = {}; + + for (const line of (sections.DOCKER_STATS || "").trim().split("\n").filter(Boolean)) { + const [name, cpu, memory] = line.split("||"); + stats[name] = { cpu: parsePercent(cpu || "0"), memory: memory || "n/a" }; + } + + return (sections.DOCKER_ALL || "") + .trim() + .split("\n") + .filter(Boolean) + .map(line => { + const [id, name, image, statusText, state, health, ports, restart] = line.split("||"); + return { + id: id || "", + name: name || "unknown", + image: image || "n/a", + status: statusText || "n/a", + state: state || "unknown", + health: health || "n/a", + ports: ports || "n/a", + restart: restart || "n/a", + cpu: stats[name]?.cpu || 0, + memory: stats[name]?.memory || "n/a", + }; + }); +} + +function parseDisks(output: string) { + return output + .trim() + .split("\n") + .filter(Boolean) + .map(line => { + const [filesystem, size, used, available, percent, mount] = line.split("||"); + return { + filesystem: filesystem || "n/a", + size: size || "n/a", + used: used || "n/a", + available: available || "n/a", + percent: parsePercent(percent || "0"), + mount: mount || "n/a", + }; + }); +} + +function parseServiceHealth(output: string) { + const health: Record = {}; + + for (const line of output.trim().split("\n").filter(Boolean)) { + const [name, code] = line.split("||"); + health[name] = code || "n/a"; + } + + return health; +} + +function buildAlerts(nextStatus: ServerStatus) { + const alerts: string[] = []; + + if (nextStatus.dockerUnhealthy > 0) { + alerts.push(`${nextStatus.dockerUnhealthy} unhealthy container`); + } + if (nextStatus.dockerContainers.some(container => container.state !== "running")) { + alerts.push(`${nextStatus.dockerStopped} stopped container`); + } + if (nextStatus.userDiskPercent >= 90 || nextStatus.rootDiskPercent >= 90) { + alerts.push("Disk usage high"); + } + if (nextStatus.load1 >= 4) { + alerts.push("High load"); + } + if (nextStatus.smart.toLowerCase().includes("prefail") || nextStatus.smart.toLowerCase().includes("failed")) { + alerts.push("SMART warning"); + } + if (nextStatus.array.toLowerCase().includes("disabled") || nextStatus.array.toLowerCase().includes("invalid")) { + alerts.push("Array warning"); + } + + return alerts.length ? alerts : ["Alles ruhig"]; +} + +function parseStatus(output: string): ServerStatus { + const sections = parseSections(output); + const updated = GLib.DateTime.new_now_local().format("%H:%M:%S") || ""; + const load = sections.LOAD?.trim() || "n/a"; + const dockerContainers = parseDocker(sections); + const dockerRunning = dockerContainers.filter(container => container.state === "running").length; + const dockerStopped = dockerContainers.filter(container => container.state !== "running").length; + const dockerUnhealthy = dockerContainers.filter(container => container.health.toLowerCase().includes("unhealthy")).length; + + const nextStatus: ServerStatus = { + host: sections.HOST?.trim() || UNRAID_HOST, + uptime: sections.UPTIME?.trim() || "n/a", + kernel: sections.KERNEL?.trim() || "n/a", + load, + load1: Math.max(0, Number.parseFloat(load.split(/\s+/)[0] || "0") || 0), + cpuPercent: parsePercent(sections.CPU_PERCENT || "0"), + memory: sections.MEMORY?.trim() || "n/a", + memoryPercent: parsePercent(sections.MEMORY_PERCENT || "0"), + rootDisk: sections.ROOT_DISK?.trim() || "n/a", + rootDiskPercent: parsePercent(sections.ROOT_DISK_PERCENT || "0"), + userDisk: sections.USER_DISK?.trim() || "n/a", + userDiskPercent: parsePercent(sections.USER_DISK_PERCENT || "0"), + array: sections.ARRAY?.trim() || "n/a", + parity: sections.PARITY?.trim() || "n/a", + temps: sections.TEMPS?.trim() || "n/a", + docker: sections.DOCKER?.trim() || "n/a", + dockerRunning, + dockerStopped, + dockerUnhealthy, + dockerContainers, + disks: parseDisks(sections.DISKS || ""), + smart: sections.SMART?.trim() || "n/a", + network: sections.NETWORK?.trim() || "n/a", + networkRxBytes: Math.max(0, Number.parseInt(sections.NETWORK_RX_BYTES?.trim() || "0", 10) || 0), + networkTxBytes: Math.max(0, Number.parseInt(sections.NETWORK_TX_BYTES?.trim() || "0", 10) || 0), + dockerNetworks: sections.DOCKER_NETWORKS?.trim() || "n/a", + ports: sections.PORTS?.trim() || "n/a", + updates: sections.UPDATES?.trim() || "n/a", + serviceHealth: parseServiceHealth(sections.SERVICE_HEALTH || ""), + alerts: [], + updated, + }; + + nextStatus.alerts = buildAlerts(nextStatus); + return nextStatus; +} + +const statusCommand = String.raw` +printf '===HOST===\n' +hostname 2>/dev/null || uname -n +printf '===UPTIME===\n' +uptime -p 2>/dev/null || uptime +printf '===KERNEL===\n' +uname -r +printf '===LOAD===\n' +awk '{print $1" "$2" "$3}' /proc/loadavg 2>/dev/null +printf '===CPU_PERCENT===\n' +read _ u1 n1 s1 i1 iw1 irq1 si1 st1 _ < /proc/stat +idle1=$((i1 + iw1)) +total1=$((u1 + n1 + s1 + i1 + iw1 + irq1 + si1 + st1)) +sleep 1 +read _ u2 n2 s2 i2 iw2 irq2 si2 st2 _ < /proc/stat +idle2=$((i2 + iw2)) +total2=$((u2 + n2 + s2 + i2 + iw2 + irq2 + si2 + st2)) +awk -v idle="$((idle2 - idle1))" -v total="$((total2 - total1))" 'BEGIN { if (total > 0) printf "%.0f\n", (1 - idle / total) * 100; else print 0 }' +printf '===MEMORY===\n' +free -h | awk '/Mem:/ {print $3 " / " $2 " used"}' +printf '===MEMORY_PERCENT===\n' +free | awk '/Mem:/ { if ($2 > 0) printf "%.0f\n", ($3 / $2) * 100; else print 0 }' +printf '===ROOT_DISK===\n' +df -h / | awk 'NR==2 {print $3 " / " $2 " used (" $5 ")"}' +printf '===ROOT_DISK_PERCENT===\n' +df -P / | awk 'NR==2 {gsub("%", "", $5); print $5}' +printf '===USER_DISK===\n' +df -h /mnt/user 2>/dev/null | awk 'NR==2 {print $3 " / " $2 " used (" $5 ")"}' +printf '===USER_DISK_PERCENT===\n' +df -P /mnt/user 2>/dev/null | awk 'NR==2 {gsub("%", "", $5); print $5}' +printf '===ARRAY===\n' +if command -v mdcmd >/dev/null 2>&1; then + mdcmd status | grep -E '^(mdState|mdNumDisabled|mdNumInvalid|sbSynced|mdResync|mdResyncPos|mdResyncSize)=' | head -30 +elif [ -r /proc/mdcmd ]; then + grep -E '^(mdState|mdNumDisabled|mdNumInvalid|sbSynced|mdResync|mdResyncPos|mdResyncSize)=' /proc/mdcmd | head -30 +else + echo 'Array status unavailable' +fi +printf '===PARITY===\n' +if command -v mdcmd >/dev/null 2>&1; then + mdcmd status | grep -E '^(sbSynced|mdResync|mdResyncPos|mdResyncSize|mdResyncDt|mdResyncDb)=' | head -20 +else + echo 'Parity details unavailable' +fi +printf '===TEMPS===\n' +if command -v sensors >/dev/null 2>&1; then + sensors | grep -E 'Package id|Tctl|Tdie|Core [0-9]|temp[0-9]' | head -8 +fi +if command -v smartctl >/dev/null 2>&1; then + for disk in /dev/sd? /dev/nvme?n1; do + [ -b "$disk" ] || continue + smartctl -A "$disk" 2>/dev/null | awk -v disk="$disk" '/Temperature_Celsius|Current Drive Temperature|Composite Temperature/ {print disk ": " $0; exit}' + done | head -8 +fi +printf '===DOCKER===\n' +if command -v docker >/dev/null 2>&1; then + docker ps --format '{{.Names}} | {{.Status}}' | head -12 +else + echo 'Docker unavailable' +fi +printf '===DOCKER_ALL===\n' +if command -v docker >/dev/null 2>&1; then + docker ps -aq | while read id; do + [ -n "$id" ] || continue + name=$(docker inspect -f '{{.Name}}' "$id" 2>/dev/null | sed 's#^/##') + image=$(docker inspect -f '{{.Config.Image}}' "$id" 2>/dev/null) + status=$(docker inspect -f '{{.State.Status}}' "$id" 2>/dev/null) + health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}n/a{{end}}' "$id" 2>/dev/null) + restart=$(docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$id" 2>/dev/null) + ports=$(docker port "$id" 2>/dev/null | paste -sd ', ' -) + shown=$(docker ps -a --filter id="$id" --format '{{.Status}}' | head -1) + [ -n "$ports" ] || ports=n/a + [ -n "$restart" ] || restart=n/a + echo "$id||$name||$image||$shown||$status||$health||$ports||$restart" + done +fi +printf '===DOCKER_STATS===\n' +if command -v docker >/dev/null 2>&1; then + docker stats --no-stream --format '{{.Name}}||{{.CPUPerc}}||{{.MemUsage}}' 2>/dev/null +fi +printf '===DISKS===\n' +df -hP | awk 'NR>1 && ($6 ~ /^\/mnt/ || $6 == "/") {gsub("%", "", $5); print $1 "||" $2 "||" $3 "||" $4 "||" $5 "||" $6}' +printf '===SMART===\n' +if command -v smartctl >/dev/null 2>&1; then + for disk in /dev/sd? /dev/nvme?n1; do + [ -b "$disk" ] || continue + health=$(smartctl -H "$disk" 2>/dev/null | awk -F: '/overall-health|SMART Health Status/ {gsub(/^ +/, "", $2); print $2; exit}') + temp=$(smartctl -A "$disk" 2>/dev/null | awk '/Temperature_Celsius|Current Drive Temperature|Composite Temperature/ {print $NF; exit}') + [ -n "$health" ] || health=n/a + [ -n "$temp" ] || temp=n/a + echo "$disk | health=$health | temp=$temp" + done | head -16 +else + echo 'smartctl unavailable' +fi +printf '===NETWORK===\n' +ip -br addr 2>/dev/null | head -12 +printf '===NETWORK_RX_BYTES===\n' +awk -F'[: ]+' 'NR>2 && $2 !~ /^lo$/ {rx += $3} END {print rx+0}' /proc/net/dev +printf '===NETWORK_TX_BYTES===\n' +awk -F'[: ]+' 'NR>2 && $2 !~ /^lo$/ {tx += $11} END {print tx+0}' /proc/net/dev +printf '===DOCKER_NETWORKS===\n' +if command -v docker >/dev/null 2>&1; then + docker network ls --format '{{.Name}} | {{.Driver}} | {{.Scope}}' +else + echo 'Docker networks unavailable' +fi +printf '===PORTS===\n' +(ss -tulpen 2>/dev/null || netstat -tulpen 2>/dev/null) | head -24 +printf '===SERVICE_HEALTH===\n' +if command -v curl >/dev/null 2>&1; then + while IFS='|' read name url; do + [ -n "$name" ] || continue + code=$(curl -skL --max-time 3 -o /dev/null -w '%{http_code}' "$url" 2>/dev/null) + [ -n "$code" ] || code=000 + echo "$name||$code" + done <<'SERVICES' +Unraid|http://127.0.0.1 +Navidrome|http://127.0.0.1:4533 +Paperless|http://127.0.0.1:8000 +Nextcloud|http://127.0.0.1:8080 +Ollama|http://127.0.0.1:11434 +n8n|http://127.0.0.1:5678 +Home Assistant|http://127.0.0.1:8123 +Authentik|http://127.0.0.1:9000 +SERVICES +else + echo 'curl||unavailable' +fi +printf '===UPDATES===\n' +if command -v plugin >/dev/null 2>&1; then + echo 'Plugin tool available' +else + echo 'Update check unavailable from shell' +fi +`; + +const quickStatusCommand = String.raw` +printf '===HOST===\n' +hostname 2>/dev/null || uname -n +printf '===UPTIME===\n' +uptime -p 2>/dev/null || uptime +printf '===KERNEL===\n' +uname -r +printf '===LOAD===\n' +awk '{print $1" "$2" "$3}' /proc/loadavg 2>/dev/null +printf '===CPU_PERCENT===\n' +awk '{print int($1 * 100)}' /proc/loadavg 2>/dev/null +printf '===MEMORY===\n' +free -h | awk '/Mem:/ {print $3 " / " $2 " used"}' +printf '===MEMORY_PERCENT===\n' +free | awk '/Mem:/ { if ($2 > 0) printf "%.0f\n", ($3 / $2) * 100; else print 0 }' +printf '===ROOT_DISK===\n' +df -h / | awk 'NR==2 {print $3 " / " $2 " used (" $5 ")"}' +printf '===ROOT_DISK_PERCENT===\n' +df -P / | awk 'NR==2 {gsub("%", "", $5); print $5}' +printf '===USER_DISK===\n' +df -h /mnt/user 2>/dev/null | awk 'NR==2 {print $3 " / " $2 " used (" $5 ")"}' +printf '===USER_DISK_PERCENT===\n' +df -P /mnt/user 2>/dev/null | awk 'NR==2 {gsub("%", "", $5); print $5}' +printf '===ARRAY===\n' +echo 'Detail refresh laeuft' +printf '===PARITY===\n' +echo 'Detail refresh laeuft' +printf '===TEMPS===\n' +echo 'Detail refresh laeuft' +printf '===DOCKER===\n' +if command -v docker >/dev/null 2>&1; then + docker ps --format '{{.Names}} | {{.Status}}' | head -12 +else + echo 'Docker unavailable' +fi +printf '===DOCKER_ALL===\n' +if command -v docker >/dev/null 2>&1; then + docker ps -a --format '{{.ID}}||{{.Names}}||{{.Image}}||{{.Status}}||{{.State}}||n/a||n/a||n/a' +fi +printf '===DISKS===\n' +df -hP | awk 'NR>1 && ($6 ~ /^\/mnt/ || $6 == "/") {gsub("%", "", $5); print $1 "||" $2 "||" $3 "||" $4 "||" $5 "||" $6}' +printf '===SMART===\n' +echo 'Detail refresh laeuft' +printf '===NETWORK===\n' +ip -br addr 2>/dev/null | head -12 +printf '===NETWORK_RX_BYTES===\n' +awk -F'[: ]+' 'NR>2 && $2 !~ /^lo$/ {rx += $3} END {print rx+0}' /proc/net/dev +printf '===NETWORK_TX_BYTES===\n' +awk -F'[: ]+' 'NR>2 && $2 !~ /^lo$/ {tx += $11} END {print tx+0}' /proc/net/dev +printf '===DOCKER_NETWORKS===\n' +echo 'Detail refresh laeuft' +printf '===PORTS===\n' +echo 'Detail refresh laeuft' +printf '===UPDATES===\n' +echo 'Detail refresh laeuft' +`; + +function clampNumber(value: number, min: number, max: number) { + if (!Number.isFinite(value)) { + return min; + } + return Math.max(min, Math.min(max, value)); +} + +function formatRate(bytesPerSecond: number) { + if (bytesPerSecond >= 1024 * 1024) { + return `${(bytesPerSecond / 1024 / 1024).toFixed(1)} MiB/s`; + } + if (bytesPerSecond >= 1024) { + return `${(bytesPerSecond / 1024).toFixed(1)} KiB/s`; + } + return `${Math.round(bytesPerSecond)} B/s`; +} + +function addSample(nextStatus: ServerStatus) { + const timestamp = Date.now(); + const previous = history[history.length - 1]; + const seconds = previous ? Math.max(1, (timestamp - previous.timestamp) / 1000) : REFRESH_INTERVAL_SECONDS; + const networkRxRate = previous ? Math.max(0, (nextStatus.networkRxBytes - previous.rxBytes) / seconds) : 0; + const networkTxRate = previous ? Math.max(0, (nextStatus.networkTxBytes - previous.txBytes) / seconds) : 0; + + history = [ + ...history, + { + time: nextStatus.updated, + load1: nextStatus.load1, + cpuPercent: nextStatus.cpuPercent, + memoryPercent: nextStatus.memoryPercent, + rootDiskPercent: nextStatus.rootDiskPercent, + userDiskPercent: nextStatus.userDiskPercent, + dockerRunning: nextStatus.dockerRunning, + networkRxRate, + networkTxRate, + rxBytes: nextStatus.networkRxBytes, + txBytes: nextStatus.networkTxBytes, + timestamp, + }, + ].slice(-HISTORY_LIMIT); +} + +function sparkline(values: number[], maxValue = 100) { + const blocks = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]; + if (!values.length) { + return "·".repeat(HISTORY_LIMIT); + } + + return values + .map(value => { + const ratio = clampNumber(value / Math.max(maxValue, 1), 0, 1); + return blocks[Math.round(ratio * (blocks.length - 1))]; + }) + .join(""); +} + +function secondsUntilRefresh() { + if (!authed || !nextRefreshAt) { + return REFRESH_INTERVAL_SECONDS; + } + return Math.max(0, Math.ceil((nextRefreshAt - Date.now()) / 1000)); +} + +function scheduleNextRefresh() { + nextRefreshAt = Date.now() + REFRESH_INTERVAL_SECONDS * 1000; +} + +function windowLayout() { + const geometry = app.monitors[0]?.get_geometry(); + const screenWidth = geometry?.width || 1280; + const screenHeight = geometry?.height || 720; + const width = Math.round(clampNumber(screenWidth - WINDOW_EDGE_GAP * 2, 760, 1180)); + const height = Math.round(clampNumber(screenHeight - WINDOW_MARGIN_TOP - WINDOW_EDGE_GAP, 420, 720)); + + return { + width, + height, + pageWidth: Math.max(420, width - 290), + pageHeight: Math.max(260, height - 92), + }; +} + +function setBusy(value: boolean) { + busy = value; + app.get_window("homelab-control")?.queue_draw(); +} + +function applyStatus(output: string) { + const nextStatus = parseStatus(output); + status = nextStatus; + addSample(nextStatus); + authed = true; +} + +function refresh(command = statusCommand, showBusy = true) { + if (!password) { + errorMessage = "Passwort fehlt."; + return; + } + + if (showBusy) { + setBusy(true); + } + errorMessage = ""; + scheduleNextRefresh(); + + runRemote(command) + .then(output => { + applyStatus(output); + }) + .catch(error => { + console.error(error); + errorMessage = "Verbindung fehlgeschlagen. Passwort, Netzwerk oder SSH pruefen."; + }) + .finally(() => { + if (showBusy) { + setBusy(false); + } + rebuild(); + }); +} + +function login() { + if (!password) { + errorMessage = "Passwort fehlt."; + rebuild(); + return; + } + + setBusy(true); + errorMessage = ""; + + runRemote(quickStatusCommand) + .then(output => { + applyStatus(output); + scheduleNextRefresh(); + setBusy(false); + rebuild(); + GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { + refresh(statusCommand, false); + return GLib.SOURCE_REMOVE; + }); + }) + .catch(error => { + console.error(error); + errorMessage = "Verbindung fehlgeschlagen. Passwort, Netzwerk oder SSH pruefen."; + setBusy(false); + rebuild(); + }); +} + +function runAction(label: string, command: string) { + armedAction = ""; + setBusy(true); + runRemote(command) + .then(() => { + notify(`${label} ausgefuehrt.`); + setBusy(false); + refresh(); + }) + .catch(error => { + console.error(error); + errorMessage = `${label} fehlgeschlagen.`; + setBusy(false); + rebuild(); + }); +} + +function loadContainerLogs(container: string) { + setBusy(true); + runRemote(`docker logs --tail 160 ${shQuote(container)} 2>&1`) + .then(output => { + logTitle = `Logs: ${container}`; + logOutput = output.trim() || "Keine Logs."; + activePage = "logs"; + }) + .catch(error => { + console.error(error); + logTitle = `Logs: ${container}`; + logOutput = "Logs konnten nicht geladen werden."; + activePage = "logs"; + }) + .finally(() => { + setBusy(false); + rebuild(); + }); +} + +function openContainerShell(container: string) { + const remote = `docker exec -it ${shQuote(container)} sh`; + const ssh = sshCommand(remote); + const launch = `kitty bash -lc ${shQuote(ssh)} || alacritty -e bash -lc ${shQuote(ssh)} || foot bash -lc ${shQuote(ssh)} || xterm -e bash -lc ${shQuote(ssh)}`; + execAsync(["bash", "-lc", launch]).catch(error => { + console.error(error); + notify("Kein Terminal fuer Exec Shell gefunden."); + }); +} + +function openUrl(url: string) { + execAsync(["xdg-open", url]).catch(console.error); +} + +function restartServiceCommand(query: string) { + if (!query) { + return "echo 'No container mapping configured for this service'"; + } + + return `id=$(docker ps -aq --filter name=${shQuote(query)} | head -1); [ -n "$id" ] && docker restart "$id" || echo 'Container not found'`; +} + +function Card({ title, value, className = "" }: { title: string; value: string; className?: string }) { + return ( + + + ); +} + +function StatCard({ title, value, percent }: { title: string; value: string; percent: number }) { + const safePercent = clampNumber(percent, 0, 100); + + return ( + + + + + + + + ); +} + +function ChartCard({ title, value, values, maxValue = 100, accent = "" }: { title: string; value: string; values: number[]; maxValue?: number; accent?: string }) { + const current = values.length ? values[values.length - 1] : 0; + + return ( + + + + + ); +} + +function startRefreshTimer() { + if (timerStarted) { + return; + } + + timerStarted = true; + GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { + if (authed && !busy && Date.now() >= nextRefreshAt) { + refresh(); + } else if (authed) { + rebuild(); + } + + return GLib.SOURCE_CONTINUE; + }); +} + +function HomelabWindow() { + return ( + + { + if (keyval === ESC_KEYVAL) { + app.quit(); + return true; + } + return false; + }} /> + + {authed ? : } + + + ); +} + +function rebuild() { + const win = app.get_window("homelab-control"); + if (!win) { + return; + } + win.set_child({authed ? : } as Gtk.Widget); +} + +app.start({ + css, + instanceName: "homelab-control", + main() { + startRefreshTimer(); + HomelabWindow(); + }, +}); diff --git a/dotfiles/hypr/ags/package-manager.css b/dotfiles/hypr/ags/package-manager.css new file mode 100644 index 0000000..c90d869 --- /dev/null +++ b/dotfiles/hypr/ags/package-manager.css @@ -0,0 +1,180 @@ +* { + all: unset; + font-family: "JetBrainsMono Nerd Font", "Noto Sans", sans-serif; + font-size: 13px; +} + +.package-window { + background: transparent; +} + +.package-panel { + min-width: 940px; + min-height: 620px; + border: 1px solid rgba(205, 214, 244, 0.16); + border-radius: 16px; + background: rgba(20, 20, 30, 0.97); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45); + color: #cdd6f4; + padding: 18px; +} + +.header { + min-height: 42px; +} + +.title { + color: #00ff9c; + font-size: 21px; + font-weight: 800; +} + +.subtitle, +.muted, +.package-meta { + color: #a6adc8; + font-size: 12px; +} + +entry { + padding: 10px 12px; + border: 1px solid rgba(205, 214, 244, 0.14); + border-radius: 10px; + background: rgba(40, 40, 55, 0.82); + color: #cdd6f4; +} + +entry:focus { + border-color: rgba(0, 255, 156, 0.78); +} + +.button, +.tool-button, +.icon-button { + padding: 8px 10px; + border-radius: 8px; + background: rgba(40, 40, 55, 0.82); + color: #cdd6f4; +} + +.button:hover, +.button:focus, +.tool-button:hover, +.tool-button:focus, +.icon-button:hover, +.icon-button:focus { + background: rgba(0, 255, 156, 0.22); +} + +.primary, +.tool-button.active { + background: rgba(0, 255, 156, 0.24); + color: #00ff9c; +} + +.close { + color: #f38ba8; +} + +.icon-button { + min-width: 34px; + min-height: 34px; + padding: 0; +} + +.toolbar { + min-height: 38px; +} + +.status-strip { + min-height: 30px; + padding: 7px 10px; + border: 1px solid rgba(205, 214, 244, 0.10); + border-radius: 8px; + background: rgba(40, 40, 55, 0.48); +} + +.operation-panel { + min-height: 220px; + padding: 10px; + border: 1px solid rgba(205, 214, 244, 0.12); + border-radius: 8px; + background: rgba(10, 10, 16, 0.58); +} + +.operation-header, +.operation-input-row { + min-height: 34px; +} + +.operation-title { + color: #00ff9c; + font-weight: 800; +} + +.operation-scroll { + min-height: 142px; + border: 1px solid rgba(205, 214, 244, 0.10); + border-radius: 8px; + background: rgba(4, 4, 8, 0.72); +} + +.operation-output { + padding: 10px; + color: #cdd6f4; + font-family: "JetBrainsMono Nerd Font", monospace; + font-size: 12px; +} + +.danger { + color: #f38ba8; +} + +.results-scroll { + min-height: 420px; +} + +.package-row { + min-height: 76px; + padding: 10px; + border: 1px solid rgba(205, 214, 244, 0.10); + border-radius: 8px; + background: rgba(40, 40, 55, 0.62); +} + +.package-row:hover { + border-color: rgba(0, 255, 156, 0.35); + background: rgba(40, 40, 55, 0.82); +} + +.package-name { + color: #cdd6f4; + font-weight: 800; +} + +.package-desc { + color: #cdd6f4; +} + +.repo-pill, +.installed-pill { + padding: 4px 8px; + border-radius: 999px; + font-size: 11px; + background: rgba(116, 199, 236, 0.16); + color: #89dceb; +} + +.installed-pill { + background: rgba(0, 255, 156, 0.16); + color: #00ff9c; +} + +.empty { + min-height: 180px; + color: #a6adc8; +} + +.error { + color: #f38ba8; +} diff --git a/dotfiles/hypr/ags/package-manager.tsx b/dotfiles/hypr/ags/package-manager.tsx new file mode 100644 index 0000000..f8b6b37 --- /dev/null +++ b/dotfiles/hypr/ags/package-manager.tsx @@ -0,0 +1,456 @@ +import app from "ags/gtk4/app"; +import { Astal, Gtk } from "ags/gtk4"; +import { execAsync, subprocess, Process } from "ags/process"; +import GLib from "gi://GLib"; +import css from "./package-manager.css"; + +const WINDOW_MARGIN_TOP = 48; +const ESC_KEYVAL = 65307; +const RUNNER = `${GLib.get_home_dir()}/.config/hypr/Scripts/ags-package-runner.py`; + +type Helper = "pacman" | "paru"; + +type PackageResult = { + repo: string; + name: string; + version: string; + installed: boolean; + description: string; +}; + +let helper: Helper = "paru"; +let query = ""; +let results: PackageResult[] = []; +let busy = false; +let statusMessage = "Suchbegriff eingeben und Enter druecken."; +let errorMessage = ""; +let activeProcess: Process | null = null; +let operationTitle = ""; +let operationOutput = ""; +let operationInput = ""; +let secretInput = false; +let lastOutput = ""; + +function shQuote(value: string) { + return `'${value.replace(/'/g, `'\\''`)}'`; +} + +function notify(message: string) { + execAsync(["notify-send", "Pakete", message]).catch(console.error); +} + +function runShell(command: string) { + return execAsync(["bash", "-lc", command]); +} + +function setBusy(value: boolean) { + busy = value; + rebuild(); +} + +function appendOperationOutput(data: string) { + operationOutput = `${operationOutput}${data}`.slice(-30000); + lastOutput = data; + secretInput = /(\[sudo\].*password|passwort|password).*:/i.test(data); + rebuild(); +} + +function commandLabel(command: string[]) { + return command.map(shQuote).join(" "); +} + +function helperAvailable(nextHelper: Helper) { + return runShell(`command -v ${nextHelper} >/dev/null 2>&1`); +} + +function parseSearch(output: string) { + const parsed: PackageResult[] = []; + const lines = output.split("\n"); + + for (let index = 0; index < lines.length; index += 1) { + const header = lines[index]; + const match = header.match(/^([^/\s]+)\/([^\s]+)\s+([^\s]+)(?:\s+\[(installed[^\]]*)\])?/); + + if (!match) { + continue; + } + + const descriptionLines: string[] = []; + let next = index + 1; + while (next < lines.length && /^\s+/.test(lines[next])) { + descriptionLines.push(lines[next].trim()); + next += 1; + } + + parsed.push({ + repo: match[1], + name: match[2], + version: match[3], + installed: Boolean(match[4]), + description: descriptionLines.join(" ") || "Keine Beschreibung.", + }); + + index = next - 1; + } + + return parsed.slice(0, 80); +} + +function searchPackages() { + const term = query.trim(); + if (!term) { + results = []; + statusMessage = "Suchbegriff fehlt."; + rebuild(); + return; + } + + setBusy(true); + errorMessage = ""; + statusMessage = `Suche mit ${helper} nach "${term}"...`; + + helperAvailable(helper) + .then(() => runShell(`${helper} -Ss ${shQuote(term)} 2>/dev/null || true`)) + .then(output => { + results = parseSearch(output); + statusMessage = results.length + ? `${results.length} Treffer fuer "${term}" mit ${helper}.` + : `Keine Treffer fuer "${term}".`; + }) + .catch(error => { + console.error(error); + results = []; + errorMessage = `${helper} ist nicht verfuegbar oder die Suche ist fehlgeschlagen.`; + statusMessage = "Suche fehlgeschlagen."; + }) + .finally(() => { + setBusy(false); + }); +} + +function startOperation(title: string, command: string[]) { + if (activeProcess) { + statusMessage = "Es laeuft bereits ein Paketprozess."; + rebuild(); + return; + } + + operationTitle = title; + operationOutput = `$ ${commandLabel(command)}\n\n`; + operationInput = ""; + errorMessage = ""; + statusMessage = `${title} laeuft in AGS.`; + busy = true; + + const process = subprocess( + [RUNNER, "--", ...command], + line => { + try { + const event = JSON.parse(line); + if (event.type === "out") { + appendOperationOutput(event.data || ""); + } else if (event.type === "exit") { + const suffix = event.signaled ? `Signal ${event.code}` : `Exit ${event.code}`; + appendOperationOutput(`\n[${suffix}]\n`); + } + } catch { + appendOperationOutput(`${line}\n`); + } + }, + line => appendOperationOutput(`${line}\n`), + ); + + activeProcess = process; + process.connect("exit", (_, code: number, signaled: boolean) => { + activeProcess = null; + busy = false; + secretInput = false; + statusMessage = signaled + ? `${title} wurde beendet.` + : code === 0 + ? `${title} abgeschlossen.` + : `${title} fehlgeschlagen (Exit ${code}).`; + notify(statusMessage); + rebuild(); + }); + + rebuild(); +} + +function sendOperationInput(value = operationInput, allowEmpty = false) { + if (!activeProcess || (!allowEmpty && value.length === 0)) { + return; + } + + activeProcess.writeAsync(`${JSON.stringify({ type: "input", data: `${value}\n` })}\n`).catch(error => { + console.error(error); + errorMessage = "Eingabe konnte nicht gesendet werden."; + rebuild(); + }); + operationInput = ""; + secretInput = false; + rebuild(); +} + +function yesAnswer() { + return /[\[(][YyJj]\/[Nn][\])]/.test(lastOutput) ? "" : "y"; +} + +function sendYesAnswer() { + const answer = yesAnswer(); + appendOperationOutput(answer ? `\n> ${answer}\n` : "\n> Enter\n"); + sendOperationInput(answer, true); +} + +function sendNoAnswer() { + appendOperationOutput("\n> n\n"); + sendOperationInput("n"); +} + +function cancelOperation() { + if (!activeProcess) { + return; + } + + activeProcess.writeAsync(`${JSON.stringify({ type: "signal", signal: 15 })}\n`).catch(console.error); + statusMessage = "Abbruch angefordert."; + rebuild(); +} + +function installPackage(pkg: PackageResult) { + const command = helper === "pacman" + ? ["sudo", "pacman", "-S", pkg.name] + : ["paru", "-S", pkg.name]; + + notify(`Installation gestartet: ${pkg.name}`); + startOperation(`Installation: ${pkg.name}`, command); +} + +function updateSystem() { + const command = helper === "pacman" + ? ["sudo", "pacman", "-Syu"] + : ["paru", "-Syu"]; + + notify(`Update gestartet mit ${helper}.`); + startOperation(`Systemupdate mit ${helper}`, command); +} + +function HelperButton({ id, label }: { id: Helper; label: string }) { + return ( + + + + { + query = entry.get_text(); + }} + onActivate={entry => { + query = entry.get_text(); + searchPackages(); + }} + /> + + + + { + query = entry.get_text(); + }} + onActivate={entry => { + query = entry.get_text(); + searchPackages(); + }} + /> + + ); + + if (item.type === "theme") { + return applyButton; + } + + return ( + + {applyButton} + + + ); +} + +function SwitcherWindow(mode: string) { + const isTheme = mode === "theme" || mode === "themes"; + const items = isTheme ? loadThemes() : loadWallpapers(); + const title = isTheme ? "Theme wechseln" : "Wallpaper wechseln"; + const empty = isTheme ? `Keine Themes in ${THEME_DIR}` : `Keine Bilder in ${WALLPAPER_DIR}`; + + return ( + + + + + {items.length > 0 + ? ( + + + {items.map(item => )} + + + ) + : ( + + + )} + + + ); +} + +app.start({ + css: themeCss(activeTheme()), + instanceName: "hypr-switcher", + main(mode = "wallpaper") { + SwitcherWindow(mode); + }, +}); diff --git a/dotfiles/hypr/ags/widget-panel.css b/dotfiles/hypr/ags/widget-panel.css new file mode 100644 index 0000000..f309c17 --- /dev/null +++ b/dotfiles/hypr/ags/widget-panel.css @@ -0,0 +1,138 @@ +* { + all: unset; + font-family: "JetBrainsMono Nerd Font", "Noto Sans", sans-serif; + font-size: 14px; +} + +.widget-window { + background: transparent; +} + +.panel-scroll { + border: 1px solid alpha(@ags_fg, 0.16); + border-radius: 16px; + background: @ags_bg; + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.48); +} + +.panel { + padding: 16px; + color: @ags_fg; +} + +.header { + min-height: 42px; +} + +.title { + font-size: 22px; + font-weight: 800; +} + +.subtitle, +.card-subtitle { + color: @ags_muted; + font-size: 12px; +} + +.icon-button { + min-width: 36px; + min-height: 36px; + border-radius: 8px; + color: @ags_fg; + background: alpha(@ags_panel, 0.64); +} + +.icon-button:hover, +.icon-button:focus { + background: alpha(@ags_accent, 0.28); +} + +.close { + color: @ags_accent; +} + +.card { + padding: 14px; + border: 1px solid alpha(@ags_fg, 0.10); + border-radius: 12px; + background: alpha(@ags_panel, 0.62); +} + +.card-title { + font-size: 16px; + font-weight: 800; +} + +.metric { + min-height: 45px; +} + +.metric-name { + color: @ags_fg; + font-weight: 700; +} + +.metric-value { + color: @ags_accent; + font-weight: 800; +} + +.progress { + min-height: 8px; + border-radius: 999px; + background: alpha(@ags_bg_soft, 0.70); +} + +.progress-fill { + min-height: 8px; + border-radius: 999px; + background: @ags_accent; +} + +.meta-row { + color: @ags_muted; + font-size: 12px; +} + +.sparkline { + color: @ags_accent_2; + font-size: 18px; +} + +.calendar-grid { + min-width: 280px; +} + +.weekday, +.day { + min-width: 34px; + min-height: 30px; + border-radius: 8px; +} + +.weekday { + color: @ags_muted; + font-size: 12px; + font-weight: 800; +} + +.day { + background: alpha(@ags_bg_soft, 0.42); +} + +.day.today { + color: @ags_bg; + background: @ags_accent; + font-weight: 900; +} + +.day.muted { + background: transparent; +} + +.weather-main { + font-size: 19px; + font-weight: 800; + color: @ags_accent_2; +} diff --git a/dotfiles/hypr/ags/widget-panel.tsx b/dotfiles/hypr/ags/widget-panel.tsx new file mode 100644 index 0000000..6b819f6 --- /dev/null +++ b/dotfiles/hypr/ags/widget-panel.tsx @@ -0,0 +1,435 @@ +import app from "ags/gtk4/app"; +import { Astal, Gtk } from "ags/gtk4"; +import { readFile } from "ags/file"; +import { execAsync } from "ags/process"; +import { createRoot } from "gnim"; +import css from "./widget-panel.css"; +import GLib from "gi://GLib"; + +const HYPR_DIR = GLib.getenv("HYPR_DIR") || `${GLib.get_home_dir()}/.config/hypr`; +const THEME_DIR = `${HYPR_DIR}/Themes`; +const CURRENT_WALLPAPER = `${HYPR_DIR}/current-wallpaper`; +const REFRESH_SECONDS = 2; +const WEATHER_SECONDS = 20 * 60; +const HISTORY_LIMIT = 22; +const ESC_KEYVAL = 65307; +const START_HIDDEN = GLib.getenv("WIDGET_PANEL_START_HIDDEN") === "1"; + +type UiTheme = { + accent: string; + accent2: string; + background: string; + backgroundSoft: string; + foreground: string; + muted: string; + panelHex: string; +}; + +type SystemSnapshot = { + cpu: number; + memory: number; + disk: number; + temp: string; + uptime: string; +}; + +let lastCpuTotal = 0; +let lastCpuIdle = 0; +let system: SystemSnapshot = { + cpu: 0, + memory: 0, + disk: 0, + temp: "n/a", + uptime: "n/a", +}; +let cpuHistory: number[] = []; +let weather = "Wetter wird geladen..."; +let weatherUpdated = 0; +let timerStarted = false; +let disposeRebuild: (() => void) | null = null; +let panelWindow: Gtk.Window | null = null; + +function readText(path: string) { + try { + return readFile(path); + } catch { + return ""; + } +} + +function listFiles(dir: string, predicate: (path: string, name: string) => boolean) { + try { + const directory = GLib.Dir.open(dir, 0); + const files: string[] = []; + let name = directory.read_name(); + + while (name !== null) { + const path = `${dir}/${name}`; + if (predicate(path, name)) { + files.push(path); + } + name = directory.read_name(); + } + + return files.sort((a, b) => a.localeCompare(b)); + } catch { + return []; + } +} + +function shellValue(contents: string, key: string) { + const regex = new RegExp(`^${key}=(["']?)(.*?)\\1$`, "m"); + return contents.match(regex)?.[2] || ""; +} + +function currentWallpaper() { + return readText(CURRENT_WALLPAPER).trim(); +} + +function activeTheme(): UiTheme { + const fallback = { + accent: "#f38ba8", + accent2: "#cba6f7", + background: "rgba(24, 20, 31, 0.96)", + backgroundSoft: "rgba(49, 50, 68, 0.82)", + foreground: "#f5e0dc", + muted: "#cdd6f4", + panelHex: "#313244", + }; + + const activeWallpaper = currentWallpaper(); + const themeFile = listFiles(THEME_DIR, (_path, name) => name.endsWith(".theme")) + .find(path => shellValue(readText(path), "WALLPAPER") === activeWallpaper); + + if (!themeFile) { + return fallback; + } + + const contents = readText(themeFile); + return { + accent: shellValue(contents, "ACCENT") || fallback.accent, + accent2: shellValue(contents, "ACCENT_2") || fallback.accent2, + background: shellValue(contents, "BACKGROUND") || fallback.background, + backgroundSoft: shellValue(contents, "BACKGROUND_SOFT") || fallback.backgroundSoft, + foreground: shellValue(contents, "FOREGROUND") || fallback.foreground, + muted: shellValue(contents, "MUTED") || fallback.muted, + panelHex: shellValue(contents, "PANEL_HEX") || fallback.panelHex, + }; +} + +function themeCss(theme: UiTheme) { + return [ + `@define-color ags_accent ${theme.accent};`, + `@define-color ags_accent_2 ${theme.accent2};`, + `@define-color ags_bg ${theme.background};`, + `@define-color ags_bg_soft ${theme.backgroundSoft};`, + `@define-color ags_fg ${theme.foreground};`, + `@define-color ags_muted ${theme.muted};`, + `@define-color ags_panel ${theme.panelHex};`, + css, + ].join("\n"); +} + +function clamp(value: number, min = 0, max = 100) { + return Math.max(min, Math.min(max, value)); +} + +function percentLabel(value: number) { + return `${Math.round(clamp(value))}%`; +} + +function updateSystem() { + const cpuLine = readText("/proc/stat").split("\n")[0] || ""; + const cpuValues = cpuLine.trim().split(/\s+/).slice(1).map(value => Number(value) || 0); + const idle = (cpuValues[3] || 0) + (cpuValues[4] || 0); + const total = cpuValues.reduce((sum, value) => sum + value, 0); + const totalDelta = total - lastCpuTotal; + const idleDelta = idle - lastCpuIdle; + + if (lastCpuTotal > 0 && totalDelta > 0) { + system.cpu = clamp(((totalDelta - idleDelta) / totalDelta) * 100); + } + + lastCpuTotal = total; + lastCpuIdle = idle; + + const meminfo = readText("/proc/meminfo"); + const memTotal = Number(meminfo.match(/^MemTotal:\s+(\d+)/m)?.[1] || 0); + const memAvailable = Number(meminfo.match(/^MemAvailable:\s+(\d+)/m)?.[1] || 0); + if (memTotal > 0) { + system.memory = clamp(((memTotal - memAvailable) / memTotal) * 100); + } + + const uptimeSeconds = Number(readText("/proc/uptime").split(" ")[0] || 0); + const hours = Math.floor(uptimeSeconds / 3600); + const minutes = Math.floor((uptimeSeconds % 3600) / 60); + system.uptime = `${hours}h ${minutes}m`; + + const temps = listFiles("/sys/class/thermal", (_path, name) => name.startsWith("thermal_zone")) + .map(path => Number(readText(`${path}/temp`).trim()) / 1000) + .filter(value => Number.isFinite(value) && value > 0); + system.temp = temps.length ? `${Math.round(Math.max(...temps))} C` : "n/a"; + + execAsync(["bash", "-lc", "df -P / | awk 'NR==2 {gsub(/%/, \"\", $5); print $5}'"]) + .then(output => { + system.disk = clamp(Number(output.trim()) || system.disk); + }) + .catch(console.error); + + cpuHistory = [...cpuHistory, system.cpu].slice(-HISTORY_LIMIT); +} + +function updateWeather(force = false) { + if (!force && Date.now() - weatherUpdated < WEATHER_SECONDS * 1000) { + return; + } + + weatherUpdated = Date.now(); + execAsync(["bash", "-lc", "curl -fsS --max-time 5 'https://wttr.in/?format=%l:+%c+%t+%w' 2>/dev/null || true"]) + .then(output => { + weather = output.trim() || "Wetter nicht erreichbar"; + }) + .catch(() => { + weather = "Wetter nicht erreichbar"; + }); +} + +function sparkline(values: number[]) { + const blocks = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]; + if (!values.length) { + return "·".repeat(HISTORY_LIMIT); + } + + return values + .map(value => blocks[Math.round((clamp(value) / 100) * (blocks.length - 1))]) + .join(""); +} + +function monthDays() { + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth(); + const first = new Date(year, month, 1); + const last = new Date(year, month + 1, 0); + const startOffset = (first.getDay() + 6) % 7; + const cells: { day: string; today: boolean; muted: boolean }[] = []; + + for (let i = 0; i < startOffset; i += 1) { + cells.push({ day: "", today: false, muted: true }); + } + + for (let day = 1; day <= last.getDate(); day += 1) { + cells.push({ day: String(day), today: day === now.getDate(), muted: false }); + } + + while (cells.length % 7 !== 0) { + cells.push({ day: "", today: false, muted: true }); + } + + return cells; +} + +function monthTitle() { + return new Intl.DateTimeFormat("de-DE", { month: "long", year: "numeric" }).format(new Date()); +} + +function Metric({ icon, label, value }: { icon: string; label: string; value: number }) { + return ( + + + + + + + + ); +} + +function SystemMonitor() { + return ( + + + ); +} + +function Calendar() { + const cells = monthDays(); + const weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; + + return ( + + + ); +} + +function Weather() { + return ( + + + + + ); +} + +function hidePanel() { + const win = panelWindow || app.get_window("widget-panel"); + win?.set_visible(false); +} + +function PanelContent() { + return ( + + + + + + + + + + + ); +} + +function panelLayout() { + const geometry = app.monitors[0]?.get_geometry(); + const screenWidth = geometry?.width || 1280; + const screenHeight = geometry?.height || 720; + + return { + width: Math.round(clamp(screenWidth * 0.28, 360, 460)), + height: Math.round(clamp(screenHeight - 96, 560, 900)), + }; +} + +function WidgetPanelWindow() { + const layout = panelLayout(); + + return ( + + { + if (keyval === ESC_KEYVAL) { + hidePanel(); + return true; + } + return false; + }} /> + + + + + + + ); +} + +function rebuild() { + const win = app.get_window("widget-panel"); + if (!win) { + return; + } + + disposeRebuild?.(); + const layout = panelLayout(); + createRoot(dispose => { + disposeRebuild = dispose; + win.set_child( + + + + + as Gtk.Widget, + ); + }); +} + +function startTimer() { + if (timerStarted) { + return; + } + + timerStarted = true; + updateSystem(); + updateWeather(true); + + GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, REFRESH_SECONDS, () => { + updateSystem(); + updateWeather(); + rebuild(); + return GLib.SOURCE_CONTINUE; + }); +} + +app.start({ + instanceName: "widget-panel", + css: themeCss(activeTheme()), + main() { + panelWindow = WidgetPanelWindow() as Gtk.Window; + app.add_window(panelWindow); + if (!START_HIDDEN) { + panelWindow.present(); + } + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + startTimer(); + rebuild(); + return GLib.SOURCE_REMOVE; + }); + }, +}); diff --git a/dotfiles/hypr/current-theme.conf b/dotfiles/hypr/current-theme.conf new file mode 100644 index 0000000..43dcf34 --- /dev/null +++ b/dotfiles/hypr/current-theme.conf @@ -0,0 +1,5 @@ +# Generated by ~/.config/hypr/Scripts/theme-menu.sh +general { + col.active_border = rgba(00ff9cee) rgba(00cc88ee) 45deg + col.inactive_border = rgba(2a2a2aaa) +} diff --git a/dotfiles/hypr/current-wallpaper b/dotfiles/hypr/current-wallpaper new file mode 100644 index 0000000..4cdb615 --- /dev/null +++ b/dotfiles/hypr/current-wallpaper @@ -0,0 +1 @@ +/home/pascal/Bilder/Wallpaper/forest.jpg diff --git a/dotfiles/hypr/hyprland.conf b/dotfiles/hypr/hyprland.conf new file mode 100644 index 0000000..3edd8af --- /dev/null +++ b/dotfiles/hypr/hyprland.conf @@ -0,0 +1,310 @@ +# ####################################################################################### +# AUTOGENERATED HYPRLAND CONFIG. +# EDIT THIS CONFIG ACCORDING TO THE WIKI INSTRUCTIONS. +# ####################################################################################### + + + +# This is an example Hyprland config file. +# Refer to the wiki for more information. +# https://wiki.hypr.land/Configuring/ + +# Please note not all available settings / options are set here. +# For a full list, see the wiki + +# You can split this configuration into multiple files +# Create your files separately and then link them to this file like this: +# source = ~/.config/hypr/myColors.conf + + +################ +### MONITORS ### +################ + +# See https://wiki.hypr.land/Configuring/Monitors/ +monitor=,preferred,auto,1 + + +################### +### MY PROGRAMS ### +################### + +# See https://wiki.hypr.land/Configuring/Keywords/ + +# Set programs that you use +$terminal = kitty +$fileManager = nautilus +$menu = wofi + + +################# +### AUTOSTART ### +################# + +# Autostart necessary processes (like notifications daemons, status bars, etc.) +# Or execute your favorite apps at launch like this: + +exec-once = sh -c 'if command -v awww-daemon >/dev/null 2>&1; then awww-daemon; elif command -v swww-daemon >/dev/null 2>&1; then swww-daemon; else hyprpaper; fi' +exec-once = waybar +exec-once = swaync +exec-once = env WIDGET_PANEL_START_HIDDEN=1 ~/.config/hypr/Scripts/widget-panel.sh + +############################# +### ENVIRONMENT VARIABLES ### +############################# + +# See https://wiki.hypr.land/Configuring/Environment-variables/ + +env = XCURSOR_SIZE,24 +env = HYPRCURSOR_SIZE,24 +env = QT_QPA_PLATFORMTHEME,qt6ct +env = QT_STYLE_OVERRIDE,Fusion + +# https://wiki.hypr.land/Configuring/Variables/#general +general { + gaps_in = 6 + gaps_out = 25 + + border_size = 2 + + col.active_border = rgba(00ff9cee) rgba(00cc88ee) 45deg + col.inactive_border = rgba(2a2a2aaa) + + resize_on_border = false + allow_tearing = false + + layout = dwindle +} + +source = ~/.config/hypr/current-theme.conf + +# https://wiki.hypr.land/Configuring/Variables/#decoration +decoration { + rounding = 14 + rounding_power = 3 + + active_opacity = 0.90 + inactive_opacity = 0.75 + + shadow { + enabled = true + range = 20 + render_power = 4 + color = rgba(000000aa) + } + + blur { + enabled = true + size = 6 + passes = 2 + + vibrancy = 0.25 + vibrancy_darkness = 0.3 + } +} + +# https://wiki.hypr.land/Configuring/Variables/#animations +animations { + enabled = yes + + # Curves + bezier = smoothOut, 0.22, 1, 0.36, 1 + bezier = smoothIn, 0.64, 0, 0.78, 0 + bezier = smoothInOut, 0.83, 0, 0.17, 1 + bezier = soft, 0.25, 0.1, 0.25, 1 + bezier = quick, 0.3, 0, 0.1, 1 + + # Animations + animation = global, 1, 8, soft + + animation = border, 1, 5, smoothOut + animation = fade, 1, 5, soft + + animation = windows, 1, 6, smoothOut + animation = windowsIn, 1, 6, smoothOut, popin 85% + animation = windowsOut, 1, 5, smoothIn, popin 85% + + animation = layers, 1, 5, smoothOut + animation = layersIn, 1, 5, smoothOut, fade + animation = layersOut, 1, 4, smoothIn, fade + + animation = fadeIn, 1, 4, soft + animation = fadeOut, 1, 4, soft + animation = fadeLayersIn, 1, 4, soft + animation = fadeLayersOut, 1, 4, soft + + animation = workspaces, 1, 5, smoothInOut, slide + animation = workspacesIn, 1, 5, smoothInOut, slide + animation = workspacesOut, 1, 5, smoothInOut, slide + + animation = zoomFactor, 1, 6, quick +} + +# See https://wiki.hypr.land/Configuring/Dwindle-Layout/ for more +dwindle { + pseudotile = true # Master switch for pseudotiling. Enabling is bound to mainMod + P in the keybinds section below + preserve_split = true # You probably want this +} + +# See https://wiki.hypr.land/Configuring/Master-Layout/ for more +master { + new_status = master +} + +# https://wiki.hypr.land/Configuring/Variables/#misc +misc { + force_default_wallpaper = -1 # Set to 0 or 1 to disable the anime mascot wallpapers + disable_hyprland_logo = false # If true disables the random hyprland logo / anime girl background. :( +} + + +############# +### INPUT ### +############# + +# https://wiki.hypr.land/Configuring/Variables/#input +input { + kb_layout = de + kb_variant = + kb_model = + kb_options = + kb_rules = + + follow_mouse = 1 + + sensitivity = 0 # -1.0 - 1.0, 0 means no modification. + + touchpad { + natural_scroll = false + } +} + +# See https://wiki.hypr.land/Configuring/Gestures +gesture = 3, horizontal, workspace + +# Example per-device config +# See https://wiki.hypr.land/Configuring/Keywords/#per-device-input-configs for more +device { + name = epic-mouse-v1 + sensitivity = -0.5 +} + + +################### +### KEYBINDINGS ### +################### + +# See https://wiki.hypr.land/Configuring/Keywords/ +$mainMod = SUPER # Sets "Windows" key as main modifier + +# Example binds, see https://wiki.hypr.land/Configuring/Binds/ for more +bind = $mainMod, T, exec, $terminal +bind = $mainMod, Q, killactive, +bind = $mainMod, M, exec, ~/.config/hypr/Scripts/main-menu.sh +bind = $mainMod, E, exec, $fileManager +bind = $mainMod, N, exec, swaync-client -t +bind = $mainMod, V, togglefloating, +bind = $mainMod, R, exec, $menu +bind = $mainMod, W, exec, ~/.config/hypr/Scripts/widget-panel.sh +bind = $mainMod SHIFT, W, exec, ~/.config/hypr/Scripts/ags-switcher.sh wallpaper +bind = $mainMod, P, exec, ~/.config/hypr/Scripts/power-menu.py +bind = $mainMod, L, exec, hyprlock +bind = $mainMod SHIFT, T, exec, ~/.config/hypr/Scripts/ags-switcher.sh theme +bind = $mainMod SHIFT, S, exec, ~/.config/hypr/Scripts/screenshot-menu.sh annotate-region +bind = $mainMod SHIFT, P, pseudo, # dwindle +bind = $mainMod, J, layoutmsg, togglesplit # dwindle +bind = $mainMod, SPACE, exec, ~/.config/hypr/Scripts/toggle-wofi.sh +# Move focus with mainMod + arrow keys +bind = $mainMod, left, movefocus, l +bind = $mainMod, right, movefocus, r +bind = $mainMod, up, movefocus, u +bind = $mainMod, down, movefocus, d + +# Switch existing workspaces with CTRL + mainMod + arrow keys +bind = CTRL $mainMod, left, workspace, e-1 +bind = CTRL $mainMod, right, workspace, e+1 + +# Switch workspaces with mainMod + [0-9] +bind = $mainMod, 1, workspace, 1 +bind = $mainMod, 2, workspace, 2 +bind = $mainMod, 3, workspace, 3 +bind = $mainMod, 4, workspace, 4 +bind = $mainMod, 5, workspace, 5 +bind = $mainMod, 6, workspace, 6 +bind = $mainMod, 7, workspace, 7 +bind = $mainMod, 8, workspace, 8 +bind = $mainMod, 9, workspace, 9 +bind = $mainMod, 0, workspace, 10 + +# Move active window to a workspace with mainMod + SHIFT + [0-9] +bind = $mainMod SHIFT, 1, movetoworkspace, 1 +bind = $mainMod SHIFT, 2, movetoworkspace, 2 +bind = $mainMod SHIFT, 3, movetoworkspace, 3 +bind = $mainMod SHIFT, 4, movetoworkspace, 4 +bind = $mainMod SHIFT, 5, movetoworkspace, 5 +bind = $mainMod SHIFT, 6, movetoworkspace, 6 +bind = $mainMod SHIFT, 7, movetoworkspace, 7 +bind = $mainMod SHIFT, 8, movetoworkspace, 8 +bind = $mainMod SHIFT, 9, movetoworkspace, 9 +bind = $mainMod SHIFT, 0, movetoworkspace, 10 + +# Example special workspace (scratchpad) +bind = $mainMod, S, togglespecialworkspace, magic +bind = CTRL $mainMod, S, movetoworkspace, special:magic + +# Scroll through existing workspaces with mainMod + scroll +bind = $mainMod, mouse_down, workspace, e+1 +bind = $mainMod, mouse_up, workspace, e-1 + +# Move/resize windows with mainMod + LMB/RMB and dragging +bindm = $mainMod, mouse:272, movewindow +bindm = $mainMod, mouse:273, resizewindow + +# Laptop multimedia keys for volume and LCD brightness +bindel = ,XF86AudioRaiseVolume, exec, wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+ +bindel = ,XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- +bindel = ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle +bindel = ,XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle +bindel = ,XF86MonBrightnessUp, exec, brightnessctl -e4 -n2 set 5%+ +bindel = ,XF86MonBrightnessDown, exec, brightnessctl -e4 -n2 set 5%- + +# Requires playerctl +bindl = , XF86AudioNext, exec, playerctl next +bindl = , XF86AudioPause, exec, playerctl play-pause +bindl = , XF86AudioPlay, exec, playerctl play-pause +bindl = , XF86AudioPrev, exec, playerctl previous + +############################## +### WINDOWS AND WORKSPACES ### +############################## + +windowrule { + # Ignore maximize requests from all apps. You'll probably like this. + name = suppress-maximize-events + match:class = .* + + suppress_event = maximize +} + +windowrule { + # Fix some dragging issues with XWayland + name = fix-xwayland-drags + match:class = ^$ + match:title = ^$ + match:xwayland = true + match:float = true + match:fullscreen = false + match:pin = false + + no_focus = true +} + +# Hyprland-run windowrule +windowrule { + name = move-hyprland-run + + match:class = hyprland-run + + move = 20 monitor_h-120 + float = yes +} diff --git a/dotfiles/hypr/hyprlock.conf b/dotfiles/hypr/hyprlock.conf new file mode 100644 index 0000000..cdf4cbe --- /dev/null +++ b/dotfiles/hypr/hyprlock.conf @@ -0,0 +1,98 @@ +# Generated by ~/.config/hypr/Scripts/theme-menu.sh + +auth { + pam:enabled = true + pam:module = hyprlock + + fingerprint:enabled = true + fingerprint:ready_message = Finger auflegen zum Entsperren + fingerprint:present_message = Fingerabdruck wird gelesen... + fingerprint:retry_delay = 250 +} + +general { + disable_loading_bar = true + hide_cursor = true + grace = 0 + ignore_empty_input = true +} + +background { + monitor = + path = /home/pascal/Bilder/Wallpaper/forest.jpg + blur_passes = 3 + blur_size = 8 + noise = 0.011 + contrast = 1.05 + brightness = 0.72 + vibrancy = 0.18 + vibrancy_darkness = 0.25 +} + +label { + monitor = + text = 󰌪 Forest Neon + color = rgba(0,204,136, 0.90) + font_size = 22 + font_family = JetBrainsMono Nerd Font + position = 0, 260 + halign = center + valign = center +} + +label { + monitor = + text = cmd[update:1000] date +"%H:%M" + color = rgba(0,255,156, 0.96) + font_size = 92 + font_family = JetBrainsMono Nerd Font + position = 0, 165 + halign = center + valign = center +} + +label { + monitor = + text = cmd[update:60000] date +"%A, %d. %B" + color = rgba(205,214,244, 0.82) + font_size = 22 + font_family = JetBrainsMono Nerd Font + position = 0, 92 + halign = center + valign = center +} + +input-field { + monitor = + size = 390, 62 + outline_thickness = 2 + dots_size = 0.24 + dots_spacing = 0.28 + dots_center = true + fade_on_empty = false + rounding = 16 + + outer_color = rgba(0,255,156, 0.72) + inner_color = rgba(20,20,30, 0.76) + font_color = rgba(205,214,244, 0.92) + placeholder_text = Passwort oder Fingerprint + + check_color = rgba(0,204,136, 0.88) + fail_color = rgba(243,139,168, 0.95) + capslock_color = rgba(249,226,175, 0.95) + + position = 0, -20 + halign = center + valign = center +} + +label { + monitor = + text = cmd[update:1000] echo " pascal@PDEV-Yoga" + color = rgba(204,204,204, 0.70) + font_size = 16 + font_family = JetBrainsMono Nerd Font + position = 0, -92 + halign = center + valign = center +} diff --git a/dotfiles/hypr/hyprpaper.conf b/dotfiles/hypr/hyprpaper.conf new file mode 100644 index 0000000..c06409c --- /dev/null +++ b/dotfiles/hypr/hyprpaper.conf @@ -0,0 +1,5 @@ +wallpaper { + monitor = + path = /home/pascal/Bilder/Wallpaper/forest.jpg + fit_mode = cover +} diff --git a/dotfiles/hypr/sddm-theme/pascal-hypr/Main.qml b/dotfiles/hypr/sddm-theme/pascal-hypr/Main.qml new file mode 100644 index 0000000..fdd22b9 --- /dev/null +++ b/dotfiles/hypr/sddm-theme/pascal-hypr/Main.qml @@ -0,0 +1,279 @@ +import QtQuick 2.0 +import SddmComponents 2.0 + +Rectangle { + id: root + width: 1920 + height: 1080 + color: config.backgroundColor || "#18141f" + + property int sessionIndex: session.index + property color accent: config.accent || "#f38ba8" + property color accent2: config.accent2 || "#cba6f7" + property color backgroundColor: config.backgroundColor || "#18141f" + property color panelColor: config.panelColor || "#313244" + property color foreground: config.foreground || "#f5e0dc" + property color muted: config.muted || "#cdd6f4" + property color selectedText: config.selectedText || "#11111b" + property string themeName: config.themeName || "Rose Night" + + TextConstants { id: textConstants } + + Connections { + target: sddm + + function onLoginSucceeded() { + message.text = textConstants.loginSucceeded + message.color = accent2 + } + + function onLoginFailed() { + password.text = "" + message.text = textConstants.loginFailed + message.color = "#f38ba8" + } + + function onInformationMessage(text) { + message.text = text + message.color = "#f38ba8" + } + } + + Background { + anchors.fill: parent + source: config.background || "" + fillMode: Image.PreserveAspectCrop + onStatusChanged: { + if (status === Image.Error) { + source = "" + } + } + } + + Rectangle { + anchors.fill: parent + color: "#000000" + opacity: 0.42 + } + + Rectangle { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0.10) } + GradientStop { position: 0.58; color: Qt.rgba(0, 0, 0, 0.38) } + GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.62) } + } + } + + Timer { + interval: 1000 + running: true + repeat: true + triggeredOnStart: true + onTriggered: { + clock.text = Qt.formatDateTime(new Date(), "HH:mm") + date.text = Qt.formatDateTime(new Date(), "dddd, dd. MMMM yyyy") + } + } + + Column { + anchors.left: parent.left + anchors.leftMargin: Math.max(42, parent.width * 0.055) + anchors.top: parent.top + anchors.topMargin: Math.max(40, parent.height * 0.055) + spacing: 2 + + Text { + id: clock + color: foreground + font.pixelSize: Math.max(64, root.height * 0.105) + font.weight: Font.Light + } + + Text { + id: date + color: muted + opacity: 0.88 + font.pixelSize: Math.max(16, root.height * 0.023) + } + } + + Rectangle { + id: panel + width: Math.min(430, root.width - 48) + height: 430 + anchors.right: parent.right + anchors.rightMargin: Math.max(28, parent.width * 0.075) + anchors.verticalCenter: parent.verticalCenter + radius: 18 + color: panelColor + opacity: 0.90 + border.color: accent + border.width: 1 + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: 4 + radius: 2 + gradient: Gradient { + GradientStop { position: 0.0; color: accent } + GradientStop { position: 1.0; color: accent2 } + } + } + + Column { + anchors.fill: parent + anchors.margins: 34 + spacing: 14 + + Text { + text: sddm.hostName + color: foreground + font.pixelSize: 26 + font.bold: true + elide: Text.ElideRight + width: parent.width + } + + Text { + text: themeName + color: accent + font.pixelSize: 14 + width: parent.width + opacity: 0.92 + } + + Item { width: 1; height: 10 } + + Text { + text: "Benutzer" + color: muted + font.pixelSize: 12 + opacity: 0.78 + } + + TextBox { + id: username + width: parent.width + height: 42 + text: userModel.lastUser + color: backgroundColor + borderColor: Qt.rgba(accent.r, accent.g, accent.b, 0.40) + focusColor: accent + hoverColor: accent2 + textColor: foreground + radius: 10 + font.pixelSize: 16 + KeyNavigation.tab: password + } + + Text { + text: "Passwort" + color: muted + font.pixelSize: 12 + opacity: 0.78 + } + + PasswordBox { + id: password + width: parent.width + height: 42 + color: backgroundColor + borderColor: Qt.rgba(accent.r, accent.g, accent.b, 0.40) + focusColor: accent + hoverColor: accent2 + textColor: foreground + radius: 10 + font.pixelSize: 16 + KeyNavigation.backtab: username + KeyNavigation.tab: loginButton + + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + sddm.login(username.text, password.text, sessionIndex) + event.accepted = true + } + } + } + + ComboBox { + id: session + width: parent.width + height: 36 + model: sessionModel + index: sessionModel.lastIndex + color: backgroundColor + borderColor: Qt.rgba(accent.r, accent.g, accent.b, 0.40) + focusColor: accent + hoverColor: accent2 + menuColor: panelColor + textColor: foreground + arrowColor: accent + arrowIcon: "angle-down.png" + KeyNavigation.backtab: password + KeyNavigation.tab: loginButton + } + + Button { + id: loginButton + width: parent.width + height: 44 + text: "Anmelden" + color: accent + activeColor: accent2 + pressedColor: accent2 + disabledColor: muted + borderColor: accent2 + textColor: selectedText + font.pixelSize: 15 + onClicked: sddm.login(username.text, password.text, sessionIndex) + KeyNavigation.backtab: session + KeyNavigation.tab: shutdownButton + } + + Text { + id: message + width: parent.width + color: muted + text: "" + font.pixelSize: 13 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + } + + Row { + anchors.right: parent.right + anchors.rightMargin: Math.max(28, parent.width * 0.075) + anchors.bottom: parent.bottom + anchors.bottomMargin: Math.max(26, parent.height * 0.045) + spacing: 10 + + Button { + id: shutdownButton + text: "Ausschalten" + color: Qt.rgba(0, 0, 0, 0.42) + activeColor: accent + pressedColor: accent2 + borderColor: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.35) + textColor: foreground + onClicked: sddm.powerOff() + KeyNavigation.tab: rebootButton + } + + Button { + id: rebootButton + text: "Neustart" + color: Qt.rgba(0, 0, 0, 0.42) + activeColor: accent + pressedColor: accent2 + borderColor: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.35) + textColor: foreground + onClicked: sddm.reboot() + KeyNavigation.backtab: shutdownButton + } + } +} diff --git a/dotfiles/hypr/sddm-theme/pascal-hypr/metadata.desktop b/dotfiles/hypr/sddm-theme/pascal-hypr/metadata.desktop new file mode 100644 index 0000000..0696ac5 --- /dev/null +++ b/dotfiles/hypr/sddm-theme/pascal-hypr/metadata.desktop @@ -0,0 +1,13 @@ +[SddmGreeterTheme] +Name=Pascal Hypr +Description=Theme-aware SDDM login screen for Pascal's Hyprland setup +Author=Pascal + Codex +Copyright=2026 +License=MIT +Type=sddm-theme +Version=1.0 +MainScript=Main.qml +ConfigFile=theme.conf +Theme-Id=pascal-hypr +Theme-API=2.0 + diff --git a/dotfiles/hypr/sddm-theme/pascal-hypr/theme.conf b/dotfiles/hypr/sddm-theme/pascal-hypr/theme.conf new file mode 100644 index 0000000..84f3523 --- /dev/null +++ b/dotfiles/hypr/sddm-theme/pascal-hypr/theme.conf @@ -0,0 +1,11 @@ +[General] +background=/var/lib/pascal-sddm-theme/wallpaper.jpg +themeName=Rose Night +accent=#f38ba8 +accent2=#cba6f7 +backgroundColor=#18141f +panelColor=#313244 +foreground=#f5e0dc +muted=#cdd6f4 +selectedText=#11111b + diff --git a/dotfiles/hypr/sddm-theme/sddm.conf b/dotfiles/hypr/sddm-theme/sddm.conf new file mode 100644 index 0000000..452b39d --- /dev/null +++ b/dotfiles/hypr/sddm-theme/sddm.conf @@ -0,0 +1,6 @@ +[Autologin] +Session=hyprland + +[Theme] +Current=pascal-hypr + diff --git a/dotfiles/kitty/kitty.conf b/dotfiles/kitty/kitty.conf new file mode 100644 index 0000000..5e93e99 --- /dev/null +++ b/dotfiles/kitty/kitty.conf @@ -0,0 +1 @@ +background_opacity 0.70 diff --git a/dotfiles/qt5ct/colors/ForestNeon.conf b/dotfiles/qt5ct/colors/ForestNeon.conf new file mode 100644 index 0000000..88ff7ee --- /dev/null +++ b/dotfiles/qt5ct/colors/ForestNeon.conf @@ -0,0 +1,4 @@ +[ColorScheme] +active_colors=#ffcdd6f4, #ff282837, #ffffffff, #ffcccccc, #ff14141e, #ff282837, #ffcdd6f4, #ffffffff, #ffcdd6f4, #ff14141e, #ff14141e, #ff000000, #ff00ff9c, #ff000000, #ff00cc88, #fff38ba8, #ff282837, #ffcdd6f4, #ff282837, #ffcdd6f4, #80cccccc +disabled_colors=#ffcccccc, #ff282837, #ffffffff, #ffcccccc, #ff14141e, #ff282837, #ffcccccc, #ffffffff, #ffcccccc, #ff14141e, #ff14141e, #ff000000, #ff282837, #ffcccccc, #ff00cc88, #fff38ba8, #ff282837, #ffcccccc, #ff282837, #ffcccccc, #80cccccc +inactive_colors=#ffcdd6f4, #ff282837, #ffffffff, #ffcccccc, #ff14141e, #ff282837, #ffcdd6f4, #ffffffff, #ffcdd6f4, #ff14141e, #ff14141e, #ff000000, #ff00ff9c, #ff000000, #ff00cc88, #fff38ba8, #ff282837, #ffcdd6f4, #ff282837, #ffcdd6f4, #80cccccc diff --git a/dotfiles/qt5ct/colors/RoseNight.conf b/dotfiles/qt5ct/colors/RoseNight.conf new file mode 100644 index 0000000..74004c3 --- /dev/null +++ b/dotfiles/qt5ct/colors/RoseNight.conf @@ -0,0 +1,4 @@ +[ColorScheme] +active_colors=#fff5e0dc, #ff313244, #ffffffff, #ffcdd6f4, #ff18141f, #ff313244, #fff5e0dc, #ffffffff, #fff5e0dc, #ff18141f, #ff18141f, #ff000000, #fff38ba8, #ff11111b, #ffcba6f7, #fff38ba8, #ff313244, #fff5e0dc, #ff313244, #fff5e0dc, #80cdd6f4 +disabled_colors=#ffcdd6f4, #ff313244, #ffffffff, #ffcdd6f4, #ff18141f, #ff313244, #ffcdd6f4, #ffffffff, #ffcdd6f4, #ff18141f, #ff18141f, #ff000000, #ff313244, #ffcdd6f4, #ffcba6f7, #fff38ba8, #ff313244, #ffcdd6f4, #ff313244, #ffcdd6f4, #80cdd6f4 +inactive_colors=#fff5e0dc, #ff313244, #ffffffff, #ffcdd6f4, #ff18141f, #ff313244, #fff5e0dc, #ffffffff, #fff5e0dc, #ff18141f, #ff18141f, #ff000000, #fff38ba8, #ff11111b, #ffcba6f7, #fff38ba8, #ff313244, #fff5e0dc, #ff313244, #fff5e0dc, #80cdd6f4 diff --git a/dotfiles/qt5ct/qt5ct.conf b/dotfiles/qt5ct/qt5ct.conf new file mode 100644 index 0000000..a0c2305 --- /dev/null +++ b/dotfiles/qt5ct/qt5ct.conf @@ -0,0 +1,28 @@ +[Appearance] +color_scheme_path=/home/pascal/.config/qt5ct/colors/ForestNeon.conf +custom_palette=true +icon_theme=Papirus-ForestNeon +standard_dialogs=default +style=Fusion + +[Fonts] +fixed="JetBrainsMono Nerd Font,10,-1,5,50,0,0,0,0,0" +general="JetBrainsMono Nerd Font,10,-1,5,50,0,0,0,0,0" + +[Interface] +activate_item_on_single_click=1 +buttonbox_layout=0 +cursor_flash_time=1000 +dialog_buttons_have_icons=1 +double_click_interval=400 +gui_effects=@Invalid() +keyboard_scheme=2 +menus_have_icons=true +show_shortcuts_in_context_menus=true +stylesheets=@Invalid() +toolbutton_style=4 +underline_shortcut=1 +wheel_scroll_lines=3 + +[SettingsWindow] +geometry=@ByteArray() diff --git a/dotfiles/qt6ct/colors/ForestNeon.conf b/dotfiles/qt6ct/colors/ForestNeon.conf new file mode 100644 index 0000000..88ff7ee --- /dev/null +++ b/dotfiles/qt6ct/colors/ForestNeon.conf @@ -0,0 +1,4 @@ +[ColorScheme] +active_colors=#ffcdd6f4, #ff282837, #ffffffff, #ffcccccc, #ff14141e, #ff282837, #ffcdd6f4, #ffffffff, #ffcdd6f4, #ff14141e, #ff14141e, #ff000000, #ff00ff9c, #ff000000, #ff00cc88, #fff38ba8, #ff282837, #ffcdd6f4, #ff282837, #ffcdd6f4, #80cccccc +disabled_colors=#ffcccccc, #ff282837, #ffffffff, #ffcccccc, #ff14141e, #ff282837, #ffcccccc, #ffffffff, #ffcccccc, #ff14141e, #ff14141e, #ff000000, #ff282837, #ffcccccc, #ff00cc88, #fff38ba8, #ff282837, #ffcccccc, #ff282837, #ffcccccc, #80cccccc +inactive_colors=#ffcdd6f4, #ff282837, #ffffffff, #ffcccccc, #ff14141e, #ff282837, #ffcdd6f4, #ffffffff, #ffcdd6f4, #ff14141e, #ff14141e, #ff000000, #ff00ff9c, #ff000000, #ff00cc88, #fff38ba8, #ff282837, #ffcdd6f4, #ff282837, #ffcdd6f4, #80cccccc diff --git a/dotfiles/qt6ct/colors/RoseNight.conf b/dotfiles/qt6ct/colors/RoseNight.conf new file mode 100644 index 0000000..74004c3 --- /dev/null +++ b/dotfiles/qt6ct/colors/RoseNight.conf @@ -0,0 +1,4 @@ +[ColorScheme] +active_colors=#fff5e0dc, #ff313244, #ffffffff, #ffcdd6f4, #ff18141f, #ff313244, #fff5e0dc, #ffffffff, #fff5e0dc, #ff18141f, #ff18141f, #ff000000, #fff38ba8, #ff11111b, #ffcba6f7, #fff38ba8, #ff313244, #fff5e0dc, #ff313244, #fff5e0dc, #80cdd6f4 +disabled_colors=#ffcdd6f4, #ff313244, #ffffffff, #ffcdd6f4, #ff18141f, #ff313244, #ffcdd6f4, #ffffffff, #ffcdd6f4, #ff18141f, #ff18141f, #ff000000, #ff313244, #ffcdd6f4, #ffcba6f7, #fff38ba8, #ff313244, #ffcdd6f4, #ff313244, #ffcdd6f4, #80cdd6f4 +inactive_colors=#fff5e0dc, #ff313244, #ffffffff, #ffcdd6f4, #ff18141f, #ff313244, #fff5e0dc, #ffffffff, #fff5e0dc, #ff18141f, #ff18141f, #ff000000, #fff38ba8, #ff11111b, #ffcba6f7, #fff38ba8, #ff313244, #fff5e0dc, #ff313244, #fff5e0dc, #80cdd6f4 diff --git a/dotfiles/qt6ct/qt6ct.conf b/dotfiles/qt6ct/qt6ct.conf new file mode 100644 index 0000000..7f41693 --- /dev/null +++ b/dotfiles/qt6ct/qt6ct.conf @@ -0,0 +1,28 @@ +[Appearance] +color_scheme_path=/home/pascal/.config/qt6ct/colors/ForestNeon.conf +custom_palette=true +icon_theme=Papirus-ForestNeon +standard_dialogs=default +style=Fusion + +[Fonts] +fixed="JetBrainsMono Nerd Font,10,-1,5,50,0,0,0,0,0" +general="JetBrainsMono Nerd Font,10,-1,5,50,0,0,0,0,0" + +[Interface] +activate_item_on_single_click=1 +buttonbox_layout=0 +cursor_flash_time=1000 +dialog_buttons_have_icons=1 +double_click_interval=400 +gui_effects=@Invalid() +keyboard_scheme=2 +menus_have_icons=true +show_shortcuts_in_context_menus=true +stylesheets=@Invalid() +toolbutton_style=4 +underline_shortcut=1 +wheel_scroll_lines=3 + +[SettingsWindow] +geometry=@ByteArray() diff --git a/dotfiles/starship.toml b/dotfiles/starship.toml new file mode 100644 index 0000000..f6bf187 --- /dev/null +++ b/dotfiles/starship.toml @@ -0,0 +1,123 @@ +# Generated by ~/.config/hypr/Scripts/theme-menu.sh +add_newline = true +palette = "hypr_theme" + +format = """ +[╭─](fg:accent_2)$os$username$hostname$directory$git_branch$git_status$nodejs$python$rust$golang$cmd_duration$status$fill$time +[╰─](fg:accent)$character +""" + +[palettes.hypr_theme] +accent = "#00ff9c" +accent_2 = "#00cc88" +background = "#14141e" +panel = "#282837" +foreground = "#cdd6f4" +muted = "#cccccc" +selected = "#000000" +success = "#a6e3a1" +warning = "#f9e2af" +danger = "#f38ba8" +blue = "#89b4fa" +cyan = "#89dceb" +orange = "#fab387" +purple = "#cba6f7" + +[fill] +symbol = " " +style = "fg:panel" + +[os] +disabled = false +format = "[ $symbol ](fg:selected bg:accent)[](fg:accent bg:panel)" +style = "fg:selected bg:accent" + +[os.symbols] +Arch = "" +CachyOS = "" +Linux = "" +Ubuntu = "" +Debian = "" +Fedora = "" +NixOS = "" + +[username] +show_always = true +style_user = "fg:foreground bg:panel bold" +style_root = "fg:danger bg:panel bold" +format = "[ $user](fg:foreground bg:panel bold)" + +[hostname] +ssh_only = false +style = "fg:muted bg:panel" +format = "[@$hostname ](fg:muted bg:panel)[](fg:panel bg:background)" + +[directory] +style = "fg:accent bg:background bold" +read_only_style = "fg:danger bg:background bold" +truncation_length = 3 +truncate_to_repo = true +read_only = " " +format = "[ 󰉋 $path$read_only ](fg:accent bg:background bold)" + +[git_branch] +symbol = "" +style = "fg:purple bg:background bold" +format = "[on $symbol $branch ](fg:purple bg:background bold)" + +[git_status] +style = "fg:warning bg:background bold" +format = "([$all_status$ahead_behind ](fg:warning bg:background bold))" +conflicted = "=${count} " +ahead = "⇡${count} " +behind = "⇣${count} " +diverged = "⇕⇡${ahead_count}⇣${behind_count} " +up_to_date = "" +untracked = "?${count} " +stashed = "*${count} " +modified = "!${count} " +staged = "+${count} " +renamed = "»${count} " +deleted = "x${count} " + +[nodejs] +symbol = "" +style = "fg:success bg:background" +format = "[$symbol $version ](fg:success bg:background)" + +[python] +symbol = "" +style = "fg:warning bg:background" +format = "[$symbol $version ](fg:warning bg:background)" + +[rust] +symbol = "" +style = "fg:orange bg:background" +format = "[$symbol $version ](fg:orange bg:background)" + +[golang] +symbol = "" +style = "fg:cyan bg:background" +format = "[$symbol $version ](fg:cyan bg:background)" + +[cmd_duration] +min_time = 300 +style = "fg:orange bg:background" +format = "[took $duration ](fg:orange bg:background)" + +[status] +disabled = false +style = "fg:danger bg:background bold" +symbol = "✖" +format = "[$symbol $status ](fg:danger bg:background bold)" + +[time] +disabled = false +time_format = "%H:%M" +style = "fg:muted" +format = "[ $time ]($style)" + +[character] +success_symbol = "[❯](fg:accent bold)" +error_symbol = "[❯](fg:danger bold)" +vimcmd_symbol = "[❮](fg:accent_2 bold)" diff --git a/dotfiles/swaync/config.json b/dotfiles/swaync/config.json new file mode 100644 index 0000000..4df5c5e --- /dev/null +++ b/dotfiles/swaync/config.json @@ -0,0 +1,70 @@ +{ + "positionX": "right", + "positionY": "top", + "layer": "overlay", + "control-center-width": 420, + "control-center-height": 650, + "notification-window-width": 360, + "timeout": 5, + "timeout-low": 3, + "timeout-critical": 0, + "fit-to-screen": true, + "keyboard-shortcuts": true, + "image-visibility": "when-available", + "transition-time": 200, + "hide-on-clear": true, + + "widgets": [ + "title", + "dnd", + "mpris", + "buttons-grid", + "notifications" + ], + + "widget-config": { + "title": { + "text": "Control Center", + "clear-all-button": true, + "button-text": "Leeren" + }, + + "dnd": { + "text": "Nicht stören" + }, + + "mpris": { + "image-size": 96, + "image-radius": 12 + }, + + "buttons-grid": { + "actions": [ + { + "label": "󰖩 WLAN", + "command": "nm-connection-editor" + }, + { + "label": "󰂯 Bluetooth", + "command": "blueman-manager" + }, + { + "label": " Screenshot", + "command": "mkdir -p ~/Bilder/Screenshots && grim -g \"$(slurp)\" ~/Bilder/Screenshots/$(date +%F_%H-%M-%S).png" + }, + { + "label": " Lock", + "command": "hyprlock" + }, + { + "label": "󰐥 Power", + "command": "wlogout" + }, + { + "label": "󰅶 Terminal", + "command": "kitty" + } + ] + } + } +} \ No newline at end of file diff --git a/dotfiles/swaync/style.css b/dotfiles/swaync/style.css new file mode 100644 index 0000000..35869a9 --- /dev/null +++ b/dotfiles/swaync/style.css @@ -0,0 +1,27 @@ +* { + font-family: "JetBrainsMono Nerd Font"; + font-size: 13px; +} + +.notification { + background: rgba(20, 20, 30, 0.95); + border-radius: 12px; + border: 2px solid #00ff9c; + box-shadow: 0 0 10px #00ff9c; + padding: 10px; +} + +.control-center { + background: rgba(20, 20, 30, 0.95); + border-radius: 15px; + border: 2px solid #00ff9c; +} + +.notification-title { + color: #00cc88; + font-weight: bold; +} + +.notification-body { + color: #cccccc; +} diff --git a/dotfiles/wallpapers/forest.jpg b/dotfiles/wallpapers/forest.jpg new file mode 100644 index 0000000..afcbdbe Binary files /dev/null and b/dotfiles/wallpapers/forest.jpg differ diff --git a/dotfiles/wallpapers/rose-pink.jpg b/dotfiles/wallpapers/rose-pink.jpg new file mode 100644 index 0000000..2480908 Binary files /dev/null and b/dotfiles/wallpapers/rose-pink.jpg differ diff --git a/dotfiles/waybar/config.jsonc b/dotfiles/waybar/config.jsonc new file mode 100644 index 0000000..8638fb7 --- /dev/null +++ b/dotfiles/waybar/config.jsonc @@ -0,0 +1,174 @@ +{ + "layer": "top", + "position": "top", + "height": 42, + "margin-top": 8, + "margin-left": 12, + "margin-right": 12, + "spacing": 6, + + "modules-left": [ + "hyprland/workspaces" + ], + + "modules-center": [ + "group/dynamic-island" + ], + + "modules-right": [ + "cpu", + "memory", + "temperature", + "pulseaudio", + "network", + "battery", + "custom/notifications", + "tray" + ], + + "hyprland/workspaces": { + "format": "{icon}", + "format-icons": { + "1": "1", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", + "9": "9", + "10": "10", + "urgent": "!", + "active": "●", + "default": "○" + }, + "on-click": "activate" + }, + + "hyprland/window": { + "format": "{}", + "max-length": 28, + "separate-outputs": true + }, + + "group/dynamic-island": { + "orientation": "horizontal", + "drawer": { + "transition-duration": 350, + "children-class": "island-drawer", + "transition-left-to-right": true + }, + "modules": [ + "custom/dynamic-island", + "custom/media-prev", + "custom/media-toggle", + "custom/media-next", + "hyprland/window" + ] + }, + + "custom/dynamic-island": { + "exec": "/home/pascal/.config/waybar/scripts/island-media.sh", + "return-type": "json", + "interval": 1, + "format": "{}", + "on-click": "playerctl play-pause", + "on-click-middle": "playerctl previous", + "on-click-right": "playerctl next" + }, + + "custom/media-prev": { + "format": "󰒮", + "tooltip-format": "Zurück", + "on-click": "playerctl previous" + }, + + "custom/media-toggle": { + "exec": "playerctl status 2>/dev/null | grep -q Playing && echo 󰏤 || echo 󰐊", + "interval": 1, + "format": "{}", + "tooltip-format": "Start/Pause", + "on-click": "playerctl play-pause" + }, + + "custom/media-next": { + "format": "󰒭", + "tooltip-format": "Weiter", + "on-click": "playerctl next" + }, + + "clock": { + "format": " {:%H:%M}", + "tooltip-format": "{:%A, %d. %B %Y}" + }, + + "cpu": { + "format": " {usage}%", + "tooltip": true + }, + + "memory": { + "format": " {}%", + "tooltip": true + }, + + "temperature": { + "hwmon-path-abs": "/sys/devices/platform/coretemp.0/hwmon", + "input-filename": "temp1_input", + "critical-threshold": 85, + "format": " {temperatureC}°C", + "format-critical": " {temperatureC}°C", + "tooltip": true + }, + + "battery": { + "format": "{icon} {capacity}%", + "format-icons": ["", "", "", "", ""], + "states": { + "warning": 30, + "critical": 15 + } + }, + + "network": { + "format-wifi": " {signalStrength}%", + "format-ethernet": "󰈀 LAN", + "format-disconnected": "󰤮 Offline", + "tooltip-format-wifi": "{essid} ({signalStrength}%)", + "tooltip-format-ethernet": "{ifname}: {ipaddr}/{cidr}" + }, + + "pulseaudio": { + "format": "{icon} {volume}%", + "format-muted": "󰖁 Muted", + "format-icons": { + "default": ["", "", ""] + } + }, + + "custom/notifications": { + "tooltip": true, + "format": "{icon}", + "format-icons": { + "notification": "󱅫", + "none": "", + "dnd-notification": "", + "dnd-none": "󰂛", + "inhibited-notification": "", + "inhibited-none": "", + "dnd-inhibited-notification": "", + "dnd-inhibited-none": "󰂛" + }, + "return-type": "json", + "exec-if": "which swaync-client", + "exec": "swaync-client -swb", + "on-click": "swaync-client -t", + "on-click-right": "swaync-client -d", + "escape": true + }, + + "tray": { + "spacing": 10 + } +} diff --git a/dotfiles/waybar/scripts/island-media.sh b/dotfiles/waybar/scripts/island-media.sh new file mode 100755 index 0000000..5251f54 --- /dev/null +++ b/dotfiles/waybar/scripts/island-media.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +json() { + jq -Rn --arg value "$1" '$value' +} + +status="$(playerctl status 2>/dev/null || true)" +time_now="$(date +%H:%M)" + +if [[ "$status" == "Playing" || "$status" == "Paused" ]]; then + title="$(playerctl metadata --format '{{ title }}' 2>/dev/null || true)" + artist="$(playerctl metadata --format '{{ artist }}' 2>/dev/null || true)" + player="$(playerctl metadata --format '{{ playerName }}' 2>/dev/null || true)" + + [[ -z "$title" ]] && title="Media" + + if [[ "$status" == "Playing" ]]; then + icon="󰏤" + class="playing" + else + icon="󰐊" + class="paused" + fi + + text="$icon $title" + [[ -n "$artist" ]] && text="$text · $artist" + text="$text  $time_now" + tooltip="${player:-Player} (${status})" + [[ -n "$artist$title" ]] && tooltip="$tooltip"$'\n'"$artist - $title" +else + text=" $time_now" + tooltip="$(date '+%A, %d. %B %Y')" + class="idle" +fi + +printf '{"text":%s,"tooltip":%s,"class":%s}\n' \ + "$(json "$text")" \ + "$(json "$tooltip")" \ + "$(json "$class")" diff --git a/dotfiles/waybar/style.css b/dotfiles/waybar/style.css new file mode 100644 index 0000000..9fa6586 --- /dev/null +++ b/dotfiles/waybar/style.css @@ -0,0 +1,206 @@ +* { + min-height: 0; + font-family: "JetBrainsMono Nerd Font", "JetBrains Mono", sans-serif; + font-size: 13px; + font-weight: 700; + border: none; + border-radius: 0; +} + +window#waybar { + background: transparent; + color: #cdd6f4; +} + +tooltip { + background: rgba(20, 20, 30, 0.95); + border: 1px solid #00ff9c; + border-radius: 10px; +} + +tooltip label { + color: #cdd6f4; + padding: 6px 8px; +} + +#workspaces { + margin: 0 0 0 2px; + padding: 4px; + background: rgba(24, 24, 37, 0.72); + border: 1px solid rgba(40, 40, 55, 0.8); + border-radius: 18px; +} + +#workspaces button { + min-width: 26px; + margin: 0 2px; + padding: 0 7px; + color: #7f849c; + background: transparent; + border-radius: 14px; + transition: all 180ms ease; +} + +#workspaces button.active { + min-width: 34px; + color: #000000; + background: linear-gradient(135deg, #00ff9c, #00cc88); + box-shadow: 0 0 14px rgba(40, 40, 55, 0.8); +} + +#workspaces button.urgent { + color: #000000; + background: #f38ba8; +} + +#workspaces button:hover { + color: #cdd6f4; + background: rgba(40, 40, 55, 0.8); +} + +#dynamic-island { + margin: 0; + padding: 0; + background: rgba(5, 5, 9, 0.88); + border-top: 1px solid rgba(40, 40, 55, 0.8); + border-bottom: 1px solid #00ff9c; + border-radius: 21px; + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.36); +} + +#custom-dynamic-island, +#custom-media-prev, +#custom-media-toggle, +#custom-media-next, +#window, +#clock { + margin: 0; + padding: 0 11px; + background: transparent; + color: #cdd6f4; +} + +#custom-dynamic-island { + min-width: 142px; + padding-left: 16px; + padding-right: 14px; + color: #cdd6f4; + border-radius: 21px; +} + +#custom-dynamic-island.playing { + color: #a6e3a1; +} + +#custom-dynamic-island.paused { + color: #f9e2af; +} + +#custom-dynamic-island.idle { + color: #7f849c; +} + +#custom-media-prev, +#custom-media-toggle, +#custom-media-next { + min-width: 20px; + padding: 0 9px; + color: #cdd6f4; + border-radius: 14px; +} + +#custom-media-prev:hover, +#custom-media-toggle:hover, +#custom-media-next:hover { + color: #000000; + background: #00ff9c; +} + +#window { + min-width: 90px; + color: #cccccc; +} + +window#waybar.empty #window { + min-width: 0; + padding: 0; + color: transparent; +} + +#clock { + padding-right: 16px; + color: #a6e3a1; +} + +.island-drawer { + color: #7f849c; +} + +#cpu, +#memory, +#temperature, +#pulseaudio, +#network, +#battery, +#custom-notifications, +#tray { + margin: 0 2px; + padding: 0 10px; + background: rgba(24, 24, 37, 0.72); + border: 1px solid rgba(40, 40, 55, 0.8); + border-radius: 16px; +} + +#cpu { + color: #89b4fa; +} + +#memory { + color: #f9e2af; +} + +#temperature { + color: #a6e3a1; +} + +#temperature.critical { + color: #f38ba8; + background: rgba(40, 40, 55, 0.8); +} + +#pulseaudio { + color: #fab387; +} + +#pulseaudio.muted { + color: #7f849c; +} + +#network { + color: #89dceb; +} + +#network.disconnected { + color: #f38ba8; +} + +#battery { + color: #a6e3a1; +} + +#battery.warning { + color: #f9e2af; +} + +#battery.critical { + color: #f38ba8; + background: rgba(40, 40, 55, 0.8); +} + +#custom-notifications { + color: #cba6f7; +} + +#tray { + padding-right: 12px; +} diff --git a/dotfiles/wofi/config b/dotfiles/wofi/config new file mode 100644 index 0000000..8303cbf --- /dev/null +++ b/dotfiles/wofi/config @@ -0,0 +1,11 @@ +width=600 +height=400 +location=center +show=drun +prompt= Launch +allow_images=true +term=kitty +style=/home/pascal/.config/wofi/style.css +hide_scroll=true +no_actions=true + diff --git a/dotfiles/wofi/style.css b/dotfiles/wofi/style.css new file mode 100644 index 0000000..58ad703 --- /dev/null +++ b/dotfiles/wofi/style.css @@ -0,0 +1,55 @@ +* { + font-family: "JetBrainsMono Nerd Font"; + font-size: 14px; +} + +window { + margin: 0px; + border: 2px solid #00ff9c; + border-radius: 12px; + background-color: rgba(20, 20, 30, 0.95); +} + +#input { + margin: 10px; + padding: 10px; + border-radius: 8px; + border: none; + background-color: rgba(40, 40, 55, 0.8); + color: #00ff9c; +} + +#inner-box { + margin: 5px; +} + +#outer-box { + margin: 10px; +} + +#scroll { + margin: 5px; +} + +#text { + margin: 5px; + color: #cdd6f4; +} + +#entry { + padding: 8px; + border-radius: 8px; +} + +#entry:selected { + background-color: #00ff9c; + color: #000000; +} + +#entry:hover { + background-color: rgba(40, 40, 55, 0.8); +} + +#img { + margin-right: 8px; +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..cf59d9d --- /dev/null +++ b/install.sh @@ -0,0 +1,338 @@ +#!/usr/bin/env bash +# Omeron - Modular System Setup Framework +# Usage: ./install.sh [--fresh] [--modules m1,m2] [--skip-packages] [--with-sddm] [--help] +set -euo pipefail + +OMERON_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +export OMERON_ROOT +export OMERON_PROJECT_DIR="$OMERON_ROOT" + +source "$OMERON_ROOT/lib/log.sh" +source "$OMERON_ROOT/lib/tui.sh" +source "$OMERON_ROOT/lib/utils.sh" +source "$OMERON_ROOT/lib/config.sh" +source "$OMERON_ROOT/lib/modules.sh" + +OMERON_FRESH_INSTALL="${OMERON_FRESH_INSTALL:-0}" +export OMERON_FRESH_INSTALL + +DEFAULT_MODULES=( + "core/preflight" + "core/packages" + "core/dotfiles" + "core/services" + "homelab/setup" + "optional/install" + "core/sddm" + "post/apply-theme" +) + +FRESH_MODULES=( + "core/packages" + "core/dotfiles" + "core/services" + "core/sddm" + "post/apply-theme" +) + +RUN_MODULES=() +SKIP_MODULES=() +WITH_SDDM=0 + +usage() { + cat </dev/null && declare -F "module_description" >/dev/null 2>&1; then + description="$(module_description)" + fi + printf " %-30s %s\n" "$rel_path" "$description" + done + exit 0 +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --fresh) + OMERON_FRESH_INSTALL=1 + shift + ;; + --modules) + IFS=',' read -ra RUN_MODULES <<< "$2" + shift 2 + ;; + --skip) + IFS=',' read -ra SKIP_MODULES <<< "$2" + shift 2 + ;; + --skip-packages) + SKIP_MODULES+=("core/packages") + shift + ;; + --with-sddm) + WITH_SDDM=1 + shift + ;; + --list-modules) + list_modules + ;; + -h|--help) + usage + ;; + *) + echo "Unknown option: $1" >&2 + usage + ;; + esac + done +} + +collect_modules() { + if ((${#RUN_MODULES[@]})); then + local result=() + for mod in "${RUN_MODULES[@]}"; do + result+=("$OMERON_MODULE_DIR/$mod.sh") + done + printf '%s\n' "${result[@]}" + return + fi + + local modules=() + if ((OMERON_FRESH_INSTALL)); then + modules=("${FRESH_MODULES[@]}") + else + modules=("${DEFAULT_MODULES[@]}") + fi + + local module_order=() + + for mod in "${modules[@]}"; do + local skip=0 + for skipped in "${SKIP_MODULES[@]}"; do + if [[ "$mod" == "$skipped" || "$mod" == "core/$skipped" ]]; then + skip=1 + break + fi + done + + [[ "$mod" == "core/sddm" && "$WITH_SDDM" -eq 0 && "$OMERON_FRESH_INSTALL" -eq 0 ]] && skip=1 + + ((skip)) && continue + + local module_file="$OMERON_MODULE_DIR/$mod.sh" + [[ -f "$module_file" ]] && module_order+=("$module_file") + done + + printf '%s\n' "${module_order[@]}" +} + +collect_all_interactive() { + local modules=() + if ((OMERON_FRESH_INSTALL)); then + modules=("${FRESH_MODULES[@]}") + else + modules=("${DEFAULT_MODULES[@]}") + fi + + local total=${#modules[@]} + local idx=1 + local module_order=() + + for mod in "${modules[@]}"; do + local module_file="$OMERON_MODULE_DIR/$mod.sh" + [[ -f "$module_file" ]] || continue + + local description="" + if source "$module_file" 2>/dev/null && declare -F "module_description" >/dev/null 2>&1; then + description="$(module_description)" + fi + + printf '\n' + log_step "$idx" "$total" "${description:-$mod}" + + if declare -F "module_required" >/dev/null 2>&1; then + source "$module_file" + if module_required; then + log_info "Required module - will run" + module_order+=("$module_file") + ((idx++)) + continue + fi + fi + + if tui_confirm "Run this step?"; then + module_order+=("$module_file") + else + log_info "Skipped" + fi + + ((idx++)) + done + + printf '%s\n' "${module_order[@]}" +} + +show_banner() { + tui_header " Ꮎ Ꮇ Ꭼ Ꮢ Ꮎ Ꮑ " + tui_format "" + tui_format "#{bold}Modular System Setup Framework#{normal}" + tui_format "#{italic}Arch / Hyprland / CachyOS#{normal}" + tui_format "" + + if ((OMERON_FRESH_INSTALL)); then + tui_format "#{bold}#{yellow}⚡ FRESH INSTALL MODE#{normal}" + tui_format "Will install Hyprland, GPU drivers, and all dependencies." + tui_format "" + fi +} + +show_summary() { + local modules=("$@") + + tui_format "" + tui_format "#{bold}Installation Summary:#{normal}" + tui_format " Modules to run: ${#modules[@]}" + + local gpu + gpu="$(detect_gpu)" + tui_format " Detected GPU: ${gpu}" + + if ((${#modules[@]})); then + tui_format "" + tui_format "#{bold}Steps:#{normal}" + local i=1 + for mod in "${modules[@]}"; do + local name + name="$(basename "$mod" .sh)" + tui_format " $i. ${name}" + ((i++)) + done + fi + + tui_format "" + if ! tui_confirm "Proceed with installation?"; then + tui_format "#{bold}Installation cancelled by user.#{normal}" + exit 0 + fi +} + +main() { + OMERON_LOG_FILE="${OMERON_LOG_FILE:-$HOME/.local/share/omeron/install-$(date +%Y%m%d-%H%M%S).log}" + export OMERON_LOG_FILE + + parse_args "$@" + + tui_style + show_banner + + mkdir -p "$(dirname "$OMERON_LOG_FILE")" + log_info "Omeron installation started" + log_info "Log file: $OMERON_LOG_FILE" + log_info "Project: $OMERON_ROOT" + log_info "Fresh install: $OMERON_FRESH_INSTALL" + + if ((!OMERON_FRESH_INSTALL)) && ((${#RUN_MODULES[@]} == 0)); then + log_info "Checking for fresh install..." + if is_fresh_install; then + OMERON_FRESH_INSTALL=1 + export OMERON_FRESH_INSTALL + log_info "Fresh system detected — switching to full setup mode" + fi + fi + + local modules_to_run=() + if ((${#RUN_MODULES[@]})); then + mapfile -t modules_to_run < <(collect_modules) + else + mapfile -t modules_to_run < <(collect_all_interactive) + fi + + if ((${#modules_to_run[@]} == 0)); then + log_warn "No modules selected. Nothing to do." + exit 0 + fi + + show_summary "${modules_to_run[@]}" + + local total=${#modules_to_run[@]} + local idx=1 + + for module_path in "${modules_to_run[@]}"; do + local description="" + if source "$module_path" 2>/dev/null && declare -F "module_description" >/dev/null 2>&1; then + description="$(module_description)" + fi + + printf '\n' + log_step "$idx" "$total" "${description:-$(basename "$module_path" .sh)}" + module_run "$module_path" || { + local rc=$? + if declare -F "module_required" >/dev/null 2>&1; then + source "$module_path" + if module_required 2>/dev/null; then + log_error "Required module failed. Aborting." + exit $rc + fi + fi + log_warn "Module completed with warnings" + } + ((idx++)) + done + + printf '\n' + tui_header " Ꮎ Ꮇ Ꭼ Ꮢ Ꮎ Ꮑ " + tui_format "" + tui_format "#{bold}#{green}Installation Complete!#{normal}" + tui_format "" + tui_format "Log: ${OMERON_LOG_FILE}" + tui_format "" + + if ((OMERON_FRESH_INSTALL)); then + tui_format "#{bold}Your system is ready for Hyprland!#{normal}" + tui_format "" + tui_format " Start Hyprland: Hyprland" + tui_format " Or enable SDDM: sudo systemctl enable --now sddm" + tui_format " Reload config: hyprctl reload" + else + tui_format "What next?" + tui_format " - Reload config with: hyprctl reload" + tui_format " - Re-run installer: ./install.sh" + fi + + tui_format "" + + if have notify-send; then + notify-send "Omeron" "Installation complete! ✨" >/dev/null 2>&1 || true + fi +} + +main "$@" diff --git a/lib/config.sh b/lib/config.sh new file mode 100755 index 0000000..f526732 --- /dev/null +++ b/lib/config.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash + +OMERON_CONFIG_DIR="${OMERON_CONFIG_DIR:-$HOME/.config/omeron}" +OMERON_PROJECT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" + +config_load() { + local config_file="$1" + + if [[ ! -f "$config_file" ]]; then + log_warn "Configuration not found: $config_file" + return 1 + fi + + if command -v yq >/dev/null 2>&1; then + config_parse_yq "$config_file" + elif command -v python3 >/dev/null 2>&1; then + config_parse_python "$config_file" + else + config_parse_shell "$config_file" + fi +} + +config_parse_yq() { + local config_file="$1" + local key + + while IFS='=' read -r key value; do + [[ -z "$key" ]] && continue + printf 'export OMERON_CFG_%s="%s"\n' "$key" "$value" + done < <(yq -o shell "$config_file" 2>/dev/null) +} + +config_parse_python() { + local config_file="$1" + + python3 -c " +import yaml, os, sys + +with open('$config_file') as f: + data = yaml.safe_load(f) + +if not data: + sys.exit(0) + +def flatten(d, prefix=''): + for k, v in d.items(): + key = f'{prefix}_{k}' if prefix else k + if isinstance(v, dict): + flatten(v, key) + elif isinstance(v, list): + print(f'export OMERON_CFG_{key}=\"{chr(32).join(str(x) for x in v)}\"') + else: + print(f'export OMERON_CFG_{key}=\"{v}\"') + +flatten(data) +" 2>/dev/null || config_parse_shell "$config_file" +} + +config_parse_shell() { + local config_file="$1" + local line key value + + while IFS= read -r line; do + line="${line%%#*}" + [[ -z "$line" ]] && continue + if [[ "$line" =~ ^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*):[[:space:]]*(.*) ]]; then + key="${BASH_REMATCH[1]}" + value="${BASH_REMATCH[2]}" + value="${value#[\"\']}" + value="${value%[\"\']}" + printf 'export OMERON_CFG_%s="%s"\n' "$key" "$value" + fi + done < "$config_file" +} + +config_save() { + local config_file="$1" + local key value + + mkdir -p "$(dirname "$config_file")" + + { + printf '# Omeron Configuration\n' + printf '# Generated: %s\n\n' "$(date --rfc-3339=seconds)" + for entry in "$@"; do + key="${entry%%=*}" + value="${entry#*=}" + printf '%s: "%s"\n' "$key" "$value" + done + } > "$config_file" +} + +config_get() { + local key="$1" + local var_name="OMERON_CFG_${key}" + + printf '%s' "${!var_name:-}" +} + +config_merge() { + local base_file="$1" + local override_file="$2" + + if command -v yq >/dev/null 2>&1; then + yq eval-all '. as $item ireduce ({}; . * $item)' "$base_file" "$override_file" + elif command -v python3 >/dev/null 2>&1; then + python3 -c " +import yaml, sys + +with open('$base_file') as f: + base = yaml.safe_load(f) or {} +with open('$override_file') as f: + override = yaml.safe_load(f) or {} + +def merge(a, b): + for k in b: + if k in a and isinstance(a[k], dict) and isinstance(b[k], dict): + merge(a[k], b[k]) + else: + a[k] = b[k] + return a + +print(yaml.dump(merge(base, override), default_flow_style=False)) +" 2>/dev/null || cat "$base_file" + else + cat "$base_file" + fi +} diff --git a/lib/log.sh b/lib/log.sh new file mode 100755 index 0000000..97fca4a --- /dev/null +++ b/lib/log.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +OMERON_LOG_FILE="${OMERON_LOG_FILE:-$HOME/.local/share/omeron/install.log}" +OMERON_LOG_LEVEL="${OMERON_LOG_LEVEL:-INFO}" + +__log_init() { + mkdir -p "$(dirname "$OMERON_LOG_FILE")" +} + +__log_timestamp() { + date '+%Y-%m-%d %H:%M:%S' +} + +__log_write() { + local level="$1" + local message="$2" + local timestamp + timestamp="$(__log_timestamp)" + + __log_init + printf '[%s] [%s] %s\n' "$timestamp" "$level" "$message" | tee -a "$OMERON_LOG_FILE" +} + +log_info() { __log_write "INFO" "$1"; } +log_warn() { __log_write "WARN" "$1" >&2; } +log_error() { __log_write "ERROR" "$1" >&2; } +log_success() { __log_write "SUCCESS" "$1"; } +log_debug() { + [[ "$OMERON_LOG_LEVEL" == "DEBUG" ]] && __log_write "DEBUG" "$1" +} + +log_step() { + local num="$1" + local total="$2" + local message="$3" + printf '\n━━━ [%d/%d] %s ━━━\n' "$num" "$total" "$message" + __log_write "STEP" "[$num/$total] $message" +} + +log_section() { + local message="$1" + local line + line="$(printf '━%.0s' $(seq 1 "${#message}"))" + printf '\n%s\n%s\n%s\n' "$line" "$message" "$line" + __log_write "SECTION" "$message" +} diff --git a/lib/modules.sh b/lib/modules.sh new file mode 100755 index 0000000..4fc0197 --- /dev/null +++ b/lib/modules.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +OMERON_MODULE_DIR="${OMERON_MODULE_DIR:-$OMERON_PROJECT_DIR/modules}" + +module_list() { + local category="${1:-}" + + if [[ -n "$category" ]]; then + find "$OMERON_MODULE_DIR/$category" -maxdepth 1 -name '*.sh' -type f | sort + else + find "$OMERON_MODULE_DIR" -name '*.sh' -type f | sort + fi +} + +module_run() { + local module_path="$1" + local module_name + module_name="$(basename "$module_path" .sh)" + + if [[ ! -f "$module_path" ]]; then + log_error "Module not found: $module_path" + return 1 + fi + + log_info "Running module: $module_name" + OMERON_CURRENT_MODULE="$module_name" source "$module_path" + + if declare -F "module_main" >/dev/null 2>&1; then + module_main + local rc=$? + if [[ $rc -eq 0 ]]; then + log_success "Module '$module_name' completed" + else + log_error "Module '$module_name' failed (exit: $rc)" + fi + return $rc + else + log_error "Module '$module_name' has no module_main function" + return 1 + fi +} + +module_check_prereqs() { + local module_path="$1" + + if ! declare -F "module_prereqs" >/dev/null 2>&1; then + return 0 + fi + + module_prereqs +} + +module_skip() { + local module_path="$1" + + if declare -F "module_should_skip" >/dev/null 2>&1; then + module_should_skip + return $? + fi + + return 1 +} + +module_describe() { + local module_path="$1" + + if declare -F "module_description" >/dev/null 2>&1; then + module_description + else + basename "$module_path" .sh + fi +} + +module_confirm() { + local module_path="$1" + local description + description="$(module_describe "$module_path")" + + if declare -F "module_required" >/dev/null 2>&1 && module_required; then + return 0 + fi + + tui_confirm "Run '$description'?" +} diff --git a/lib/tui.sh b/lib/tui.sh new file mode 100755 index 0000000..4b8a197 --- /dev/null +++ b/lib/tui.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash + +TUI_STYLE="${TUI_STYLE:-gum}" + +tui_check() { + if [[ "$TUI_STYLE" == "gum" ]] && ! command -v gum >/dev/null 2>&1; then + printf "gum is not installed. Install it or set TUI_STYLE=basic\n" >&2 + return 1 + fi +} + +tui_style() { + if command -v gum >/dev/null 2>&1; then + TUI_STYLE="gum" + else + TUI_STYLE="basic" + fi +} + +tui_choose() { + local prompt="$1" + shift + + if [[ "$TUI_STYLE" == "gum" ]]; then + gum choose "$@" + else + select __choice in "$@"; do + printf '%s\n' "$__choice" + break + done + fi +} + +tui_confirm() { + local prompt="$1" + + if [[ "$TUI_STYLE" == "gum" ]]; then + gum confirm "$prompt" + else + printf '%s [y/N]: ' "$prompt" >&2 + read -r response + [[ "$response" =~ ^[yY](es)?$ ]] + fi +} + +tui_input() { + local prompt="$1" + + if [[ "$TUI_STYLE" == "gum" ]]; then + gum input --prompt "$prompt " + else + printf '%s: ' "$prompt" >&2 + read -r response + printf '%s\n' "$response" + fi +} + +tui_password() { + local prompt="$1" + + if [[ "$TUI_STYLE" == "gum" ]]; then + gum input --password --prompt "$prompt " + else + printf '%s: ' "$prompt" >&2 + read -rs response + printf '\n' + printf '%s\n' "$response" + fi +} + +tui_multiselect() { + local prompt="$1" + shift + + if [[ "$TUI_STYLE" == "gum" ]]; then + gum choose --no-limit "$@" + else + printf '%s (space-separated indices):\n' "$prompt" >&2 + local i=0 + local items=("$@") + for item in "${items[@]}"; do + printf ' [%d] %s\n' "$i" "$item" >&2 + ((i++)) + done + printf '> ' >&2 + read -ra selections + for idx in "${selections[@]}"; do + printf '%s\n' "${items[$idx]}" + done + fi +} + +tui_spin() { + local title="$1" + shift + + if [[ "$TUI_STYLE" == "gum" ]]; then + gum spin --title "$title" -- "$@" + else + printf '%s... ' "$title" >&2 + "$@" + printf 'done\n' >&2 + fi +} + +tui_header() { + local title="$1" + + if [[ "$TUI_STYLE" == "gum" ]]; then + gum style --foreground 212 --border-foreground 212 --border double --align center --width 60 --margin "1 2" --padding "1 2" "$title" + else + printf '╔══════════════════════════════════════════════════╗\n' + printf '║ %s\n' "$title" + printf '╚══════════════════════════════════════════════════╝\n' + fi +} + +tui_status() { + local ok="$1" + local message="$2" + + if [[ "$TUI_STYLE" == "gum" ]]; then + if [[ "$ok" == "0" ]]; then + gum style --foreground 10 "✓ $message" + else + gum style --foreground 9 "✗ $message" + fi + else + if [[ "$ok" == "0" ]]; then + printf '✓ %s\n' "$message" + else + printf '✗ %s\n' "$message" + fi + fi +} + +tui_file_pick() { + local directory="$1" + local pattern="$2" + + if [[ "$TUI_STYLE" == "gum" ]]; then + find "$directory" -maxdepth 1 -type f -name "$pattern" -printf '%f\n' | sort | gum choose + else + local files=() + while IFS= read -r -d '' f; do + files+=("$(basename "$f")") + done < <(find "$directory" -maxdepth 1 -type f -name "$pattern" -print0 | sort -z) + select __file in "${files[@]}"; do + printf '%s\n' "$__file" + break + done + fi +} + +tui_format() { + if [[ "$TUI_STYLE" == "gum" ]]; then + gum format "$@" + else + printf '%s\n' "$*" + fi +} diff --git a/lib/utils.sh b/lib/utils.sh new file mode 100755 index 0000000..c38a8d5 --- /dev/null +++ b/lib/utils.sh @@ -0,0 +1,263 @@ +#!/usr/bin/env bash + +require() { + local cmd="$1" + local pkg="${2:-$1}" + + if ! command -v "$cmd" >/dev/null 2>&1; then + log_error "Required command '$cmd' not found. Install with: sudo pacman -S $pkg" + return 1 + fi +} + +require_optional() { + local cmd="$1" + local pkg="${2:-$1}" + + if ! command -v "$cmd" >/dev/null 2>&1; then + log_warn "'$cmd' not found. Install with: sudo pacman -S $pkg" + return 1 + fi +} + +have() { + command -v "$1" >/dev/null 2>&1 +} + +is_arch() { + [[ -f /etc/arch-release ]] || [[ -d /etc/pacman.d ]] +} + +is_root() { + [[ "$EUID" -eq 0 ]] +} + +sudo_run() { + if is_root; then + "$@" + else + sudo "$@" + fi +} + +backup_file() { + local target="$1" + local backup_dir="${2:-$HOME/.dotfiles-backup/$(date +%Y%m%d-%H%M%S)}" + local relative="${target#"$HOME"/}" + local backup="$backup_dir/$relative" + + [[ -e "$target" || -L "$target" ]] || return 0 + + mkdir -p "$(dirname "$backup")" + cp -a "$target" "$backup" + log_info "Backed up $target -> $backup" + printf '%s' "$backup_dir" +} + +symlink_path() { + local source="$1" + local target="$2" + + backup_file "$target" + rm -rf "$target" + mkdir -p "$(dirname "$target")" + ln -sfn "$source" "$target" + log_info "Linked $source -> $target" +} + +copy_path() { + local source="$1" + local target="$2" + + backup_file "$target" + rm -rf "$target" + mkdir -p "$(dirname "$target")" + cp -a "$source" "$target" + log_info "Copied $source -> $target" +} + +platform_packages() { + if is_arch; then + printf '%s\n' "arch" + fi + if [[ -f /etc/os-release ]]; then + local id + id="$(grep -oP '^ID=\K.*' /etc/os-release)" + printf '%s\n' "$id" + fi +} + +is_package_installed() { + local pkg="$1" + + if command -v pacman >/dev/null 2>&1; then + pacman -Qi "$pkg" >/dev/null 2>&1 + return $? + fi + + return 1 +} + +install_pacman() { + local packages=("$@") + local to_install=() + local pkg + + for pkg in "${packages[@]}"; do + if ! pacman -Si "$pkg" >/dev/null 2>&1; then + log_warn "Package '$pkg' not found in pacman repositories" + continue + fi + if ! is_package_installed "$pkg"; then + to_install+=("$pkg") + fi + done + + if ((${#to_install[@]})); then + log_info "Installing: ${to_install[*]}" + sudo_run pacman -S --needed --noconfirm "${to_install[@]}" + else + log_info "All packages already installed" + fi +} + +install_aur() { + local packages=("$@") + local aur_helper="" + + if have paru; then + aur_helper="paru" + elif have yay; then + aur_helper="yay" + else + log_error "No AUR helper found (install paru or yay)" + return 1 + fi + + local to_install=() + local pkg + + for pkg in "${packages[@]}"; do + if ! is_package_installed "$pkg"; then + to_install+=("$pkg") + fi + done + + if ((${#to_install[@]})); then + log_info "Installing from AUR: ${to_install[*]}" + "$aur_helper" -S --needed --noconfirm "${to_install[@]}" + else + log_info "All AUR packages already installed" + fi +} + +replace_home_paths() { + local dir="$1" + local original_home="${2:-/home/pascal}" + + if [[ "$original_home" == "$HOME" ]]; then + return 0 + fi + + find "$dir" -type f -exec sed -i "s#${original_home}#${HOME}#g" {} + 2>/dev/null || true + log_info "Rewrote home paths in $dir" +} + +detect_gpu() { + if ! command -v lspci >/dev/null 2>&1; then + if have pacman; then + sudo_run pacman -S --needed --noconfirm pciutils >/dev/null 2>&1 || true + fi + fi + + local gpu_info + gpu_info="$(lspci -nn 2>/dev/null | grep -E '\[0300\]|\[0302\]|\[0380\]' || true)" + [[ -z "$gpu_info" ]] && gpu_info="$(lspci -nn 2>/dev/null | grep -iE '(VGA|3D|Display).*controller' || true)" + + if printf '%s' "$gpu_info" | grep -qi "\[10de:"; then + printf 'nvidia' + elif printf '%s' "$gpu_info" | grep -qi "\[1002:\|\[1022:"; then + printf 'amd' + elif printf '%s' "$gpu_info" | grep -qi "\[8086:"; then + printf 'intel' + elif printf '%s' "$gpu_info" | grep -qi "vmware\|qemu\|virtualbox\|virtio"; then + printf 'vm' + elif printf '%s' "$gpu_info" | grep -qi "nvidia"; then + printf 'nvidia' + elif printf '%s' "$gpu_info" | grep -qi "amd\|advanced micro devices"; then + printf 'amd' + elif printf '%s' "$gpu_info" | grep -qi "intel"; then + printf 'intel' + else + printf 'unknown' + fi +} + +gpu_packages() { + local gpu_type + gpu_type="$(detect_gpu)" + + case "$gpu_type" in + intel) + printf '%s\n' "xf86-video-intel" "vulkan-intel" "intel-media-driver" "libva-intel-driver" + ;; + amd) + printf '%s\n' "xf86-video-amdgpu" "vulkan-radeon" "libva-mesa-driver" "mesa-vdpau" + ;; + nvidia) + printf '%s\n' "nvidia-dkms" "nvidia-utils" "nvidia-settings" "libva-nvidia-driver" "vulkan-tools" + ;; + vm) + printf '%s\n' "xf86-video-vmware" "mesa" "vulkan-swrast" + ;; + *) + printf '%s\n' "mesa" "vulkan-swrast" + ;; + esac +} + +is_fresh_install() { + have hyprctl && return 1 + + if have hyprland; then + hyprland --version >/dev/null 2>&1 && return 1 + fi + + if [[ -n "${WAYLAND_DISPLAY:-}" || -n "${DISPLAY:-}" ]]; then + return 1 + fi + + return 0 +} + +count_missing_packages() { + local packages=("$@") + local missing=0 + local pkg + + for pkg in "${packages[@]}"; do + if ! is_package_installed "$pkg"; then + ((missing++)) + fi + done + + printf '%d' "$missing" +} + +system_summary() { + local gpu + gpu="$(detect_gpu)" + + cat </dev/null | sed 's/"$//' || echo "Arch Linux") +│ Kernel: $(uname -r) +│ GPU: ${gpu} ($(lspci -nn 2>/dev/null | grep -E '\[0300\]|\[0302\]|\[0380\]' | sed 's/.*: //' | head -1 || echo "unknown")) +│ Session: $(printenv XDG_SESSION_TYPE || echo "none (TTY)") +│ Hyprland: $(have hyprctl && echo "installed" || echo "NOT installed") +│ AUR: $(have paru && echo "paru" || (have yay && echo "yay" || echo "none")) +└─────────────────────────────────────────────┘ +SUMMARY +} diff --git a/modules/core/dotfiles.sh b/modules/core/dotfiles.sh new file mode 100755 index 0000000..6102d11 --- /dev/null +++ b/modules/core/dotfiles.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +module_description() { + printf "Dotfiles - deploy configuration files to ~/.config\n" +} + +module_required() { return 0; } +module_should_skip() { return 1; } + +module_prereqs() { + return 0 +} + +module_main() { + log_section "Dotfile Deployment" + + local dotfiles_dir="$OMERON_PROJECT_DIR/dotfiles" + local config_items=() + local backup_dir + + if [[ -f "$OMERON_PROJECT_DIR/config/omeron.yaml" ]] && command -v python3 >/dev/null 2>&1; then + log_info "Loading config items from omeron.yaml" + config_items=() + local raw_items + raw_items="$(python3 -c " +import yaml +with open('$OMERON_PROJECT_DIR/config/omeron.yaml') as f: + data = yaml.safe_load(f) +items = data.get('dotfiles', {}).get('items', []) +print(' '.join(items)) +" 2>/dev/null)" + + if [[ -n "$raw_items" ]]; then + read -ra config_items <<< "$raw_items" + fi + fi + + if ((${#config_items[@]} == 0)); then + config_items=(hypr waybar wofi swaync kitty gtk-3.0 gtk-4.0 qt5ct qt6ct) + fi + + if ! tui_confirm "Deploy dotfiles to ~/.config? (existing files will be backed up)"; then + log_info "Dotfile deployment skipped by user" + return 0 + fi + + backup_dir="$(backup_file "$HOME/.config/hypr" "$HOME/.dotfiles-backup/$(date +%Y%m%d-%H%M%S)")" + backup_dir="$(dirname "$backup_dir" 2>/dev/null || printf '%s' "$HOME/.dotfiles-backup/$(date +%Y%m%d-%H%M%S)")" + + for item in "${config_items[@]}"; do + local source="$dotfiles_dir/$item" + local target="$HOME/.config/$item" + + if [[ ! -d "$source" ]] && [[ ! -f "$source" ]]; then + log_warn "Source not found: $source" + continue + fi + + log_info "Deploying $item..." + copy_path "$source" "$target" + done + + if [[ -f "$dotfiles_dir/starship.toml" ]]; then + copy_path "$dotfiles_dir/starship.toml" "$HOME/.config/starship.toml" + fi + + local wallpaper_source="$dotfiles_dir/wallpapers" + local wallpaper_target="$HOME/Bilder/Wallpaper" + if [[ -d "$wallpaper_source" ]]; then + copy_path "$wallpaper_source" "$wallpaper_target" + fi + + chmod +x "$HOME/.config/hypr/Scripts/"*.sh 2>/dev/null || true + chmod +x "$HOME/.config/hypr/Scripts/"*.py 2>/dev/null || true + chmod +x "$HOME/.config/waybar/scripts/"*.sh 2>/dev/null || true + + replace_home_paths "$HOME/.config/hypr" "/home/pascal" + replace_home_paths "$HOME/.config/gtk-3.0" "/home/pascal" + replace_home_paths "$HOME/.config/gtk-4.0" "/home/pascal" + replace_home_paths "$HOME/.config/qt5ct" "/home/pascal" + replace_home_paths "$HOME/.config/qt6ct" "/home/pascal" + + log_success "Dotfiles deployed (backup: $backup_dir)" +} diff --git a/modules/core/packages.sh b/modules/core/packages.sh new file mode 100755 index 0000000..841c12f --- /dev/null +++ b/modules/core/packages.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash + +module_description() { + printf "System Packages - install Hyprland, GPU drivers, and all dependencies\n" +} + +module_required() { return 0; } +module_should_skip() { return 1; } + +module_prereqs() { + require pacman pacman + is_arch || { + log_error "This module requires an Arch-based system" + return 1 + } +} + +module_main() { + log_section "Package Installation" + + local groups=() + local all_packages=() + + if ((OMERON_FRESH_INSTALL)); then + tui_format "#{bold}Fresh install mode — installing everything#{normal}" + groups=( + "hyprland" + "gpu" + "audio" + "network" + "fonts" + "tools" + "qt" + "development" + ) + else + tui_format "#{bold}Existing system — checking for missing packages#{normal}" + groups=( + "hyprland" + "audio" + "network" + "tools" + ) + fi + + for group in "${groups[@]}"; do + local pkgs=() + mapfile -t pkgs < <(get_group_packages "$group") + all_packages+=("${pkgs[@]}") + done + + all_packages=("$(remove_duplicates "${all_packages[@]}")") + + local total=${#all_packages[@]} + local existing=0 + local missing=0 + local pkg + + for pkg in "${all_packages[@]}"; do + if is_package_installed "$pkg"; then + ((existing++)) + else + ((missing++)) + fi + done + + tui_format "" + tui_format "#{bold}Package Summary:#{normal}" + tui_format " Total packages: ${total}" + tui_format " Already installed: ${existing}" + tui_format " To install: ${missing}" + tui_format "" + + if ((missing == 0)); then + log_success "All packages already installed" + return 0 + fi + + if ! tui_confirm "Install ${missing} missing packages?"; then + log_info "Package installation skipped by user" + return 0 + fi + + if ((OMERON_FRESH_INSTALL)); then + log_info "Running system update first..." + sudo_run pacman -Syu --noconfirm 2>&1 | tail -3 || true + fi + + local pacman_pkgs=() + local aur_pkgs=() + local gpu_pkgs=() + + for pkg in "${all_packages[@]}"; do + if is_package_installed "$pkg"; then + continue + fi + + if pacman -Si "$pkg" >/dev/null 2>&1; then + pacman_pkgs+=("$pkg") + else + aur_pkgs+=("$pkg") + fi + done + + if ((${#pacman_pkgs[@]})); then + log_info "Installing ${#pacman_pkgs[@]} pacman packages..." + tui_spin "Installing core packages..." sudo_run pacman -S --needed --noconfirm "${pacman_pkgs[@]}" + log_success "Core packages installed" + fi + + if ((${#aur_pkgs[@]})); then + if have paru || have yay; then + log_info "Installing ${#aur_pkgs[@]} AUR packages..." + install_aur "${aur_pkgs[@]}" || log_warn "Some AUR packages may not have been installed" + else + log_warn "No AUR helper found. Install manually: ${aur_pkgs[*]}" + fi + fi + + log_success "Package installation complete" + log_info "Installed: $((missing - ${#aur_pkgs[@]})) | AUR/optional: ${#aur_pkgs[@]}" +} + +get_group_packages() { + local group="$1" + + case "$group" in + hyprland) + printf '%s\n' \ + hyprland hyprpaper hyprlock hypridle \ + waybar wofi swaync kitty \ + brightnessctl playerctl \ + grim slurp swappy hyprshot \ + wl-clipboard libnotify sshpass + ;; + + gpu) + gpu_packages + printf '%s\n' \ + mesa mesa-utils \ + vulkan-tools vulkan-icd-loader \ + libva-utils + ;; + + audio) + printf '%s\n' \ + pipewire pipewire-pulse pipewire-alsa pipewire-jack \ + wireplumber pavucontrol helvum \ + sof-firmware + ;; + + network) + printf '%s\n' \ + networkmanager network-manager-applet \ + bluez bluez-utils blueman + ;; + + fonts) + printf '%s\n' \ + noto-fonts noto-fonts-emoji ttf-jetbrains-mono-nerd \ + ttf-font-awesome ttf-nerd-fonts-symbols ttf-dejavu + ;; + + tools) + printf '%s\n' \ + nautilus papirus-icon-theme starship \ + python-gobject gtk3 gtk4 \ + polkit-gnome xdg-desktop-portal xdg-desktop-portal-hyprland \ + qt5ct qt6ct \ + gum + ;; + + qt) + printf '%s\n' \ + qt5-wayland qt6-wayland \ + qt5-base qt6-base + ;; + + development) + printf '%s\n' \ + git base-devel \ + zip unzip unrar p7zip \ + ripgrep fd bat lsd \ + htop btop fastfetch + ;; + esac +} + +remove_duplicates() { + printf '%s\n' "$@" | sort -u +} diff --git a/modules/core/preflight.sh b/modules/core/preflight.sh new file mode 100755 index 0000000..b5e8234 --- /dev/null +++ b/modules/core/preflight.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash + +OMERON_FRESH_INSTALL=0 +OMERON_DETECTED_GPU="" + +module_description() { + printf "System Preflight - detect system state and missing dependencies\n" +} + +module_required() { return 0; } +module_should_skip() { return 1; } + +module_prereqs() { + require pacman pacman + is_arch || { + log_error "This module requires an Arch-based system" + return 1 + } +} + +module_main() { + log_section "System Preflight Check" + + OMERON_DETECTED_GPU="$(detect_gpu)" + export OMERON_DETECTED_GPU + + system_summary + + if is_fresh_install; then + OMERON_FRESH_INSTALL=1 + export OMERON_FRESH_INSTALL + + tui_format "" + tui_format "#{bold}#{yellow}⚠ Fresh system detected!#{normal}" + tui_format "Hyprland is not installed and no desktop session is running." + tui_format "" + + local gpu_name + gpu_name="$(lspci -nn 2>/dev/null | grep -i 'vga\|3d\|display' | sed 's/.*: //' | head -1 || echo "unknown")" + + tui_format "#{bold}This installation will:#{normal}" + tui_format " • Hyprland compositor + tools" + tui_format " • GPU drivers for: ${gpu_name}" + tui_format " • Audio system (PipeWire)" + tui_format " • Network services (NetworkManager)" + tui_format " • Fonts and icon themes" + tui_format " • Dotfiles deployment" + tui_format "" + + if ! tui_confirm "Proceed with full system setup?"; then + log_info "Fresh installation aborted by user" + exit 0 + fi + + if ! have paru && ! have yay; then + log_info "No AUR helper found. Installing paru..." + install_aur_helper + fi + + if ! have gum; then + log_info "Installing gum for better TUI..." + sudo_run pacman -S --needed --noconfirm gum 2>/dev/null || true + tui_style + fi + + log_success "Preflight complete — ready for full installation" + else + tui_format "" + tui_format "#{bold}#{green}✓ Existing Hyprland system detected#{normal}" + tui_format "Will check for missing packages and updates." + tui_format "" + fi +} + +install_aur_helper() { + if have paru || have yay; then + return 0 + fi + + if ! command -v git >/dev/null 2>&1; then + sudo_run pacman -S --needed --noconfirm git base-devel + fi + + local build_dir + build_dir="$(mktemp -d)" + + log_info "Building paru from AUR..." + sudo_run pacman -S --needed --noconfirm rustup 2>/dev/null || true + + if have rustup; then + rustup default stable >/dev/null 2>&1 || true + fi + + if git clone https://aur.archlinux.org/paru.git "$build_dir/paru" 2>/dev/null; then + (cd "$build_dir/paru" && makepkg -si --needed --noconfirm) 2>&1 | tail -5 || { + log_warn "paru build failed, trying yay..." + if git clone https://aur.archlinux.org/yay.git "$build_dir/yay" 2>/dev/null; then + (cd "$build_dir/yay" && makepkg -si --needed --noconfirm) 2>&1 | tail -5 || { + log_warn "yay build failed too. Install manually: paru -S paru-bin" + } + fi + } + fi + + rm -rf "$build_dir" +} diff --git a/modules/core/sddm.sh b/modules/core/sddm.sh new file mode 100755 index 0000000..b290e63 --- /dev/null +++ b/modules/core/sddm.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +module_description() { + printf "SDDM Theme - install custom login theme\n" +} + +module_required() { return 1; } +module_should_skip() { + if have sddm; then + return 1 + fi + return 0 +} + +module_prereqs() { + if ! have sddm; then + log_warn "SDDM is not installed. Install with: sudo pacman -S sddm" + return 1 + fi +} + +module_main() { + log_section "SDDM Theme Installation" + + local sddm_theme_dir="$OMERON_PROJECT_DIR/dotfiles/hypr/sddm-theme" + + if [[ ! -d "$sddm_theme_dir/pascal-hypr" ]]; then + log_warn "SDDM theme not found at $sddm_theme_dir" + return 1 + fi + + log_info "Installing SDDM theme..." + sudo_run mkdir -p /usr/share/sddm/themes /etc/sddm.conf.d + + if [[ -d "/usr/share/sddm/themes/pascal-hypr" ]]; then + sudo_run rm -rf "/usr/share/sddm/themes/pascal-hypr" + fi + + sudo_run cp -a "$sddm_theme_dir/pascal-hypr" /usr/share/sddm/themes/ + + if [[ -f "$sddm_theme_dir/sddm.conf" ]]; then + sudo_run cp -a "$sddm_theme_dir/sddm.conf" /etc/sddm.conf.d/10-pascal-hypr.conf + fi + + log_success "SDDM theme installed" + + if tui_confirm "Enable SDDM as display manager?"; then + sudo_run systemctl enable sddm --now 2>/dev/null || log_warn "Could not enable SDDM" + fi +} diff --git a/modules/core/services.sh b/modules/core/services.sh new file mode 100755 index 0000000..6ea987a --- /dev/null +++ b/modules/core/services.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +module_description() { + printf "System Services - enable and start NetworkManager and Bluetooth\n" +} + +module_required() { return 0; } +module_should_skip() { + have systemctl || return 0 + return 1 +} + +module_prereqs() { + require systemctl systemd +} + +module_main() { + log_section "System Services" + + local services=("NetworkManager.service" "bluetooth.service") + + if [[ -f "$OMERON_PROJECT_DIR/config/omeron.yaml" ]] && command -v python3 >/dev/null 2>&1; then + local raw_services + raw_services="$(python3 -c " +import yaml +with open('$OMERON_PROJECT_DIR/config/omeron.yaml') as f: + data = yaml.safe_load(f) +svcs = data.get('services', []) +print(' '.join(svcs)) +" 2>/dev/null)" + + if [[ -n "$raw_services" ]]; then + read -ra services <<< "$raw_services" + for i in "${!services[@]}"; do + if ! [[ "${services[$i]}" == *".service" ]]; then + services[$i]="${services[$i]}.service" + fi + done + fi + fi + + for svc in "${services[@]}"; do + log_info "Enabling $svc..." + if sudo_run systemctl enable --now "$svc" >/dev/null 2>&1; then + log_success "$svc enabled and started" + else + log_warn "Failed to enable $svc (may already be running)" + fi + done + + log_success "Services configured" +} diff --git a/modules/homelab/setup.sh b/modules/homelab/setup.sh new file mode 100755 index 0000000..4c6e11b --- /dev/null +++ b/modules/homelab/setup.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +module_description() { + printf "Homelab Configuration - set up Unraid server access\n" +} + +module_required() { return 1; } +module_should_skip() { return 1; } + +module_prereqs() { + return 0 +} + +module_main() { + log_section "Homelab Configuration" + + local homelab_config_dir="$HOME/.config/homelab" + local homelab_config_file="$homelab_config_dir/config.yaml" + + tui_format "#{bold}Homelab Control Center Setup#{normal}" + tui_format "This configures SSH access and connection details to your Unraid server." + tui_format "" + + local server_address server_username + + server_address="$(tui_input "Server address (IP or domain)")" + if [[ -z "$server_address" ]]; then + log_info "Homelab setup skipped (no server address)" + return 0 + fi + + server_username="$(tui_input "SSH username")" + if [[ -z "$server_username" ]]; then + server_username="root" + fi + + mkdir -p "$homelab_config_dir" + + cat > "$homelab_config_file" <&1; then + log_success "SSH connection successful" + else + log_warn "SSH connection failed. You may need to set up SSH keys or the server may not be reachable." + fi + fi +} diff --git a/modules/optional/install.sh b/modules/optional/install.sh new file mode 100755 index 0000000..dc2f6a7 --- /dev/null +++ b/modules/optional/install.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +module_description() { + printf "Optional Software - select and install additional packages\n" +} + +module_required() { return 1; } +module_should_skip() { return 1; } + +module_prereqs() { + return 0 +} + +module_main() { + log_section "Optional Software Selection" + + tui_format "#{bold}Select software to install:#{normal}" + tui_format "Use space to select, enter to confirm." + tui_format "" + + local choices + choices="$( + gum choose --no-limit \ + --header "Select optional packages (space to toggle, enter to confirm)" \ + "Obsidian" \ + "Neovim" \ + "Visual Studio Code" \ + "Spotify" \ + "Brave Browser" \ + "Chromium" \ + "VLC" \ + "PipeWire Tools" \ + "Docker" \ + "Blender" 2>&1 + )" + + if [[ -z "$choices" ]]; then + log_info "No optional software selected" + return 0 + fi + + log_info "Selected: $(printf '%s' "$choices" | tr '\n' ' ')" + + while IFS= read -r selection; do + [[ -z "$selection" ]] && continue + local module_script="$OMERON_PROJECT_DIR/modules/optional/packages/$(echo "$selection" | tr '[:upper:]' '[:lower:]' | tr ' ' '-').sh" + if [[ -f "$module_script" ]]; then + log_info "Running installer for: $selection" + module_run "$module_script" + else + log_warn "No installer found for: $selection" + local pkg_name="${selection,,}" + pkg_name="${pkg_name// /-}" + install_standard_package "$selection" "$pkg_name" + fi + done <<< "$choices" + + log_success "Optional software installation complete" +} + +install_standard_package() { + local display_name="$1" + local pkg_name="$2" + + if tui_confirm "Install $display_name (searching pacman)?"; then + if pacman -Si "$pkg_name" >/dev/null 2>&1; then + install_pacman "$pkg_name" + log_success "$display_name installed" + else + log_info "$pkg_name not in pacman, trying AUR..." + install_aur "$pkg_name" 2>/dev/null || log_warn "Could not install $display_name" + fi + fi +} diff --git a/modules/optional/packages/brave-browser.sh b/modules/optional/packages/brave-browser.sh new file mode 100755 index 0000000..faa6281 --- /dev/null +++ b/modules/optional/packages/brave-browser.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +module_description() { printf "Install Brave Browser\n"; } +module_required() { return 1; } +module_should_skip() { return 1; } +module_prereqs() { return 0; } + +module_main() { + log_info "Installing Brave Browser..." + + if pacman -Si brave-browser >/dev/null 2>&1; then + install_pacman brave-browser + elif pacman -Si brave-bin >/dev/null 2>&1; then + install_pacman brave-bin + else + install_aur brave-bin + fi +} diff --git a/modules/optional/packages/chromium.sh b/modules/optional/packages/chromium.sh new file mode 100755 index 0000000..bbd5720 --- /dev/null +++ b/modules/optional/packages/chromium.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +module_description() { printf "Install Chromium\n"; } +module_required() { return 1; } +module_should_skip() { return 1; } +module_prereqs() { return 0; } + +module_main() { + log_info "Installing Chromium..." + install_pacman chromium +} diff --git a/modules/optional/packages/neovim.sh b/modules/optional/packages/neovim.sh new file mode 100755 index 0000000..861ed7f --- /dev/null +++ b/modules/optional/packages/neovim.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +module_description() { printf "Install Neovim\n"; } +module_required() { return 1; } +module_should_skip() { return 1; } +module_prereqs() { return 0; } + +module_main() { + log_info "Installing Neovim..." + install_pacman neovim +} diff --git a/modules/optional/packages/obsidian.sh b/modules/optional/packages/obsidian.sh new file mode 100755 index 0000000..7830c3e --- /dev/null +++ b/modules/optional/packages/obsidian.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +module_description() { printf "Install Obsidian\n"; } +module_required() { return 1; } +module_should_skip() { return 1; } +module_prereqs() { return 0; } + +module_main() { + log_info "Installing Obsidian..." + if pacman -Si obsidian >/dev/null 2>&1; then + install_pacman obsidian + elif pacman -Si obsidian-bin >/dev/null 2>&1; then + install_pacman obsidian-bin + else + install_aur obsidian-bin + fi +} diff --git a/modules/optional/packages/pipewire-tools.sh b/modules/optional/packages/pipewire-tools.sh new file mode 100755 index 0000000..5c9dbda --- /dev/null +++ b/modules/optional/packages/pipewire-tools.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +module_description() { printf "Install PipeWire audio tools\n"; } +module_required() { return 1; } +module_should_skip() { return 1; } +module_prereqs() { return 0; } + +module_main() { + log_section "PipeWire Audio Tools" + + local packages=( + pipewire + pipewire-pulse + pipewire-alsa + pipewire-jack + wireplumber + pavucontrol + helvum + easyeffects + ) + + local to_install=() + local pkg + + for pkg in "${packages[@]}"; do + if pacman -Si "$pkg" >/dev/null 2>&1 && ! is_package_installed "$pkg"; then + to_install+=("$pkg") + fi + done + + if ((${#to_install[@]})); then + log_info "Installing PipeWire tools: ${to_install[*]}" + install_pacman "${to_install[@]}" + log_success "PipeWire tools installed" + else + log_info "All PipeWire tools already installed" + fi + + if have systemctl; then + systemctl --user enable --now pipewire pipewire-pulse wireplumber 2>/dev/null || true + log_info "PipeWire services enabled" + fi +} diff --git a/modules/optional/packages/spotify.sh b/modules/optional/packages/spotify.sh new file mode 100755 index 0000000..86b8778 --- /dev/null +++ b/modules/optional/packages/spotify.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +module_description() { printf "Install Spotify\n"; } +module_required() { return 1; } +module_should_skip() { return 1; } +module_prereqs() { return 0; } + +module_main() { + log_info "Installing Spotify..." + + if pacman -Si spotify >/dev/null 2>&1; then + install_pacman spotify + elif pacman -Si spotify-launcher >/dev/null 2>&1; then + install_pacman spotify-launcher + else + install_aur spotify-launcher + fi +} diff --git a/modules/optional/packages/visual-studio-code.sh b/modules/optional/packages/visual-studio-code.sh new file mode 100755 index 0000000..2dc12e6 --- /dev/null +++ b/modules/optional/packages/visual-studio-code.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +module_description() { printf "Install Visual Studio Code\n"; } +module_required() { return 1; } +module_should_skip() { return 1; } +module_prereqs() { return 0; } + +module_main() { + log_info "Installing Visual Studio Code..." + + if pacman -Si code >/dev/null 2>&1; then + install_pacman code + elif pacman -Si visual-studio-code-bin >/dev/null 2>&1; then + install_pacman visual-studio-code-bin + else + install_aur visual-studio-code-bin + fi +} diff --git a/modules/optional/packages/vlc.sh b/modules/optional/packages/vlc.sh new file mode 100755 index 0000000..ea23155 --- /dev/null +++ b/modules/optional/packages/vlc.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +module_description() { printf "Install VLC\n"; } +module_required() { return 1; } +module_should_skip() { return 1; } +module_prereqs() { return 0; } + +module_main() { + log_info "Installing VLC..." + install_pacman vlc +} diff --git a/modules/post/apply-theme.sh b/modules/post/apply-theme.sh new file mode 100755 index 0000000..b34bb83 --- /dev/null +++ b/modules/post/apply-theme.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +module_description() { + printf "Apply Theme - set default Hyprland theme and restart services\n" +} + +module_required() { return 1; } +module_should_skip() { return 1; } + +module_prereqs() { + if ! have hyprctl && ! have notify-send; then + log_warn "Neither hyprctl nor notify-send found. Theme can still be applied later." + return 0 + fi +} + +module_main() { + log_section "Theme Application" + + local theme_script="$HOME/.config/hypr/Scripts/theme-menu.sh" + local themes_dir="$HOME/.config/hypr/Themes" + + if [[ ! -f "$theme_script" ]]; then + log_warn "Theme script not found (dotfiles may not be deployed yet)" + return 1 + fi + + local default_theme="${OMERON_DEFAULT_THEME:-forest-neon}" + + local theme_file="" + if [[ -f "$themes_dir/$default_theme.theme" ]]; then + theme_file="$themes_dir/$default_theme.theme" + elif [[ -f "$themes_dir/forest-neon.theme" ]]; then + theme_file="$themes_dir/forest-neon.theme" + elif [[ -f "$themes_dir/rose-night.theme" ]]; then + theme_file="$themes_dir/rose-night.theme" + else + local available + available="$(find "$themes_dir" -name '*.theme' -type f | head -1)" + if [[ -n "$available" ]]; then + theme_file="$available" + fi + fi + + if [[ -z "$theme_file" ]]; then + log_warn "No theme files found in $themes_dir" + return 1 + fi + + if tui_confirm "Apply theme: $(basename "$theme_file" .theme).theme?"; then + log_info "Applying theme: $(basename "$theme_file")" + tui_spin "Applying theme..." bash "$theme_script" --apply "$theme_file" + + if have notify-send; then + notify-send "Omeron" "Theme applied: $(basename "$theme_file" .theme)" >/dev/null 2>&1 || true + fi + + log_success "Theme applied: $(basename "$theme_file" .theme)" + fi +}