From 7dc46ff89badb6e355928456fbfbf8696e4b37ea Mon Sep 17 00:00:00 2001
From: Leriart
Date: Wed, 10 Jun 2026 21:47:28 -0600
Subject: [PATCH 01/14] feat: native wallpaper renderer, per-screen support,
performance & bug fixes over upstream v0.1.0
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Files added (not in upstream)
- src/windows/Wallpaper.qml — Native QML wallpaper renderer (Image/GIF/Video)
- src/windows/ScreenCorners.qml — Rounded screen corner masks
- src/shapes/RoundCorner.qml — Corner mask shape
- src/services/AppSearch.qml — Native DesktopEntries app provider + fuzzy search
- src/services/FocusGrabManager.qml — Popup focus coordination
- src/services/GpuDetector.qml — GPU vendor detection (NVIDIA/AMD/Intel)
- src/services/HyprlandService.qml — Centralized Hyprland compositor integration
- src/services/HyprlandSyncService.qml — Keybind sync to hyprland.lua
- src/services/IdleService.qml — User idle time monitoring
- src/services/ScreenshotTool.qml — Screenshot capture tool
- src/services/ShellConfigService.qml — Reactive JSON config persistence
- src/services/UsageTracker.qml — App launch frequency tracking
- src/services/VideoWallpaperService.qml — HW-accelerated video optimization
- src/theme/Anim.qml — Unified animation system (durations, easing curves)
- src/components/AnimatedBehavior.qml — Reusable animation Behavior wrapper
- src/components/StateLayer.qml — M3 interaction overlay (hover/press/ripple)
- src/components/StyledRect.qml — Themed rectangle container
- src/components/Surface.qml — Elevated surface component
- src/components/qmldir — Component type registrations
- src/scripts/colorpicker.py — Screen color picker
- src/scripts/lockwall.py — Lockscreen wallpaper frame generator
- src/scripts/thumbgen.py + thumbgen_batch.py — Thumbnail generators
- src/config/shaders/video/ — Motion interpolation + palette tint shaders
- src/config/templates/ — App theming templates (discord, gtk, kitty, nvchad)
- src/config/matugen-apps.toml — matugen app theming config
- cli.sh — Standalone CLI launcher & manager
- wrapper.sh — QS wrapper script
- version + .gitignore + AGENTS.md
## Files removed (from upstream)
- src/scripts/list_apps.py — Replaced by native AppSearch DesktopEntries
## Files modified
- shell.qml — Added Wallpaper, ScreenCorners, HyprlandSyncService
- src/qmldir — 12 new singleton registrations
- src/services/qmldir — IdleService, GpuDetector, AppSearch, UsageTracker
- src/services/WallpaperService.qml — Thumbnails, per-screen, filters, watcher, matugen -j fix
- src/services/UpdateService.qml — Git repo pre-check
- src/services/config_tab/KeybindService.qml — Disabled duplicate _ensureInclude
- src/services/config_tab/ShellConfig.qml — Restored upstream placeholder tabs
- src/services/system/EnvyControlService.qml — Binary pre-check
- src/services/HyprlandSyncService.qml — Disabled bind skipping, overrides fix
- src/state/IpcManager.qml — QVariant IPC fix, removed dead commands
- src/state/Popups.qml — Removed aiOpen
- src/state/ShellState.qml — Wallpaper rendering toggles
- src/theme/ColorLoader.qml — 3s polling fallback
- src/theme/Metrics.qml — Reactive barEnabled from ShellConfigService
- src/theme/Theme.qml + qmldir — Aggregated Colors + Metrics + Anim
- src/popups/Dashboard.qml — Restored original visibility-toggle pattern
- src/popups/WallpaperPopup.qml — Thumbnails, filters, viewport-aware loading
- src/popups/PopupLayer.qml — Removed AssistantSidebar
- src/components/TabSwitcher.qml — Anim.standardSmall + theme import
- src/windows/TopBar.qml, Border.qml — Minor improvements
- flake.nix — PR#60: flake-utils follows nixpkgs, NixOS module cleanup
- flake.lock — Updated nixpkgs-unstable + flake-utils
- Various popups — Animation fixes (Theme.animDuration → Anim.*)
---
.github/workflows/ci.yml | 245 ++++
.gitignore | 1 +
AGENTS.md | 365 ++++++
LICENSE | 21 +
README.md | 311 +++++
cli.sh | 744 +++++++++++
dots-extra/install-arch.sh | 519 ++++++++
dots-extra/validate-install.sh | 211 ++++
flake.lock | 61 +
flake.nix | 195 +++
install.sh | 390 ++++++
shell.qml | 66 +-
.../wallpapers/brain-shell-default-0.png | Bin 0 -> 30593 bytes
.../wallpapers/brain-shell-default-1.png | Bin 0 -> 1598744 bytes
.../wallpapers/brain-shell-default-2.jpg | Bin 0 -> 565022 bytes
.../wallpapers/brain-shell-default-3.jpg | Bin 0 -> 1235111 bytes
.../wallpapers/brain-shell-default-4.jpg | Bin 0 -> 2936580 bytes
.../wallpapers/brain-shell-default-5.jpg | Bin 0 -> 181340 bytes
src/components/AnimatedBehavior.qml | 55 +
src/components/DiskBar.qml | 89 ++
src/components/IconBtn.qml | 4 +-
src/components/PopupPage.qml | 63 +
src/components/PopupSlide.qml | 122 ++
src/components/ProfileButton.qml | 66 +
src/components/Speedometer.qml | 137 +++
src/components/StatCard.qml | 34 +
src/components/StatRow.qml | 32 +
src/components/StateLayer.qml | 53 +
src/components/StyledRect.qml | 66 +
src/components/Surface.qml | 43 +
src/components/TabSwitcher.qml | 232 ++++
src/components/TimeInput.qml | 150 +++
src/components/qmldir | 14 +
src/config/brain-shell-colors.json.example | 8 +
src/config/colors.conf.template | 13 +
src/config/hypridle.conf | 27 +
src/config/hyprlock.conf | 151 +++
src/config/matugen-apps.toml | 49 +
src/config/matugen.toml | 10 +
src/config/shaders/Chroma.glsl | 19 +
src/config/shaders/Grayscale.glsl | 13 +
src/config/shaders/HDR.glsl | 20 +
src/config/shaders/HighContrast.glsl | 15 +
src/config/shaders/Sepia.glsl | 17 +
src/config/shaders/invert-colors.glsl | 11 +
src/config/shaders/video/interpol.frag | 172 +++
src/config/shaders/video/interpol.frag.qsb | Bin 0 -> 9390 bytes
src/config/shaders/video/interpol.vert | 22 +
src/config/shaders/video/interpol.vert.qsb | Bin 0 -> 1941 bytes
src/config/shaders/video/palette.frag | 64 +
src/config/shaders/video/palette.frag.qsb | Bin 0 -> 3148 bytes
src/config/shaders/video/palette.vert | 19 +
src/config/shaders/video/palette.vert.qsb | Bin 0 -> 1754 bytes
src/config/templates/discord.css | 24 +
src/config/templates/gtk-colors.css | 24 +
src/config/templates/kitty.conf | 41 +
src/config/templates/nvchad.lua | 53 +
src/modules/Center/CenterContent.qml | 915 +++++++++++++-
src/modules/Center/DashStats.qml | 163 +++
src/modules/Center/DiskPanel.qml | 121 ++
src/modules/Center/FanPanel.qml | 46 +
src/modules/Center/NetStatsPanel.qml | 48 +
src/modules/Center/PowerPanel.qml | 96 ++
src/modules/Center/TempPanel.qml | 82 ++
src/modules/Left/ControlPanel.qml | 12 +-
src/modules/Left/LayoutDisplayer.qml | 180 +++
src/modules/Left/LeftContent.qml | 25 +-
src/modules/Left/Performance.qml | 10 -
src/modules/Left/Workspaces.qml | 100 +-
src/modules/Right/Audio.qml | 88 +-
src/modules/Right/Battery.qml | 32 +-
src/modules/Right/Clock.qml | 83 +-
src/modules/Right/Network.qml | 172 ++-
src/modules/Right/Notifications.qml | 16 +-
src/modules/Right/RightContent.qml | 47 +-
src/modules/Right/SysTray.qml | 95 +-
src/popups/ArchMenu.qml | 115 ++
src/popups/AudioPopup.qml | 112 ++
src/popups/BluetoothTab.qml | 659 ++++++++++
src/popups/ClipboardPopup.qml | 107 ++
src/popups/Dashboard.qml | 220 ++++
src/popups/HistoryTab.qml | 639 ++++++++++
src/popups/HotspotTab.qml | 207 ++++
src/popups/NetworkPopup.qml | 179 +++
src/popups/NotificationToast.qml | 340 ++++++
src/popups/NotificationsPopup.qml | 128 ++
src/popups/PopupLayer.qml | 67 +
src/popups/QuickControl.qml | 318 +++++
src/popups/ScreenRecOptionsPopup.qml | 150 +++
src/popups/VPNTab.qml | 634 ++++++++++
src/popups/WallpaperPopup.qml | 917 ++++++++++++++
src/popups/WifiTab.qml | 622 ++++++++++
src/qmldir | 16 +-
src/scripts/GfxSwitch.sh | 3 +
src/scripts/PowerControl.sh | 13 +
src/scripts/colorpicker.py | 116 ++
src/scripts/lockwall.py | 149 +++
src/scripts/thumbgen.py | 75 ++
src/scripts/thumbgen_batch.py | 197 +++
src/services/AppLauncher.qml | 331 +++++
src/services/AppSearch.qml | 228 ++++
src/services/AudioControl.qml | 393 ++++++
src/services/BatteryStatus.qml | 157 +++
src/services/BatteryWarning.qml | 116 ++
src/services/CavaService.qml | 56 +
src/services/ClipboardService.qml | 242 ++++
src/services/FocusGrabManager.qml | 52 +
src/services/GpuDetector.qml | 77 ++
src/services/HyprlandService.qml | 201 +++
src/services/HyprlandSyncService.qml | 195 +++
src/services/IdleService.qml | 117 ++
src/services/KanbanBoard.qml | 1087 +++++++++++++++++
src/services/PowerMenu.qml | 129 ++
src/services/ScreenRecService.qml | 450 +++++++
src/services/ScreenshotTool.qml | 114 ++
src/services/ShellConfigService.qml | 102 ++
src/services/UpdateService.qml | 307 +++++
src/services/UsageTracker.qml | 76 ++
src/services/VideoWallpaperService.qml | 228 ++++
src/services/WallpaperService.qml | 344 ++++++
src/services/config_tab/KeybindService.qml | 407 ++++++
src/services/config_tab/KeybindsPage.qml | 609 +++++++++
src/services/config_tab/ShellConfig.qml | 85 ++
src/services/home/CalendarCard.qml | 171 +++
src/services/home/ClockCard.qml | 780 ++++++++++++
src/services/home/DashHome.qml | 115 ++
src/services/home/PlayerCard.qml | 502 ++++++++
src/services/home/ProfileCard.qml | 157 +++
src/services/home/QuickSettings.qml | 1034 ++++++++++++++++
src/services/home/qmldir | 5 +
.../notifications/NotificationList.qml | 366 ++++++
.../notifications/NotificationService.qml | 162 +++
src/services/qmldir | 30 +
src/services/system/CpuFreqService.qml | 140 +++
src/services/system/CpuService.qml | 68 ++
src/services/system/DiskService.qml | 79 ++
src/services/system/EnvyControl.qml | 172 +++
src/services/system/EnvyControlService.qml | 75 ++
src/services/system/FanControl.qml | 44 +
src/services/system/GpuService.qml | 132 ++
src/services/system/MemService.qml | 61 +
src/services/system/NetService.qml | 104 ++
src/services/system/SystemStats.qml | 123 ++
src/services/system/ThermalService.qml | 111 ++
src/shapes/NotchBackground.qml | 24 -
src/shapes/PopupShape.qml | 141 +++
src/shapes/RoundCorner.qml | 65 +
src/shapes/SeamlessBarShape.qml | 124 +-
src/state/ClockState.qml | 32 +
src/state/IpcManager.qml | 537 ++++++++
src/state/Popups.qml | 83 ++
src/state/ShellState.qml | 110 ++
src/theme/Anim.qml | 153 +++
src/theme/ColorLoader.qml | 59 +
src/theme/Colors.qml | 28 +
src/theme/Metrics.qml | 58 +
src/theme/Theme.qml | 112 +-
src/theme/qmldir | 5 +
src/windows/Border.qml | 197 ++-
src/windows/ConfirmDialog.qml | 328 +++++
src/windows/Notch.qml | 115 --
src/windows/PopupDismiss.qml | 93 ++
src/windows/ScreenCorners.qml | 85 ++
src/windows/TopBar.qml | 171 ++-
src/windows/UpdatePopup.qml | 437 +++++++
src/windows/Wallpaper.qml | 390 ++++++
version | 1 +
wrapper.sh | 23 +
168 files changed, 26743 insertions(+), 537 deletions(-)
create mode 100644 .github/workflows/ci.yml
create mode 100644 .gitignore
create mode 100644 AGENTS.md
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100755 cli.sh
create mode 100644 dots-extra/install-arch.sh
create mode 100644 dots-extra/validate-install.sh
create mode 100644 flake.lock
create mode 100644 flake.nix
create mode 100755 install.sh
create mode 100644 src/assets/wallpapers/brain-shell-default-0.png
create mode 100644 src/assets/wallpapers/brain-shell-default-1.png
create mode 100644 src/assets/wallpapers/brain-shell-default-2.jpg
create mode 100644 src/assets/wallpapers/brain-shell-default-3.jpg
create mode 100644 src/assets/wallpapers/brain-shell-default-4.jpg
create mode 100644 src/assets/wallpapers/brain-shell-default-5.jpg
create mode 100644 src/components/AnimatedBehavior.qml
create mode 100644 src/components/DiskBar.qml
create mode 100644 src/components/PopupPage.qml
create mode 100644 src/components/PopupSlide.qml
create mode 100644 src/components/ProfileButton.qml
create mode 100644 src/components/Speedometer.qml
create mode 100644 src/components/StatCard.qml
create mode 100644 src/components/StatRow.qml
create mode 100644 src/components/StateLayer.qml
create mode 100644 src/components/StyledRect.qml
create mode 100644 src/components/Surface.qml
create mode 100644 src/components/TabSwitcher.qml
create mode 100644 src/components/TimeInput.qml
create mode 100644 src/components/qmldir
create mode 100644 src/config/brain-shell-colors.json.example
create mode 100644 src/config/colors.conf.template
create mode 100644 src/config/hypridle.conf
create mode 100644 src/config/hyprlock.conf
create mode 100644 src/config/matugen-apps.toml
create mode 100644 src/config/matugen.toml
create mode 100644 src/config/shaders/Chroma.glsl
create mode 100644 src/config/shaders/Grayscale.glsl
create mode 100644 src/config/shaders/HDR.glsl
create mode 100644 src/config/shaders/HighContrast.glsl
create mode 100644 src/config/shaders/Sepia.glsl
create mode 100644 src/config/shaders/invert-colors.glsl
create mode 100755 src/config/shaders/video/interpol.frag
create mode 100755 src/config/shaders/video/interpol.frag.qsb
create mode 100755 src/config/shaders/video/interpol.vert
create mode 100755 src/config/shaders/video/interpol.vert.qsb
create mode 100755 src/config/shaders/video/palette.frag
create mode 100755 src/config/shaders/video/palette.frag.qsb
create mode 100755 src/config/shaders/video/palette.vert
create mode 100755 src/config/shaders/video/palette.vert.qsb
create mode 100644 src/config/templates/discord.css
create mode 100644 src/config/templates/gtk-colors.css
create mode 100644 src/config/templates/kitty.conf
create mode 100644 src/config/templates/nvchad.lua
create mode 100644 src/modules/Center/DashStats.qml
create mode 100644 src/modules/Center/DiskPanel.qml
create mode 100644 src/modules/Center/FanPanel.qml
create mode 100644 src/modules/Center/NetStatsPanel.qml
create mode 100644 src/modules/Center/PowerPanel.qml
create mode 100644 src/modules/Center/TempPanel.qml
create mode 100644 src/modules/Left/LayoutDisplayer.qml
delete mode 100644 src/modules/Left/Performance.qml
create mode 100644 src/popups/ArchMenu.qml
create mode 100644 src/popups/AudioPopup.qml
create mode 100644 src/popups/BluetoothTab.qml
create mode 100644 src/popups/ClipboardPopup.qml
create mode 100644 src/popups/Dashboard.qml
create mode 100644 src/popups/HistoryTab.qml
create mode 100644 src/popups/HotspotTab.qml
create mode 100644 src/popups/NetworkPopup.qml
create mode 100644 src/popups/NotificationToast.qml
create mode 100644 src/popups/NotificationsPopup.qml
create mode 100644 src/popups/PopupLayer.qml
create mode 100644 src/popups/QuickControl.qml
create mode 100644 src/popups/ScreenRecOptionsPopup.qml
create mode 100644 src/popups/VPNTab.qml
create mode 100644 src/popups/WallpaperPopup.qml
create mode 100644 src/popups/WifiTab.qml
create mode 100755 src/scripts/GfxSwitch.sh
create mode 100755 src/scripts/PowerControl.sh
create mode 100644 src/scripts/colorpicker.py
create mode 100755 src/scripts/lockwall.py
create mode 100644 src/scripts/thumbgen.py
create mode 100755 src/scripts/thumbgen_batch.py
create mode 100644 src/services/AppLauncher.qml
create mode 100644 src/services/AppSearch.qml
create mode 100644 src/services/AudioControl.qml
create mode 100644 src/services/BatteryStatus.qml
create mode 100644 src/services/BatteryWarning.qml
create mode 100644 src/services/CavaService.qml
create mode 100644 src/services/ClipboardService.qml
create mode 100644 src/services/FocusGrabManager.qml
create mode 100644 src/services/GpuDetector.qml
create mode 100644 src/services/HyprlandService.qml
create mode 100644 src/services/HyprlandSyncService.qml
create mode 100644 src/services/IdleService.qml
create mode 100644 src/services/KanbanBoard.qml
create mode 100644 src/services/PowerMenu.qml
create mode 100644 src/services/ScreenRecService.qml
create mode 100644 src/services/ScreenshotTool.qml
create mode 100644 src/services/ShellConfigService.qml
create mode 100644 src/services/UpdateService.qml
create mode 100644 src/services/UsageTracker.qml
create mode 100644 src/services/VideoWallpaperService.qml
create mode 100644 src/services/WallpaperService.qml
create mode 100644 src/services/config_tab/KeybindService.qml
create mode 100644 src/services/config_tab/KeybindsPage.qml
create mode 100644 src/services/config_tab/ShellConfig.qml
create mode 100644 src/services/home/CalendarCard.qml
create mode 100644 src/services/home/ClockCard.qml
create mode 100644 src/services/home/DashHome.qml
create mode 100644 src/services/home/PlayerCard.qml
create mode 100644 src/services/home/ProfileCard.qml
create mode 100644 src/services/home/QuickSettings.qml
create mode 100644 src/services/home/qmldir
create mode 100644 src/services/notifications/NotificationList.qml
create mode 100644 src/services/notifications/NotificationService.qml
create mode 100644 src/services/qmldir
create mode 100644 src/services/system/CpuFreqService.qml
create mode 100644 src/services/system/CpuService.qml
create mode 100644 src/services/system/DiskService.qml
create mode 100644 src/services/system/EnvyControl.qml
create mode 100644 src/services/system/EnvyControlService.qml
create mode 100644 src/services/system/FanControl.qml
create mode 100644 src/services/system/GpuService.qml
create mode 100644 src/services/system/MemService.qml
create mode 100644 src/services/system/NetService.qml
create mode 100644 src/services/system/SystemStats.qml
create mode 100644 src/services/system/ThermalService.qml
delete mode 100644 src/shapes/NotchBackground.qml
create mode 100644 src/shapes/PopupShape.qml
create mode 100644 src/shapes/RoundCorner.qml
create mode 100644 src/state/ClockState.qml
create mode 100644 src/state/IpcManager.qml
create mode 100644 src/state/Popups.qml
create mode 100644 src/state/ShellState.qml
create mode 100644 src/theme/Anim.qml
create mode 100644 src/theme/ColorLoader.qml
create mode 100644 src/theme/Colors.qml
create mode 100644 src/theme/Metrics.qml
create mode 100644 src/theme/qmldir
create mode 100644 src/windows/ConfirmDialog.qml
delete mode 100644 src/windows/Notch.qml
create mode 100644 src/windows/PopupDismiss.qml
create mode 100644 src/windows/ScreenCorners.qml
create mode 100644 src/windows/UpdatePopup.qml
create mode 100644 src/windows/Wallpaper.qml
create mode 100644 version
create mode 100755 wrapper.sh
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..dd739b4
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,245 @@
+name: Brain Shell CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ # ─────────────────────────────────────────────
+ # Arch Linux — pacman + AUR dry-run
+ # ─────────────────────────────────────────────
+ arch-validate:
+ name: Arch Linux — Dependency & Lint Check
+ runs-on: ubuntu-latest
+ container:
+ image: archlinux:latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Update pacman & install base tools
+ run: |
+ pacman -Syu --noconfirm
+ pacman -S --needed --noconfirm git base-devel python
+
+ - name: Validate pacman dependencies exist in repos
+ run: |
+ PACMAN_DEPS=(
+ hyprland qt6-base qt6-declarative qt6-multimedia
+ qt6-5compat qt6ct pipewire pipewire-pulse wireplumber
+ networkmanager bluez bluez-utils brightnessctl upower
+ libnotify polkit python wl-clipboard slurp xdg-user-dirs
+ wf-recorder cava imagemagick wtype lm_sensors util-linux
+ hyprsunset hyprlock hyprpolkitagent hypridle
+ xdg-desktop-portal-hyprland ttf-jetbrains-mono-nerd
+ ttf-nerd-fonts-symbols-common hyprshutdown cliphist
+ )
+ FAILED=()
+ for pkg in "${PACMAN_DEPS[@]}"; do
+ if pacman -Si "$pkg" &>/dev/null; then
+ echo "✓ $pkg"
+ else
+ echo "✗ $pkg (not found in repos)"
+ FAILED+=("$pkg")
+ fi
+ done
+ if [ ${#FAILED[@]} -gt 0 ]; then
+ echo ""
+ echo "Missing packages: ${FAILED[*]}"
+ exit 1
+ fi
+
+ - name: Validate AUR packages exist on AUR RPC
+ run: |
+ AUR_DEPS=(
+ quickshell-git
+ awww-git
+ matugen-bin
+ envycontrol
+ auto-cpufreq
+ nbfc-linux
+ grimblast-git
+ )
+ FAILED=()
+ for pkg in "${AUR_DEPS[@]}"; do
+ STATUS=$(curl -s "https://aur.archlinux.org/rpc/v5/info?arg[]=${pkg}" \
+ | python3 -c "import json,sys; d=json.load(sys.stdin); print('ok' if d.get('resultcount',0)>0 else 'missing')")
+ if [[ "$STATUS" == "ok" ]]; then
+ echo "✓ $pkg (AUR)"
+ else
+ echo "✗ $pkg not found on AUR"
+ FAILED+=("$pkg")
+ fi
+ done
+ if [ ${#FAILED[@]} -gt 0 ]; then
+ echo ""
+ echo "Missing AUR packages: ${FAILED[*]}"
+ exit 1
+ fi
+
+ - name: Lint install.sh
+ run: |
+ pacman -S --needed --noconfirm bash
+ bash -n install.sh
+ echo "✓ install.sh syntax OK"
+
+ - name: Lint dots-extra/install-arch.sh
+ run: |
+ bash -n dots-extra/install-arch.sh
+ echo "✓ install-arch.sh syntax OK"
+
+ - name: Lint dots-extra/validate-install.sh
+ run: |
+ bash -n dots-extra/validate-install.sh
+ echo "✓ validate-install.sh syntax OK"
+
+ - name: Lint shell scripts in src/scripts/
+ run: |
+ bash -n src/scripts/GfxSwitch.sh
+ bash -n src/scripts/PowerControl.sh
+ echo "✓ src/scripts/ syntax OK"
+
+ - name: Validate QML files exist and are non-empty
+ run: |
+ FAILED=()
+ while IFS= read -r -d '' f; do
+ if [ ! -s "$f" ]; then
+ echo "✗ Empty file: $f"
+ FAILED+=("$f")
+ else
+ echo "✓ $f"
+ fi
+ done < <(find src -name "*.qml" -print0)
+ if [ ${#FAILED[@]} -gt 0 ]; then
+ echo "Empty QML files found!"
+ exit 1
+ fi
+
+ - name: Validate qmldir files
+ run: |
+ for f in $(find src -name "qmldir"); do
+ echo "✓ Found qmldir: $f"
+ cat "$f"
+ done
+
+ # ─────────────────────────────────────────────
+ # NixOS — flake check
+ # ─────────────────────────────────────────────
+ nix-validate:
+ name: NixOS — Flake & Package Check
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install Nix
+ uses: cachix/install-nix-action@v27
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+ extra_nix_config: |
+ experimental-features = nix-command flakes
+ accept-flake-config = true
+
+ - name: Validate flake.nix syntax
+ run: |
+ # Check file exists and is valid UTF-8 text
+ cat flake.nix > /dev/null
+ # Use nix-instantiate for pure syntax check (no network, no path access)
+ nix-instantiate --parse flake.nix > /dev/null
+ echo "✓ flake.nix syntax OK"
+
+ - name: Validate flake.lock structure
+ run: |
+ python3 -c "
+ import json, sys
+ with open('flake.lock') as f:
+ lock = json.load(f)
+ assert lock.get('version') == 7, 'Expected lock version 7'
+ nodes = lock.get('nodes', {})
+ print(f'Lock version: {lock[\"version\"]}')
+ print(f'Nodes: {list(nodes.keys())}')
+ for name, node in nodes.items():
+ if 'locked' in node:
+ rev = node['locked'].get('rev', 'N/A')[:10]
+ print(f' ✓ {name}: {rev}')
+ "
+
+ - name: Validate flake.nix declares required outputs
+ run: |
+ python3 -c "
+ with open('flake.nix') as f:
+ content = f.read()
+ checks = {
+ 'packages': 'packages' in content,
+ 'devShells': 'devShells' in content,
+ 'nixosModules': 'nixosModules' in content,
+ 'description': 'description' in content,
+ 'brain-shell': 'brain-shell' in content,
+ }
+ failed = []
+ for key, ok in checks.items():
+ if ok:
+ print(f' ✓ {key} declared')
+ else:
+ print(f' ✗ {key} MISSING')
+ failed.append(key)
+ if failed:
+ raise SystemExit(f'Missing from flake.nix: {failed}')
+ "
+
+ # ─────────────────────────────────────────────
+ # Structure Sanity Check (both runners agree)
+ # ─────────────────────────────────────────────
+ structure-check:
+ name: Repo Structure Sanity
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Check required files exist
+ run: |
+ REQUIRED=(
+ "shell.qml"
+ "flake.nix"
+ "flake.lock"
+ "install.sh"
+ "dots-extra/install-arch.sh"
+ "dots-extra/validate-install.sh"
+ "src/config/brain-shell-colors.json.example"
+ "src/theme/Theme.qml"
+ "src/theme/ColorLoader.qml"
+ "src/state/ShellState.qml"
+ "src/state/Popups.qml"
+ "src/windows/TopBar.qml"
+ "src/popups/Dashboard.qml"
+ )
+ FAILED=()
+ for f in "${REQUIRED[@]}"; do
+ if [[ -f "$f" ]]; then
+ echo "✓ $f"
+ else
+ echo "✗ MISSING: $f"
+ FAILED+=("$f")
+ fi
+ done
+ if [ ${#FAILED[@]} -gt 0 ]; then
+ echo ""
+ echo "Missing required files: ${FAILED[*]}"
+ exit 1
+ fi
+
+ - name: Count QML files per module
+ run: |
+ echo "QML file counts by directory:"
+ find src -name "*.qml" | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn
+
+ - name: Check install.sh is executable or has correct shebang
+ run: |
+ head -1 install.sh | grep -q "^#!/" && echo "✓ shebang OK" || (echo "✗ Missing shebang in install.sh" && exit 1)
+ head -1 dots-extra/install-arch.sh | grep -q "^#!/" && echo "✓ shebang OK (arch)" || (echo "✗ Missing shebang in install-arch.sh" && exit 1)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..60a2ba2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+src/config/colors.conf
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..58d138f
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,365 @@
+# AGENTS.md — Brain_Shell
+
+**Project:** Brain_Shell
+**Version:** 0.1.0
+**Framework:** QtQuick / Quickshell
+**Primary Languages:** QML, JavaScript, Python, Bash
+**Compositor:** Hyprland (via `hyprctl`)
+**Target Platforms:** Arch Linux, Fedora, NixOS
+
+---
+
+## 1. Project Overview
+
+Brain_Shell is a dynamic, highly modular Wayland desktop shell built on [Quickshell](https://git.outfoxxed.me/outfoxxed/quickshell). It provides a top bar with three notches (left, center, right), a popup system (dashboard, audio, network, notifications, clipboard, wallpaper, arch menu), and system services for monitoring, power management, and more.
+
+The shell uses a reactive JSON configuration system (`ShellConfigService`) and supports Material You dynamic colors via `matugen`.
+
+### Key Features
+- **Modular bar** — Left/Center/Right notches with independent content
+- **Dashboard** — Home, System Stats, Kanban Tasks, App Launcher, Config tabs
+- **Popup system** — Audio, Network (WiFi/BT/VPN/Hotspot), Notifications, Clipboard, Wallpaper
+- **App Launcher** — Native DesktopEntries fuzzy search with usage tracking
+- **System monitoring** — CPU, RAM, GPU, Disk, Thermal, Fan control
+- **Unified animations** — `Anim.qml` with standard/emphasized/spatial/spring curves
+- **Reusable components** — `StyledRect`, `Surface`, `StateLayer`, `AnimatedBehavior`
+- **Standalone CLI** — `cli.sh` provides launch, IPC, install/remove without touching system files
+- **Multi-shell coexistence** — Multiple shells can run on the same PC with isolated configs
+
+---
+
+## 2. Technology Stack
+
+| Layer | Technology | Purpose |
+|-------|-----------|---------|
+| **UI Framework** | Qt 6 (QtQuick, QtQuick.Controls, QtQuick.Layouts) | Rendering, animations, controls |
+| **Shell Runtime** | Quickshell (`qs`) | Wayland panel/surface manager, QML engine |
+| **Compositor Bridge** | `hyprctl` CLI | Window manager interaction (submaps, gaps, shaders) |
+| **Configuration** | JSON on disk + `FileView` | Reactive, file-backed persistent config |
+| **Color Generation** | `matugen` | Material You color extraction from wallpapers |
+| **Packaging** | Nix Flake (`flake.nix`) | Reproducible builds, dev shells |
+| **Install Script** | `install.sh` (Bash) | Arch / Fedora dependency install, Hyprland config |
+
+### Runtime Dependencies
+- **Core:** `quickshell`, `qt6-base`, `qt6-declarative`, `qt6-wayland`, `qt6-svg`
+- **Compositor:** `hyprland`
+- **System:** `brightnessctl`, `upower`, `power-profiles-daemon`, `NetworkManager`, `bluetooth`
+- **Media:** `playerctl`, `wf-recorder`, `gpu-screen-recorder`
+- **Tools:** `kitty`, `matugen`, `cliphist`, `zenity`, `jq`
+- **Fonts:** Nerd Fonts symbols, Phosphor icons (mixed usage currently)
+
+---
+
+## 3. Project Structure
+
+```
+./
+├── shell.qml # Entry point: ShellRoot, Variants per screen
+├── install.sh # Distribution-aware installer
+├── flake.nix # Nix flake
+│
+├── src/
+│ ├── qmldir # Singleton + type registrations
+│ │
+│ ├── theme/ # Theming system
+│ │ ├── Theme.qml # Aggregator: Colors + Metrics → single import
+│ │ ├── Colors.qml # Dynamic color palette from matugen output
+│ │ ├── Metrics.qml # Layout constants (sizes, spacing, radii)
+│ │ ├── Anim.qml # Unified animation system (durations, easing curves)
+│ │ └── ColorLoader.qml # matugen JSON watcher
+│ │
+│ ├── components/ # Reusable UI primitives
+│ │ ├── StyledRect.qml # Themed rectangle container
+│ │ ├── Surface.qml # Elevated surface (StyledRect + StateLayer)
+│ │ ├── StateLayer.qml # M3 interaction overlay (hover/press/ripple)
+│ │ ├── AnimatedBehavior.qml # Reusable animation behavior
+│ │ ├── IconBtn.qml # Icon button with hover/press feedback
+│ │ ├── TabSwitcher.qml # Horizontal/vertical tab navigation
+│ │ ├── PopupPage.qml # Animated page container with header
+│ │ ├── PopupSlide.qml # Slide-in animation wrapper
+│ │ ├── StatCard.qml # Metric display card
+│ │ ├── StatRow.qml # Horizontal stat bar
+│ │ ├── DiskBar.qml # Disk usage visualization
+│ │ ├── Speedometer.qml # Circular gauge
+│ │ ├── TimeInput.qml # Time input control
+│ │ └── ProfileButton.qml # User profile button
+│ │
+│ ├── shapes/ # Shared shape primitives
+│ │ ├── PopupShape.qml # Rounded rect with attached-edge flare
+│ │ └── SeamlessBarShape.qml # Bar background shape
+│ │
+│ ├── windows/ # Screen-level PanelWindows
+│ │ ├── TopBar.qml # Main bar (left/center/right notches)
+│ │ ├── Border.qml # Screen border overlays
+│ │ ├── PopupDismiss.qml # Click-outside popup dismissal
+│ │ ├── ConfirmDialog.qml # GPU mode change confirmation
+│ │ └── UpdatePopup.qml # Shell update notification
+│ │
+│ ├── modules/ # Bar content modules
+│ │ ├── Center/ # Center notch content (clock, workspace dots)
+│ │ ├── Left/ # Left notch content (arch menu trigger)
+│ │ └── Right/ # Right notch content (audio, network, battery, notifications triggers)
+│ │
+│ ├── popups/ # All popup windows
+│ │ ├── PopupLayer.qml # Single instantiation point for all popups
+│ │ ├── Dashboard.qml # Main dashboard with tab navigation
+│ │ ├── AudioPopup.qml # Audio output/input control
+│ │ ├── NetworkPopup.qml # WiFi/Bluetooth/VPN/Hotspot tabs
+│ │ ├── NotificationsPopup.qml # Notification history
+│ │ ├── NotificationToast.qml # Incoming notification toast
+│ │ ├── ClipboardPopup.qml # Clipboard history
+│ │ ├── WallpaperPopup.qml # Wallpaper browser
+│ │ ├── ArchMenu.qml # System menu (power, settings)
+│ │ ├── QuickControl.qml # Quick settings panel
+│ │ ├── ScreenRecOptionsPopup.qml # Screen recording options
+│ │ ├── WifiTab.qml # WiFi network list
+│ │ ├── BluetoothTab.qml # Bluetooth device list
+│ │ ├── VPNTab.qml # VPN connection management
+│ │ ├── HotspotTab.qml # Hotspot configuration
+│ │ └── HistoryTab.qml # Notification history panel
+│ │
+│ ├── services/ # Backend singletons
+│ │ ├── ShellConfigService.qml # JSON config persistence
+│ │ ├── PerMonitorConfig.qml # Per-monitor overrides
+│ │ ├── FocusGrabManager.qml # Input focus coordination
+│ │ ├── AppSearch.qml # DesktopEntries + fuzzy search
+│ │ ├── UsageTracker.qml # App launch frequency tracking
+│ │ ├── AppLauncher.qml # App launcher UI
+│ │ ├── WallpaperService.qml # Wallpaper management
+│ │ ├── ScreenRecService.qml # Screen recording
+│ │ ├── ScreenshotTool.qml # Screenshot capture
+│ │ ├── ClipboardService.qml # Clipboard history (cliphist)
+│ │ ├── CavaService.qml # Audio visualization
+│ │ ├── AudioControl.qml # PipeWire volume/device control
+│ │ ├── BatteryStatus.qml # Battery monitoring
+│ │ ├── BatteryWarning.qml# Low battery alert
+│ │ ├── UpdateService.qml # Shell update checker
+│ │ ├── KanbanBoard.qml # Task board
+│ │ ├── PowerMenu.qml # Power actions
+│ │ ├── home/ # Dashboard home page components
+│ │ │ ├── DashHome.qml # Home layout
+│ │ │ ├── QuickSettings.qml # Toggles + brightness
+│ │ │ ├── ClockCard.qml # Date/time display
+│ │ │ ├── PlayerCard.qml# Media player
+│ │ │ ├── ProfileCard.qml # User avatar
+│ │ │ └── CalendarCard.qml # Calendar widget
+│ │ ├── config_tab/ # Settings UI
+│ │ │ ├── ShellConfig.qml # Shell settings panel
+│ │ │ ├── KeybindService.qml # Keybind configuration
+│ │ │ └── KeybindsPage.qml # Keybind UI
+│ │ ├── notifications/ # Notification system
+│ │ │ ├── NotificationService.qml # DBus notification server
+│ │ │ └── NotificationList.qml # Notification list component
+│ │ └── system/ # Hardware monitors
+│ │ ├── SystemStats.qml # Aggregated stats
+│ │ ├── CpuService.qml # CPU usage
+│ │ ├── CpuFreqService.qml # CPU frequency
+│ │ ├── MemService.qml # Memory usage
+│ │ ├── GpuService.qml # GPU stats
+│ │ ├── DiskService.qml # Disk usage
+│ │ ├── NetService.qml # Network throughput
+│ │ ├── ThermalService.qml # Temperature sensors
+│ │ ├── FanControl.qml # Fan speed control
+│ │ ├── EnvyControl.qml # NVIDIA Optimus control
+│ │ └── EnvyControlService.qml # GPU mode service
+│ │
+│ ├── state/ # Global singleton state
+│ │ ├── ShellState.qml # WiFi/BT/DND/focus/hotspot/VPN state + submap control
+│ │ ├── Popups.qml # Per-popup open/close state + universal controls
+│ │ ├── ClockState.qml # Clock formatting state
+│ │ └── IpcManager.qml # IPC message handler
+│ │
+│ ├── config/ # Static config files
+│ │ ├── brain-shell-colors.json.example
+│ │ ├── colors.conf.template
+│ │ ├── hypridle.conf
+│ │ ├── hyprlock.conf
+│ │ ├── matugen.toml
+│ │ └── shaders/
+│ │
+│ └── scripts/ # Shell scripts invoked by services
+│ ├── GfxSwitch.sh # GPU mode switching
+│ ├── PowerControl.sh # Power profile control
+│ └── colorpicker.py # Screen color picker
+│
+├── dots-extra/ # Installer extras
+│ ├── install-arch.sh
+│ └── validate-install.sh
+│
+└── assets/ # Wallpapers and static resources
+ └── wallpapers/
+```
+
+---
+
+## 4. Build, Run, and Test Commands
+
+### Running Locally
+```bash
+# Via the CLI wrapper (recommended — standalone, no system edits)
+./cli.sh
+
+# Or direct Quickshell launch
+qs -p shell.qml
+
+# IPC commands (requires running shell)
+./cli.sh run dashboard-home # Open dashboard on Home tab
+./cli.sh run launcher # Open app launcher
+./cli.sh reload # Restart the shell
+./cli.sh quit # Stop the shell
+
+# Hyprland integration
+./cli.sh install hyprland # Add to autostart
+./cli.sh remove hyprland # Remove from autostart
+```
+
+### Nix Development Shell
+```bash
+nix develop
+```
+
+### Testing
+**There is currently no automated test suite.** The project relies on manual runtime testing on Hyprland.
+
+---
+
+## 5. Runtime Architecture
+
+### Entry Point (`shell.qml`)
+1. `ShellRoot` initializes singletons: `KeybindService`, `UpdateService`, `IpcManager`
+2. Per-screen `Variants` create: `TopBar`, `Border` (left/right/bottom), `PopupDismiss`, `ConfirmDialog`, `UpdatePopup`
+3. `PopupLayer` instantiates all popup windows, receiving anchor window references
+
+### Config Lifecycle
+- `ShellConfigService.qml` watches `~/.config/Brain_Shell/src/user_data/shell_config.json`
+- Properties: `animationSpeed`, `barEnabled`, `dashboardWidth`, `dashboardHeight`, `focusModeOnStartup`, `autoUpdateCheck`
+- Changes auto-persist via `save()`
+
+### Color System
+- `ColorLoader.qml` watches `~/.cache/Brain_Shell/colors.json` (generated by `matugen`)
+- `Colors.qml` exposes semantic color properties
+- `Theme.qml` aggregates `Colors` + `Metrics` for single-import convenience
+
+### Compositor Integration
+- `ShellState.qml` manages Hyprland submap switching for keybind interception
+- `QuickSettings.qml` reads/writes Hyprland gaps and shader settings
+- `WallpaperService.qml` applies active border color to Hyprland
+- `ScreenshotTool.qml` and `ScreenRecService.qml` query Hyprland for window/monitor info
+
+---
+
+## 6. Code Organization and Key Symbols
+
+### Singletons (registered in `src/qmldir`)
+| Symbol | File | Responsibility |
+|--------|------|----------------|
+| `Theme` | `src/theme/Theme.qml` | Aggregated Colors + Metrics |
+| `Anim` | `src/theme/Anim.qml` | Unified animation system |
+| `Popups` | `src/state/Popups.qml` | Popup visibility state |
+| `ShellState` | `src/state/ShellState.qml` | System state (WiFi, BT, DND, etc.) |
+| `ClockState` | `src/state/ClockState.qml` | Clock formatting |
+| `IpcManager` | `src/state/IpcManager.qml` | IPC messages + named pipe reader + unified command dispatcher |
+| `ShellConfigService` | `src/services/ShellConfigService.qml` | JSON config persistence |
+| `PerMonitorConfig` | `src/services/PerMonitorConfig.qml` | Per-monitor overrides |
+| `FocusGrabManager` | `src/services/FocusGrabManager.qml` | Popup focus coordination |
+| `AppSearch` | `src/services/AppSearch.qml` | App listing + fuzzy search |
+| `UsageTracker` | `src/services/UsageTracker.qml` | App launch tracking |
+| `WallpaperService` | `src/services/WallpaperService.qml` | Wallpaper management |
+| `ScreenRecService` | `src/services/ScreenRecService.qml` | Screen recording |
+| `ScreenshotTool` | `src/services/ScreenshotTool.qml` | Screenshot tool |
+| `ClipboardService` | `src/services/ClipboardService.qml` | Clipboard history |
+| `CavaService` | `src/services/CavaService.qml` | Audio visualization |
+| `KeybindService` | `src/services/config_tab/KeybindService.qml` | Keybind management |
+| `UpdateService` | `src/services/UpdateService.qml` | Update checker |
+| `NotificationService` | `src/services/notifications/NotificationService.qml` | DBus notification server |
+
+### Component Primitives
+| Component | File | Usage |
+|-----------|------|-------|
+| `StyledRect` | `src/components/StyledRect.qml` | Themed container with gradient/border/shadow support |
+| `Surface` | `src/components/Surface.qml` | Elevated surface (StyledRect + StateLayer) |
+| `StateLayer` | `src/components/StateLayer.qml` | Hover/press/ripple interaction feedback |
+| `AnimatedBehavior` | `src/components/AnimatedBehavior.qml` | Reusable animation Behavior wrapper |
+| `IconBtn` | `src/components/IconBtn.qml` | Icon button |
+| `TabSwitcher` | `src/components/TabSwitcher.qml` | Tab bar navigation |
+
+---
+
+## 7. Development Conventions
+
+### QML & JavaScript
+- **Indentation:** 4 spaces
+- **Imports:** Use relative paths. Files at depth 1 use `import "../theme"`; files at depth 2 use `import "../../theme"`
+- **Null safety:** Always null-check nested properties
+- **Singletons:** Registered in `src/qmldir` with `singleton` keyword
+- **Services qmldir:** `src/services/qmldir` provides legacy aliases for some singletons
+
+### Animation System
+All UI motion must go through `Anim.qml`:
+
+- **Use `AnimatedBehavior`** for every `Behavior`:
+ ```qml
+ Behavior on opacity {
+ AnimatedBehavior { type: "standard"; size: "normal" }
+ }
+ ```
+ Valid `type` values: `"standard"`, `"emphasized"`, `"spatial"`, `"spring"`
+ Valid `size` values: `"small"`, `"normal"`, `"large"`, `"extraLarge"` (standard); `"fast"`, `"default"`, `"slow"` (spatial); `"small"`, `"normal"`, `"large"` (emphasized/spring)
+
+- **When `AnimatedBehavior` is not enough**, use `Anim.duration(type, size)` and `Anim.easing(name)`:
+ ```qml
+ NumberAnimation {
+ duration: Anim.duration("standard", "normal")
+ easing.type: Anim.easing("standard").type
+ easing.bezierCurve: Anim.easing("standard").bezierCurve
+ }
+ ```
+
+- **Convenience properties** available directly on `Anim`:
+ - `Anim.standardNormal`, `Anim.standardLarge`, etc.
+
+### Color Resolution
+- Never hardcode colors. Use `Theme.background`, `Theme.text`, `Theme.active`, etc.
+- `Theme` aggregates both `Colors` and `Metrics` for convenience
+
+### Anti-Patterns (Strictly Avoid)
+1. **Hardcoding colors, sizes, or durations** — Use `Theme.*`, `Anim.*`
+2. **Using `Qt.rgba()` for structural colors** — Use `Theme.*` or `StyledRect` variants
+3. **Creating raw `Rectangle` containers** — Use `StyledRect` or `Surface`
+4. **Animation bypass** — Never use raw `Easing.OutCubic` or hardcoded durations
+5. **Missing popup transitions** — All popups must animate open/close with opacity + scale/size
+6. **`Qt.createQmlObject('Process { }', ...)`** — Prefer declarative `Process {}` where possible
+7. **`Metrics.animDuration`** — Removed. Use `Anim.*` instead
+
+---
+
+## 8. Where to Look for Common Tasks
+
+| Task | Primary Location | Notes |
+|------|------------------|-------|
+| **Add config key** | `src/services/ShellConfigService.qml` | Add property + parse + save |
+| **Change bar layout** | `src/windows/TopBar.qml` | Bar sizing, notch arrangement |
+| **Add popup** | `src/popups/PopupLayer.qml` | Single instantiation point |
+| **Theme / colors** | `src/theme/Colors.qml`, `src/theme/Theme.qml` | Watches `~/.cache/Brain_Shell/colors.json` |
+| **Animations** | `src/theme/Anim.qml` | Standard / Emphasized / Spatial / Spring curves |
+| **System monitoring** | `src/services/system/` | CPU, RAM, GPU, Disk, Thermal, Fan |
+| **Notifications** | `src/services/notifications/NotificationService.qml` | DBus `NotificationServer` |
+| **Clipboard** | `src/services/ClipboardService.qml` | cliphist wrapper |
+| **Lockscreen config** | `src/config/hyprlock.conf` | Hyprlock theme |
+| **Screenshots** | `src/services/ScreenshotTool.qml` | Uses grim/slurp |
+| **Screen recording** | `src/services/ScreenRecService.qml` | Uses wf-recorder / gpu-screen-recorder |
+| **Dashboard pages** | `src/services/home/`, `src/services/system/`, `src/services/KanbanBoard.qml` | Tab content |
+| **Settings UI** | `src/services/config_tab/` | Shell config + keybinds |
+
+---
+
+## 9. Known Issues & Planned Work
+
+See the project's internal task tracker for the complete list. Key areas:
+
+- **Dashboard StackView** — Currently uses `visible: root.page === "x"` pattern; planned migration to `StackView` with push/pop and animated transitions
+- **HyprlandService** — Multiple files contain scattered `hyprctl` calls; planned centralization into a single typed singleton
+- **Notification actions** — Action buttons from DBus notifications are not rendered yet
+- **StyledRect/StateLayer consistency** — Some popups still use raw `Rectangle` + `Qt.rgba()` instead of component primitives
+- **Typography system** — Font sizes are hardcoded in many places; planned `Theme.fontSize()` system
+- **Icon unification** — Mixed usage of Nerd Fonts and Phosphor icons
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..91f43bc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Venkat Saahit Kamu (Brainitech)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d455de5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,311 @@
+ Brain_Shell
+
+
+ A dynamic, highly modular Wayland desktop shell built with Quickshell and QML, tailored for Hyprland.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+Showcase
+
+
+
+
+
+---
+
+Features
+
+- **Modular Setup** — Unintrusive setup
+- **Material You Integration** — Dynamic colors via Matugen
+- **Lua-Based Config** — Hyprland v0.55+ compatible
+- **System Dashboard** — Monitor CPU, RAM, battery, temps, and more
+- **Kanban/Tasks** — To Do, Ongoing and Competed lists with Prioiry and Deadlines
+- **App Launcher** — Native fuzzy search with usage-based ranking
+- **Screenshot Tool** — Built-in capture with region/window/screen modes
+- **Color Picker** — Screen color sampler
+- **Keybinds** — Set your own keybinds for each popup
+- **Theming Engine** — Live wallpaper-synced color updates
+- **Network Manager** — WiFi, Bluetooth, VPN integration
+- **Notifications** — DBus Notifcations via libnotify
+- **Audio Control** — PipeWire volume & device management
+- **Screen Recorder** — Built-in recording with wf-recorder
+- **Clipboard Manager** — Cliphist integration for history management
+- **Highly Customizable** — QML-based UI, easily extended
+
+> **Note:** Brain Shell is currently in its `v0.1.0` release. While the core architecture and theming pipeline are feature-complete, you may encounter bugs. Please report them on our [Discord](https://discord.gg/BV8UduvABx) or via GitHub Issues!
+
+---
+
+
+ Installation
+
+
+### One line installer
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/Brainitech/Brain_Shell/main/install.sh | bash
+```
+
+### What the installer does
+
+- ✓ Clones the repo to `~/.local/src/Brain_Shell`
+- ✓ Creates global `brain-shell` command (symlink to `/usr/local/bin`)
+- ✓ Adds Brain_Shell to Hyprland autostart
+- ✓ Nothing else is touched — fully standalone
+
+**After install, just type:** `brain-shell`
+
+---
+
+### Standalone CLI (No System Modifications)
+
+Brain_Shell ships with a standalone CLI that lets you run and manage the shell **without touching any system files**. Multiple shells can coexist on the same PC — each with its own isolated configuration.
+
+```bash
+# Clone and run — no install needed
+git clone https://github.com/Brainitech/Brain_Shell.git
+cd Brain_Shell
+./cli.sh
+
+# IPC commands (requires running shell)
+./cli.sh run dashboard-home # Open dashboard on Home tab
+./cli.sh run launcher # Open app launcher
+./cli.sh run screenshot # Take a region screenshot
+./cli.sh run color-picker # Pick a color from screen
+
+# Manage the shell
+./cli.sh reload # Restart the shell
+./cli.sh quit # Stop the shell
+./cli.sh lock # Lock the screen
+./cli.sh update # Git pull + restart
+
+# Hyprland autostart (optional)
+./cli.sh install hyprland # Add to Hyprland autostart
+./cli.sh install hyprland --lua # For Hyprland ≥0.48 with Lua config
+./cli.sh remove hyprland # Remove from autostart
+
+# Help
+./cli.sh help # Show all commands
+./cli.sh version # Show version
+```
+
+> **Tip:** Symlink `cli.sh` to your PATH for global access:
+> `sudo ln -s $(pwd)/cli.sh /usr/local/bin/brain-shell`
+
+The installer automatically:
+
+- ✓ Detects your Linux distribution
+- ✓ Detects your Window Manager and Hyprland Config
+- ✓ Backs up your entire `~/.config`
+- ✓ Installs all required dependencies
+- ✓ Clones the repository to `~/.local/src/Brain_Shell`
+- ✓ Updates your Hyprland config to auto-start Brain_Shell and required dependencies
+- ✓ Creates configuration directories
+
+**After installation, restart Hyprland for changes to take effect.**
+
+---
+
+
+ Requirements
+
+
+> [!IMPORTANT]
+> **Matugen is required** for dynamic color generation. Brain Shell will not function correctly without it.
+
+### Core Dependencies
+
+
+Runtime & Rendering
+
+- **Hyprland** v0.55+ – Wayland compositor
+- **Quickshell** – QML shell framework
+- **Qt6** – Qt6 libraries and QML engine
+- **qt6ct** – Qt6 theme configuration
+
+
+
+
+System Tools
+
+- **PipeWire** – Audio server (pipewire, pipewire-pulse, wireplumber)
+- **NetworkManager** – Network management
+- **BlueZ** – Bluetooth stack (bluez, bluez-utils)
+- **Brightnessctl** – Backlight control
+- **Mpris** – Media Retrival
+- **Playerctl** – Player controls
+- **UPower** – Battery and power info
+- **libnotify** – Desktop notifications
+- **Polkit** – Privilege escalation
+- **wl-clipboard** – Wayland clipboard (wl-copy/wl-paste)
+
+
+
+
+Theming & Wallpaper
+
+- **Matugen** – Material You color generation **(REQUIRED)**
+- **awww** – Wallpaper daemon (Wayland)
+- **ImageMagick** – Image manipulation
+
+
+
+
+Recording & Utilities
+
+- **wf-recorder** – Screen recording (Wayland)
+- **cava** – Audio visualizer
+- **slurp** – Region/window selection
+- **wtype** – Keyboard input emulation
+- **cliphist** – Clipboard history manager
+
+
+
+
+Hardware Management
+
+- **lm_sensors** – CPU temperature & fan monitoring
+- **rfkill** – Airplane mode control
+- **envycontrol** – GPU switching (NVIDIA/Intel)
+- **auto-cpufreq** – CPU frequency scaling
+- **nbfc-linux** – Laptop fan control
+
+
+
+
+Hyprland Integration
+
+- **hyprlock** – Lock screen
+- **hypridle** – Idle management daemon
+- **hyprsunset** – Blue light filter
+- **hyprshutdown** – Graceful shutdown
+- **xdg-desktop-portal-hyprland** – Portal backend
+
+
+
+
+Fonts
+
+- **ttf-jetbrains-mono-nerd** – Primary font (Nerd Font variant)
+- **ttf-noto-nerd** – Emoji and CJK support
+
+
+
+---
+
+
+ Roadmap
+
+
+### Current (v0.1.0)
+
+- [x] Core shell framework
+- [x] System monitoring dashboard
+- [x] Keybind editor with live conflict detection
+- [x] Network management (WiFi, Bluetooth, VPN)
+- [x] Audio control panel
+- [x] Screen recording integration
+- [x] Clipboard manager
+- [x] Material You color integration
+- [x] Lua config generation
+- [x] Professional installer (Arch/NixOS)
+- [x] Auto-update mechanism
+- [x] Native app launcher with fuzzy search
+- [x] Screenshot tool (region/window/screen)
+- [x] Color picker
+- [x] Unified animation system
+- [x] Reusable UI components (StyledRect, Surface, StateLayer)
+
+### Upcoming (Post-v0.1.0)
+
+- [ ] Scaling on Different Screen-Sizes
+- [x] Config Pages for Shell Customization
+- [x] Multi-Monitor Support
+- [ ] Additional theme options
+- [x] App launcher enhancements (pinned/recent)
+- [ ] Unified popup configuration layer
+- [x] Extended documentation
+- [ ] Community themes
+- [x] CLI
+- [x] More Linux distribution support (Fedora, Debian)
+
+---
+
+
+Known Issues
+
+
+- **Multi-Monitor Scaling:** Global scaling across mixed-resolution monitors (e.g., 4K paired with 1080p) is currently inconsistent. UI elements may appear misproportioned or poorly sized on non-1080p screens.
+
+- **Shutdown Menu (Hyprshutdown) State:** Canceling a shutdown or logout action can sometimes leave the Hyprland session in an empty state with most applications unintentionally closed. It may also occasionally struggle to terminate all running apps smoothly.
+
+---
+
+
+ Contributing
+
+
+Brain Shell is actively developed and welcomes contributions!
+
+- Found a bug? → [Open an issue](https://github.com/Brainitech/Brain_Shell/issues)
+- Have an idea? → [Start a discussion](https://github.com/Brainitech/Brain_Shell/discussions)
+- Want to contribute? → Fork, branch, and submit a pull request
+- Want to join the community? → [Join Discord](https://discord.com/invite/BV8UduvABx)
+
+---
+
+
+ Special Thanks
+
+
+- **[Hyprland Community](https://github.com/hyprwm)** – For creating an exceptional Wayland compositor and fostering an amazing community
+- **[Quickshell Contributors](https://github.com/quickshell/quickshell)** – For the powerful QML framework that powers this shell
+- **[Matugen Team](https://github.com/InioX/matugen)** – For Material You color generation technology
+- **[Wayland Project](https://wayland.freedesktop.org)** – For the modern display protocol foundation
+- **[Celestial Shell](https://github.com/caelestia-dots/shell)** & **[AX-Shell](https://github.com/Axenide/ax-shell)** — For the inspiration
+- **[NotCandy001](https://github.com/notcandy001)** — For the installer
+- **All the Testers & Contributors** — For their time put into testing and suggesting fixes.
+
+---
+
+ Brain Cells Collected
+
+
+
+
+---
+
+
+ License
+
+
+This project is licensed under the MIT License – see the [LICENSE](LICENSE) file for details.
diff --git a/cli.sh b/cli.sh
new file mode 100755
index 0000000..65cb7ae
--- /dev/null
+++ b/cli.sh
@@ -0,0 +1,744 @@
+#!/usr/bin/env bash
+# ═══════════════════════════════════════════════════════════════════════════════
+# Brain Shell CLI — Standalone launcher & manager
+# github.com/Brainitech/Brain_Shell
+# ═══════════════════════════════════════════════════════════════════════════════
+set -euo pipefail
+
+_script_src="${BASH_SOURCE[0]}"
+# Resolve symlinks to get the real script directory (for global brain-shell command)
+if [[ -L "$_script_src" ]]; then
+ _script_src=$(readlink -f "$_script_src" 2>/dev/null || readlink "$_script_src")
+fi
+SCRIPT_DIR="${_script_src%/*}"
+if [[ "$SCRIPT_DIR" == "$_script_src" ]] && [[ ! -f "$_script_src" ]]; then
+ SCRIPT_DIR="$PWD"
+fi
+if [[ "$SCRIPT_DIR" != /* ]]; then
+ SCRIPT_DIR="$PWD/$SCRIPT_DIR"
+fi
+unset _script_src
+
+QS_BIN="${BRAIN_SHELL_QS:-qs}"
+NIXGL_BIN="${BRAIN_SHELL_NIXGL:-}"
+if [[ -n "${QML2_IMPORT_PATH:-}" ]] && [[ -z "${QML_IMPORT_PATH:-}" ]]; then
+ export QML_IMPORT_PATH="$QML2_IMPORT_PATH"
+fi
+
+SHELL_QML="${SCRIPT_DIR}/shell.qml"
+CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/Brain_Shell"
+DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/Brain_Shell"
+PID_FILE="/tmp/brain_shell.pid"
+PIPE="/tmp/brain_shell_ipc.pipe"
+VERSION_FILE="${SCRIPT_DIR}/version"
+BRIGHTNESS_SAVE_FILE="/tmp/brain_shell_brightness_saved.txt"
+
+if [[ -f "$VERSION_FILE" ]]; then
+ VERSION=$(<"$VERSION_FILE")
+else
+ VERSION="0.1.0"
+fi
+
+show_help() {
+ cat <<'EOF'
+Brain Shell CLI — Desktop Environment Control
+
+Usage: brain-shell [COMMAND]
+
+Commands:
+ (none) Launch Brain_Shell
+ update Update Brain_Shell
+ reload Restart Brain_Shell
+ sync Regenerate hyprland binds
+ quit Stop Brain_Shell
+ lock Activate lockscreen
+ screen [on|off] Turn screen on/off
+ suspend Suspend the system
+
+ brightness [monitor] Set brightness (0-100)
+ brightness +/- [monitor] Adjust brightness relatively
+ brightness -s [monitor] Save current brightness
+ brightness -r [monitor] Restore saved brightness
+ brightness -l List monitors
+
+ volume-up Increase volume
+ volume-down Decrease volume
+ volume-mute Toggle volume mute
+ mic-mute Toggle microphone mute
+ caffeine Toggle caffeine (idle inhibition)
+ gamemode Toggle game mode (reduced motion)
+ nightlight Toggle night light (blue light filter)
+
+ run Send IPC command to running shell
+ Available: dashboard, dashboard-home, dashboard-stats, dashboard-kanban,
+ dashboard-launcher, dashboard-config, launcher,
+ audio, network, notifications, clipboard, wallpaper,
+ arch-menu, screenshot, color-picker, focus, close-all
+
+ help, -h, --help Show this help message
+ version, -v, --version Show Brain_Shell version
+ goodbye Uninstall Brain_Shell :(
+ install Install compositor integration
+ brain-shell install hyprland Add to Hyprland autostart (.conf)
+ brain-shell install hyprland --lua Add to Hyprland autostart (Lua)
+ brain-shell install hyprland --conf Add to Hyprland autostart (safe)
+ remove Remove compositor integration
+ brain-shell remove hyprland Remove from Hyprland config
+
+Examples:
+ brain-shell brightness 75 Set all monitors to 75%
+ brain-shell brightness 50 HDMI-A-1 Set HDMI-A-1 to 50%
+ brain-shell brightness +10 Increase brightness by 10%
+ brain-shell brightness -s Save current brightness
+ brain-shell brightness -r Restore saved brightness
+ brain-shell run dashboard-home Open dashboard on Home tab
+ brain-shell run launcher Open app launcher
+ brain-shell run screenshot Take a region screenshot
+ brain-shell install hyprland Auto-start with Hyprland
+ brain-shell reload Quick restart after edits
+
+EOF
+}
+
+# ── Config bootstrap ──────────────────────────────────────────────────────────
+ensure_config_files() {
+ local cfg_dir="${CONFIG_DIR}/src/user_data"
+ mkdir -p "$cfg_dir"
+
+ local provider_json="${cfg_dir}/config_Provider.json"
+ if [[ ! -f "$provider_json" ]]; then
+ printf '{"configProvider":"lua"}' > "$provider_json"
+ fi
+
+ local shell_json="${cfg_dir}/shell_config.json"
+ if [[ ! -f "$shell_json" ]]; then
+ printf '{\n "animationSpeed": 1.0,\n "barEnabled": true,\n "dashboardWidth": 900,\n "dashboardHeight": 520,\n "focusModeOnStartup": false,\n "autoUpdateCheck": true\n}\n' > "$shell_json"
+ fi
+
+ local permon_json="${cfg_dir}/per_monitor.json"
+ if [[ ! -f "$permon_json" ]]; then
+ printf '{}' > "$permon_json"
+ fi
+
+ mkdir -p "${XDG_CACHE_HOME:-$HOME/.cache}/Brain_Shell"
+}
+
+# ── Process management ────────────────────────────────────────────────────────
+find_brain_shell_pid() {
+ local pid
+ pid=$(pgrep -f "qs.*${SCRIPT_DIR}/shell.qml" 2>/dev/null | head -1)
+ if [[ -z "$pid" ]]; then
+ pid=$(pgrep -f "quickshell.*${SCRIPT_DIR}/shell.qml" 2>/dev/null | head -1)
+ fi
+ if [[ -z "$pid" ]]; then
+ pid=$(pgrep -f "qs.*shell.qml" 2>/dev/null | head -1)
+ fi
+ if [[ -z "$pid" ]]; then
+ pid=$(pgrep -f "quickshell.*shell.qml" 2>/dev/null | head -1)
+ fi
+ if [[ -z "$pid" ]]; then
+ pid=$(pgrep -a "qs" 2>/dev/null | grep -F "$SCRIPT_DIR" | awk '{print $1}' | head -1)
+ fi
+ if [[ -z "$pid" ]]; then
+ pid=$(pgrep -a quickshell 2>/dev/null | grep -F "$SCRIPT_DIR" | awk '{print $1}' | head -1)
+ fi
+ echo "$pid"
+}
+
+find_brain_shell_pid_cached() {
+ local pid
+ if [[ -f "$PID_FILE" ]]; then
+ pid=$(<"$PID_FILE" 2>/dev/null)
+ if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
+ echo "$pid"
+ return 0
+ fi
+ rm -f "$PID_FILE"
+ fi
+ pid=$(find_brain_shell_pid)
+ echo "$pid"
+}
+
+launch_shell() {
+ local pid
+ pid=$(find_brain_shell_pid)
+ if [[ -n "$pid" ]]; then
+ echo "Brain_Shell is already running (PID $pid)"
+ return 0
+ fi
+
+ ensure_config_files
+
+ echo "Starting Brain_Shell v${VERSION}..."
+
+ local launch_cmd=("$QS_BIN" -p "$SHELL_QML")
+ if [[ -n "$NIXGL_BIN" ]] && command -v "$NIXGL_BIN" &>/dev/null; then
+ launch_cmd=("$NIXGL_BIN" "${launch_cmd[@]}")
+ fi
+
+ nohup "${launch_cmd[@]}" > /dev/null 2>&1 &
+ local new_pid=$!
+ echo "$new_pid" > "$PID_FILE"
+
+ # Wait for shell to initialize (Quickshell takes 2-4 seconds)
+ sleep 3
+ if ! kill -0 "$new_pid" 2>/dev/null; then
+ echo "Error: Brain_Shell failed to start. Check logs with: qs -p ${SHELL_QML}"
+ rm -f "$PID_FILE"
+ exit 1
+ fi
+
+ echo "Brain_Shell started (PID $new_pid)"
+}
+
+restart_shell() {
+ local pid
+ pid=$(find_brain_shell_pid_cached)
+
+ if [[ -n "$pid" ]]; then
+ echo "Stopping Brain_Shell (PID $pid)..."
+ kill "$pid" 2>/dev/null || true
+ local waited=0
+ while kill -0 "$pid" 2>/dev/null && [[ $waited -lt 30 ]]; do
+ sleep 0.1
+ waited=$((waited + 1))
+ done
+ if kill -0 "$pid" 2>/dev/null; then
+ kill -9 "$pid" 2>/dev/null || true
+ fi
+ rm -f "$PID_FILE"
+ fi
+
+ launch_shell
+}
+
+quit_shell() {
+ local pid
+ pid=$(find_brain_shell_pid_cached)
+
+ if [[ -n "$pid" ]]; then
+ echo "Stopping Brain_Shell (PID $pid)..."
+ kill "$pid" 2>/dev/null || true
+ sleep 0.3
+ if kill -0 "$pid" 2>/dev/null; then
+ kill -9 "$pid" 2>/dev/null || true
+ fi
+ rm -f "$PID_FILE"
+ echo "Brain_Shell stopped."
+ else
+ echo "Brain_Shell is not running."
+ fi
+}
+
+lock_screen() {
+ if [[ -p "$PIPE" ]]; then
+ echo "lockscreen" > "$PIPE" &
+ exit 0
+ fi
+
+ local pid
+ pid=$(find_brain_shell_pid_cached)
+ if [[ -n "$pid" ]] && command -v qs &>/dev/null; then
+ qs ipc --pid "$pid" call brain-shell run lockscreen 2>/dev/null && exit 0
+ fi
+
+ if command -v hyprlock &>/dev/null; then
+ hyprlock &
+ elif command -v loginctl &>/dev/null; then
+ loginctl lock-session
+ else
+ echo "Error: Could not lock screen"
+ exit 1
+ fi
+}
+
+# ── IPC ───────────────────────────────────────────────────────────────────────
+# ── IPC target mapping (friendly name → original IpcManager target) ──────────
+# Translates brain-shell run to the correct qs ipc call toggle
+declare -A IPC_MAP=(
+ # Dashboard
+ ["dashboard"]="dashboard-home"
+ ["dashboard-home"]="dashboard-home"
+ ["dashboard-stats"]="dashboard-stats"
+ ["dashboard-kanban"]="dashboard-kanban"
+ ["dashboard-launcher"]="dashboard-launcher"
+ ["dashboard-config"]="dashboard-config"
+ ["launcher"]="dashboard-launcher"
+ # Popups
+ ["audio"]="audioOut-toggle"
+ ["network"]="wifi-toggle"
+ ["notifications"]="notifications-toggle"
+ ["clipboard"]="clipboard-toggle"
+ ["wallpaper"]="wallpaper-toggle"
+ ["arch-menu"]="PowerMenu-toggle"
+ # Network tabs
+ ["wifi-toggle"]="wifi-toggle"
+ ["bt-toggle"]="bt-toggle"
+ ["vpn-toggle"]="vpn-toggle"
+ ["hotspot-toggle"]="hotspot-toggle"
+ # Tools
+ ["focus"]="focus-toggle"
+ ["screen-record"]="screenrec-on"
+ ["close-all"]="focus-toggle"
+)
+
+send_ipc() {
+ local cmd="$1"
+ if [[ -p "$PIPE" ]]; then
+ echo "$cmd" > "$PIPE" &
+ return 0
+ fi
+
+ local pid
+ pid=$(find_brain_shell_pid_cached)
+ if [[ -z "$pid" ]]; then
+ echo "Error: Brain_Shell is not running"
+ return 1
+ fi
+
+ # Translate friendly name to original IPC target
+ local target="${IPC_MAP[$cmd]:-$cmd}"
+ qs ipc --pid "$pid" call "$target" toggle 2>/dev/null || {
+ # Fallback: try as brain-shell run (for screenshot/color-picker/etc.)
+ qs ipc --pid "$pid" call brain-shell "$cmd" 2>/dev/null || {
+ echo "Error: Could not send command '$cmd'"
+ return 1
+ }
+ }
+}
+
+# ── Brightness ────────────────────────────────────────────────────────────────
+brightness_list_monitors() {
+ echo "Monitors:"
+ if command -v hyprctl &>/dev/null; then
+ hyprctl monitors -j 2>/dev/null | jq -r '.[] | " \(.name)"' 2>/dev/null || {
+ echo "Error: Could not list monitors (jq required)"
+ exit 1
+ }
+ else
+ echo "Error: hyprctl not found"
+ exit 1
+ fi
+}
+
+brightness_get_current() {
+ local monitor="${1:-}"
+ if command -v brightnessctl &>/dev/null; then
+ if [[ -z "$monitor" ]]; then
+ brightnessctl -m 2>/dev/null | while IFS=, read -r dev _ cur max; do
+ dev="${dev// /}"
+ local pct=$(( cur * 100 / max ))
+ echo "${dev}:${pct}"
+ done
+ else
+ local info
+ info=$(brightnessctl -d "$monitor" -m 2>/dev/null) || return 1
+ IFS=, read -r _ _ cur max <<< "$info"
+ echo $(( cur * 100 / max ))
+ fi
+ else
+ echo "Warning: brightnessctl not found"
+ return 1
+ fi
+}
+
+brightness_save() {
+ local monitor="${1:-}"
+ if [[ -z "$monitor" ]]; then
+ brightness_get_current > "$BRIGHTNESS_SAVE_FILE" 2>/dev/null || {
+ echo "Warning: Could not query current brightness"
+ }
+ if [[ -s "$BRIGHTNESS_SAVE_FILE" ]]; then
+ echo "Saved current brightness for all monitors"
+ fi
+ else
+ local cur
+ cur=$(brightness_get_current "$monitor" 2>/dev/null) || {
+ echo "Error: Monitor $monitor not found"
+ exit 1
+ }
+ if [[ -f "$BRIGHTNESS_SAVE_FILE" ]]; then
+ grep -v "^${monitor}:" "$BRIGHTNESS_SAVE_FILE" > "${BRIGHTNESS_SAVE_FILE}.tmp" 2>/dev/null || true
+ echo "${monitor}:${cur}" >> "${BRIGHTNESS_SAVE_FILE}.tmp"
+ mv "${BRIGHTNESS_SAVE_FILE}.tmp" "$BRIGHTNESS_SAVE_FILE"
+ else
+ echo "${monitor}:${cur}" > "$BRIGHTNESS_SAVE_FILE"
+ fi
+ echo "Saved current brightness for $monitor (${cur}%)"
+ fi
+}
+
+brightness_restore() {
+ local monitor="${1:-}"
+ if [[ ! -f "$BRIGHTNESS_SAVE_FILE" ]]; then
+ echo "Error: No saved brightness found. Use -s to save first."
+ exit 1
+ fi
+
+ if [[ -z "$monitor" ]]; then
+ while IFS=: read -r name value; do
+ [[ -n "$name" && -n "$value" ]] || continue
+ brightnessctl -d "$name" set "${value}%" 2>/dev/null || echo "Warning: Could not restore brightness for $name"
+ done < "$BRIGHTNESS_SAVE_FILE"
+ echo "Restored brightness for all monitors"
+ else
+ local value
+ value=$(grep "^${monitor}:" "$BRIGHTNESS_SAVE_FILE" | cut -d: -f2)
+ if [[ -z "$value" ]]; then
+ echo "Error: No saved brightness for monitor $monitor"
+ exit 1
+ fi
+ brightnessctl -d "$monitor" set "${value}%" 2>/dev/null || {
+ echo "Error: Could not restore brightness for $monitor"
+ exit 1
+ }
+ echo "Restored brightness for $monitor to ${value}%"
+ fi
+}
+
+brightness_set() {
+ local value="$1"
+ local monitor="${2:-}"
+ local save_flag="$3"
+
+ if [[ "$value" -lt 0 ]] || [[ "$value" -gt 100 ]]; then
+ echo "Error: Brightness must be between 0 and 100"
+ exit 1
+ fi
+
+ if [[ "$save_flag" == "true" ]]; then
+ brightness_save "$monitor"
+ fi
+
+ if [[ -z "$monitor" ]]; then
+ brightnessctl set "${value}%" 2>/dev/null || {
+ echo "Error: Could not set brightness"
+ exit 1
+ }
+ echo "Set brightness to ${value}% for all monitors"
+ else
+ brightnessctl -d "$monitor" set "${value}%" 2>/dev/null || {
+ echo "Error: Could not set brightness for $monitor"
+ exit 1
+ }
+ echo "Set brightness to ${value}% for $monitor"
+ fi
+}
+
+brightness_adjust() {
+ local delta="$1"
+ local monitor="${2:-}"
+
+ if [[ -z "$monitor" ]]; then
+ brightnessctl set "${delta}%" 2>/dev/null || {
+ echo "Error: Could not adjust brightness"
+ exit 1
+ }
+ echo "Adjusted brightness by ${delta}% for all monitors"
+ else
+ brightnessctl -d "$monitor" set "${delta}%" 2>/dev/null || {
+ echo "Error: Could not adjust brightness for $monitor"
+ exit 1
+ }
+ echo "Adjusted brightness by ${delta}% for $monitor"
+ fi
+}
+
+# ── Volume ────────────────────────────────────────────────────────────────────
+_vol_ctl() {
+ if command -v wpctl &>/dev/null; then echo "wpctl"; else echo "pactl"; fi
+}
+
+volume_up() {
+ case "$(_vol_ctl)" in
+ wpctl) wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ 2>/dev/null ;;
+ pactl) pactl set-sink-volume @DEFAULT_SINK@ +5% 2>/dev/null ;;
+ esac
+}
+volume_down() {
+ case "$(_vol_ctl)" in
+ wpctl) wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- 2>/dev/null ;;
+ pactl) pactl set-sink-volume @DEFAULT_SINK@ -5% 2>/dev/null ;;
+ esac
+}
+volume_mute() {
+ case "$(_vol_ctl)" in
+ wpctl) wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle 2>/dev/null ;;
+ pactl) pactl set-sink-mute @DEFAULT_SINK@ toggle 2>/dev/null ;;
+ esac
+}
+mic_mute() {
+ case "$(_vol_ctl)" in
+ wpctl) wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle 2>/dev/null ;;
+ pactl) pactl set-source-mute @DEFAULT_SOURCE@ toggle 2>/dev/null ;;
+ esac
+}
+
+caffeine_toggle() {
+ if pgrep -f "systemd-inhibit.*BrainShell.*Caffeine" &>/dev/null; then
+ pkill -f "systemd-inhibit.*BrainShell.*Caffeine" 2>/dev/null || true
+ echo "Caffeine: OFF"
+ else
+ systemd-inhibit --what=idle:sleep --who="Brain Shell" --why="Caffeine mode" sleep infinity &
+ echo "Caffeine: ON"
+ fi
+}
+
+# ── Hyprland integration ──────────────────────────────────────────────────────
+HYPR_DIR="$HOME/.config/hypr"
+
+BRAIN_SHELL_CONF_BLOCK='# Brain_Shell
+source = ~/.local/share/Brain_Shell/hyprland.conf
+
+# OVERRIDES
+# Add your own Hyprland settings below. They will override Brain_Shell defaults.'
+
+BRAIN_SHELL_LUA_BLOCK='-- Brain_Shell
+loadfile(os.getenv("HOME") .. "/.local/share/Brain_Shell/hyprland.lua")()
+
+-- OVERRIDES
+-- Add your own Hyprland settings below. They will override Brain_Shell defaults.'
+
+append_hyprland_block() {
+ local conf="$1"
+ local source="$2"
+ local block="$3"
+
+ if [[ -f "$conf" ]] && grep -qF "$source" "$conf" 2>/dev/null; then
+ echo "Brain_Shell is already integrated in $conf"
+ return 0
+ fi
+
+ if [[ -f "$conf" ]] && [[ -s "$conf" ]]; then
+ printf "\n%s\n" "$block" >> "$conf"
+ else
+ printf "%s\n" "$block" > "$conf"
+ fi
+
+ echo "Brain_Shell integrated into $conf"
+}
+
+remove_hyprland_block() {
+ local conf="$1"
+ local source="$2"
+
+ if [[ ! -f "$conf" ]]; then
+ return 0
+ fi
+
+ awk '
+ /^# Brain_Shell$/ { skip=1; next }
+ /^-- Brain_Shell$/ { skip=1; next }
+ /^source =.*Brain_Shell/ { next }
+ /^loadfile.*Brain_Shell/ { next }
+ /^exec-once = brain-shell/ { next }
+ /^# OVERRIDES$/ { if(skip) next }
+ /^-- OVERRIDES$/ { if(skip) next }
+ /^# Add your own/ { if(skip) { skip=0; next } }
+ /^-- Add your own/ { if(skip) { skip=0; next } }
+ { if(!skip) print }
+ ' "$conf" > "${conf}.tmp" && mv "${conf}.tmp" "$conf"
+
+ echo "Brain_Shell removed from $conf"
+}
+
+detect_hyprland_config() {
+ if [[ -f "${HYPR_DIR}/hyprland.lua" ]]; then echo "lua"
+ elif [[ -f "${HYPR_DIR}/hyprland.conf" ]]; then echo "conf"
+ else echo "conf"; fi
+}
+
+generate_hyprland_configs() {
+ mkdir -p "$DATA_DIR"
+
+ cat > "${DATA_DIR}/hyprland.conf" <<'HYPRCONF'
+# Brain_Shell — managed Hyprland configuration
+exec-once = brain-shell
+
+windowrulev2 = noblur, class:^(quickshell)$
+windowrulev2 = noborder, class:^(quickshell)$
+windowrulev2 = noanim, class:^(quickshell)$
+windowrulev2 = rounding 0, class:^(quickshell)$
+windowrulev2 = stayfocused, class:^(quickshell)$
+windowrulev2 = allowspecialworkspace, class:^(quickshell)$
+windowrulev2 = nomaxsize, class:^(quickshell)$
+
+layerrule = blur, ^(quickshell)$
+layerrule = ignorezero, ^(quickshell)$
+layerrule = noanim, ^(quickshell)$
+HYPRCONF
+
+ cat > "${DATA_DIR}/hyprland.lua" <<'HYPRLUA'
+-- Brain_Shell — generated by HyprlandSyncService
+-- This file is regenerated automatically when keybinds change.
+-- Do not edit manually.
+HYPRLUA
+}
+
+install_hyprland() {
+ local mode="${1:-auto}"
+ if [[ "$mode" == "auto" ]]; then mode=$(detect_hyprland_config); fi
+
+ mkdir -p "$HYPR_DIR" "$DATA_DIR"
+ generate_hyprland_configs
+
+ if [[ "$mode" == "lua" ]]; then
+ local lua_source='loadfile(os.getenv("HOME") .. "/.local/share/Brain_Shell/hyprland.lua")()'
+ append_hyprland_block "${HYPR_DIR}/hyprland.lua" "$lua_source" "$BRAIN_SHELL_LUA_BLOCK"
+ remove_hyprland_block "${HYPR_DIR}/hyprland.conf" "source = ${DATA_DIR}/hyprland.conf" 2>/dev/null || true
+ else
+ local conf_source="source = ${DATA_DIR}/hyprland.conf"
+ append_hyprland_block "${HYPR_DIR}/hyprland.conf" "$conf_source" "$BRAIN_SHELL_CONF_BLOCK"
+ remove_hyprland_block "${HYPR_DIR}/hyprland.lua" 'loadfile(os.getenv("HOME") .. "/.local/share/Brain_Shell/hyprland.lua")()' 2>/dev/null || true
+ fi
+
+ echo ""
+ echo " Restart Hyprland or run: brain-shell"
+}
+
+remove_hyprland() {
+ remove_hyprland_block "${HYPR_DIR}/hyprland.conf" "source = ${DATA_DIR}/hyprland.conf" 2>/dev/null || true
+ remove_hyprland_block "${HYPR_DIR}/hyprland.lua" 'loadfile(os.getenv("HOME") .. "/.local/share/Brain_Shell/hyprland.lua")()' 2>/dev/null || true
+ rm -f "${DATA_DIR}/hyprland.conf" "${DATA_DIR}/hyprland.lua"
+}
+
+# ── Update ────────────────────────────────────────────────────────────────────
+update_shell() {
+ echo "Updating Brain_Shell..."
+ if [[ -d "${SCRIPT_DIR}/.git" ]]; then
+ cd "$SCRIPT_DIR"
+ git pull --ff-only || { echo "Error: git pull failed."; exit 1; }
+ echo "Brain_Shell updated."
+ else
+ echo "Error: Not a git repository."
+ exit 1
+ fi
+
+ local pid
+ pid=$(find_brain_shell_pid)
+ if [[ -n "$pid" ]]; then
+ echo "Restarting..."
+ restart_shell
+ fi
+}
+
+# ── Goodbye ───────────────────────────────────────────────────────────────────
+goodbye() {
+ echo "This will remove Brain_Shell from your system."
+ echo "Your configs will be preserved at: $CONFIG_DIR"
+ echo ""
+ read -rp "Continue? [y/N] " confirm
+ if [[ ! "$confirm" =~ ^[Yy] ]]; then
+ echo "Cancelled."
+ exit 0
+ fi
+
+ quit_shell 2>/dev/null || true
+ remove_hyprland
+
+ if [[ -L /usr/local/bin/brain-shell ]]; then
+ sudo rm -f /usr/local/bin/brain-shell 2>/dev/null || echo " Please remove /usr/local/bin/brain-shell manually"
+ fi
+
+ echo ""
+ echo "Brain_Shell removed."
+ echo " Configs: $CONFIG_DIR"
+ echo " Full wipe: rm -rf $CONFIG_DIR ${DATA_DIR} $SCRIPT_DIR"
+}
+
+# ═══════════════════════════════════════════════════════════════════════════════
+# MAIN
+# ═══════════════════════════════════════════════════════════════════════════════
+case "${1:-}" in
+ ""|launch) launch_shell ;;
+ help|-h|--help) show_help ;;
+ version|-v|--version) echo "Brain_Shell v${VERSION}" ;;
+ update) update_shell ;;
+ reload|restart) restart_shell ;;
+ sync) send_ipc "sync-binds" 2>/dev/null || echo "Brain_Shell not running — binds will sync on next start" ;;
+ quit|stop|exit) quit_shell ;;
+ lock|lockscreen) lock_screen ;;
+
+ screen)
+ case "${2:-}" in
+ off) hyprctl dispatch dpms off 2>/dev/null || echo "Error: hyprctl not found" ;;
+ on) hyprctl dispatch dpms on 2>/dev/null || echo "Error: hyprctl not found" ;;
+ *) echo "Usage: brain-shell screen [on|off]"; exit 1 ;;
+ esac ;;
+
+ suspend)
+ if command -v systemctl &>/dev/null; then systemctl suspend
+ elif command -v loginctl &>/dev/null; then loginctl suspend
+ else dbus-send --system --print-reply --dest=org.freedesktop.login1 /org/freedesktop/login1 org.freedesktop.login1.Manager.Suspend boolean:true
+ fi ;;
+
+ brightness)
+ ARG2="${2:-}"; ARG3="${3:-}"; ARG4="${4:-}"
+ case "$ARG2" in
+ -l|--list) brightness_list_monitors ;;
+ -r|--restore) brightness_restore "${ARG3:-}" ;;
+ -s|--save) brightness_save "${ARG3:-}" ;;
+ [+-][0-9]*)
+ MONITOR=""
+ [[ -n "$ARG3" && "$ARG3" != "-s" && "$ARG3" != "--save" ]] && MONITOR="$ARG3"
+ brightness_adjust "$ARG2" "$MONITOR" ;;
+ [0-9]*)
+ VALUE="$ARG2"; MONITOR=""; SAVE="false"
+ [[ "$ARG3" == "-s" || "$ARG3" == "--save" ]] && { SAVE="true"; }
+ [[ -n "$ARG3" && "$ARG3" != "-s" && "$ARG3" != "--save" ]] && { MONITOR="$ARG3"; [[ "${ARG4:-}" == "-s" || "${ARG4:-}" == "--save" ]] && SAVE="true"; }
+ brightness_set "$VALUE" "$MONITOR" "$SAVE" ;;
+ *) echo "Error: Invalid brightness argument '$ARG2'."; echo "Usage: brain-shell brightness <0-100|+/-delta|-s|-r|-l> [monitor]"; exit 1 ;;
+ esac ;;
+
+ volume-up) volume_up ;;
+ volume-down) volume_down ;;
+ volume-mute) volume_mute ;;
+ mic-mute) mic_mute ;;
+ caffeine) caffeine_toggle ;;
+ gamemode) send_ipc "gamemode" 2>/dev/null || echo "Error: Brain_Shell is not running" ;;
+ nightlight)
+ if command -v hyprsunset &>/dev/null; then
+ if pgrep -x hyprsunset &>/dev/null; then
+ pkill -x hyprsunset 2>/dev/null && echo "Night light: OFF"
+ else
+ hyprsunset -t 4000 &>/dev/null &
+ echo "Night light: ON (4000K)"
+ fi
+ else
+ echo "Error: hyprsunset not found. Install hyprsunset for night light."
+ fi
+ ;;
+
+ run)
+ CMD="${2:-}"
+ if [[ -z "$CMD" ]]; then
+ echo "Error: No command specified."
+ echo "Run 'brain-shell help' for available commands."
+ exit 1
+ fi
+ send_ipc "$CMD" ;;
+
+ install)
+ TARGET="${2:-}"
+ if [[ "$TARGET" == "hyprland" ]]; then
+ MODE="auto"
+ for arg in "${@:3}"; do
+ case "$arg" in --lua) MODE="lua" ;; --conf) MODE="conf" ;; esac
+ done
+ install_hyprland "$MODE"
+ else
+ echo "Error: Unknown install target '$TARGET'."
+ echo "Usage: brain-shell install hyprland [--lua|--conf]"
+ exit 1
+ fi ;;
+
+ remove)
+ TARGET="${2:-}"
+ if [[ "$TARGET" == "hyprland" ]]; then remove_hyprland
+ else echo "Error: Unknown remove target '$TARGET'."; echo "Usage: brain-shell remove hyprland"; exit 1
+ fi ;;
+
+ goodbye) goodbye ;;
+
+ *) echo "Error: Unknown command '$1'."; echo "Run 'brain-shell help' for usage information."; exit 1 ;;
+esac
diff --git a/dots-extra/install-arch.sh b/dots-extra/install-arch.sh
new file mode 100644
index 0000000..40380bc
--- /dev/null
+++ b/dots-extra/install-arch.sh
@@ -0,0 +1,519 @@
+#!/bin/bash
+# ─────────────────────────────────────────────────────────────────────────────
+# Brain Shell — Arch Linux Installer
+# Invoked by install.sh: $1=HYPRLAND_CONF $2=BACKUP_DIR $3=CONFIG_TYPE
+# ─────────────────────────────────────────────────────────────────────────────
+
+set -eo pipefail
+
+# ── Arguments (validated up-front) ───────────────────────────────────────────
+HYPRLAND_CONF="${1:?Missing arg: HYPRLAND_CONF path}"
+BACKUP_DIR="${2:?Missing arg: BACKUP_DIR}"
+CONFIG_TYPE="${3:?Missing arg: CONFIG_TYPE (conf|lua)}"
+REPO_DIR="$HOME/.local/src/Brain_Shell"
+
+# ── Colors ────────────────────────────────────────────────────────────────────
+RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
+BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'
+DIM='\033[2m'; NC='\033[0m'
+
+# ── Logging ───────────────────────────────────────────────────────────────────
+log_info() { echo -e " ${BLUE}·${NC} $1"; }
+log_ok() { echo -e " ${GREEN}✓${NC} $1"; }
+log_warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
+log_error() { echo -e " ${RED}✗${NC} $1" >&2; }
+die() { echo ""; log_error "$1"; exit 1; }
+
+TOTAL_STEPS=6
+step() {
+ echo ""
+ echo -e "${BOLD}${CYAN} [$1/$TOTAL_STEPS] $2${NC}"
+ echo -e " ${DIM}$(printf '%.0s─' {1..50})${NC}"
+}
+
+# ── Failure Tracking ──────────────────────────────────────────────────────────
+# Packages that couldn't be installed are collected here and shown in the
+# final summary with manual fix commands instead of aborting the whole install.
+declare -a FAILED_PKGS=()
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# PACKAGE HELPERS
+# ══════════════════════════════════════════════════════════════════════════════
+
+# pacman_install [ ...]
+#
+# Strategy (three attempts, most to least aggressive):
+#
+# 1. Bulk install — fastest; skips already-installed packages via --needed.
+#
+# 2. Per-package retry — if the bulk transaction fails because ONE package
+# has a conflict, the entire batch is rejected. Retrying individually
+# isolates which package is actually broken so the rest can still install.
+#
+# 3. --overwrite='*' per package — resolves FILE-OWNERSHIP conflicts, where
+# two packages both claim the same path. Safe in practice: the new package
+# just wins the ownership. This does NOT help with hard PKGBUILD conflicts
+# (ConflictsWith). Those need manual resolution (see summary output).
+#
+pacman_install() {
+ local -a pkgs=("$@")
+ local total=${#pkgs[@]}
+
+ log_info "Installing $total packages via pacman..."
+
+ # Attempt 1 — bulk
+ if sudo pacman -S --needed --noconfirm "${pkgs[@]}" 2>/dev/null; then
+ log_ok "All $total packages installed."
+ return 0
+ fi
+
+ # Bulk failed — at least one conflict. Fall back to one-by-one.
+ echo ""
+ log_warn "Bulk install hit a conflict. Retrying individually..."
+ echo ""
+
+ local installed=0 already=0
+ local -a failed=()
+
+ for pkg in "${pkgs[@]}"; do
+ # Skip silently if already present
+ if pacman -Qi "$pkg" &>/dev/null; then
+ already=$(( already + 1 ))
+ continue
+ fi
+
+ printf " ${DIM}%-32s${NC} " "$pkg"
+
+ # Attempt 2 — standard per-package
+ if sudo pacman -S --needed --noconfirm "$pkg" &>/dev/null; then
+ echo -e "${GREEN}✓${NC}"
+ installed=$(( installed + 1 ))
+ continue
+ fi
+
+ # Attempt 3 — overwrite (file-ownership conflicts)
+ if sudo pacman -S --needed --noconfirm --overwrite='*' "$pkg" &>/dev/null; then
+ echo -e "${YELLOW}✓ overwrite${NC}"
+ installed=$(( installed + 1 ))
+ continue
+ fi
+
+ # Hard conflict — needs manual resolution
+ echo -e "${RED}✗ conflict${NC}"
+ failed+=("$pkg")
+ done
+
+ echo ""
+ log_ok "$installed installed, $already already present"
+
+ if [[ ${#failed[@]} -gt 0 ]]; then
+ log_warn "${#failed[@]} package(s) failed (hard conflict — see summary):"
+ for pkg in "${failed[@]}"; do
+ log_warn " $pkg"
+ FAILED_PKGS+=("pacman:$pkg")
+ done
+ fi
+}
+
+# aur_install [ ...]
+#
+# Installs packages individually via yay/paru. AUR helpers build from source,
+# so they don't support the same --overwrite shortcut. Failures are tracked but
+# non-fatal; quickshell is checked explicitly afterwards.
+#
+aur_install() {
+ local helper="$1"; shift
+ local -a pkgs=("$@")
+ local -a failed=()
+
+ echo ""
+ for pkg in "${pkgs[@]}"; do
+ printf " ${DIM}%-32s${NC} " "$pkg"
+
+ if $helper -Q "$pkg" &>/dev/null; then
+ echo -e "${GREEN}✓ already installed${NC}"
+ continue
+ fi
+
+ if $helper -S --noconfirm "$pkg" &>/dev/null; then
+ echo -e "${GREEN}✓${NC}"
+ else
+ echo -e "${RED}✗${NC}"
+ failed+=("$pkg")
+ fi
+ done
+
+ echo ""
+
+ if [[ ${#failed[@]} -gt 0 ]]; then
+ log_warn "${#failed[@]} AUR package(s) failed (see summary):"
+ for pkg in "${failed[@]}"; do
+ log_warn " $pkg"
+ FAILED_PKGS+=("aur:$pkg")
+ done
+ else
+ log_ok "All AUR packages installed."
+ fi
+}
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# STEP 1 — AUR Helper
+# ══════════════════════════════════════════════════════════════════════════════
+step 1 "AUR Helper"
+
+AUR_HELPER=""
+
+if command -v yay &>/dev/null; then
+ AUR_HELPER="yay"
+ log_ok "yay detected"
+elif command -v paru &>/dev/null; then
+ AUR_HELPER="paru"
+ log_ok "paru detected"
+else
+ log_warn "No AUR helper found (yay / paru)."
+ echo ""
+ echo -e " ${BOLD}Select one to install:${NC}"
+ echo " 1) yay — more interactive, widely used"
+ echo " 2) paru — faster builds, more features"
+ echo " 3) Skip — pacman-only (quickshell will be missing)"
+ echo ""
+ read -rp " Choice [1/2/3]: " _aur_choice < /dev/tty
+
+ _bootstrap_aur_helper() {
+ local name="$1"
+ log_info "Bootstrapping $name from AUR..."
+ sudo pacman -S --needed --noconfirm git base-devel
+ local tmp; tmp=$(mktemp -d)
+ git clone "https://aur.archlinux.org/${name}.git" "$tmp/$name"
+ ( cd "$tmp/$name" && makepkg -si --noconfirm )
+ rm -rf "$tmp"
+ log_ok "$name installed."
+ }
+
+ case "$_aur_choice" in
+ 1) _bootstrap_aur_helper yay; AUR_HELPER="yay" ;;
+ 2) _bootstrap_aur_helper paru; AUR_HELPER="paru" ;;
+ 3)
+ log_warn "Skipping AUR helper."
+ log_warn "quickshell is required — install yay or paru later and re-run."
+ AUR_HELPER="none"
+ ;;
+ *) die "Invalid choice." ;;
+ esac
+fi
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# STEP 2 — Pacman Packages
+# ══════════════════════════════════════════════════════════════════════════════
+step 2 "Pacman Packages"
+
+PACMAN_DEPS=(
+ # Qt6 runtime
+ qt6-base qt6-declarative qt6-multimedia qt6-5compat qt6ct
+
+ # Audio / PipeWire
+ pipewire pipewire-pulse wireplumber
+
+ # Media & player control
+ playerctl mpv-mpris mpd-mpris
+
+ # Network / Bluetooth
+ networkmanager bluez bluez-utils
+
+ # System services
+ brightnessctl upower libnotify polkit
+ python wl-clipboard slurp xdg-user-dirs
+
+ # Screen recording
+ wf-recorder cava
+
+ # Wallpaper / theming
+ imagemagick
+
+ # Input simulation
+ wtype
+
+ # Hardware sensors
+ lm_sensors rfkill
+
+ # Hyprland ecosystem (hyprshutdown is AUR-only — kept out of here)
+ hyprland hyprsunset hyprlock hyprpolkitagent hypridle
+ xdg-desktop-portal-hyprland
+
+ # Fonts
+ ttf-jetbrains-mono-nerd ttf-nerd-fonts-symbols-common
+)
+
+log_info "Syncing package database..."
+sudo pacman -Syu --noconfirm 2>/dev/null || {
+ log_warn "System update failed — continuing with current DB. Some packages may be stale."
+}
+
+pacman_install "${PACMAN_DEPS[@]}"
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# STEP 3 — AUR Packages
+# ══════════════════════════════════════════════════════════════════════════════
+step 3 "AUR Packages"
+
+AUR_DEPS=(
+ quickshell # REQUIRED — the shell runtime
+ awww # animation daemon
+ matugen # Material You color generation
+ envycontrol # GPU switching
+ auto-cpufreq # CPU power management
+ nbfc-linux # fan control
+ cliphist # clipboard history
+ hyprshutdown # power menu backend
+ grimblast-git # screenshot tool
+)
+
+if [[ "$AUR_HELPER" == "none" ]]; then
+ log_warn "No AUR helper — skipping all AUR packages."
+ for pkg in "${AUR_DEPS[@]}"; do
+ FAILED_PKGS+=("aur:$pkg (no helper)")
+ done
+else
+ log_info "Using: $AUR_HELPER"
+ aur_install "$AUR_HELPER" "${AUR_DEPS[@]}"
+fi
+
+# quickshell is non-negotiable
+if ! "$AUR_HELPER" -Q quickshell &>/dev/null 2>&1; then
+ die "quickshell failed to install. Brain Shell cannot run without it."
+fi
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# STEP 4 — Systemd Services
+# ══════════════════════════════════════════════════════════════════════════════
+step 4 "Systemd Services"
+
+_svc_system() {
+ sudo systemctl enable --now "$1" 2>/dev/null \
+ && log_ok "system: $1" \
+ || log_warn "system: $1 (failed to enable — may not apply to your setup)"
+}
+_svc_user() {
+ systemctl --user enable --now "$1" 2>/dev/null \
+ && log_ok "user: $1" \
+ || log_warn "user: $1 (failed to enable)"
+}
+
+_svc_system NetworkManager
+_svc_system bluetooth
+_svc_system upower
+_svc_user pipewire
+_svc_user pipewire-pulse
+_svc_user wireplumber
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# STEP 5 — Hyprland Config
+# ══════════════════════════════════════════════════════════════════════════════
+step 5 "Hyprland Config"
+
+# Marker used to detect whether the block was already appended
+_MARKER="quickshell.*Brain_Shell"
+
+_append_conf() {
+ cat << 'EOF' >> "$1"
+
+# Brain Shell Autostarts
+exec-once = awww-daemon
+exec-once = hypridle -c $HOME/.local/src/Brain_Shell/src/config/hypridle.conf
+exec-once = quickshell -c $HOME/.local/src/Brain_Shell/.
+exec-once = systemctl --user start hyprpolkitagent
+exec-once = wl-paste --type text --watch cliphist store
+exec-once = wl-paste --type image --watch cliphist store
+EOF
+}
+
+_append_lua() {
+ cat << 'EOF' >> "$1"
+
+-- Brain Shell Autostarts
+hl.on("hyprland.start", function()
+ hl.exec_cmd("awww-daemon")
+ hl.exec_cmd("hypridle -c " .. os.getenv("HOME") .. "/.local/src/Brain_Shell/src/config/hypridle.conf")
+ hl.exec_cmd("quickshell -c " .. os.getenv("HOME") .. "/.local/src/Brain_Shell")
+ hl.exec_cmd("systemctl --user start hyprpolkitagent")
+ hl.exec_cmd("wl-paste --type text --watch cliphist store")
+ hl.exec_cmd("wl-paste --type image --watch cliphist store")
+end)
+EOF
+}
+
+if grep -q "$_MARKER" "$HYPRLAND_CONF" 2>/dev/null; then
+ log_warn "Autostart block already present — skipping."
+else
+ case "$CONFIG_TYPE" in
+ conf)
+ _append_conf "$HYPRLAND_CONF"
+ log_ok "Autostart block appended to hyprland.conf"
+ ;;
+ lua)
+ # Extra safety backup before touching a Lua config
+ cp "$HYPRLAND_CONF" "${HYPRLAND_CONF}.pre-brain-shell"
+ log_info "Safety backup: ${HYPRLAND_CONF}.pre-brain-shell"
+ _append_lua "$HYPRLAND_CONF"
+ log_ok "Autostart block appended to hyprland.lua"
+ ;;
+ *)
+ log_warn "Unknown config type '$CONFIG_TYPE' — skipping Hyprland config update."
+ ;;
+ esac
+fi
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# STEP 6 — Brain Shell Config & Keybind Check
+# ══════════════════════════════════════════════════════════════════════════════
+step 6 "Brain Shell Config"
+
+USER_DATA="$HOME/.config/Brain_Shell/src/user_data"
+
+mkdir -p "$USER_DATA" \
+ "$HOME/.config/hypr/shaders" \
+ "$HOME/.config/matugen/templates"
+
+# Copy hypridle config; -n = do not overwrite if already customised
+if cp -n "$REPO_DIR/src/config/hypridle.conf" "$HOME/.config/hypr/" 2>/dev/null; then
+ log_ok "hypridle.conf → ~/.config/hypr/"
+else
+ log_info "hypridle.conf already exists — not overwritten"
+fi
+
+printf '{"configProvider": "%s"}\n' "$CONFIG_TYPE" > "$USER_DATA/config_Provider.json"
+printf '{}\n' > "$USER_DATA/keybinds.json"
+
+log_ok "Config dirs created"
+log_ok "config_Provider.json → $CONFIG_TYPE"
+
+log_info "Initializing cache directories..."
+mkdir -p "$HOME/.cache/brain-shell"
+touch "$HOME/.cache/brain-shell/colors.json"
+mkdir -p "$HOME/Pictures/Wallpapers"
+cp -n -r "$REPO_DIR/src/assets/wallpapers"/* "$HOME/Pictures/Wallpapers/" 2>/dev/null || true
+
+log_ok "Cache directories initialized"
+
+# ── Keybind Conflict Detection ────────────────────────────────────────────────
+echo ""
+log_info "Checking keybind conflicts against active Hyprland session..."
+
+python3 << 'PYEOF' || log_warn "Keybind check skipped (Python error or no Hyprland session)."
+import subprocess, json, os, sys
+
+DEFAULTS = {
+ "dashboard-home": {"mods": "SUPER", "key": "D", "label": "Dashboard: System"},
+ "dashboard-stats": {"mods": "CTRL + SHIFT", "key": "ESCAPE", "label": "Dashboard: Home"},
+ "dashboard-kanban": {"mods": "SUPER", "key": "Z", "label": "Dashboard: Tasks"},
+ "dashboard-launcher": {"mods": "SUPER", "key": "Q", "label": "Dashboard: Apps"},
+ "dashboard-config": {"mods": "SUPER", "key": "C", "label": "Dashboard: Config"},
+ "PowerMenu-toggle": {"mods": "SUPER", "key": "ESCAPE", "label": "Power Menu"},
+ "notification-toggle": {"mods": "SUPER", "key": "N", "label": "Notifications"},
+ "wallpaper-toggle": {"mods": "SUPER", "key": "W", "label": "Wallpaper"},
+ "clipboard-toggle": {"mods": "SUPER", "key": "V", "label": "Clipboard"},
+ "wifi-toggle": {"mods": "SUPER + ALT", "key": "W", "label": "Network: Wi-Fi"},
+ "bluetooth-toggle": {"mods": "SUPER + ALT", "key": "B", "label": "Network: Bluetooth"},
+ "vpn-toggle": {"mods": "SUPER + ALT", "key": "G", "label": "Network: VPN"},
+ "hotspot-toggle": {"mods": "SUPER + ALT", "key": "H", "label": "Network: Hotspot"},
+ "audioOut-toggle": {"mods": "SUPER", "key": "A", "label": "Audio: Output"},
+ "audioIn-toggle": {"mods": "SUPER + ALT", "key": "I", "label": "Audio: Input"},
+ "audioMix-toggle": {"mods": "SUPER", "key": "M", "label": "Audio: Mixer"},
+ "focus-toggle": {"mods": "SUPER", "key": "B", "label": "Focus Mode"},
+ "screenrec-on": {"mods": "ALT", "key": "F9", "label": "Screen Record"},
+}
+
+MOD_BITS = {"SHIFT": 1, "CTRL": 4, "ALT": 8, "SUPER": 64}
+
+def mods_to_mask(mods_str):
+ mask = 0
+ for part in mods_str.upper().split("+"):
+ mask |= MOD_BITS.get(part.strip(), 0)
+ return mask
+
+try:
+ raw = subprocess.check_output(["hyprctl", "binds", "-j"], stderr=subprocess.DEVNULL).decode()
+ hypr_binds = json.loads(raw)
+except Exception:
+ print(" \033[2m(not inside Hyprland — skipping live conflict check)\033[0m")
+ sys.exit(0)
+
+conflicts = {}
+for action, data in DEFAULTS.items():
+ mask = mods_to_mask(data["mods"])
+ key = data["key"].lower()
+ for hb in hypr_binds:
+ if hb.get("submap", "") or hb.get("mouse"):
+ continue
+ if hb.get("modmask") == mask and str(hb.get("key", "")).lower() == key:
+ desc = hb.get("dispatcher", "")
+ arg = hb.get("arg", "")
+ conflicts[action] = {
+ "bind": f"{data['mods']} + {data['key']}",
+ "label": data["label"],
+ "used_by": f"{desc} {arg}".strip(),
+ }
+ break
+
+if not conflicts:
+ print(" \033[0;32m✓\033[0m No keybind conflicts detected.")
+ sys.exit(0)
+
+print(f"\n \033[0;31m✗\033[0m {len(conflicts)} conflict(s) found:\n")
+unbound = {}
+for action, info in conflicts.items():
+ print(f" \033[1m{info['bind']:<24}\033[0m {info['label']}")
+ print(f" {'':24} already used by: {info['used_by']}\n")
+ unbound[action] = {"mods": "", "key": ""}
+
+config_path = os.path.expanduser("~/.config/Brain_Shell/src/user_data/keybinds.json")
+with open(config_path, "w") as f:
+ json.dump(unbound, f, indent=2)
+
+print(" \033[1;33m⚠\033[0m Conflicting binds left unbound in Brain Shell.")
+print(" Re-assign them: Dashboard → Config → Keybinds\n")
+PYEOF
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# SUMMARY
+# ══════════════════════════════════════════════════════════════════════════════
+echo ""
+echo -e " ${DIM}$(printf '%.0s─' {1..50})${NC}"
+
+if [[ ${#FAILED_PKGS[@]} -eq 0 ]]; then
+ log_ok "Arch installation complete — no failures."
+ echo ""
+else
+ log_warn "Installation finished with ${#FAILED_PKGS[@]} unresolved package(s)."
+ echo ""
+ echo -e " ${BOLD}Retry commands:${NC}"
+
+ for entry in "${FAILED_PKGS[@]}"; do
+ _src="${entry%%:*}"
+ _pkg="${entry#*:}"
+ # Strip any parenthetical note before displaying the install command
+ _pkg_name="${_pkg%% (*}"
+ if [[ "$_src" == "pacman" ]]; then
+ log_info "sudo pacman -S $_pkg_name"
+ else
+ log_info "$AUR_HELPER -S $_pkg_name"
+ fi
+ done
+
+ echo ""
+ echo -e " ${BOLD}Resolving hard package conflicts (ConflictsWith):${NC}"
+ log_info "Find what conflicts: pacman -Si | grep Conflicts"
+ log_info "Remove the old one: sudo pacman -Rdd "
+ log_info "Then retry: sudo pacman -S "
+ echo ""
+fi
+
+exit 0
diff --git a/dots-extra/validate-install.sh b/dots-extra/validate-install.sh
new file mode 100644
index 0000000..44e74a3
--- /dev/null
+++ b/dots-extra/validate-install.sh
@@ -0,0 +1,211 @@
+#!/bin/bash
+
+# Brain Shell — Post-Installation Validator
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+BOLD='\033[1m'
+NC='\033[0m'
+
+INSTALLED=0
+MISSING=0
+OPTIONAL_MISSING=0
+
+log_installed() {
+ echo -e "${GREEN}[✓]${NC} $1"
+ ((INSTALLED++))
+}
+
+log_missing() {
+ echo -e "${RED}[✗]${NC} $1 ${YELLOW}(MISSING)${NC}"
+ ((MISSING++))
+}
+
+log_optional() {
+ echo -e "${YELLOW}[○]${NC} $1 ${YELLOW}(optional)${NC}"
+ ((OPTIONAL_MISSING++))
+}
+
+log_info() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+check_command() {
+ if command -v "$1" &> /dev/null; then
+ log_installed "$1"
+ else
+ log_missing "$1"
+ fi
+}
+
+check_optional() {
+ if command -v "$1" &> /dev/null; then
+ log_installed "$1 (optional)"
+ else
+ log_optional "$1"
+ fi
+}
+
+check_package() {
+ local pkg="$1"
+ local cmd="$2"
+ [[ -z "$cmd" ]] && cmd="$pkg"
+
+ if command -v "$cmd" &> /dev/null; then
+ log_installed "$pkg"
+ else
+ log_missing "$pkg"
+ fi
+}
+
+clear
+echo "Brain Shell — Post-Installation Validator"
+echo "Verify all dependencies are installed"
+echo ""
+
+if [[ -f /etc/os-release ]]; then
+ . /etc/os-release
+ DISTRO="$ID"
+else
+ DISTRO="unknown"
+fi
+
+log_info "Detected distribution: $DISTRO"
+echo ""
+
+echo "# CORE RUNTIME"
+check_command "quickshell"
+check_command "hyprland"
+check_command "hyprctl"
+
+echo ""
+echo "# QT6 & RENDERING"
+check_package "qt6-base" "qdbus"
+check_command "qt6ct"
+
+echo ""
+echo "# SYSTEM TOOLS"
+check_command "pactl" || check_command "pacmd"
+check_command "bluetoothctl"
+check_command "brightnessctl"
+check_command "upower"
+check_command "notify-send"
+check_command "pkexec"
+check_command "python"
+check_command "wl-copy"
+check_command "slurp"
+
+echo ""
+echo "# SCREEN RECORDING"
+check_command "wf-recorder"
+check_command "cava"
+
+echo ""
+echo "# WALLPAPER & THEMING"
+check_command "magick"
+check_optional "awww"
+check_optional "matugen"
+
+echo ""
+echo "# CLIPBOARD"
+check_command "wtype"
+check_optional "cliphist"
+
+echo ""
+echo "# POWER & HARDWARE"
+check_optional "envycontrol"
+check_optional "auto-cpufreq"
+check_command "sensors"
+check_optional "nbfc"
+check_command "rfkill"
+
+echo ""
+echo "# HYPRLAND ECOSYSTEM"
+check_command "hyprsunset"
+check_command "hyprlock"
+check_command "hypridle"
+check_optional "hyprshutdown"
+
+echo ""
+echo "# FONTS"
+if fc-list | grep -q "JetBrains Mono"; then
+ log_installed "JetBrains Mono Nerd Font"
+else
+ log_missing "JetBrains Mono Nerd Font"
+fi
+
+echo ""
+echo "# CONFIGURATION FILES"
+
+if [[ -f "$HOME/.config/hypr/hyprland.conf" ]]; then
+ log_installed "Hyprland config"
+
+ if grep -q "quickshell.*-c.*Brain_Shell" "$HOME/.config/hypr/hyprland.conf"; then
+ log_installed "Brain Shell exec-once in hyprland.conf"
+ else
+ log_missing "Brain Shell exec-once in hyprland.conf"
+ fi
+else
+ log_missing "Hyprland config"
+fi
+
+if [[ -f "$HOME/.config/hypr/hyprland.lua" ]]; then
+ log_installed "Hyprland Lua config"
+
+ if grep -q "quickshell.*Brain_Shell" "$HOME/.config/hypr/hyprland.lua"; then
+ log_installed "Brain Shell exec-once in hyprland.lua"
+ else
+ log_optional "Brain Shell exec-once in hyprland.lua (optional)"
+ fi
+else
+ log_optional "Hyprland Lua config (optional)"
+fi
+
+if [[ -d "$HOME/.local/src/Brain_Shell" ]]; then
+ log_installed "Brain Shell repository"
+else
+ log_missing "Brain Shell repository"
+fi
+
+if [[ -d "$HOME/.config/Brain_Shell" ]]; then
+ log_installed "Brain Shell config directory"
+else
+ log_missing "Brain Shell config directory"
+fi
+
+echo ""
+echo "# BACKUPS"
+BACKUP_COUNT=$(ls -d $HOME/.config.backup-* 2>/dev/null | wc -l)
+
+if [[ $BACKUP_COUNT -gt 0 ]]; then
+ log_info "Found $BACKUP_COUNT config backup(s)"
+ ls -d $HOME/.config.backup-* 2>/dev/null | while read backup; do
+ echo -e " ${BLUE}→${NC} ${backup##*/}"
+ done
+else
+ log_optional "No config backups found"
+fi
+
+echo ""
+echo "# SUMMARY"
+TOTAL=$((INSTALLED + MISSING + OPTIONAL_MISSING))
+
+echo -e "${GREEN}✓ Installed: $INSTALLED${NC}"
+echo -e "${RED}✗ Missing: $MISSING${NC}"
+echo -e "${YELLOW}○ Optional: $OPTIONAL_MISSING${NC}"
+echo ""
+
+if [[ $MISSING -eq 0 ]]; then
+ echo -e "${GREEN}All required dependencies are installed!${NC}"
+ exit 0
+else
+ echo -e "${YELLOW}Some required dependencies are missing.${NC}"
+ echo ""
+ echo "To fix:"
+ echo " • Arch: Re-run install-arch.sh or install missing packages with pacman/yay"
+ echo " • NixOS: Re-run install-nix.sh or add packages to your flake.nix"
+ echo ""
+ exit 1
+fi
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..2d59697
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,61 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1780749050,
+ "narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "a799d3e3886da994fa307f817a6bc705ae538eeb",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "ref": "nixos-unstable",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..931054c
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,195 @@
+{
+ description = "Brain Shell — Modular Quickshell/QML desktop shell for Hyprland";
+
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
+
+ flake-utils = {
+ url = "github:numtide/flake-utils";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ };
+
+ outputs =
+ {
+ self,
+ nixpkgs,
+ flake-utils,
+ }:
+ flake-utils.lib.eachDefaultSystem (
+ system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+
+ # ── Runtime dependencies (required at launch) ──────────────────────
+ runtimeDeps = with pkgs; [
+ # Core shell runtime
+ quickshell
+ hyprland
+ qt6.full
+ qt6ct
+
+ # Audio
+ pipewire
+ pipewire-pulse
+ wireplumber
+ playerctl
+ mpv-mpris
+ mpd-mpris
+
+ # Network / Bluetooth
+ networkmanager
+ bluez
+ bluez-utils
+
+ # Display / input utilities
+ brightnessctl
+ wl-clipboard
+ slurp
+ xdg-user-dirs
+ xdg-desktop-portal-hyprland
+
+ # System info
+ upower
+ libnotify
+ polkit
+ lm_sensors
+ rfkill
+
+ # Media & visualiser
+ cava
+ python3
+
+ # Screen recording
+ wf-recorder
+
+ # Wallpaper & theming
+ imagemagick
+ awww
+ matugen
+
+ # Clipboard integration
+ wtype
+ cliphist
+
+ # Power & hardware management
+ envycontrol
+ auto-cpufreq
+
+ # Hyprland ecosystem
+ hyprsunset
+ hyprlock
+ hypridle
+ ];
+
+ # ── Development extras (not needed at runtime) ─────────────────────
+ devDeps = with pkgs; [
+ git
+ bash
+ shellcheck
+ python3Packages.python-lsp-server
+ ];
+
+ # ── Fonts ──────────────────────────────────────────────────────────
+ fonts = with pkgs; [
+ (nerdfonts.override { fonts = [ "JetBrainsMono" ]; })
+ ];
+
+ # ── The Brain Shell package ────────────────────────────────────────
+ brain-shell = pkgs.stdenv.mkDerivation {
+ pname = "brain-shell";
+ version = "0.1.0";
+
+ src = ./.;
+
+ nativeBuildInputs = [ pkgs.makeWrapper ];
+ buildInputs = runtimeDeps ++ fonts;
+
+ installPhase = ''
+ runHook preInstall
+
+ mkdir -p $out/share/brain-shell
+ cp -r . $out/share/brain-shell/
+
+ mkdir -p $out/bin
+ makeWrapper ${pkgs.quickshell}/bin/quickshell $out/bin/brain-shell \
+ --add-flags "-c $out/share/brain-shell" \
+ --set QT_QPA_PLATFORMTHEME qt6ct \
+ --prefix PATH : ${pkgs.lib.makeBinPath runtimeDeps}
+
+ runHook postInstall
+ '';
+
+ meta = with pkgs.lib; {
+ description = "A modular Quickshell/QML desktop shell for Hyprland";
+ homepage = "https://github.com/Brainitech/Brain_Shell";
+ license = licenses.mit;
+ platforms = platforms.linux;
+ mainProgram = "brain-shell";
+ };
+ };
+
+ in
+ {
+ # ── Packages ───────────────────────────────────────────────────────
+ packages = {
+ default = brain-shell;
+ brain-shell = brain-shell;
+ };
+
+ # ── Dev shell (nix develop) ────────────────────────────────────────
+ devShells.default = pkgs.mkShell {
+ name = "brain-shell-dev";
+
+ buildInputs = runtimeDeps ++ devDeps ++ fonts;
+
+ shellHook = ''
+ export QT_QPA_PLATFORMTHEME=qt6ct
+ export BRAIN_SHELL_ROOT="$(pwd)"
+
+ echo ""
+ echo " Brain Shell dev environment"
+ echo " Run: quickshell -c \$BRAIN_SHELL_ROOT"
+ echo " Lint: shellcheck install.sh dots-extra/install-arch.sh"
+ echo ""
+ '';
+ };
+
+ # ── NixOS module ───────────────────────────────────────────────────
+ nixosModules.default =
+ {
+ config,
+ lib,
+ pkgs,
+ ...
+ }:
+ let
+ cfg = config.programs.brain-shell;
+ in
+ {
+ options.programs.brain-shell = {
+ enable = lib.mkEnableOption "Brain Shell desktop shell";
+
+ autostart = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = "Add brain-shell to Hyprland exec-once.";
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ environment.systemPackages = [ brain-shell ];
+
+ # Hyprland exec-once can't be configured reliably via NixOS
+ # modules due to how Home Manager handles it. Users should
+ # add the exec-once entries manually or use the install.sh.
+ };
+ };
+
+ # ── Checks (run by `nix flake check`) ─────────────────────────────
+ checks = {
+ build = brain-shell;
+ };
+ }
+ );
+}
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..2db96ad
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,390 @@
+#!/bin/bash
+# ─────────────────────────────────────────────────────────────────────────────
+# Brain Shell — Main Installer
+# github.com/Brainitech/Brain_Shell v0.1.0
+# ─────────────────────────────────────────────────────────────────────────────
+
+set -eo pipefail
+
+# ── Colors ────────────────────────────────────────────────────────────────────
+RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
+BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'
+DIM='\033[2m'; NC='\033[0m'
+
+# ── Logging ───────────────────────────────────────────────────────────────────
+log_info() { echo -e " ${BLUE}·${NC} $1"; }
+log_ok() { echo -e " ${GREEN}✓${NC} $1"; }
+log_warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
+log_error() { echo -e " ${RED}✗${NC} $1" >&2; }
+die() { echo ""; log_error "$1"; exit 1; }
+
+TOTAL_STEPS=7
+step() {
+ echo ""
+ echo -e "${BOLD}${CYAN} [$1/$TOTAL_STEPS] $2${NC}"
+ echo -e " ${DIM}$(printf '%.0s─' {1..50})${NC}"
+}
+
+# ── Trap ──────────────────────────────────────────────────────────────────────
+trap 'echo ""; log_error "Installation aborted unexpectedly (line $LINENO)."; exit 1' ERR
+
+# ── Banner ────────────────────────────────────────────────────────────────────
+clear
+echo -e "${BOLD}"
+echo " ███████████ ███████████ █████████ █████ ██████ █████ █████████ █████ █████ ██████████ █████ █████ "
+echo "▒▒███▒▒▒▒▒███▒▒███▒▒▒▒▒███ ███▒▒▒▒▒███ ▒▒███ ▒▒██████ ▒▒███ ███▒▒▒▒▒███▒▒███ ▒▒███ ▒▒███▒▒▒▒▒█▒▒███ ▒▒███ "
+echo " ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███▒███ ▒███ ▒███ ▒▒▒ ▒███ ▒███ ▒███ █ ▒ ▒███ ▒███ "
+echo " ▒██████████ ▒██████████ ▒███████████ ▒███ ▒███▒▒███▒███ ▒▒█████████ ▒███████████ ▒██████ ▒███ ▒███ "
+echo " ▒███▒▒▒▒▒███ ▒███▒▒▒▒▒███ ▒███▒▒▒▒▒███ ▒███ ▒███ ▒▒██████ ▒▒▒▒▒▒▒▒███ ▒███▒▒▒▒▒███ ▒███▒▒█ ▒███ ▒███ "
+echo " ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒▒█████ ███ ▒███ ▒███ ▒███ ▒███ ▒ █ ▒███ █ ▒███ █"
+echo " ███████████ █████ █████ █████ █████ █████ █████ ▒▒█████ ▒▒█████████ █████ █████ ██████████ ███████████ ███████████"
+echo -e "${NC}"
+echo -e " ${DIM}v0.1.0 · github.com/Brainitech/Brain_Shell${NC}"
+echo ""
+
+# ══════════════════════════════════════════════════════════════════════════════
+# STEP 1 — Pre-Flight Checks
+# ══════════════════════════════════════════════════════════════════════════════
+step 1 "Pre-Flight Checks"
+
+# OS
+[[ "$OSTYPE" =~ ^linux ]] || die "This installer only supports Linux."
+log_ok "Linux confirmed"
+
+# No root
+[[ "$EUID" -eq 0 ]] && die "Do not run as root. Use sudo where needed."
+log_ok "Running as user (not root)"
+
+# ── Distro Detection ─────────────────────────────────────────────────────────
+DISTRO_TYPE=""
+if [[ -f /etc/os-release ]]; then
+ source /etc/os-release
+ case "${ID:-}" in
+ arch|manjaro|garuda|cachyos|endeavouros)
+ log_ok "Distro: ${ID} (Arch-based)"
+ DISTRO_TYPE="arch"
+ ;;
+ fedora)
+ log_ok "Distro: ${ID} (Fedora)"
+ DISTRO_TYPE="fedora"
+ ;;
+ debian|ubuntu|pop)
+ log_ok "Distro: ${ID} (Debian-based)"
+ DISTRO_TYPE="debian"
+ ;;
+ nixos)
+ log_ok "Distro: NixOS"
+ DISTRO_TYPE="nix"
+ ;;
+ *)
+ die "Unsupported distro: ${ID:-unknown}. Supported: Arch-based, Fedora, Debian-based, NixOS."
+ ;;
+ esac
+else
+ die "Cannot detect distro — /etc/os-release not found."
+fi
+
+# ── Essential Commands ───────────────────────────────────────────────────────
+has_cmd() { command -v "$1" >/dev/null 2>&1; }
+
+for cmd in git curl bash; do
+ has_cmd "$cmd" || die "Missing essential command: $cmd"
+done
+log_ok "Essential commands available (git, curl, bash)"
+
+# Hyprland session (warn only, don't abort)
+if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then
+ log_warn "Not running inside a Hyprland session."
+ log_info "Changes will apply after you restart Hyprland."
+else
+ log_ok "Hyprland session active"
+fi
+
+# Hyprland config
+HYPR_DIR="$HOME/.config/hypr"
+HYPRLAND_CONF=""
+CONFIG_TYPE=""
+
+# Hyprland loads .lua first when both exist; mirror that priority here so
+# the installer always targets the file Hyprland is actually reading.
+if [[ -f "$HYPR_DIR/hyprland.lua" ]]; then
+ HYPRLAND_CONF="$HYPR_DIR/hyprland.lua"
+ CONFIG_TYPE="lua"
+ if [[ -f "$HYPR_DIR/hyprland.conf" ]]; then
+ log_ok "Hyprland config: hyprland.lua ${DIM}(hyprland.conf also present but ignored by Hyprland)${NC}"
+ else
+ log_ok "Hyprland config: hyprland.lua"
+ fi
+elif [[ -f "$HYPR_DIR/hyprland.conf" ]]; then
+ HYPRLAND_CONF="$HYPR_DIR/hyprland.conf"
+ CONFIG_TYPE="conf"
+ log_ok "Hyprland config: hyprland.conf"
+ log_warn "hyprland.conf support is deprecated as of 0.55 and will be removed in a future release."
+ log_info "Consider migrating to hyprland.lua — see https://wiki.hypr.land/Configuring/Start/"
+else
+ die "No Hyprland config found in $HYPR_DIR. Set up Hyprland first."
+fi
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# STEP 2 — Backup
+# ══════════════════════════════════════════════════════════════════════════════
+step 2 "Backup"
+
+BACKUP_TS=$(date +%Y%m%d_%H%M%S)
+BACKUP_DIR="$HOME/.config.backup-${BACKUP_TS}-Brain_Shell"
+mkdir -p "$BACKUP_DIR"
+
+if [[ -d "$HYPR_DIR" ]]; then
+ cp -r "$HYPR_DIR" "$BACKUP_DIR/"
+ log_ok "Backed up: ~/.config/hypr → $BACKUP_DIR"
+else
+ log_warn "~/.config/hypr not found — nothing to back up."
+fi
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# STEP 3 — Repository
+# ══════════════════════════════════════════════════════════════════════════════
+step 3 "Repository"
+
+REPO_PARENT="$HOME/.local/src"
+REPO_DIR="$REPO_PARENT/Brain_Shell"
+mkdir -p "$REPO_PARENT"
+
+if [[ -d "$REPO_DIR/.git" ]]; then
+ log_info "Existing clone found — updating..."
+ git -C "$REPO_DIR" fetch origin main 2>/dev/null || true
+ git -C "$REPO_DIR" checkout main 2>/dev/null || true
+
+ # Compare hashes before pulling
+ REMOTE_HASH=$(git -C "$REPO_DIR" rev-parse origin/main 2>/dev/null || echo "")
+ LOCAL_HASH=$(git -C "$REPO_DIR" rev-parse HEAD 2>/dev/null || echo "")
+
+ if [[ -n "$REMOTE_HASH" && "$REMOTE_HASH" == "$LOCAL_HASH" ]]; then
+ log_ok "Repository already up to date ($(echo "$LOCAL_HASH" | cut -c1-8))"
+ else
+ git -C "$REPO_DIR" pull origin main 2>/dev/null || true
+ log_ok "Repository updated: $REPO_DIR"
+ fi
+else
+ log_info "Cloning from GitHub..."
+ git clone -b main https://github.com/Brainitech/Brain_Shell.git "$REPO_DIR"
+ log_ok "Repository cloned: $REPO_DIR"
+fi
+
+# Ensure CLI is executable
+chmod +x "$REPO_DIR/cli.sh"
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# STEP 4 — Distro-Specific Install
+# ══════════════════════════════════════════════════════════════════════════════
+step 4 "Distro-Specific Installation"
+echo ""
+
+# Check if distro-specific installer exists
+DISTRO_INSTALLER="$REPO_DIR/dots-extra/install-${DISTRO_TYPE}.sh"
+if [[ -f "$DISTRO_INSTALLER" ]]; then
+ chmod +x "$DISTRO_INSTALLER"
+ bash "$DISTRO_INSTALLER" "$HYPRLAND_CONF" "$BACKUP_DIR" "$CONFIG_TYPE"
+else
+ log_warn "Distro installer not found: $DISTRO_INSTALLER"
+ log_info "Skipping dependency installation."
+ log_info "Make sure you have the required packages installed."
+fi
+
+# ── Quickshell verification ──────────────────────────────────────────────────
+if ! has_cmd qs && ! has_cmd quickshell; then
+ log_warn "quickshell not found in PATH after distro install."
+ log_info "Attempting to build from source..."
+
+ QUICKSHELL_REPO="https://git.outfoxxed.me/outfoxxed/quickshell"
+ BUILD_DIR=$(mktemp -d)
+
+ git clone --recursive "$QUICKSHELL_REPO" "$BUILD_DIR" 2>/dev/null || {
+ log_warn "Failed to clone Quickshell repo. Manual install required."
+ rm -rf "$BUILD_DIR"
+ }
+
+ if [[ -d "$BUILD_DIR" ]]; then
+ (
+ cd "$BUILD_DIR"
+ cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$HOME/.local" 2>/dev/null || {
+ log_warn "cmake failed — installing build tools..."
+ case "$DISTRO_TYPE" in
+ arch) sudo pacman -S --needed --noconfirm cmake ninja gcc 2>/dev/null || true ;;
+ fedora) sudo dnf install -y cmake ninja-build gcc-c++ 2>/dev/null || true ;;
+ debian) sudo apt-get install -y cmake ninja-build g++ 2>/dev/null || true ;;
+ esac
+ cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$HOME/.local" 2>/dev/null || {
+ log_warn "cmake still failed. Skipping Quickshell build."
+ exit 1
+ }
+ }
+ cmake --build build 2>/dev/null || { log_warn "Build failed."; exit 1; }
+ cmake --install build 2>/dev/null || { log_warn "Install failed."; exit 1; }
+ ) && {
+ log_ok "Quickshell built and installed to ~/.local/bin/"
+ export PATH="$HOME/.local/bin:$PATH"
+ } || {
+ log_warn "Quickshell build failed. Please install manually."
+ }
+ rm -rf "$BUILD_DIR"
+ fi
+else
+ log_ok "quickshell is available"
+fi
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# STEP 5 — Global Command
+# ══════════════════════════════════════════════════════════════════════════════
+step 5 "Global Command"
+
+BIN_DIR="/usr/local/bin"
+BIN_NAME="brain-shell"
+BIN_PATH="$BIN_DIR/$BIN_NAME"
+REPO_DIR="$HOME/.local/src/Brain_Shell"
+
+# ── Install wrapper (not a symlink — wrapper sets up PATH + QML_IMPORT_PATH first) ──
+if [[ -f "$BIN_PATH" ]]; then
+ # Check if it's our wrapper or an old symlink
+ if [[ -L "$BIN_PATH" ]]; then
+ log_info "Replacing old symlink with wrapper..."
+ sudo rm -f "$BIN_PATH"
+ elif grep -q "Brain_Shell.*wrapper" "$BIN_PATH" 2>/dev/null; then
+ log_ok "Wrapper already installed: $BIN_PATH"
+ else
+ log_warn "$BIN_PATH exists but is not our wrapper — backing up."
+ sudo mv "$BIN_PATH" "${BIN_PATH}.bak-${BACKUP_TS}"
+ fi
+fi
+
+if [[ ! -f "$BIN_PATH" ]] || ! grep -q "Brain_Shell.*wrapper" "$BIN_PATH" 2>/dev/null; then
+ sudo mkdir -p "$BIN_DIR"
+ # Generate wrapper that sets up environment before calling cli.sh
+ sudo tee "$BIN_PATH" > /dev/null << WRAPPEREOF
+#!/usr/bin/env bash
+# Brain_Shell — global launcher wrapper
+INSTALL_DIR="\${BRAIN_SHELL_DIR:-\$HOME/.local/src/Brain_Shell}"
+case ":\$PATH:" in
+ *:"\$HOME/.local/bin":*) ;;
+ *) export PATH="\$HOME/.local/bin:\$PATH" ;;
+esac
+case ":\$QML2_IMPORT_PATH:" in
+ *:"\$HOME/.local/lib/qml":*) ;;
+ *) export QML2_IMPORT_PATH="\$HOME/.local/lib/qml:\$QML2_IMPORT_PATH" ;;
+esac
+export QML_IMPORT_PATH="\$QML2_IMPORT_PATH"
+exec "\${INSTALL_DIR}/cli.sh" "\$@"
+WRAPPEREOF
+ sudo chmod +x "$BIN_PATH"
+ log_ok "Global command created: ${BIN_NAME}"
+fi
+
+# Verify
+if command -v "$BIN_NAME" &>/dev/null; then
+ log_ok "'${BIN_NAME}' is now available system-wide"
+else
+ log_info "Restart your terminal or run: export PATH=\"$BIN_DIR:\$PATH\""
+fi
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# STEP 6 — Fonts & Assets
+# ══════════════════════════════════════════════════════════════════════════════
+step 6 "Fonts & Assets"
+
+# Install JetBrains Mono if missing (Brain Shell primary font)
+if ! fc-list 2>/dev/null | grep -qi "JetBrainsMono"; then
+ log_info "JetBrains Mono not detected — installing..."
+ if ! has_cmd unzip; then
+ log_warn "unzip not found — installing..."
+ case "$DISTRO_TYPE" in
+ arch) sudo pacman -S --needed --noconfirm unzip 2>/dev/null || true ;;
+ fedora) sudo dnf install -y unzip 2>/dev/null || true ;;
+ debian) sudo apt-get install -y unzip 2>/dev/null || true ;;
+ esac
+ fi
+ FONT_DIR="$HOME/.local/share/fonts/jetbrains-mono"
+ mkdir -p "$FONT_DIR"
+ TEMP_DIR=$(mktemp -d)
+ curl -sL "https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip" -o "$TEMP_DIR/jb.zip" 2>/dev/null || {
+ log_warn "Could not download JetBrains Mono. Install manually if text looks off."
+ rm -rf "$TEMP_DIR"
+ }
+ if [[ -f "$TEMP_DIR/jb.zip" ]]; then
+ unzip -q "$TEMP_DIR/jb.zip" -d "$TEMP_DIR"
+ find "$TEMP_DIR" -name "*.ttf" -exec cp {} "$FONT_DIR/" \;
+ rm -rf "$TEMP_DIR"
+ fc-cache -f "$FONT_DIR" 2>/dev/null || true
+ log_ok "JetBrains Mono installed"
+ fi
+else
+ log_ok "JetBrains Mono already available"
+fi
+
+# Ensure wallpapers directory exists
+mkdir -p "$HOME/Pictures/Wallpapers"
+if [[ -d "$REPO_DIR/src/assets/wallpapers" ]]; then
+ cp -n -r "$REPO_DIR/src/assets/wallpapers"/* "$HOME/Pictures/Wallpapers/" 2>/dev/null || true
+ log_ok "Default wallpapers copied to ~/Pictures/Wallpapers/"
+fi
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# STEP 7 — Hyprland Integration
+# ══════════════════════════════════════════════════════════════════════════════
+step 7 "Hyprland Integration"
+
+log_info "Configuring Hyprland autostart..."
+"$REPO_DIR/cli.sh" install hyprland "--${CONFIG_TYPE}" 2>/dev/null && {
+ log_ok "Brain_Shell added to Hyprland autostart (${CONFIG_TYPE})"
+} || {
+ log_warn "Could not auto-configure Hyprland."
+ log_info "Run manually: brain-shell install hyprland"
+}
+
+# ── Post-install verification ────────────────────────────────────────────────
+echo ""
+echo -e " ${DIM}$(printf '%.0s─' {1..50})${NC}"
+log_info "Post-install verification:"
+
+# Check essential binaries
+MISSING=()
+for bin in qs quickshell hyprctl matugen; do
+ has_cmd "$bin" || MISSING+=("$bin")
+done
+
+if [[ ${#MISSING[@]} -eq 0 ]]; then
+ log_ok "All essential binaries available"
+else
+ log_warn "Missing binaries: ${MISSING[*]}"
+ log_info "Some features may not work until these are installed."
+fi
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# DONE
+# ══════════════════════════════════════════════════════════════════════════════
+echo ""
+log_ok "Brain Shell is installed."
+echo ""
+echo -e " ${BOLD}Quick start:${NC}"
+log_info "brain-shell ${DIM}Launch the shell${NC}"
+log_info "brain-shell help ${DIM}Show all commands${NC}"
+log_info "brain-shell run launcher ${DIM}Open app launcher${NC}"
+echo ""
+echo -e " ${BOLD}Restart Hyprland to activate the bar:${NC}"
+log_info "Log out and log back in ${DIM}(recommended)${NC}"
+log_info "hyprctl dispatch exit"
+log_info "Ctrl+Alt+Q ${DIM}(if configured)${NC}"
+echo ""
+echo -e " ${BOLD}Paths:${NC}"
+log_info "Config: ~/.config/Brain_Shell"
+log_info "Source: $REPO_DIR"
+log_info "Data: ~/.local/share/Brain_Shell"
+echo ""
+
+exit 0
diff --git a/shell.qml b/shell.qml
index 4e3f20e..7334af2 100644
--- a/shell.qml
+++ b/shell.qml
@@ -1,44 +1,54 @@
-// shell.qml - FINAL WORKING VERSION
import Quickshell
import QtQuick
import "./src/windows"
-import "./src/components"
-import "./src/theme/"
+import "./src/popups"
+import "./src/"
ShellRoot {
+ // Force-instantiate lazy singletons that need startup behavior
+ property var _keybinds: KeybindService
+ property var _updater: UpdateService
+ property var _ipc: IpcManager
+ property var _hyprSync: HyprlandSyncService
+
Variants {
model: Quickshell.screens
-
+
delegate: Component {
Scope {
required property var modelData
-
- // Store screen name for easy access
- property string screenName: modelData.name
-
- // ===========================================
- // WINDOWS
- // ===========================================
-
- TopBar {
- screen: modelData
- }
-
- Border {
- screen: modelData
- edge: "left"
- }
- Border {
- screen: modelData
- edge: "right"
- }
+ // ── Windows ──────────────────────────────────────
+ // Wallpaper sits at WlrLayer.Background — behind everything
+ Wallpaper { id: wallpaper; screen: modelData }
+
+ TopBar { id: topBar; screen: modelData }
+
+ Border { id: leftBorder; screen: modelData; edge: "left" }
+ Border { id: rightBorder; screen: modelData; edge: "right" }
+ Border { id: bottomBorder; screen: modelData; edge: "bottom" }
+
+ // Rounded screen corners — masks sharp monitor edges
+ ScreenCorners { screen: modelData }
+
+ // ── Overlays ─────────────────────────────────────
+ // Dismisses all popups on click-outside or Escape
+ PopupDismiss { screen: modelData }
+
+ // GPU mode change confirmation modal
+ ConfirmDialog { screen: modelData }
+
+ // Shell update notification
+ UpdatePopup { screen: modelData }
- Border {
- screen: modelData
- edge: "bottom"
+ // ── All popups ───────────────────────────────────
+ // Add new popups in src/popups/PopupLayer.qml only
+ PopupLayer {
+ topBar: topBar
+ leftBorder: leftBorder
+ rightBorder: rightBorder
+ bottomBorder: bottomBorder
}
-
}
}
}
diff --git a/src/assets/wallpapers/brain-shell-default-0.png b/src/assets/wallpapers/brain-shell-default-0.png
new file mode 100644
index 0000000000000000000000000000000000000000..282999dddd213b5d284004709764d15bd85b003a
GIT binary patch
literal 30593
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%0D*=}B5VdR^N!2lD#yCR3G8gF
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