commit 6eb922c41796d6118edfbad9142195f6d7c19951 Author: Pascal Date: Tue Apr 28 03:59:07 2026 +0200 Initial ThinkPad Hyprland dotfiles diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0566416 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +*.swp +*.swo +*~ +__pycache__/ +*.pyc +.cache/ +.codex +*.bak +*.before-theme-switcher diff --git a/README.md b/README.md new file mode 100644 index 0000000..aae1a86 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Thinkpad Hyprland Dotfiles + +Personal Hyprland rice for Pascal's ThinkPad setup. + +## Included + +- Hyprland, Hyprlock, Hyprpaper +- AGS switcher and Homelab Controlcenter +- Waybar, Wofi, SwayNC, Kitty +- GTK 3/4, Qt5ct/Qt6ct, Starship theming +- Rose Night and Forest Neon themes +- Bundled wallpapers used by the themes +- Optional SDDM theme + +## Install + +```bash +git clone /Thinkpad-Hyprland-Dotfiles.git +cd Thinkpad-Hyprland-Dotfiles +./install.sh +``` + +The installer backs up replaced files to: + +```text +~/.dotfiles-backup/YYYYMMDD-HHMMSS +``` + +Useful options: + +```bash +./install.sh --skip-packages +./install.sh --with-sddm +./install.sh --no-apply-theme +``` + +## Notes + +This repo targets an Arch/CachyOS-style system. The installer uses `pacman` first and tries `paru` or `yay` for packages that are not available in configured pacman repositories. + +The dotfiles were captured from `/home/pascal`; during install, absolute `/home/pascal` paths inside the copied configs are rewritten to the current `$HOME`. diff --git a/config/gtk-3.0/bookmarks b/config/gtk-3.0/bookmarks new file mode 100644 index 0000000..a6b264b --- /dev/null +++ b/config/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/config/gtk-3.0/gtk.css b/config/gtk-3.0/gtk.css new file mode 100644 index 0000000..e8b810f --- /dev/null +++ b/config/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/config/gtk-3.0/settings.ini b/config/gtk-3.0/settings.ini new file mode 100644 index 0000000..279b5e6 --- /dev/null +++ b/config/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/config/gtk-4.0/gtk.css b/config/gtk-4.0/gtk.css new file mode 100644 index 0000000..e8b810f --- /dev/null +++ b/config/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/config/gtk-4.0/settings.ini b/config/gtk-4.0/settings.ini new file mode 100644 index 0000000..279b5e6 --- /dev/null +++ b/config/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/config/hypr/Scripts/ags-switcher.sh b/config/hypr/Scripts/ags-switcher.sh new file mode 100755 index 0000000..d610268 --- /dev/null +++ b/config/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/config/hypr/Scripts/appearance-menu.sh b/config/hypr/Scripts/appearance-menu.sh new file mode 100755 index 0000000..af800e7 --- /dev/null +++ b/config/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/config/hypr/Scripts/audio-menu.sh b/config/hypr/Scripts/audio-menu.sh new file mode 100755 index 0000000..2b2ba39 --- /dev/null +++ b/config/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/config/hypr/Scripts/bluetooth-menu.sh b/config/hypr/Scripts/bluetooth-menu.sh new file mode 100755 index 0000000..5bb758e --- /dev/null +++ b/config/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/config/hypr/Scripts/dev-menu.sh b/config/hypr/Scripts/dev-menu.sh new file mode 100755 index 0000000..ba82c5a --- /dev/null +++ b/config/hypr/Scripts/dev-menu.sh @@ -0,0 +1,369 @@ +#!/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 +} + +choice="$( + printf '%s\n' \ + "📁 Projekt Management" \ + "󰌘 Homelab Controlcenter" \ + "🐳 Docker Control" \ + " 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 + ;; + *"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/config/hypr/Scripts/display-menu.sh b/config/hypr/Scripts/display-menu.sh new file mode 100755 index 0000000..0136665 --- /dev/null +++ b/config/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/config/hypr/Scripts/homelab-control.sh b/config/hypr/Scripts/homelab-control.sh new file mode 100755 index 0000000..cc2517f --- /dev/null +++ b/config/hypr/Scripts/homelab-control.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +HYPR_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" + +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 + +cd "$HYPR_DIR" +ags quit --instance homelab-control >/dev/null 2>&1 || true +exec ags run "$HYPR_DIR/ags/homelab.tsx" diff --git a/config/hypr/Scripts/main-menu.sh b/config/hypr/Scripts/main-menu.sh new file mode 100755 index 0000000..193f89c --- /dev/null +++ b/config/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/config/hypr/Scripts/network-menu.sh b/config/hypr/Scripts/network-menu.sh new file mode 100755 index 0000000..bcacfdf --- /dev/null +++ b/config/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/config/hypr/Scripts/power-menu.py b/config/hypr/Scripts/power-menu.py new file mode 100755 index 0000000..b8fb838 --- /dev/null +++ b/config/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/config/hypr/Scripts/screenshot-menu.sh b/config/hypr/Scripts/screenshot-menu.sh new file mode 100755 index 0000000..f1c6d9d --- /dev/null +++ b/config/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/config/hypr/Scripts/settings-menu.sh b/config/hypr/Scripts/settings-menu.sh new file mode 100755 index 0000000..c9acbe7 --- /dev/null +++ b/config/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/config/hypr/Scripts/system-menu.sh b/config/hypr/Scripts/system-menu.sh new file mode 100755 index 0000000..6ee92ff --- /dev/null +++ b/config/hypr/Scripts/system-menu.sh @@ -0,0 +1,77 @@ +#!/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" \ + "󰗽 Bildschirm heller" \ + "󰗾 Bildschirm dunkler" \ + "󰍃 Session beenden" | + wofi --dmenu --prompt "󰒓 System" --insensitive +)" + +case "$choice" in + *"Hyprland neu laden"*) + hyprctl reload + ;; + *"Waybar neu starten"*) + restart_waybar + ;; + *"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/config/hypr/Scripts/theme-menu.sh b/config/hypr/Scripts/theme-menu.sh new file mode 100755 index 0000000..b1fa5e3 --- /dev/null +++ b/config/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/config/hypr/Scripts/toggle-wofi.sh b/config/hypr/Scripts/toggle-wofi.sh new file mode 100755 index 0000000..09d1b6a --- /dev/null +++ b/config/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/config/hypr/Scripts/wallpaper-menu.sh b/config/hypr/Scripts/wallpaper-menu.sh new file mode 100755 index 0000000..125b25c --- /dev/null +++ b/config/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/config/hypr/Themes/forest-neon.theme b/config/hypr/Themes/forest-neon.theme new file mode 100644 index 0000000..44a4109 --- /dev/null +++ b/config/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/config/hypr/Themes/rose-night.theme b/config/hypr/Themes/rose-night.theme new file mode 100644 index 0000000..3966fd1 --- /dev/null +++ b/config/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/config/hypr/ags/homelab.css b/config/hypr/ags/homelab.css new file mode 100644 index 0000000..4a3faf5 --- /dev/null +++ b/config/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/config/hypr/ags/homelab.tsx b/config/hypr/ags/homelab.tsx new file mode 100644 index 0000000..67f2e84 --- /dev/null +++ b/config/hypr/ags/homelab.tsx @@ -0,0 +1,1124 @@ +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"; + +const UNRAID_HOST = "10.0.0.15"; +const UNRAID_USER = "root"; +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/config/hypr/ags/switcher.css b/config/hypr/ags/switcher.css new file mode 100644 index 0000000..64eb502 --- /dev/null +++ b/config/hypr/ags/switcher.css @@ -0,0 +1,115 @@ +* { + all: unset; + font-family: "JetBrainsMono Nerd Font", "Noto Sans", sans-serif; + font-size: 14px; +} + +.switcher-window { + background: transparent; +} + +.switcher { + min-width: 680px; + padding: 18px; + border: 1px solid alpha(@ags_fg, 0.18); + border-radius: 16px; + background: @ags_bg; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45); + color: @ags_fg; +} + +.header { + min-height: 34px; +} + +.title { + font-size: 20px; + font-weight: 700; +} + +.list-scroll { + min-height: 130px; +} + +.item { + padding: 10px; + border: 1px solid alpha(@ags_fg, 0.10); + border-radius: 10px; + background: alpha(@ags_panel, 0.54); +} + +.item-row { + min-height: 78px; +} + +.item:hover, +.item:focus { + border-color: alpha(@ags_accent, 0.72); + background: alpha(@ags_panel, 0.82); +} + +.item.active { + border-color: alpha(@ags_accent_2, 0.85); +} + +.preview { + min-width: 96px; + min-height: 56px; + border-radius: 8px; + background-color: alpha(@ags_panel, 0.80); + background-size: cover; + background-position: center; +} + +.preview-empty { + color: @ags_accent_2; +} + +.item-title { + font-size: 15px; + font-weight: 700; +} + +.item-subtitle { + margin-top: 4px; + color: @ags_muted; + font-size: 12px; +} + +.swatches { + min-width: 86px; +} + +.swatch { + min-width: 18px; + min-height: 18px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.16); +} + +.icon-button { + min-width: 34px; + min-height: 34px; + border-radius: 8px; + color: @ags_fg; + background: alpha(@ags_panel, 0.62); +} + +.preview-button { + min-width: 44px; + min-height: 78px; +} + +.icon-button:hover, +.icon-button:focus { + background: alpha(@ags_accent, 0.28); +} + +.close { + color: @ags_accent; +} + +.empty { + min-height: 150px; + color: @ags_muted; +} diff --git a/config/hypr/ags/switcher.tsx b/config/hypr/ags/switcher.tsx new file mode 100644 index 0000000..bf49729 --- /dev/null +++ b/config/hypr/ags/switcher.tsx @@ -0,0 +1,321 @@ +import app from "ags/gtk4/app"; +import { Astal, Gtk } from "ags/gtk4"; +import { readFile } from "ags/file"; +import { execAsync } from "ags/process"; +import css from "./switcher.css"; +import GLib from "gi://GLib"; + +type SwitcherItem = { + type: "theme" | "wallpaper"; + path: string; + name: string; + icon?: string; + wallpaper: string; + active?: boolean; + accent?: string; + accent2?: string; + muted?: string; +}; + +const HYPR_DIR = GLib.getenv("HYPR_DIR") || "/home/pascal/.config/hypr"; +const SCRIPT_DIR = `${HYPR_DIR}/Scripts`; +const THEME_DIR = GLib.getenv("HYPR_SWITCHER_THEME_DIR") || `${HYPR_DIR}/Themes`; +const WALLPAPER_DIR = GLib.getenv("HYPR_SWITCHER_WALLPAPER_DIR") + || GLib.getenv("WALLPAPER_DIR") + || `${GLib.get_home_dir()}/Bilder/Wallpaper`; +const CURRENT_WALLPAPER = `${HYPR_DIR}/current-wallpaper`; +const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif"]; + +type UiTheme = { + accent: string; + accent2: string; + background: string; + backgroundSoft: string; + foreground: string; + muted: string; + panelHex: string; +}; + +function notify(message: string) { + execAsync(["notify-send", "AGS Switcher", message]).catch(console.error); +} + +function fileExists(path: string) { + return GLib.file_test(path, GLib.FileTest.EXISTS); +} + +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 activeTheme(): UiTheme { + const activeWallpaper = currentWallpaper(); + const fallback = { + accent: "#00ff9c", + accent2: "#00cc88", + background: "rgba(20, 20, 30, 0.95)", + backgroundSoft: "rgba(40, 40, 55, 0.8)", + foreground: "#cdd6f4", + muted: "#cccccc", + panelHex: "#282837", + }; + + 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 basename(path: string) { + return GLib.path_get_basename(path); +} + +function currentWallpaper() { + return readText(CURRENT_WALLPAPER).trim(); +} + +function loadThemes(): SwitcherItem[] { + return listFiles(THEME_DIR, (_path, name) => name.endsWith(".theme")) + .map(path => { + const contents = readText(path); + return { + type: "theme", + path, + name: shellValue(contents, "NAME") || basename(path).replace(/\.theme$/, ""), + icon: shellValue(contents, "ICON") || "󰌪", + wallpaper: shellValue(contents, "WALLPAPER"), + accent: shellValue(contents, "ACCENT") || "#f38ba8", + accent2: shellValue(contents, "ACCENT_2") || "#cba6f7", + muted: shellValue(contents, "MUTED") || "#cdd6f4", + }; + }); +} + +function loadWallpapers(): SwitcherItem[] { + const active = currentWallpaper(); + + return listFiles(WALLPAPER_DIR, (_path, name) => + IMAGE_EXTENSIONS.some(ext => name.toLowerCase().endsWith(ext)), + ).map(path => ({ + type: "wallpaper", + path, + name: basename(path), + wallpaper: path, + active: path === active, + })); +} + +function applyItem(item: SwitcherItem) { + const command = item.type === "theme" + ? [`${SCRIPT_DIR}/theme-menu.sh`, "--apply", item.path] + : [`${SCRIPT_DIR}/wallpaper-menu.sh`, "--apply", item.path]; + + execAsync(command) + .then(() => app.quit()) + .catch(error => { + console.error(error); + notify(`${item.name} konnte nicht angewendet werden.`); + }); +} + +function previewWallpaper(item: SwitcherItem) { + execAsync([`${SCRIPT_DIR}/wallpaper-menu.sh`, "--preview", item.path]).catch(error => { + console.error(error); + notify(`${item.name} konnte nicht geoeffnet werden.`); + }); +} + +function Preview({ item }: { item: SwitcherItem }) { + const hasImage = item.wallpaper && fileExists(item.wallpaper); + + return ( + + {hasImage + ? + : + ); +} + +function Swatches({ item }: { item: SwitcherItem }) { + return ( + + {[item.accent, item.accent2, item.muted].map(color => ( + + ))} + + ); +} + +function ItemButton({ item }: { item: SwitcherItem }) { + const content = ( + + + + + {item.type === "theme" ? : } + + ); + + const applyButton = ( + + ); + + 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/config/hypr/current-theme.conf b/config/hypr/current-theme.conf new file mode 100644 index 0000000..43dcf34 --- /dev/null +++ b/config/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/config/hypr/current-wallpaper b/config/hypr/current-wallpaper new file mode 100644 index 0000000..4cdb615 --- /dev/null +++ b/config/hypr/current-wallpaper @@ -0,0 +1 @@ +/home/pascal/Bilder/Wallpaper/forest.jpg diff --git a/config/hypr/hyprland.conf b/config/hypr/hyprland.conf new file mode 100644 index 0000000..c3591a6 --- /dev/null +++ b/config/hypr/hyprland.conf @@ -0,0 +1,308 @@ +# ####################################################################################### +# 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 + +############################# +### 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/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/config/hypr/hyprlock.conf b/config/hypr/hyprlock.conf new file mode 100644 index 0000000..cdf4cbe --- /dev/null +++ b/config/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/config/hypr/hyprpaper.conf b/config/hypr/hyprpaper.conf new file mode 100644 index 0000000..c06409c --- /dev/null +++ b/config/hypr/hyprpaper.conf @@ -0,0 +1,5 @@ +wallpaper { + monitor = + path = /home/pascal/Bilder/Wallpaper/forest.jpg + fit_mode = cover +} diff --git a/config/hypr/sddm-theme/pascal-hypr/Main.qml b/config/hypr/sddm-theme/pascal-hypr/Main.qml new file mode 100644 index 0000000..fdd22b9 --- /dev/null +++ b/config/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/config/hypr/sddm-theme/pascal-hypr/metadata.desktop b/config/hypr/sddm-theme/pascal-hypr/metadata.desktop new file mode 100644 index 0000000..0696ac5 --- /dev/null +++ b/config/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/config/hypr/sddm-theme/pascal-hypr/theme.conf b/config/hypr/sddm-theme/pascal-hypr/theme.conf new file mode 100644 index 0000000..84f3523 --- /dev/null +++ b/config/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/config/hypr/sddm-theme/sddm.conf b/config/hypr/sddm-theme/sddm.conf new file mode 100644 index 0000000..452b39d --- /dev/null +++ b/config/hypr/sddm-theme/sddm.conf @@ -0,0 +1,6 @@ +[Autologin] +Session=hyprland + +[Theme] +Current=pascal-hypr + diff --git a/config/kitty/kitty.conf b/config/kitty/kitty.conf new file mode 100644 index 0000000..5e93e99 --- /dev/null +++ b/config/kitty/kitty.conf @@ -0,0 +1 @@ +background_opacity 0.70 diff --git a/config/qt5ct/colors/ForestNeon.conf b/config/qt5ct/colors/ForestNeon.conf new file mode 100644 index 0000000..88ff7ee --- /dev/null +++ b/config/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/config/qt5ct/colors/RoseNight.conf b/config/qt5ct/colors/RoseNight.conf new file mode 100644 index 0000000..74004c3 --- /dev/null +++ b/config/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/config/qt5ct/qt5ct.conf b/config/qt5ct/qt5ct.conf new file mode 100644 index 0000000..a0c2305 --- /dev/null +++ b/config/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/config/qt6ct/colors/ForestNeon.conf b/config/qt6ct/colors/ForestNeon.conf new file mode 100644 index 0000000..88ff7ee --- /dev/null +++ b/config/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/config/qt6ct/colors/RoseNight.conf b/config/qt6ct/colors/RoseNight.conf new file mode 100644 index 0000000..74004c3 --- /dev/null +++ b/config/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/config/qt6ct/qt6ct.conf b/config/qt6ct/qt6ct.conf new file mode 100644 index 0000000..7f41693 --- /dev/null +++ b/config/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/config/starship.toml b/config/starship.toml new file mode 100644 index 0000000..f6bf187 --- /dev/null +++ b/config/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/config/swaync/config.json b/config/swaync/config.json new file mode 100644 index 0000000..4df5c5e --- /dev/null +++ b/config/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/config/swaync/style.css b/config/swaync/style.css new file mode 100644 index 0000000..35869a9 --- /dev/null +++ b/config/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/config/waybar/config.jsonc b/config/waybar/config.jsonc new file mode 100644 index 0000000..8638fb7 --- /dev/null +++ b/config/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/config/waybar/scripts/island-media.sh b/config/waybar/scripts/island-media.sh new file mode 100755 index 0000000..5251f54 --- /dev/null +++ b/config/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/config/waybar/style.css b/config/waybar/style.css new file mode 100644 index 0000000..9fa6586 --- /dev/null +++ b/config/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/config/wofi/config b/config/wofi/config new file mode 100644 index 0000000..8303cbf --- /dev/null +++ b/config/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/config/wofi/style.css b/config/wofi/style.css new file mode 100644 index 0000000..58ad703 --- /dev/null +++ b/config/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/home/Bilder/Wallpaper/forest.jpg b/home/Bilder/Wallpaper/forest.jpg new file mode 100644 index 0000000..afcbdbe Binary files /dev/null and b/home/Bilder/Wallpaper/forest.jpg differ diff --git a/home/Bilder/Wallpaper/rose-pink.jpg b/home/Bilder/Wallpaper/rose-pink.jpg new file mode 100644 index 0000000..2480908 Binary files /dev/null and b/home/Bilder/Wallpaper/rose-pink.jpg differ diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..af64e61 --- /dev/null +++ b/install.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +BACKUP_DIR="${BACKUP_DIR:-$HOME/.dotfiles-backup/$(date +%Y%m%d-%H%M%S)}" +ORIGINAL_HOME="/home/pascal" + +CONFIG_ITEMS=( + hypr + waybar + wofi + swaync + kitty + gtk-3.0 + gtk-4.0 + qt5ct + qt6ct +) + +PACMAN_PACKAGES=( + hyprland + hyprpaper + hyprlock + waybar + wofi + swaync + kitty + nautilus + ags + 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 +) + +SKIP_PACKAGES=0 +WITH_SDDM=0 +NO_APPLY_THEME=0 + +usage() { + printf '%s\n' \ + "Usage: ./install.sh [--skip-packages] [--with-sddm] [--no-apply-theme]" \ + "" \ + " --skip-packages Only install dotfiles, do not install packages." \ + " --with-sddm Install the bundled SDDM theme system-wide." \ + " --no-apply-theme Do not run the Hypr theme script after copying." +} + +for arg in "$@"; do + case "$arg" in + --skip-packages) SKIP_PACKAGES=1 ;; + --with-sddm) WITH_SDDM=1 ;; + --no-apply-theme) NO_APPLY_THEME=1 ;; + -h|--help) usage; exit 0 ;; + *) printf 'Unknown option: %s\n' "$arg" >&2; usage; exit 2 ;; + esac +done + +log() { + printf '\033[1;32m==>\033[0m %s\n' "$1" +} + +warn() { + printf '\033[1;33mWARN:\033[0m %s\n' "$1" >&2 +} + +have() { + command -v "$1" >/dev/null 2>&1 +} + +backup_path() { + local target="$1" + [[ -e "$target" || -L "$target" ]] || return 0 + + local relative="${target#"$HOME"/}" + local backup="$BACKUP_DIR/$relative" + mkdir -p "$(dirname "$backup")" + cp -a "$target" "$backup" +} + +install_path() { + local source="$1" + local target="$2" + + backup_path "$target" + rm -rf "$target" + mkdir -p "$(dirname "$target")" + cp -a "$source" "$target" +} + +install_packages() { + if (( SKIP_PACKAGES )); then + log "Skipping package installation." + return + fi + + if ! have pacman; then + warn "pacman not found; skipping package installation." + return + fi + + log "Installing available pacman packages." + local installable=() + local missing=() + local pkg + + for pkg in "${PACMAN_PACKAGES[@]}"; do + if pacman -Si "$pkg" >/dev/null 2>&1; then + installable+=("$pkg") + else + missing+=("$pkg") + fi + done + + if ((${#installable[@]})); then + sudo pacman -S --needed "${installable[@]}" + fi + + if ((${#missing[@]})); then + if have paru; then + log "Trying missing packages through paru: ${missing[*]}" + paru -S --needed "${missing[@]}" || warn "Some optional AUR packages were not installed." + elif have yay; then + log "Trying missing packages through yay: ${missing[*]}" + yay -S --needed "${missing[@]}" || warn "Some optional AUR packages were not installed." + else + warn "Missing from pacman and no paru/yay found: ${missing[*]}" + fi + fi +} + +rewrite_home_paths() { + log "Rewriting absolute home paths." + local file + while IFS= read -r -d '' file; do + sed -i "s#${ORIGINAL_HOME}#${HOME}#g" "$file" + done < <( + find "$HOME/.config/hypr" "$HOME/.config/gtk-3.0" "$HOME/.config/gtk-4.0" \ + "$HOME/.config/qt5ct" "$HOME/.config/qt6ct" \ + -type f -print0 2>/dev/null + ) +} + +install_dotfiles() { + log "Installing dotfiles with backup at $BACKUP_DIR." + mkdir -p "$HOME/.config" + + local item + for item in "${CONFIG_ITEMS[@]}"; do + install_path "$REPO_DIR/config/$item" "$HOME/.config/$item" + done + + install_path "$REPO_DIR/config/starship.toml" "$HOME/.config/starship.toml" + install_path "$REPO_DIR/home/Bilder/Wallpaper" "$HOME/Bilder/Wallpaper" + + chmod +x "$HOME/.config/hypr"/Scripts/*.sh + chmod +x "$HOME/.config/hypr"/Scripts/*.py + chmod +x "$HOME/.config/waybar"/scripts/*.sh 2>/dev/null || true + + rewrite_home_paths +} + +install_sddm() { + (( WITH_SDDM )) || return 0 + + log "Installing SDDM theme." + sudo mkdir -p /usr/share/sddm/themes /etc/sddm.conf.d + sudo cp -a "$REPO_DIR/config/hypr/sddm-theme/pascal-hypr" /usr/share/sddm/themes/ + sudo cp -a "$REPO_DIR/config/hypr/sddm-theme/sddm.conf" /etc/sddm.conf.d/10-pascal-hypr.conf +} + +enable_services() { + if have systemctl; then + sudo systemctl enable --now NetworkManager.service >/dev/null 2>&1 || true + sudo systemctl enable --now bluetooth.service >/dev/null 2>&1 || true + fi +} + +apply_theme() { + (( NO_APPLY_THEME )) && return 0 + + local theme="$HOME/.config/hypr/Themes/rose-night.theme" + [[ -f "$theme" ]] || theme="$HOME/.config/hypr/Themes/forest-neon.theme" + [[ -f "$theme" ]] || return 0 + + log "Applying theme: $(basename "$theme")" + "$HOME/.config/hypr/Scripts/theme-menu.sh" --apply "$theme" || warn "Theme apply failed; dotfiles are still installed." +} + +main() { + install_packages + install_dotfiles + install_sddm + enable_services + apply_theme + + log "Done. Start Hyprland or reload with: hyprctl reload" +} + +main "$@"