-
-
-
-
-
-
-
-
-
-> [!WARNING]
-> This configuration is tailored to my preferences with little regard for others. It's not designed to be "good"—just comfortable for me.
-
-
Installation
-
-> [!NOTE]
-> You need a functioning Hyprland installation.
-
-### Arch Linux
-
-> [!NOTE]
-> You need to clone it into the Home directory.
-
-> [!TIP]
-> This command also works for updating an existing installation!
-
-```bash
-curl -fsSL https://raw.githubusercontent.com/S4NKALP/Modus/main/install.sh | bash
-```
-
-### Manual Installation
-
-1. Dependencies:
-
-```sh
-fabric fabric-cli gnome-bluetooth-3.0 wf-recorder cliphist wl-clipboard grimblast power-profile-daemon adw-gtk-theme brightnessctl swww hypridle hyprlock imagemagick libnotify swappy libadwaita wlinhibit
-```
-
-```sh
-python-materialyoucolor-git python-pillow python-toml python-ijson python-setproctitle python-requests python-numpy python-pywayland python-pyxdg
-```
-
-```sh
-ttf-font-awesome otf-font-awesome ttf-material-symbols-variable-git ttf-google-sans ttf-opensans ttf-robot
-```
-
-- Icon:
-
-```sh
-git clone https://github.com/vinceliuice/Tela-icon-theme
-cd Tela-icon-theme
-./install.sh nord
-```
-
-
Screenshots
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Todo
-
-- [x] App Launcher
-- [x] Power Menu
-- [x] Wallpaper Picker
-- [x] Todo
-- [x] Bluetooth Manager
-- [x] Clipboard Manager
-- [x] OSD
-- [x] Sh Runner
-- [x] Emoji Picker
-- [x] Update Screenshot
-- [x] Installation script
-- [x] Network Manager
-- [x] Calendar
-- [x] Power Manager
-- [x] Dock
-- [x] Web search in Launcher
-- [ ] Notification Panel
-- [ ] Improve CSS Styling
-- [ ] Update Screenshot
-
-## Special Thanks
-
-A big thank you to the following people for their incredible help with code and creative ideas. Your help made a real difference!
-
-- [darsh](https://github.com/its-darsh): for creating Fabric, which made everything possible.
-- [gummy bear album](https://github.com/muhchaudhary): for sharing fantastic code snippets that saved me time and effort.
-- [axenide](https://github.com/Axenide): for the amazing config that not only inspired parts of mine but also provided some gems I couldn’t resist borrowing.
-- [koeqaife](https://github.com/koeqaife): thanks for material-colors scripts
-
-I truly appreciate your support
+# Comming Soon
diff --git a/assets/logo.svg b/assets/logo.svg
deleted file mode 100644
index 5d19dc1..0000000
--- a/assets/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/assets/screenshots/bluetooth.png b/assets/screenshots/bluetooth.png
deleted file mode 100644
index 7033d48..0000000
Binary files a/assets/screenshots/bluetooth.png and /dev/null differ
diff --git a/assets/screenshots/cliphist.png b/assets/screenshots/cliphist.png
deleted file mode 100644
index 6e21db0..0000000
Binary files a/assets/screenshots/cliphist.png and /dev/null differ
diff --git a/assets/screenshots/emoji.png b/assets/screenshots/emoji.png
deleted file mode 100644
index 3202bf9..0000000
Binary files a/assets/screenshots/emoji.png and /dev/null differ
diff --git a/assets/screenshots/home.png b/assets/screenshots/home.png
deleted file mode 100644
index c7ba7c3..0000000
Binary files a/assets/screenshots/home.png and /dev/null differ
diff --git a/assets/screenshots/notification.png b/assets/screenshots/notification.png
deleted file mode 100644
index c8c9f30..0000000
Binary files a/assets/screenshots/notification.png and /dev/null differ
diff --git a/assets/screenshots/osd.png b/assets/screenshots/osd.png
deleted file mode 100644
index a02dec6..0000000
Binary files a/assets/screenshots/osd.png and /dev/null differ
diff --git a/assets/screenshots/powermenu.png b/assets/screenshots/powermenu.png
deleted file mode 100644
index 165fc0e..0000000
Binary files a/assets/screenshots/powermenu.png and /dev/null differ
diff --git a/assets/screenshots/shrunner.png b/assets/screenshots/shrunner.png
deleted file mode 100644
index 40f184b..0000000
Binary files a/assets/screenshots/shrunner.png and /dev/null differ
diff --git a/assets/screenshots/todo.png b/assets/screenshots/todo.png
deleted file mode 100644
index 0718afb..0000000
Binary files a/assets/screenshots/todo.png and /dev/null differ
diff --git a/assets/screenshots/wallpaper.png b/assets/screenshots/wallpaper.png
deleted file mode 100644
index 4e6789b..0000000
Binary files a/assets/screenshots/wallpaper.png and /dev/null differ
diff --git a/assets/wallpaper/example-1.png b/assets/wallpaper/example-1.png
deleted file mode 100644
index 137ceee..0000000
Binary files a/assets/wallpaper/example-1.png and /dev/null differ
diff --git a/assets/wallpaper/example-2.png b/assets/wallpaper/example-2.png
deleted file mode 100644
index 6d22cf9..0000000
Binary files a/assets/wallpaper/example-2.png and /dev/null differ
diff --git a/assets/wallpaper/example-3.jpg b/assets/wallpaper/example-3.jpg
deleted file mode 100755
index 5c2fd0d..0000000
Binary files a/assets/wallpaper/example-3.jpg and /dev/null differ
diff --git a/assets/wallpaper/example-4.jpeg b/assets/wallpaper/example-4.jpeg
deleted file mode 100644
index 7ca7ae6..0000000
Binary files a/assets/wallpaper/example-4.jpeg and /dev/null differ
diff --git a/assets/wallpaper/example-5.jpg b/assets/wallpaper/example-5.jpg
deleted file mode 100644
index eced4ab..0000000
Binary files a/assets/wallpaper/example-5.jpg and /dev/null differ
diff --git a/config/__init__.py b/config/__init__.py
deleted file mode 100644
index f94e240..0000000
--- a/config/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .data import *
-
-# Import only specific names actually defined in data.py
-# This prevents circular imports by not importing everything
-
-from .data import APP_NAME, APP_NAME_CAP, CACHE_DIR, CONFIG_FILE, STATE_DIR
diff --git a/config/assets/config.json b/config/assets/config.json
deleted file mode 100644
index 2cdda50..0000000
--- a/config/assets/config.json
+++ /dev/null
@@ -1,58 +0,0 @@
-{
- "prefix_restart": "SHIFT ALT",
- "suffix_restart": "T",
- "prefix_bluetooth": "SHIFT ALT",
- "suffix_bluetooth": "B",
- "prefix_emoji": "SUPER",
- "suffix_emoji": "E",
- "prefix_cliphist": "SUPER",
- "suffix_cliphist": "V",
- "prefix_kanban": "SUPER",
- "suffix_kanban": "T",
- "prefix_toolbox": "SUPER",
- "suffix_toolbox": "S",
- "prefix_walls": "SUPER",
- "suffix_walls": "W",
- "prefix_launcher": "SUPER",
- "suffix_launcher": "D",
- "prefix_power": "SUPER",
- "suffix_power": "X",
- "prefix_tmux": "SUPER",
- "suffix_tmux": "Z",
- "prefix_notifications": "SUPER",
- "suffix_notifications": "N",
- "prefix_window_switcher": "ALT",
- "suffix_window_switcher": "W",
- "prefix_restart_inspector": "SUPER SHIFT",
- "suffix_restart_inspector": "T",
- "vertical": false,
- "vertical_right_align": false,
- "bottom_bar": true,
- "centered_bar": true,
- "terminal_command": "kitty -e",
- "wallpapers_dir": "/home/sankalp/Pictures/wallpapers",
- "dock_enabled": true,
- "dock_always_occluded": false,
- "dock_icon_size": 28,
- "osd_enabled": true,
- "bar_button_app_visible": true,
- "bar_tray_visible": true,
- "bar_workspaces_visible": true,
- "bar_metrics_visible": true,
- "bar_language_visible": true,
- "bar_date_time_visible": true,
- "bar_updates_visible": true,
- "bar_indicators_visible": true,
- "corners_visible": true,
- "bar_metrics_disks": [
- "/"
- ],
- "metrics_visible": {
- "cpu": true,
- "ram": true,
- "disk": true,
- "swap": true,
- "gpu": false
- },
- "enabled_osd": true
-}
\ No newline at end of file
diff --git a/config/config.py b/config/config.py
index a1a1bd1..a10894e 100644
--- a/config/config.py
+++ b/config/config.py
@@ -37,9 +37,9 @@ try:
except ImportError as e:
print(f"Error importing data constants: {e}")
# Provide fallback defaults if import fails
- APP_NAME = "modus"
- APP_NAME_CAP = "Modus"
- CONFIG_DIR = "~/Modus"
+ APP_NAME = "modusv2"
+ APP_NAME_CAP = "Modusv2"
+ CONFIG_DIR = "~/Modusv2"
HOME_DIR = "~"
WALLPAPERS_DIR_DEFAULT = "~/Pictures/wallpapers"
diff --git a/config/data.py b/config/data.py
index 956159f..681e9d1 100644
--- a/config/data.py
+++ b/config/data.py
@@ -1,14 +1,14 @@
+from fabric.utils import get_relative_path
+from gi.repository import Gdk, GLib
import os
import json
import gi
gi.require_version("Gtk", "3.0")
-from gi.repository import Gdk, GLib
-from fabric.utils import get_relative_path
-APP_NAME = "modus"
-APP_NAME_CAP = "Modus"
+APP_NAME = "modusv2"
+APP_NAME_CAP = "Modusv2"
CACHE_DIR = str(GLib.get_user_cache_dir()) + f"/{APP_NAME}"
STATE_DIR = str(GLib.get_user_state_dir() + f"/{APP_NAME}")
@@ -19,6 +19,7 @@ HOSTNAME = os.uname().nodename
HOME_DIR = os.path.expanduser("~")
CONFIG_DIR = os.path.expanduser(f"~/{APP_NAME}")
+MATUGEN_STATE_FILE = os.path.join(CONFIG_DIR, "matugen")
screen = Gdk.Screen.get_default()
CURRENT_WIDTH = screen.get_width()
@@ -31,19 +32,27 @@ if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r") as f:
config = json.load(f)
WALLPAPERS_DIR = config.get("wallpapers_dir", WALLPAPERS_DIR_DEFAULT)
- VERTICAL = config.get("vertical", False) # Use saved value or False as default
- VERTICAL_RIGHT_ALIGN = config.get("vertical_right_align", False) # Right side by default
- BOTTOM_BAR = config.get("bottom_bar", False) # Use saved value or False as default
- CENTERED_BAR = config.get("centered_bar", False) # Load centered bar setting
+ # Use saved value or False as default
+ VERTICAL = config.get("vertical", False)
+ VERTICAL_RIGHT_ALIGN = config.get(
+ "vertical_right_align", False
+ ) # Right side by default
+ # Use saved value or False as default
+ BOTTOM_BAR = config.get("bottom_bar", False)
+ # Load centered bar setting
+ CENTERED_BAR = config.get("centered_bar", False)
TERMINAL_COMMAND = config.get(
"terminal_command", "kitty -e"
) # Load terminal command
- DOCK_ENABLED = config.get("dock_enabled", True) # Load dock visibility setting
+ # Load dock visibility setting
+ DOCK_ENABLED = config.get("dock_enabled", True)
DOCK_ALWAYS_OCCLUDED = config.get(
"dock_always_occluded", False
) # Load dock hover-only setting
- DOCK_ICON_SIZE = config.get("dock_icon_size", 28) # Load dock icon size setting
- OSD_ENABLED = config.get("osd_enabled", True) # Load OSD visibility setting
+ # Load dock icon size setting
+ DOCK_ICON_SIZE = config.get("dock_icon_size", 28)
+ # Load OSD visibility setting
+ OSD_ENABLED = config.get("osd_enabled", True)
# Load bar component visibility settings
BAR_COMPONENTS_VISIBILITY = {
diff --git a/config/hypr/colors.conf b/config/hypr/colors.conf
index e839fe7..1377374 100644
--- a/config/hypr/colors.conf
+++ b/config/hypr/colors.conf
@@ -1,11 +1,11 @@
-$wallpaper = /home/sankalp/Pictures/wallpapers/background.png
-$background = 0e1514
-$foreground = dde4e2
+$wallpaper = /home/sankalp/Pictures/wallpapers/1.png
+$background = 111318
+$foreground = e1e2e9
-$primary = 81d5ca
-$secondary = b1ccc7
-$tertiary = aec9e6
-$surface = 0e1514
-$surface_bright = 343a39
-$outline = 899390
+$primary = a5c8ff
+$secondary = bcc7dc
+$tertiary = dabde2
+$surface = 111318
+$surface_bright = 37393e
+$outline = 8d9199
$error = ff5945
diff --git a/config/hypr/modus.conf b/config/hypr/modus.conf
deleted file mode 100644
index edd9ec3..0000000
--- a/config/hypr/modus.conf
+++ /dev/null
@@ -1,88 +0,0 @@
-# Generated by Modus - DO NOT EDIT MANUALLY
-exec-once = python /home/sankalp/Modus/main.py
-exec = pgrep -x "hypridle" > /dev/null || hypridle
-exec = swww-daemon
-exec-once = wl-paste --type text --watch cliphist store
-exec-once = wl-paste --type image --watch cliphist store
-
-$fabricSend = fabric-cli exec modus
-$scriptsDir = /home/sankalp/Modus/config/scripts/
-
-# Key Bindings
-bind = SHIFT ALT, T, exec, killall modus; python /home/sankalp/Modus/main.py # Reload Modus
-bind = SHIFT ALT, B, exec, $fabricSend 'launcher.open("bluetooth")' # Bluetooth
-bind = SUPER, N, exec, $fabricSend 'launcher.open("notification-center")' # Notifications
-bind = SUPER, E, exec, $fabricSend 'launcher.open("emoji")' # Emoji
-bind = SUPER, V, exec, $fabricSend 'launcher.open("cliphist")' # Clipboard History
-bind = SUPER, T, exec, $fabricSend 'launcher.open("kanban")' # Kanban
-bind = SUPER, Z, exec, $fabricSend 'launcher.open("tmux")' # Tmux Manager
-bind = SUPER, S, exec, $fabricSend 'launcher.open("tools")' # Toolbox
-bind = SUPER, W, exec, $fabricSend 'launcher.open("wallpapers")' # Wallpaper Selector
-bind = SUPER, D, exec, $fabricSend 'launcher.open("launcher")' # App Launcher
-bind = ALT, W, exec, $fabricSend 'launcher.open("window-switcher")' # Window Switcher
-bind = SUPER, X, exec, $fabricSend 'launcher.open("power")' # Power Menu
-bind = SUPER SHIFT, T, exec, killall modus; GTK_DEBUG=interactive python /home/sankalp/Modus/main.py # Restart with inspector
-
-# Wallpapers directory: /home/sankalp/Pictures/wallpapers
-
-# Source color scheme
-source = /home/sankalp/Modus/config/hypr/colors.conf
-
-general {
- col.active_border = 0xff$primary
- col.inactive_border = 0xff$surface
- gaps_in = 2
- gaps_out = 4
- border_size = 2
- layout = dwindle
-}
-
-decoration {
- blur {
- enabled = yes
- size = 5
- passes = 3
- new_optimizations = yes
- contrast = 1
- brightness = 1
- }
- rounding = 14
- shadow {
- enabled = true
- range = 10
- render_power = 2
- color = rgba(0, 0, 0, 0.25)
- }
-}
-
-animations {
- enabled = true
- # Animation curves
- bezier = linear, 0, 0, 1, 1
- bezier = md3_standard, 0.2, 0, 0, 1
- bezier = md3_decel, 0.05, 0.7, 0.1, 1
- bezier = md3_accel, 0.3, 0, 0.8, 0.15
- bezier = overshot, 0.05, 0.9, 0.1, 1.1
- bezier = crazyshot, 0.1, 1.5, 0.76, 0.92
- bezier = hyprnostretch, 0.05, 0.9, 0.1, 1.0
- bezier = menu_decel, 0.1, 1, 0, 1
- bezier = menu_accel, 0.38, 0.04, 1, 0.07
- bezier = easeInOutCirc, 0.85, 0, 0.15, 1
- bezier = easeOutCirc, 0, 0.55, 0.45, 1
- bezier = easeOutExpo, 0.16, 1, 0.3, 1
- bezier = softAcDecel, 0.26, 0.26, 0.15, 1
- bezier = md2, 0.4, 0, 0.2, 1
-
- animation = windows, 1, 3, md3_decel, popin 60%
- animation = windowsIn, 1, 3, md3_decel, popin 60%
- animation = windowsOut, 1, 3, md3_accel, popin 60%
- animation = border, 1, 10, default
- animation = fade, 1, 3, md3_decel
- animation = layers, 1, 2, md3_decel, slide
- animation = layersIn, 1, 3, menu_decel, slide
- animation = layersOut, 1, 1.6, menu_accel, slide
- animation = fadeLayersIn, 1, 2, menu_decel
- animation = fadeLayersOut, 1, 4.5, menu_accel
- animation = workspaces, 1, 7, menu_decel, slide
- animation = specialWorkspace, 1, 3, md3_decel, slidevert
-}
diff --git a/config/scripts/capture.sh b/config/scripts/capture.sh
index 8204108..465e038 100755
--- a/config/scripts/capture.sh
+++ b/config/scripts/capture.sh
@@ -1,3 +1,4 @@
+
#!/bin/bash
# Print usage information
@@ -19,16 +20,28 @@ Modes:
EOF
}
-# OCR functionality
+# OCR functionality using hyprshot
do_ocr() {
sleep 0.25
- ocr_text=$(grimblast --freeze save area - | tesseract -l eng - - 2>/dev/null)
+ # Temporary file to store the snipped image
+ temp_ocr_image="$(mktemp --suffix=.png)"
+ hyprshot -s -z -m region -o "$(dirname "$temp_ocr_image")" -f "$(basename "$temp_ocr_image")"
+
+ if [ ! -f "$temp_ocr_image" ]; then
+ notify-send -a "Modus" "OCR Failed" "No image captured"
+ exit 1
+ fi
+
+ # Perform OCR with tesseract
+ ocr_text=$(tesseract "$temp_ocr_image" - -l eng 2>/dev/null)
+
+ rm -f "$temp_ocr_image" # Clean up temp file
# Check if OCR was successful
if [[ -n "$ocr_text" ]]; then
echo -n "$ocr_text" | wl-copy
- notify-send -a "Modus" "OCR Success" "Text Copied to Clipboard"
+ notify-send -a "Modus" "OCR Success" "Text copied to clipboard"
else
notify-send -a "Modus" "OCR Failed" "No text recognized or operation failed"
fi
@@ -36,10 +49,8 @@ do_ocr() {
# Screen recording functionality
do_screenrecord() {
-
if [ -z "$XDG_VIDEOS_DIR" ]; then
XDG_VIDEOS_DIR="$HOME/Videos"
-
fi
local save_dir="${XDG_VIDEOS_DIR:-$HOME/Videos}/Recordings"
mkdir -p "$save_dir"
@@ -49,8 +60,7 @@ do_screenrecord() {
sleep 1
local last_video=$(ls -t "$save_dir"/*.mp4 2>/dev/null | head -n 1)
- local action=$(notify-send -a "Modus" "⬜ Recording stopped" \
- -A "view=View" -A "open=Open folder")
+ local action=$(notify-send -a "Modus" "⬜ Recording stopped" -A "view=View" -A "open=Open folder")
if [ "$action" = "view" ] && [ -n "$last_video" ]; then
xdg-open "$last_video"
@@ -81,10 +91,10 @@ do_screenshot() {
local mockup_mode="$2"
case $1 in
- p) grimblast copysave screen "$full_path" ;;
- s) grimblast copysave area "$full_path" ;;
- sf) grimblast --freeze copysave area "$full_path" ;;
- m) grimblast copysave output "$full_path" ;;
+ p) hyprshot -s -m output -o "$save_dir" -f "$save_file" ;;
+ s) hyprshot -s -m region -o "$save_dir" -f "$save_file" ;;
+ sf) hyprshot -s -z -m region -o "$save_dir" -f "$save_file" ;;
+ m) hyprshot -s -m output -m active -o "$save_dir" -f "$save_file" ;;
*)
echo "Invalid screenshot mode"
print_usage
@@ -93,30 +103,7 @@ do_screenshot() {
esac
if [ -f "$full_path" ]; then
- if [ "$mockup_mode" = "mockup" ]; then
- local temp_file="${full_path%.png}_temp.png"
- cropped_file="${full_path%.png}_cropped.png"
- local mockup_file="${full_path%.png}_mockup.png"
-
- # First crop the top pixel using a different approach
-
- # Use +0+1 to start at y-coordinate 1 (skipping the first pixel row)
-
- magick "$full_path" -crop +0+1 +repage "$cropped_file"
-
- magick "$cropped_file" \
- \( +clone -alpha extract -draw 'fill black polygon 0,0 0,20 20,0 fill white circle 20,20 20,0' \
- \( +clone -flip \) -compose Multiply -composite \
- \( +clone -flop \) -compose Multiply -composite \
- \) -alpha off -compose CopyOpacity -composite "$temp_file"
-
- magick "$temp_file" \
- \( +clone -background black -shadow 60x20+0+10 -alpha set -channel A -evaluate multiply 1 +channel \) \
- +swap -background none -layers merge +repage "$mockup_file"
-
- rm "$temp_file" "$cropped_file"
- mv "$mockup_file" "$full_path"
-
+ if [ "$mockup_mode" != "mockup" ]; then
if command -v wl-copy >/dev/null 2>&1; then
wl-copy <"$full_path"
elif command -v xclip >/dev/null 2>&1; then
@@ -124,6 +111,54 @@ do_screenshot() {
fi
fi
+ # Process as mockup if requested
+ if [ "$mockup_mode" = "mockup" ]; then
+ temp_file="${full_path%.png}_temp.png"
+ cropped_file="${full_path%.png}_cropped.png"
+ mockup_file="${full_path%.png}_mockup.png"
+ mockup_success=true
+
+ # Crop top pixel
+ convert "$full_path" -crop +0+1 +repage "$cropped_file" || mockup_success=false
+
+ # Rounded corners and transparency
+ if [ "$mockup_success" = true ]; then
+ convert "$cropped_file" \
+ \( +clone -alpha extract -draw 'fill black polygon 0,0 0,20 20,0 fill white circle 20,20 20,0' \
+ \( +clone -flip \) -compose Multiply -composite \
+ \( +clone -flop \) -compose Multiply -composite \
+ \) -alpha off -compose CopyOpacity -composite "$temp_file" || mockup_success=false
+ fi
+
+ # Add shadow
+ if [ "$mockup_success" = true ]; then
+ convert "$temp_file" \
+ \( +clone -background black -shadow 60x20+0+10 -alpha set -channel A -evaluate multiply 1 +channel \) \
+ +swap -background none -layers merge +repage "$mockup_file" || mockup_success=false
+ fi
+
+ # Finalize
+ if [ "$mockup_success" = true ] && [ -f "$mockup_file" ]; then
+ rm "$temp_file" "$cropped_file"
+ mv "$mockup_file" "$full_path"
+ if command -v wl-copy >/dev/null 2>&1; then
+ wl-copy <"$full_path"
+ elif command -v xclip >/dev/null 2>&1; then
+ xclip -selection clipboard -t image/png <"$full_path"
+ fi
+ else
+ echo "Warning: Mockup processing failed for $full_path." >&2
+ rm -f "$temp_file" "$cropped_file" "$mockup_file"
+ if [ "$mockup_mode" = "mockup" ]; then
+ if command -v wl-copy >/dev/null 2>&1; then
+ wl-copy <"$full_path"
+ elif command -v xclip >/dev/null 2>&1; then
+ xclip -selection clipboard -t image/png <"$full_path"
+ fi
+ fi
+ fi
+ fi
+
local action=$(notify-send -a "Modus" -i "$full_path" "Screenshot saved" "in $full_path" \
-A "view=View" -A "edit=Edit" -A "open=Open Folder")
diff --git a/install.sh b/install.sh
deleted file mode 100644
index dc640c4..0000000
--- a/install.sh
+++ /dev/null
@@ -1,136 +0,0 @@
-#!/bin/bash
-
-set -e # Exit immediately if a command exits with a non-zero status
-set -u # Treat unset variables as an error
-set -o pipefail # Prevent errors in a pipeline from being masked
-
-REPO_URL="https://github.com/S4NKALP/Modus"
-INSTALL_DIR="$HOME/Modus"
-
-PACKAGES=(
- fabric-cli-git
- gnome-bluetooth-3.0
- grimblast
- hypridle
- hyprlock
- hyprpicker
- imagemagick
- libnotify
- python-fabric-git
- python-materialyoucolor-git
- python-pillow
- python-setproctitle
- python-toml
- python-requests
- python-numpy
- python-pywayland
- python-pyxdg
- python-ijson
- sddm-theme-corners-git
- swww
- swappy
- wl-clipboard
- wf-recorder
- libadwaita
- acpi
- adw-gtk-theme
- brightnessctl
- wlinhibit
- power-profile-daemon
- ttf-chakra-petch
- ttf-tabler-icons
-)
-
-# Ensure running on Arch Linux
-if ! grep -q "arch" /etc/os-release; then
- echo "This script is designed to run on Arch Linux."
- exit 1
-fi
-
-# Prevent running as root
-if [ "$(id -u)" -eq 0 ]; then
- echo "Please do not run this script as root."
- exit 1
-fi
-
-# Check for AUR helper (yay or paru)
-aur_helper=""
-if command -v yay &>/dev/null; then
- aur_helper="yay"
-elif command -v paru &>/dev/null; then
- aur_helper="paru"
-else
- echo "Installing paru..."
- tmpdir=$(mktemp -d)
- git clone https://aur.archlinux.org/paru.git "$tmpdir/paru"
- cd "$tmpdir/paru"
- makepkg -si --noconfirm
- cd - >/dev/null
- rm -rf "$tmpdir"
- aur_helper="paru"
-fi
-
-# Clone or update the Modus repository
-if [ -d "$INSTALL_DIR" ]; then
- echo "Updating Modus..."
- git -C "$INSTALL_DIR" pull
-else
- echo "Cloning Modus..."
- git clone "$REPO_URL" "$INSTALL_DIR"
-fi
-
-# Install required packages using the AUR helper
-echo "Installing required packages..."
-$aur_helper -Syy --needed --noconfirm "${PACKAGES[@]}" || true
-
-# Update outdated packages
-echo "Checking for outdated packages..."
-outdated=$($aur_helper -Qu | awk '{print $1}' || true)
-to_update=()
-for pkg in "${PACKAGES[@]}"; do
- if echo "$outdated" | grep -q "^$pkg\$"; then
- to_update+=("$pkg")
- fi
-done
-
-if [ ${#to_update[@]} -gt 0 ]; then
- echo "Updating outdated packages..."
- $aur_helper -S --noconfirm "${to_update[@]}" || true
-else
- echo "All required packages are up-to-date."
-fi
-
-# Backup and replace GTK configurations
-echo "Updating GTK configurations..."
-mkdir -p "$HOME/.config"
-[[ -d "$HOME/.config/gtk-3.0" ]] && mv "$HOME/.config/gtk-3.0" "$HOME/.config/gtk-3.0-bk-$(date +%s)"
-[[ -d "$HOME/.config/gtk-4.0" ]] && mv "$HOME/.config/gtk-4.0" "$HOME/.config/gtk-4.0-bk-$(date +%s)"
-
-cp -r "$INSTALL_DIR/assets/gtk-3.0" "$HOME/.config/" || true
-cp -r "$INSTALL_DIR/assets/gtk-4.0" "$HOME/.config/" || true
-
-# Install Icon Theme
-echo "Installing Tela icon theme..."
-tmpdir=$(mktemp -d)
-git clone https://github.com/vinceliuice/Tela-icon-theme "$tmpdir/Tela-icon-theme"
-cd "$tmpdir/Tela-icon-theme"
-./install.sh nord
-rm -rf "$tmpdir"
-
-# Install Vencord
-echo "Installing Vencord..."
-if command -v curl &>/dev/null; then
- sh -c "$(curl -sS https://raw.githubusercontent.com/Vendicated/VencordInstaller/main/install.sh)" || true
- mkdir -p "$HOME/.config/Vencord/settings/"
- ln -sf "$HOME/.cache/material/material-discord.css" "$HOME/.config/Vencord/settings/quickCss.css"
-else
- echo "Skipping Vencord installation (curl not found)."
-fi
-
-# Launch Modus
-echo "Starting Modus..."
-killall modus 2>/dev/null || true
-python "$INSTALL_DIR/main.py" >/dev/null 2>&1 &
-disown
-
-echo "Installation complete."
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..24db785
--- /dev/null
+++ b/main.py
@@ -0,0 +1,55 @@
+from loguru import logger
+import setproctitle
+from fabric import Application
+from fabric.utils import get_relative_path, exec_shell_command_async, monitor_file
+
+from config.data import APP_NAME, CACHE_DIR, CONFIG_FILE, DOCK_ICON_SIZE, APP_NAME_CAP
+from modules.bar.bar import Bar
+from modules.dock import Dock
+from modules.corners import Corners
+
+
+
+# Disable unnecessary logging immediately
+for log in [
+ "fabric.hyprland.widgets",
+ "fabric.audio.service",
+ "fabric.bluetooth.service",
+ "fabric.widgets.wayland",
+ "fabric.utils.helpers",
+]:
+ logger.disable(log)
+
+if __name__ == "__main__":
+ setproctitle.setproctitle(APP_NAME)
+
+ # Load configuration
+ from config.data import load_config
+ config = load_config()
+
+ bar = Bar()
+ dock = Dock()
+ corners = Corners()
+
+ app = Application(f"{APP_NAME}", bar, dock)
+
+ css_file = monitor_file(get_relative_path("styles"))
+ _ = css_file.connect("changed", lambda *_: set_css())
+
+ color_css_file = monitor_file(get_relative_path("./styles/colors.css"))
+ _ = color_css_file.connect("changed", lambda *_: set_css())
+
+
+ def set_css():
+ app.set_stylesheet_from_file(
+ get_relative_path("styles/main.css"),
+ exposed_functions={
+ "dock_nmargin": lambda: f"margin-bottom: -{32 + DOCK_ICON_SIZE}px;",
+ "dock_sep": lambda: f"margin: 8px 0;",
+ },
+ )
+ app.set_css = set_css
+
+ app.set_css()
+
+ app.run()
\ No newline at end of file
diff --git a/modules/bar/bar.py b/modules/bar/bar.py
index 61a2653..45b47fb 100644
--- a/modules/bar/bar.py
+++ b/modules/bar/bar.py
@@ -1,312 +1,166 @@
-from fabric.hyprland.widgets import Language, get_hyprland_connection
-from fabric.hyprland.service import HyprlandEvent
-from fabric.system_tray.widgets import SystemTray
-from fabric.widgets.shapes import Corner
from fabric.widgets.box import Box
+from fabric.widgets.eventbox import EventBox
+from fabric.widgets.datetime import DateTime
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
-from fabric.widgets.datetime import DateTime
+from fabric.widgets.revealer import Revealer
from fabric.widgets.label import Label
+from fabric.system_tray.widgets import SystemTray
from fabric.widgets.wayland import WaylandWindow as Window
-from modules.bar.components import Metrics, workspace, SystemIndicators, UpdatesWidget
-import config.data as data
-from gi.repository import Gdk, GLib
+from fabric.hyprland.widgets import ActiveWindow
+from fabric.utils import truncate, FormattedString
+
+from modules.bar.components.workspaces import workspace
+from modules.bar.components.language import LanguageWidget
+from modules.bar.components.auto_hide import AutoHideBarController
+from modules.corners import MyCorner
+from modules.bar.components.power_menu import Power
+from modules.bar.components.system_indicators import SystemIndicators
+from modules.bar.components.metrics import Metrics
import utils.icons as icons
-import os
-import json
class Bar(Window):
- def __init__(self, **kwargs):
- super().__init__(
- name="bar",
- layer="top",
- anchor="left top right"
- if not data.VERTICAL and not data.BOTTOM_BAR
- else "top right bottom"
- if data.VERTICAL and data.VERTICAL_RIGHT_ALIGN
- else "top left bottom"
- if data.VERTICAL
- else "bottom left right",
- exclusivity="auto",
- visible=True,
- all_visible=True,
- )
+ """Main bar widget for the desktop environment."""
- self.launcher = kwargs.get("launcher", None)
- self.component_visibility = data.BAR_COMPONENTS_VISIBILITY
-
- # Determine if we should use vertical layout for components
- is_vertical_layout = data.VERTICAL
-
- self.workspaces = Button(
- child=workspace,
- name="workspaces",
- )
-
- self.connection = get_hyprland_connection()
- self.tray = SystemTray(
- name="tray",
- spacing=4,
- icon_size=16,
- orientation="h" if not is_vertical_layout else "v",
- )
-
- self.date_time = Button(
- name="datetime",
- child=DateTime(
- formatters=["%-I:%M:%p %a %d %b"]
- if not is_vertical_layout
- else ["%I\n%M\n%p"],
- h_align="center" if not is_vertical_layout else "fill",
- v_align="center",
- ),
- on_clicked=lambda *_: self.calendar(),
- )
- self.date_time.connect("enter_notify_event", self.on_button_enter)
- self.date_time.connect("leave_notify_event", self.on_button_leave)
-
- self.button_app = Button(
- name="button-bar",
- child=Label(name="button-bar-label", markup=icons.apps),
- on_clicked=lambda *_: self.search_apps(),
- )
- self.button_app.connect("enter_notify_event", self.on_button_enter)
- self.button_app.connect("leave_notify_event", self.on_button_leave)
-
- self.updates = UpdatesWidget()
- self.updates.connect("enter_notify_event", self.on_button_enter)
- self.updates.connect("leave_notify_event", self.on_button_leave)
-
- self.lang_label = Label(name="lang-label")
- self.language = Button(
- name="language", h_align="center", v_align="center", child=self.lang_label
- )
- self.on_language_switch()
- self.connection.connect("event::activelayout", self.on_language_switch)
-
- self.metrics = Metrics()
- self.indicators = SystemIndicators(launcher=self.launcher)
-
- self.applets = Box(
- name="system-indicator",
- spacing=4,
- orientation="h" if not is_vertical_layout else "v",
- children=[self.language, self.indicators],
- )
-
- # Apply visibility settings to components
- self.apply_component_visibility()
-
- self.h_start_children = [self.button_app, self.updates]
-
- self.h_center_children = [
- self.date_time,
- self.workspaces,
- self.metrics,
- self.applets,
- ]
-
- self.h_end_children = [self.tray]
-
- self.v_start_children = [
- self.button_app,
- self.updates,
- self.workspaces,
- self.metrics,
- ]
- self.v_end_children = [self.tray, self.applets, self.date_time]
-
- self.v_all_children = []
- self.v_all_children.extend(self.v_start_children)
- self.v_all_children.extend(self.v_end_children)
-
- # Use centered layout when both vertical and centered_bar are enabled
- is_centered_bar = is_vertical_layout and getattr(data, "CENTERED_BAR", False)
-
- self.bar = CenterBox(
- name="bar",
- orientation="h" if not is_vertical_layout else "v",
- h_align="fill",
- v_align="fill",
- start_children=None
- if is_centered_bar
- else Box(
- name="start-container",
- spacing=4,
- orientation="h" if not is_vertical_layout else "v",
- children=self.h_start_children
- if not is_vertical_layout
- else self.v_start_children,
- ),
- center_children=Box(
- orientation="v",
- spacing=4,
- children=self.v_all_children
- if is_centered_bar
- else Box(
- name="center-container",
- spacing=4,
- orientation="h" if not is_vertical_layout else "v",
- children=self.h_center_children if not is_vertical_layout else None,
- ),
- )
- if is_vertical_layout
- else Box(
- name="center-container",
- spacing=4,
- orientation="h" if not is_vertical_layout else "v",
- children=self.h_center_children if not is_vertical_layout else None,
- ),
- end_children=None
- if is_centered_bar
- else Box(
- name="end-container",
- spacing=4,
- orientation="h" if not is_vertical_layout else "v",
- children=self.h_end_children
- if not is_vertical_layout
- else self.v_end_children,
- ),
- )
-
- self.children = self.bar
-
- def apply_component_visibility(self):
- """Apply saved visibility settings to all components"""
- components = {
- "metrics": self.metrics,
- "indicators": self.applets,
- "updates": self.updates,
- "workspaces": self.workspaces,
- "button_app": self.button_app,
- # "button_tools": self.button_tools,
- "language": self.language,
- "date_time": self.date_time,
- "tray": self.tray,
- }
-
- for component_name, widget in components.items():
- if component_name in self.component_visibility:
- widget.set_visible(self.component_visibility[component_name])
-
- def toggle_component_visibility(self, component_name):
- """Toggle visibility for a specific component"""
- components = {
- "metrics": self.metrics,
- "indicators": self.applets,
- "updates": self.updates,
- "workspaces": self.workspaces,
- "button_app": self.button_app,
- # "button_tools": self.button_tools,
- "language": self.language,
- "date_time": self.date_time,
- "tray": self.tray,
- }
-
- if component_name in components and component_name in self.component_visibility:
- # Toggle the visibility state
- self.component_visibility[component_name] = not self.component_visibility[
- component_name
- ]
- # Apply the new state
- components[component_name].set_visible(
- self.component_visibility[component_name]
- )
-
- # Update the configuration
- config_file = os.path.expanduser(
- f"~/{data.APP_NAME}/config/assets/config.json"
- )
- try:
- if os.path.exists(config_file):
- with open(config_file, "r") as f:
- config = json.load(f)
-
- # Update the config with the new visibility state
- config[f"bar_{component_name}_visible"] = self.component_visibility[
- component_name
- ]
-
- # Write the updated config back to file
- with open(config_file, "w") as f:
- json.dump(config, f, indent=4)
- except (json.JSONDecodeError, IOError) as e:
- print(f"Error updating config file: {e}")
- # Revert the visibility change if config update fails
- self.component_visibility[
- component_name
- ] = not self.component_visibility[component_name]
- components[component_name].set_visible(
- self.component_visibility[component_name]
- )
-
- return self.component_visibility[component_name]
-
- return None
-
- def on_button_enter(self, widget, event):
- window = widget.get_window()
- if window:
- window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))
-
- def on_button_leave(self, widget, event):
- window = widget.get_window()
- if window:
- window.set_cursor(None)
-
- def search_apps(self):
- self.launcher.open("launcher")
-
- def calendar(self):
- self.launcher.open("calendar")
-
- def on_language_switch(self, _=None, event: HyprlandEvent = None):
- lang = event.data[1] if event else Language().get_label()
- self.language.set_tooltip_text(lang)
- if not data.VERTICAL:
- self.lang_label.set_label(lang[:2].lower())
- else:
- self.lang_label.add_style_class("icon")
- self.lang_label.set_markup(icons.keyboard)
-
-
-class MyCorner(Box):
- def __init__(self, position):
- super().__init__(
- name="corner-container",
- children=Corner(
- name="corner",
- orientation=position,
- size=20,
- ),
- )
-
-
-class Corners(Window):
def __init__(self):
super().__init__(
layer="top",
- anchor="top left bottom right",
- pass_through=True,
- child=Box(
- orientation="vertical",
- children=[
- Box(
- children=[
- MyCorner("top-left"),
- Box(h_expand=True),
- MyCorner("top-right"),
- ]
- ),
- Box(v_expand=True),
- Box(
- children=[
- MyCorner("bottom-left"),
- Box(h_expand=True),
- MyCorner("bottom-right"),
- ]
- ),
- ],
+ anchor="top",
+ visible=True,
+ )
+
+ self.active_window = ActiveWindow(
+ name="active-window",
+ h_expand=True,
+ h_align="fill",
+ formatter=FormattedString(
+ f"{{'Desktop' if not win_class or win_class == 'unknown' else win_class}}",
+ truncate=truncate,
),
)
+ self.workspaces = Button(
+ name="workspaces",
+ child=workspace,
+ )
+
+ self.language = LanguageWidget()
+ self.date_time = DateTime(name="date-time", formatters=["%-I:%M:%p %a %d %b"])
+
+ # Tray setup
+ self._setup_tray()
+
+ self.power_menu = Power()
+ self.system_indicators = SystemIndicators()
+ self.metrics = Metrics()
+
+ # Main bar content
+
+ self.bar = CenterBox(
+ name="bar",
+ # start_children=Box(spacing=4, orientation="h", children=self.workspaces),
+ center_children=Box(
+ spacing=4,
+ orientation="h",
+ children=[
+ self.workspaces,
+ self.active_window,
+ self.date_time,
+ self.metrics,
+ self.system_indicators,
+ self.tray_button,
+ self.tray_revealer,
+ self.power_menu,
+ ],
+ ),
+ # end_children=Box(
+ # spacing=4,
+ # orientation="h",
+ # children=[
+ # self.date_time,
+ # self.tray_button,
+ # self.tray_revealer,
+ # self.power_menu,
+ # ],
+ # ),
+ )
+
+ # Event handling and corners
+ self.eventbox = EventBox()
+ self.eventbox.add(self.bar)
+
+ self.corner_left = Box(
+ name="bar-corner-left",
+ orientation="v",
+ h_align="start",
+ children=[
+ MyCorner("top-right"),
+ Box(),
+ ],
+ )
+ self.corner_left.set_margin_start(56)
+
+ self.corner_right = Box(
+ name="bar-corner-right",
+ orientation="v",
+ h_align="end",
+ children=[
+ MyCorner("top-left"),
+ Box(),
+ ],
+ )
+ self.corner_right.set_margin_end(56)
+ self.main_box = Box(
+ name="bar-container",
+ orientation="h",
+ h_expand=True,
+ children=[
+ self.corner_left,
+ self.eventbox,
+ self.corner_right,
+ ],
+ )
+
+ # Add hover activator
+ self.hover_activator = EventBox(name="hover-activator", v_align="start")
+ self.hover_activator.set_size_request(-1, 1)
+
+ # Root container
+ self.root_box = Box(
+ orientation="v", children=[self.main_box, self.hover_activator]
+ )
+ self.add(self.root_box)
+
+ # Setup auto-hide controller
+ self.auto_hide = AutoHideBarController(self)
+
+ # Connect event handlers for hover
+ self.eventbox.connect("enter-notify-event", self.auto_hide.on_bar_enter)
+ self.eventbox.connect("leave-notify-event", self.auto_hide.on_bar_leave)
+ self.hover_activator.connect(
+ "enter-notify-event", self.auto_hide.on_hover_enter
+ )
+
+ self.show_all()
+
+ def _setup_tray(self):
+ """Setup the system tray with reveal functionality."""
+ self.tray = SystemTray(name="tray", spacing=4, icon_size=16)
+ self.tray_revealer = Revealer(
+ name="tray-revealer",
+ transition_type="slide-left",
+ transition_duration=200,
+ child=self.tray,
+ reveal_child=False,
+ )
+ self.tray_label = Label(markup=icons.chevron_left)
+ self.tray_button = EventBox(name="tray-button", child=self.tray_label)
+ self.tray_button.connect("button-press-event", self._toggle_tray)
+
+ def _toggle_tray(self, *args):
+ """Toggle the tray visibility and update the icon."""
+ is_revealed = not self.tray_revealer.get_reveal_child()
+ self.tray_revealer.set_reveal_child(is_revealed)
+ self.tray_label.set_markup(
+ icons.chevron_right if is_revealed else icons.chevron_left
+ )
diff --git a/modules/bar/components/__init__.py b/modules/bar/components/__init__.py
index 161c235..cfde6ca 100644
--- a/modules/bar/components/__init__.py
+++ b/modules/bar/components/__init__.py
@@ -1,4 +1 @@
-from .metrics import Metrics
-from .workspace import workspace
-from .system_indicators import SystemIndicators
-from .updates import UpdatesWidget
+# Bar components package
\ No newline at end of file
diff --git a/modules/bar/components/auto_hide.py b/modules/bar/components/auto_hide.py
new file mode 100644
index 0000000..22a5ad5
--- /dev/null
+++ b/modules/bar/components/auto_hide.py
@@ -0,0 +1,202 @@
+import json
+from gi.repository import GLib, Gdk
+from fabric.hyprland.widgets import get_hyprland_connection
+from utils.occlusion import check_occlusion
+
+
+class AutoHideBarController:
+ """Controller for bar auto-hide behavior."""
+
+ # Constants for configuration
+ OCCLUSION_CHECK_INTERVAL = 250 # ms
+ HIDE_DELAY = 300 # ms
+
+ def __init__(self, bar):
+ self.bar = bar
+ self.is_hidden = False
+ self.hide_id = None
+ self.is_hovered = False
+ self.conn = get_hyprland_connection()
+ self.last_occlusion_state = False
+
+ self._setup_event_handlers()
+ self._start_occlusion_checking()
+
+ def _setup_event_handlers(self):
+ """Initialize event handlers for window management"""
+ # Initial state check
+ if self.conn.ready:
+ self.check_hide()
+ else:
+ self.conn.connect("event::ready", lambda *_: self.check_hide())
+
+ # Monitor window events
+ window_events = ("activewindow", "openwindow", "closewindow", "changefloatingmode")
+ for event in window_events:
+ self.conn.connect(f"event::{event}", lambda *_: self.check_hide())
+ self.conn.connect("event::workspace", lambda *_: GLib.timeout_add(50, self.check_hide))
+
+ def _start_occlusion_checking(self):
+ """Start periodic occlusion check"""
+ GLib.timeout_add(self.OCCLUSION_CHECK_INTERVAL, self.check_occlusion_state)
+
+ def _get_bar_height(self):
+ """Get the height of the bar with fallback"""
+ try:
+ height = self.bar.get_allocated_height()
+ return height if height > 0 else 30
+ except Exception:
+ return 30 # Default fallback
+
+ def _get_occlusion_region(self):
+ """Get the region to check for occlusion"""
+ return ("top", self._get_bar_height())
+
+ def check_occlusion_state(self):
+ """Periodic occlusion check"""
+ # Skip occlusion check if hovered
+ if self.is_hovered:
+ self.bar.main_box.remove_style_class("occluded")
+ return True
+
+ # Check occlusion using the top region
+ is_occluded = check_occlusion(self._get_occlusion_region())
+
+ # Only apply changes if state has changed to avoid unnecessary CSS operations
+ if is_occluded != self.last_occlusion_state:
+ self.last_occlusion_state = is_occluded
+ if is_occluded:
+ self.bar.main_box.add_style_class("occluded")
+ else:
+ self.bar.main_box.remove_style_class("occluded")
+ if not self.is_hovered and self.is_hidden:
+ # Debounce showing the bar to prevent flickering
+ GLib.timeout_add(100, lambda: self.toggle_bar(not self._should_stay_hidden()))
+
+ return True
+
+ def _should_stay_hidden(self):
+ """Determine if the bar should stay hidden based on window state"""
+ clients = self.get_clients()
+ current_ws = self.get_workspace()
+ ws_clients = [w for w in clients if w["workspace"]["id"] == current_ws]
+
+ # If we have normal windows and are not being hovered, stay hidden
+ return self._has_normal_windows(ws_clients) and not self.is_hovered
+
+ def on_bar_enter(self, widget, event):
+ """Handle hover over bar content."""
+ self.is_hovered = True
+ self.bar.main_box.remove_style_class("occluded")
+
+ # Cancel any pending hide operations
+ self._cancel_hide_timer()
+ self.toggle_bar(show=True)
+ return True # Stop event propagation
+
+ def on_bar_leave(self, widget, event):
+ """Handle leave from bar content."""
+ # Only trigger if mouse actually left the entire bar area
+ if event.detail == Gdk.NotifyType.INFERIOR:
+ return False # Ignore child-to-child mouse movements
+
+ self.is_hovered = False
+
+ # Schedule hide if needed
+ if self._should_stay_hidden() or check_occlusion(self._get_occlusion_region()):
+ self.delay_hide()
+
+ # Update occlusion state
+ if check_occlusion(self._get_occlusion_region()):
+ self.last_occlusion_state = True
+ self.bar.main_box.add_style_class("occluded")
+
+ return True # Stop event propagation
+
+ def on_hover_enter(self, *args):
+ """Handle hover over top activation area."""
+ self.bar.main_box.remove_style_class("occluded")
+ self.toggle_bar(show=True)
+ return False
+
+ def _cancel_hide_timer(self):
+ """Cancel any pending hide timer"""
+ if self.hide_id:
+ GLib.source_remove(self.hide_id)
+ self.hide_id = None
+
+ def toggle_bar(self, show):
+ """Show or hide the bar immediately."""
+ if show:
+ if self.is_hidden:
+ self.is_hidden = False
+ self.bar.main_box.remove_style_class("hide-bar")
+ self.bar.main_box.add_style_class("show-bar")
+ self._cancel_hide_timer()
+ else:
+ if not self.is_hidden:
+ self.is_hidden = True
+ self.bar.main_box.remove_style_class("show-bar")
+ self.bar.main_box.add_style_class("hide-bar")
+
+ def delay_hide(self):
+ """Schedule hiding after short delay."""
+ self._cancel_hide_timer()
+ self.hide_id = GLib.timeout_add(self.HIDE_DELAY, self.hide_bar)
+
+ def hide_bar(self):
+ """Finalize hiding procedure."""
+ if not self.is_hovered:
+ self.toggle_bar(show=False)
+ self.hide_id = None
+ return False
+
+ def get_clients(self):
+ """Get current client list."""
+ try:
+ return json.loads(self.conn.send_command("j/clients").reply.decode())
+ except (json.JSONDecodeError, AttributeError):
+ return []
+
+ def get_workspace(self):
+ """Get current workspace ID."""
+ try:
+ return json.loads(
+ self.conn.send_command("j/activeworkspace").reply.decode()
+ ).get("id", 0)
+ except (json.JSONDecodeError, AttributeError):
+ return 0
+
+ def _has_normal_windows(self, clients):
+ """Check if any non-floating, non-fullscreen windows exist"""
+ return any(not w.get("floating") and not w.get("fullscreen") for w in clients)
+
+ def check_hide(self, *args):
+ """Determine if bar should auto-hide based on window state."""
+ # Get window data
+ clients = self.get_clients()
+ current_ws = self.get_workspace()
+ ws_clients = [w for w in clients if w["workspace"]["id"] == current_ws]
+
+ # Check for occlusion
+ is_occluded = check_occlusion(self._get_occlusion_region())
+ self.last_occlusion_state = is_occluded
+
+ # Determine visibility state
+ if not ws_clients:
+ # No windows in current workspace, show the bar
+ self.bar.main_box.remove_style_class("occluded")
+ self.toggle_bar(show=True)
+ elif self._has_normal_windows(ws_clients):
+ # Normal windows exist, handle occlusion and hide if not hovered
+ if is_occluded:
+ self.bar.main_box.add_style_class("occluded")
+
+ if not self.is_hovered:
+ self.toggle_bar(show=False)
+ else:
+ # Only floating/fullscreen windows, show the bar
+ self.bar.main_box.remove_style_class("occluded")
+ self.toggle_bar(show=True)
+
+ return False
\ No newline at end of file
diff --git a/modules/bar/components/language.py b/modules/bar/components/language.py
new file mode 100644
index 0000000..858c0e2
--- /dev/null
+++ b/modules/bar/components/language.py
@@ -0,0 +1,24 @@
+from fabric.widgets.button import Button
+from fabric.widgets.label import Label
+from fabric.hyprland.widgets import Language, HyprlandEvent, get_hyprland_connection
+
+
+class LanguageWidget(Button):
+ """Widget that displays the current keyboard layout."""
+
+ def __init__(self, **kwargs):
+ self.lang_label = Label(name="lang-label")
+ super().__init__(
+ name="language", h_align="center", v_align="center",
+ child=self.lang_label, **kwargs
+ )
+
+ self.conn = get_hyprland_connection()
+ self.update_language()
+ self.conn.connect("event::activelayout", self.update_language)
+
+ def update_language(self, _=None, event: HyprlandEvent = None):
+ """Update the language indicator based on Hyprland events."""
+ lang = event.data[1] if event else Language().get_label()
+ self.set_tooltip_text(lang)
+ self.lang_label.set_label(lang[:2].lower())
\ No newline at end of file
diff --git a/modules/bar/components/metrics.py b/modules/bar/components/metrics.py
index 52f9c64..685efa3 100644
--- a/modules/bar/components/metrics.py
+++ b/modules/bar/components/metrics.py
@@ -1,7 +1,7 @@
import subprocess
import json
import psutil
-from gi.repository import GLib, Gdk
+from gi.repository import GLib, Gdk, Gtk
from fabric.widgets.label import Label
from fabric.widgets.box import Box
@@ -19,7 +19,6 @@ class MetricsProvider:
self.mem = 0.0
self.swap = 0.0
self.disk = []
- self.gpu = []
self.bat_percent = 0.0
self.bat_charging = None
@@ -34,8 +33,6 @@ class MetricsProvider:
swap = psutil.swap_memory()
self.swap = swap.percent
self.disk = [psutil.disk_usage(path).percent for path in data.BAR_METRICS_DISKS]
- info = self.get_gpu_info()
- self.gpu = [int(v["gpu_util"][:-1]) for v in info]
battery = psutil.sensors_battery()
if battery is None:
@@ -52,19 +49,12 @@ class MetricsProvider:
def get_battery(self):
return (self.bat_percent, self.bat_charging)
- def get_gpu_info(self):
- try:
- return json.loads(subprocess.check_output(["nvtop", "-s"]))
- except:
- return []
-
shared_provider = MetricsProvider()
class SingularMetric:
def __init__(self, id, name, icon):
- self.is_vertical_layout = data.VERTICAL
self.name_markup = name
self.icon_markup = icon
@@ -87,30 +77,28 @@ class SingularMetric:
)
self.circle_button.add_events(
- Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK
+ Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK | Gdk.EventMask.BUTTON_PRESS_MASK
)
self.circle_button.connect("enter-notify-event", self.on_button_enter)
self.circle_button.connect("leave-notify-event", self.on_button_leave)
-
- self.circle_button.connect("clicked", self.on_circle_clicked)
+ self.circle_button.connect("button-press-event", self.on_button_press)
self.level = Label(
name="metrics-level",
style_classes=id,
label="0%",
- style="margin: 4px 0px;" if self.is_vertical_layout else "margin: 0 4px;",
)
self.revealer = Revealer(
name=f"metrics-{id}-revealer",
transition_duration=250,
- transition_type="slide-up" if self.is_vertical_layout else "slide-left",
+ transition_type="slide-left",
child=self.level,
child_revealed=False,
)
self.box = Box(
name=f"metrics-{id}-box",
- orientation="v" if self.is_vertical_layout else "h",
+ orientation="h",
spacing=0,
children=[self.circle_button, self.revealer],
)
@@ -119,12 +107,11 @@ class SingularMetric:
self.box.connect("enter-notify-event", self.on_button_enter)
self.box.connect("leave-notify-event", self.on_button_leave)
- # Set initial tooltip - moved after box is created
+ # Set initial tooltip
self.update_tooltip("0%")
def update_tooltip(self, value_text):
tooltip = f"{self.icon_markup} {self.name_markup}: {value_text}"
- # self.box.set_tooltip_markup(tooltip)
self.circle_button.set_tooltip_markup(tooltip)
self.circle.set_tooltip_markup(tooltip)
self.icon.set_tooltip_markup(tooltip)
@@ -143,24 +130,31 @@ class SingularMetric:
if window:
window.set_cursor(None)
- def on_circle_clicked(self, button):
- # Toggle the revealed state of the label
- current_state = self.revealer.get_child_revealed()
- self.revealer.set_reveal_child(not current_state)
+ def on_button_press(self, widget, event):
+ if event.button == 1: # Left click
+ # Get the parent Metrics instance
+ parent = self.box.get_parent()
+ while parent and not isinstance(parent, Metrics):
+ parent = parent.get_parent()
+ if parent:
+ parent.toggle_all_metrics()
+ return True # Stop event propagation
+ elif event.button == 3: # Right click
+ # Toggle only this metric's revealer
+ current_state = self.revealer.get_child_revealed()
+ self.revealer.set_reveal_child(not current_state)
+ return True # Stop event propagation
+ return False
class Metrics(Box):
def __init__(self, **kwargs):
- # Determine if we should use vertical layout for components
- self.is_vertical_layout = data.VERTICAL
-
super().__init__(
name="metrics",
spacing=0,
- orientation="h" if not self.is_vertical_layout else "v",
+ orientation="h",
visible=True,
all_visible=True,
- style="padding:4px 0px" if self.is_vertical_layout else "padding: 0px 4px;",
**kwargs,
)
@@ -174,145 +168,103 @@ class Metrics(Box):
self.batt_fabricator.changed.connect(self.update_battery)
GLib.idle_add(self.update_battery, None, shared_provider.get_battery())
- visible = getattr(
- data,
- "METRICS_VISIBLE",
- {"cpu": True, "ram": True, "disk": True, "gpu": False, "swap": True},
- )
- disks = (
- [
- SingularMetric(
- "disk",
- f"DISK ({path})" if len(data.BAR_METRICS_DISKS) != 1 else "DISK",
- icons.disk,
- )
- for path in data.BAR_METRICS_DISKS
- ]
- if visible.get("disk", True)
- else []
- )
- gpu_info = shared_provider.get_gpu_info()
- gpus = (
- [
- SingularMetric(
- f"gpu",
- f"GPU ({v['device_name']})" if len(gpu_info) != 1 else "GPU",
- icons.gpu,
- )
- for v in gpu_info
- ]
- if visible.get("gpu", True)
- else []
- )
+ # Initialize all metrics
+ disks = [
+ SingularMetric(
+ "disk",
+ f"DISK ({path})" if len(data.BAR_METRICS_DISKS) != 1 else "DISK",
+ icons.disk,
+ )
+ for path in data.BAR_METRICS_DISKS
+ ]
- self.cpu = (
- SingularMetric("cpu", "CPU", icons.cpu)
- if visible.get("cpu", True)
- else None
- )
- self.ram = (
- SingularMetric("ram", "RAM", icons.memory)
- if visible.get("ram", True)
- else None
- )
- self.swap = (
- SingularMetric("swap", "SWAP", icons.swap)
- if visible.get("swap", True)
- else None
- )
- self.bat = (
- SingularMetric("bat", "BATTERY", icons.battery)
- if visible.get("bat", True)
- else None
- )
+ self.ram = SingularMetric("ram", "RAM", icons.memory)
+ self.cpu = SingularMetric("cpu", "CPU", icons.cpu)
+ self.swap = SingularMetric("swap", "SWAP", icons.swap)
+ self.bat = SingularMetric("bat", "BATTERY", icons.battery)
self.disk = disks
- self.gpu = gpus
- # Add only enabled metrics
- for disk in self.disk:
- self.add(disk.box)
- self.add(Box(name="metrics-sep"))
+ # Create container for all metrics
+ self.metrics_container = Box(name="metrics-container", orientation="h", spacing=0)
+ self.add(self.metrics_container)
+
+ # Create revealers for metrics
+ self.cpu_revealer = Revealer(
+ name="cpu-revealer",
+ transition_duration=250,
+ transition_type="slide-left",
+ child=self.cpu.box,
+ child_revealed=False,
+ )
+ self.swap_revealer = Revealer(
+ name="swap-revealer",
+ transition_duration=250,
+ transition_type="slide-left",
+ child=self.swap.box,
+ child_revealed=False,
+ )
+ self.disk_revealers = [
+ Revealer(
+ name=f"disk-revealer-{i}",
+ transition_duration=250,
+ transition_type="slide-left",
+ child=disk.box,
+ child_revealed=False,
+ )
+ for i, disk in enumerate(self.disk)
+ ]
+
+ # Create permanent separator between memory and battery
+ self.memory_battery_sep = Box(name="metrics-sep")
+
+ # Create container for revealed metrics
+ self.revealed_metrics = Box(name="revealed-metrics", orientation="h", spacing=0)
+ self.revealed_metrics.add(self.cpu_revealer)
+ for disk_revealer in self.disk_revealers:
+ self.revealed_metrics.add(disk_revealer)
+ self.revealed_metrics.add(self.swap_revealer)
+
+ # Create container for memory and battery
+ self.memory_battery_container = Box(name="memory-battery-container", orientation="h", spacing=0)
if self.ram:
- self.add(self.ram.box)
- self.add(Box(name="metrics-sep"))
- if self.cpu:
- self.add(self.cpu.box)
- for gpu in self.gpu:
- self.add(Box(name="metrics-sep"))
- self.add(gpu.box)
- if self.swap:
- self.add(Box(name="metrics-sep"))
- self.add(self.swap.box)
+ self.memory_battery_container.add(self.ram.box)
+ self.memory_battery_container.add(self.memory_battery_sep)
if self.bat:
- self.add(Box(name="metrics-sep"))
- self.add(self.bat.box)
+ self.memory_battery_container.add(self.bat.box)
- # Connect events directly to the button
- self.connect("enter-notify-event", self.on_mouse_enter)
- self.connect("leave-notify-event", self.on_mouse_leave)
+ # Add metrics in the correct order
+ self.metrics_container.add(self.revealed_metrics)
+ self.metrics_container.add(self.memory_battery_container)
+
+ # Initially hide all non-visible metrics
+ self.cpu_revealer.set_reveal_child(False)
+ self.swap_revealer.set_reveal_child(False)
+ for disk_revealer in self.disk_revealers:
+ disk_revealer.set_reveal_child(False)
+
+ self.showing_all_metrics = False
GLib.timeout_add_seconds(1, self.update_metrics)
- self.hide_timer = None
- self.hover_counter = 0
+ def toggle_all_metrics(self):
+ self.showing_all_metrics = not self.showing_all_metrics
+
+ # Toggle all revealers
+ self.cpu_revealer.set_reveal_child(self.showing_all_metrics)
+ self.swap_revealer.set_reveal_child(self.showing_all_metrics)
+ for disk_revealer in self.disk_revealers:
+ disk_revealer.set_reveal_child(self.showing_all_metrics)
+
+ # Toggle spacing for revealed metrics and memory-battery container
+ spacing = 6 if self.showing_all_metrics else 0
+ self.revealed_metrics.set_spacing(spacing)
+ self.metrics_container.set_spacing(spacing)
def _format_percentage(self, value: int) -> str:
return f"{value}%"
- def on_mouse_enter(self, widget, event):
- window = widget.get_window()
- if window:
- window.set_cursor(Gdk.Cursor(Gdk.CursorType.HAND2))
-
- if not data.VERTICAL:
- self.hover_counter += 1
- if self.hide_timer is not None:
- GLib.source_remove(self.hide_timer)
- self.hide_timer = None
- if self.cpu:
- self.cpu.revealer.set_reveal_child(True)
- if self.ram:
- self.ram.revealer.set_reveal_child(True)
- for disk in self.disk:
- disk.revealer.set_reveal_child(True)
- for gpu in self.gpu:
- gpu.revealer.set_reveal_child(True)
- if self.swap:
- self.swap.revealer.set_reveal_child(True)
- if self.bat:
- self.bat.revealer.set_reveal_child(True)
- return False
-
- def on_mouse_leave(self, widget, event):
- window = widget.get_window()
- if window:
- window.set_cursor(None)
-
- if not data.VERTICAL:
- if self.hover_counter > 0:
- self.hover_counter -= 1
- if self.hover_counter == 0:
- if self.hide_timer is not None:
- GLib.source_remove(self.hide_timer)
- self.hide_timer = GLib.timeout_add(500, self.hide_revealer)
- return False
-
- def hide_revealer(self):
- if not data.VERTICAL:
- if self.cpu:
- self.cpu.revealer.set_reveal_child(False)
- if self.ram:
- self.ram.revealer.set_reveal_child(False)
- for disk in self.disk:
- disk.revealer.set_reveal_child(False)
- for gpu in self.gpu:
- gpu.revealer.set_reveal_child(False)
- self.hide_timer = None
- return False
-
def update_metrics(self):
cpu, mem, swap, disk = shared_provider.get_metrics()
- gpu = shared_provider.gpu
if self.cpu:
self.cpu.circle.set_value(cpu / 100.0)
value_text = self._format_percentage(int(cpu))
@@ -329,12 +281,6 @@ class Metrics(Box):
disk_metric.circle.set_value(disk[i] / 100.0)
disk_metric.level.set_label(value_text)
disk_metric.update_tooltip(value_text)
- for i, gpu_metric in enumerate(self.gpu):
- if i < len(gpu):
- value_text = self._format_percentage(int(gpu[i]))
- gpu_metric.circle.set_value(gpu[i] / 100.0)
- gpu_metric.level.set_label(value_text)
- gpu_metric.update_tooltip(value_text)
if self.swap:
self.swap.circle.set_value(swap / 100.0)
value_text = self._format_percentage(int(swap))
@@ -354,8 +300,6 @@ class Metrics(Box):
if self.bat: tooltip_metrics.append(self.bat)
if self.ram: tooltip_metrics.append(self.ram)
if self.cpu: tooltip_metrics.append(self.cpu)
- if self.gpu: tooltip_metrics.extend(self.gpu)
- self.set_tooltip_markup((" - " if not data.VERTICAL else "\n").join([v.markup() for v in tooltip_metrics]))
return True
@@ -364,13 +308,10 @@ class Metrics(Box):
return
value, charging = battery_data
- if value == 0:
- if self.bat:
- self.bat.box.set_visible(False)
- else:
- if self.bat:
- self.bat.box.set_visible(True)
- self.bat.circle.set_value(value / 100)
+ # Always show battery, even if value is 0
+ if self.bat:
+ self.bat.box.set_visible(True)
+ self.bat.circle.set_value(value / 100)
percentage = int(value)
self.bat.level.set_label(self._format_percentage(percentage))
diff --git a/modules/bar/components/power_menu.py b/modules/bar/components/power_menu.py
new file mode 100644
index 0000000..fe0cd79
--- /dev/null
+++ b/modules/bar/components/power_menu.py
@@ -0,0 +1,180 @@
+from fabric.widgets.button import Button
+from fabric.widgets.box import Box
+from fabric.widgets.label import Label
+from fabric.widgets.stack import Stack
+from fabric.widgets.revealer import Revealer
+from fabric.utils import exec_shell_command_async
+from gi.repository import GLib, Gdk, Gtk
+import utils.icons as icons
+
+
+class Power(Button):
+ TRANSITION_TYPE = "slide-up-down"
+ TRANSITION_DURATION = 300 # milliseconds
+ LABEL_TRANSITION_TYPE = "slide-left"
+ LONG_PRESS_DURATION = 500 # milliseconds
+ SCROLL_COOLDOWN = 100 # milliseconds
+
+ def __init__(self):
+ self.current_action = "shutdown"
+ self.is_locked = True
+ self.long_press_timeout = None
+ self.scroll_timeout = None
+ self.just_unlocked = False
+
+ # Initialize stacks for icons and labels
+ for name in ["icon_stack", "label_stack"]:
+ setattr(
+ self,
+ name,
+ Stack(
+ transition_type=self.TRANSITION_TYPE,
+ transition_duration=self.TRANSITION_DURATION,
+ ),
+ )
+
+ # Define power actions
+ self.power_actions = {
+ "shutdown": {
+ "icon": icons.shutdown,
+ "label": "Shutdown",
+ "command": "systemctl poweroff"
+ },
+ "reboot": {
+ "icon": icons.reboot,
+ "label": "Reboot",
+ "command": "systemctl reboot"
+ },
+ "suspend": {
+ "icon": icons.suspend,
+ "label": "Suspend",
+ "command": "systemctl suspend"
+ },
+ "logout": {
+ "icon": icons.logout,
+ "label": "Logout",
+ "command": "hyprctl dispatch exit"
+ },
+ "lock": {
+ "icon": icons.lock,
+ "label": "Lock",
+ "command": "hyprlock --immediate"
+ }
+ }
+
+ # Add all actions to stacks
+ for action, data in self.power_actions.items():
+ self.icon_stack.add_named(
+ Label(
+ markup=data["icon"],
+ style_classes="powericon",
+ ),
+ name=action,
+ )
+ self.label_stack.add_named(
+ Label(
+ label=data["label"],
+ style_classes="powerlabel"
+ ),
+ name=action,
+ )
+
+ self.revealer = Revealer(
+ transition_type=self.LABEL_TRANSITION_TYPE,
+ transition_duration=self.TRANSITION_DURATION,
+ child=self.label_stack,
+ )
+
+ super().__init__(
+ name="power-menu",
+ on_enter_notify_event=lambda *args: self.revealer.set_reveal_child(True),
+ on_leave_notify_event=self.on_leave,
+ on_pressed=self.on_pressed,
+ on_released=self.on_released,
+ on_scroll_event=self.on_scroll,
+ child=Box(
+ children=[
+ self.icon_stack,
+ self.revealer,
+ ]
+ ),
+ )
+
+ self.add_events(
+ Gdk.EventMask.SCROLL_MASK |
+ Gdk.EventMask.BUTTON_PRESS_MASK |
+ Gdk.EventMask.SMOOTH_SCROLL_MASK
+ )
+ self.add_style_class("locked")
+
+ # Add cursor change on hover
+ self.connect(
+ "state-flags-changed",
+ lambda btn, *_: (
+ btn.set_cursor("pointer")
+ if btn.get_state_flags() & Gtk.StateFlags.PRELIGHT # type: ignore
+ else btn.set_cursor("default"),
+ ),
+ )
+
+ def on_pressed(self, widget, event=None):
+ if self.is_locked:
+ self.long_press_timeout = GLib.timeout_add(
+ self.LONG_PRESS_DURATION,
+ self.on_long_press
+ )
+
+ def on_released(self, widget, event=None):
+ timeout = self.long_press_timeout
+ self.long_press_timeout = None
+ if timeout:
+ GLib.source_remove(timeout)
+
+ if not self.is_locked and not self.just_unlocked:
+ exec_shell_command_async(self.power_actions[self.current_action]["command"])
+ self.is_locked = True
+ self.add_style_class("locked")
+
+ self.just_unlocked = False
+
+ def on_long_press(self):
+ if self.is_locked:
+ self.is_locked = False
+ self.just_unlocked = True
+ self.remove_style_class("locked")
+ self.long_press_timeout = None
+ return False
+
+ def _change_action(self, direction):
+ if self.is_locked:
+ return
+
+ actions = list(self.power_actions.keys())
+ current_index = actions.index(self.current_action)
+ new_index = (current_index + direction) % len(actions)
+
+ self.current_action = actions[new_index]
+ self.icon_stack.set_visible_child_name(self.current_action)
+ self.label_stack.set_visible_child_name(self.current_action)
+
+ def on_scroll(self, widget, event):
+ if self.is_locked:
+ return True
+
+ if event.direction == Gdk.ScrollDirection.SMOOTH:
+ if event.delta_y < -0.1:
+ self._change_action(-1)
+ elif event.delta_y > 0.1:
+ self._change_action(1)
+ elif event.direction == Gdk.ScrollDirection.UP:
+ self._change_action(-1)
+ elif event.direction == Gdk.ScrollDirection.DOWN:
+ self._change_action(1)
+
+ self.scroll_timeout = GLib.timeout_add(self.SCROLL_COOLDOWN, lambda: None)
+ return True
+ def on_leave(self, widget, event=None):
+ self.revealer.set_reveal_child(False)
+ if not self.is_locked:
+ self.is_locked = True
+ self.add_style_class("locked")
diff --git a/modules/bar/components/system_indicators.py b/modules/bar/components/system_indicators.py
index 7d9b453..51fc528 100644
--- a/modules/bar/components/system_indicators.py
+++ b/modules/bar/components/system_indicators.py
@@ -1,62 +1,90 @@
import subprocess
+from gi.repository import Gtk, Gdk
from fabric import Fabricator
from fabric.widgets.label import Label
from fabric.widgets.box import Box
-from fabric.widgets.button import Button
+from fabric.widgets.revealer import Revealer
from fabric.bluetooth import BluetoothClient
-from services import network_client, audio, notification_service
+from services import network_client, audio
import utils.icons as icons
-import config.data as data
+from fabric.hyprland.widgets import Language, HyprlandEvent, get_hyprland_connection
class SystemIndicators(Box):
def __init__(self, **kwargs):
- # Determine if we should use vertical layout for components
- is_vertical_layout = data.VERTICAL
-
super().__init__(
- orientation="h" if not is_vertical_layout else "v", spacing=0, **kwargs
+ orientation="h", spacing=2, name="system-indicator", **kwargs
)
- self.launcher = kwargs.get("launcher", None)
self.bluetooth_icon = Label(name="system-indicator-icon")
self.wifi_icon = Label(name="system-indicator-icon")
self.volume_icon_button = Label(name="system-indicator-icon")
- self.microphone_icon = Label(name="system-indicator-icon")
+ self.language_icon = Label(name="lang-label")
self.idle_label = Label(name="system-indicator-icon")
self.night_label = Label(name="system-indicator-icon")
self.power_label = Label(name="system-indicator-icon")
- self.notification_icon = Button(name="system-indicator-icon")
- self.notification_label = Label(name="system-notif-label")
- self.notification_icon.add(self.notification_label)
- self.notification_icon.connect("clicked", self.open_notif_center)
- for widget in [
- self.bluetooth_icon,
- self.wifi_icon,
- self.volume_icon_button,
- self.microphone_icon,
- self.idle_label,
- self.night_label,
- self.power_label,
- self.notification_icon,
- ]:
- self.add(widget)
+ # Create event box to wrap indicators
+ self.event_box = Gtk.EventBox()
+ self.indicators_box = Box(name="indicators-container", orientation="h", spacing=2)
+ self.event_box.add(self.indicators_box)
+
+ # Create revealers for additional indicators
+ self.volume_revealer = Revealer(
+ name="volume-revealer",
+ transition_duration=250,
+ transition_type="slide-left",
+ child=self.volume_icon_button,
+ child_revealed=False,
+ )
+ self.idle_revealer = Revealer(
+ name="idle-revealer",
+ transition_duration=250,
+ transition_type="slide-left",
+ child=self.idle_label,
+ child_revealed=False,
+ )
+ self.night_revealer = Revealer(
+ name="night-revealer",
+ transition_duration=250,
+ transition_type="slide-left",
+ child=self.night_label,
+ child_revealed=False,
+ )
+ self.power_revealer = Revealer(
+ name="power-revealer",
+ transition_duration=250,
+ transition_type="slide-left",
+ child=self.power_label,
+ child_revealed=False,
+ )
+
+ # Add all indicators with revealers
+ self.indicators_box.add(self.language_icon)
+ self.indicators_box.add(self.wifi_icon)
+ self.indicators_box.add(self.bluetooth_icon)
+ self.indicators_box.add(self.volume_revealer)
+ self.indicators_box.add(self.idle_revealer)
+ self.indicators_box.add(self.night_revealer)
+ self.indicators_box.add(self.power_revealer)
+
+ # Add event box to main container
+ self.add(self.event_box)
+
+ # Connect click events
+ self.event_box.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
+ self.event_box.connect("button-press-event", self.on_button_press)
self.audio_service = audio
self.bluetooth_client = BluetoothClient()
self.network_client = network_client
- self.notification_service = notification_service
+ self.hyprland = get_hyprland_connection()
# Connect signals
self.bluetooth_client.connect("changed", self.update_bluetooth_status)
- self.audio_service.connect("microphone_changed", self.update_mic_status)
self.audio_service.connect("changed", self.update_volume_status)
self.network_client.connect("device_ready", self.update_network_status)
- self.notification_service.connect("dnd", self.update_notification_status)
- self.notification_service.connect(
- "notification_count", self.update_notification_status
- )
+ self.hyprland.connect("event::activelayout", self.update_language)
if self.network_client.wifi_device:
self.network_client.wifi_device.connect(
@@ -70,14 +98,59 @@ class SystemIndicators(Box):
Fabricator(interval=1000, poll_from=self.update_all_statuses)
+ # Initial updates
self.update_bluetooth_status()
- self.update_notification_status()
+ self.update_language()
+ self.update_idle_night_status()
+ self.showing_all_indicators = False
+
+ def on_button_press(self, widget, event):
+ if event.button == 3: # Right click
+ self.toggle_labels()
+ return True # Stop event propagation
+ elif event.button == 1: # Left click
+ self.toggle_all_indicators()
+ return True # Stop event propagation
+ return False
+
+ def toggle_all_indicators(self):
+ self.showing_all_indicators = not self.showing_all_indicators
+
+ # Toggle revealers
+ self.volume_revealer.set_reveal_child(self.showing_all_indicators)
+ self.idle_revealer.set_reveal_child(self.showing_all_indicators)
+ self.night_revealer.set_reveal_child(self.showing_all_indicators)
+ self.power_revealer.set_reveal_child(self.showing_all_indicators)
+
+ def toggle_labels(self):
+ # Toggle tooltips for all visible indicators
+ for child in self.indicators_box.get_children():
+ if isinstance(child, Label):
+ current_tooltip = child.get_tooltip_text()
+ if current_tooltip:
+ child.set_tooltip_text("")
+ else:
+ # Restore original tooltip based on the indicator type
+ if child == self.wifi_icon:
+ self.update_network_status()
+ elif child == self.bluetooth_icon:
+ self.update_bluetooth_status()
+ elif child == self.volume_icon_button:
+ self.update_volume_status()
+ elif child == self.language_icon:
+ self.update_language()
def update_all_statuses(self, *_args):
self.update_idle_night_status()
self.update_power_profile()
self.update_network_status()
+ def update_language(self, _=None, event: HyprlandEvent = None):
+ """Update the language indicator based on Hyprland events."""
+ lang = event.data[1] if event else Language().get_label()
+ self.language_icon.set_tooltip_text(lang)
+ self.language_icon.set_label(lang[:2].lower())
+
def update_idle_night_status(self, *_):
# Update idle status
has_idle = (
@@ -127,26 +200,6 @@ class SystemIndicators(Box):
tooltip_text = "Muted" if is_muted else f"Volume: {volume_level}%"
self.volume_icon_button.set_tooltip_text(tooltip_text)
- def update_mic_status(self, *_):
- mic = self.audio_service.microphone
- if not mic:
- self.microphone_icon.set_visible(False)
- return
-
- self.microphone_icon.set_visible(True)
- # Normalize volume to integer percentage (0-100)
- volume_level = mic.volume
- if volume_level > 1:
- volume_level = min(int(volume_level), 100)
- else:
- volume_level = int(volume_level * 100)
-
- is_muted = mic.muted
- icon = icons.mic_muted if is_muted else icons.mic
-
- self.microphone_icon.set_markup(icon)
- tooltip_text = "Muted" if is_muted else f"Microphone: {volume_level}%"
- self.microphone_icon.set_tooltip_text(tooltip_text)
def update_bluetooth_status(self, *_):
if self.bluetooth_client.enabled:
@@ -236,23 +289,3 @@ class SystemIndicators(Box):
self.power_label.set_visible(False)
self.power_label.set_tooltip_text(f"Error fetching power profile: {e}")
- def update_notification_status(self, *_):
- """Update the notification icon based on DND state and notification count."""
- count = self.notification_service.count
-
- if count == 0 and not self.notification_service.dont_disturb:
- self.notification_icon.set_visible(False)
- return
-
- if self.notification_service.dont_disturb:
- self.notification_label.set_markup(icons.notifications_off)
- self.notification_icon.set_tooltip_text("Do Not Disturb: On")
- self.notification_icon.set_visible(True)
- else:
- # At this point count must be > 0
- self.notification_label.set_markup(icons.notifications_clear)
- self.notification_icon.set_tooltip_text(f"Notifications: {count}")
- self.notification_icon.set_visible(True)
-
- def open_notif_center(self, *_):
- self.launcher.open("notification-center")
diff --git a/modules/bar/components/updates.py b/modules/bar/components/updates.py
deleted file mode 100644
index f43add2..0000000
--- a/modules/bar/components/updates.py
+++ /dev/null
@@ -1,115 +0,0 @@
-import json
-from fabric.utils import exec_shell_command_async, get_relative_path, invoke_repeater
-from fabric.widgets.box import Box
-from fabric.widgets.centerbox import CenterBox
-from fabric.widgets.label import Label
-from fabric.widgets.button import Button
-from fabric.widgets.stack import Stack
-from gi.repository import Gtk
-import utils.icons as icons
-import config.data as data
-
-
-class UpdatesWidget(Button):
- """A widget to display the number of available updates."""
-
- def __init__(
- self,
- **kwargs,
- ):
- # Determine if we should use vertical layout for components
- is_vertical_layout = data.VERTICAL
-
- # Initialize the button with specific name and style
- super().__init__(
- name="update",
- orientation="h" if not is_vertical_layout else "v",
- **kwargs,
- )
-
- self.script_file = get_relative_path("../../../config/scripts/systemupdates.sh")
-
- self.update_level_label = Label(
- name="update-label",
- label="0",
- visible=False, # Initially hidden
- )
-
- self.update_icon = Label(name="update-icon", markup=icons.update)
- self.updated_icon = Label(name="updated-icon", markup=icons.updated)
-
- # Create boxes with proper orientation
- self.update_box = CenterBox(
- orientation="v" if is_vertical_layout else "h",
- center_children=[self.update_icon, self.update_level_label],
- )
-
- self.updated_box = CenterBox(
- orientation="v" if is_vertical_layout else "h",
- center_children=[self.updated_icon],
- )
-
- # Stack to switch between states
- self.stack = Stack()
- self.stack.add_named(self.update_box, "update_box")
- self.stack.add_named(self.updated_box, "updated_box")
- self.stack.set_visible_child_name("update_box")
-
- self.children = Box(
- orientation="v" if is_vertical_layout else "h",
- children=self.stack,
- )
-
- # Configure stack transitions
- self.stack.set_homogeneous(False)
- self.stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
-
- # Connect click handler
- self.connect("button-press-event", self.on_button_press)
-
- # Check for updates every minute
- invoke_repeater(60000, self.update, initial_call=True)
-
- def update_values(self, value: str) -> bool:
- """Update the widget state based on update check results"""
- try:
- value = json.loads(value)
- except json.JSONDecodeError:
- return False
-
- update_count = str(value["total"])
- self.update_level_label.set_label(update_count)
-
- # Show appropriate state based on update count
- if update_count == "0":
- self.stack.set_visible_child_name("updated_box")
- self.update_level_label.set_visible(False)
- else:
- self.stack.set_visible_child_name("update_box")
- self.update_level_label.set_visible(True)
-
- self.set_tooltip_text(value.get("tooltip", ""))
- return True
-
- def on_button_press(self, _, event) -> bool:
- """Handle button press events"""
- if event.button == 1: # Left click
- # Run update command
- exec_shell_command_async(
- f"{self.script_file} -arch -up",
- lambda _: None,
- )
- self.update()
- return True
- else:
- # Other clicks just refresh
- self.update()
-
- def update(self) -> bool:
- """Check for system updates"""
- exec_shell_command_async(
- f"{self.script_file} -arch",
- lambda output: self.update_values(output),
- )
-
- return True
diff --git a/modules/bar/components/workspace.py b/modules/bar/components/workspace.py
deleted file mode 100644
index 072364b..0000000
--- a/modules/bar/components/workspace.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import json
-
-from fabric.hyprland.service import Hyprland
-from fabric.widgets.label import Label
-import config.data as data
-
-connection = Hyprland()
-
-workspaceData = connection.send_command("j/activeworkspace").reply
-activeWorkspace = json.loads(workspaceData.decode("utf-8"))["name"]
-
-# Create the workspace label with proper initialization
-if not data.VERTICAL:
- workspace = Label(label=f"Workspace {activeWorkspace}", name="workspace-name")
-else:
- workspace = Label(
- v_align="center",
- label="W\no\nr\nk\ns\np\na\nc\ne\n\n" + activeWorkspace,
- name="workspace-name",
- )
-
-def on_workspace(obj, signal):
- global activeWorkspace
- activeWorkspace = json.loads(signal.data[0])
- if not data.VERTICAL:
- workspace.set_label(f"Workspace {activeWorkspace}")
- else:
- workspace.set_label(f"W\no\nr\nk\ns\np\na\nc\ne\n\n{activeWorkspace}")
-
-connection.connect("event::workspace", on_workspace)
diff --git a/modules/bar/components/workspaces.py b/modules/bar/components/workspaces.py
new file mode 100644
index 0000000..a0d8df2
--- /dev/null
+++ b/modules/bar/components/workspaces.py
@@ -0,0 +1,19 @@
+import json
+
+from fabric.hyprland.service import Hyprland
+from fabric.widgets.label import Label
+
+connection = Hyprland()
+
+workspaceData = connection.send_command("j/activeworkspace").reply
+activeWorkspace = json.loads(workspaceData.decode("utf-8"))["name"]
+workspace = Label(label=f"{activeWorkspace}")
+
+
+def on_workspace(obj, signal):
+ global activeWorkspace
+ activeWorkspace = json.loads(signal.data[0])
+ workspace.set_label(f"{activeWorkspace}")
+
+
+connection.connect("event::workspace", on_workspace)
diff --git a/modules/corners.py b/modules/corners.py
new file mode 100644
index 0000000..0df401d
--- /dev/null
+++ b/modules/corners.py
@@ -0,0 +1,62 @@
+from fabric.widgets.box import Box
+from fabric.widgets.wayland import WaylandWindow as Window
+from fabric.widgets.shapes import Corner
+
+class MyCorner(Box):
+ def __init__(self, corner):
+ super().__init__(
+ name="corner-container",
+ children=Corner(
+ name="corner",
+ orientation=corner,
+ size=20,
+ ),
+ )
+
+class Corners(Window):
+ def __init__(self):
+ super().__init__(
+ name="corners",
+ layer="top",
+ anchor="top bottom left right",
+ exclusivity="normal",
+ pass_through=True,
+ visible=False,
+ all_visible=False,
+ )
+
+ self.all_corners = Box(
+ name="all-corners",
+ orientation="v",
+ h_expand=True,
+ v_expand=True,
+ h_align="fill",
+ v_align="fill",
+ children=[
+ Box(
+ name="top-corners",
+ orientation="h",
+ h_align="fill",
+ children=[
+ MyCorner("top-left"),
+ Box(h_expand=True),
+ MyCorner("top-right"),
+ ],
+ ),
+ Box(v_expand=True),
+ Box(
+ name="bottom-corners",
+ orientation="h",
+ h_align="fill",
+ children=[
+ MyCorner("bottom-left"),
+ Box(h_expand=True),
+ MyCorner("bottom-right"),
+ ],
+ ),
+ ],
+ )
+
+ self.add(self.all_corners)
+
+ self.show_all()
\ No newline at end of file
diff --git a/modules/dock.py b/modules/dock.py
index fb89eb7..c638174 100644
--- a/modules/dock.py
+++ b/modules/dock.py
@@ -9,21 +9,52 @@ from fabric.widgets.image import Image
from fabric.widgets.eventbox import EventBox
from fabric.widgets.wayland import WaylandWindow as Window
from fabric.hyprland.widgets import get_hyprland_connection
-from fabric.utils import (
- exec_shell_command,
- exec_shell_command_async,
- idle_add,
- remove_handler,
- get_relative_path,
-)
+from fabric.utils import exec_shell_command, exec_shell_command_async, idle_add, remove_handler, get_relative_path
from fabric.utils.helpers import get_desktop_applications
-from modules.bar.bar import MyCorner # Add this import for corners
+from modules.corners import MyCorner # Add this import for corners
import config.data as data
import logging
OCCLUSION = 36 + data.DOCK_ICON_SIZE
+def read_config():
+ """Read and return the full configuration from the JSON file, handling missing file."""
+ config_path = get_relative_path("../config/dock.json")
+ try:
+ with open(config_path, "r") as file:
+ data = json.load(file)
+
+ # Migration: Convert old format (simple string array) to new format (array of objects with full app data)
+ if "pinned_apps" in data and data["pinned_apps"] and isinstance(data["pinned_apps"][0], str):
+ # Get all desktop apps for lookup during migration
+ all_apps = get_desktop_applications()
+ app_map = {app.name: app for app in all_apps if app.name}
+
+ # This is the old format - convert it with full app data
+ old_pinned = data["pinned_apps"]
+ data["pinned_apps"] = []
+
+ for app_id in old_pinned:
+ # Try to find the app by name
+ app = app_map.get(app_id)
+ if app:
+ # Create comprehensive app data
+ app_data = {
+ "name": app.name,
+ "display_name": app.display_name,
+ "window_class": app.window_class,
+ "executable": app.executable,
+ "command_line": app.command_line
+ }
+ data["pinned_apps"].append(app_data)
+ else:
+ # Fallback to just the name if app not found
+ data["pinned_apps"].append({"name": app_id})
+
+ except (FileNotFoundError, json.JSONDecodeError):
+ data = {"pinned_apps": []} # Default to empty pinned apps
+ return data
# Credit to Aylur for the createSurfaceFromWidget code
def createSurfaceFromWidget(widget: Gtk.Widget) -> cairo.ImageSurface:
@@ -40,39 +71,30 @@ def createSurfaceFromWidget(widget: Gtk.Widget) -> cairo.ImageSurface:
widget.draw(cr)
return surface
-
class Dock(Window):
# Static registry to track all active dock instances
_instances = []
-
+
def __init__(self, **kwargs):
super().__init__(
name="dock-window",
layer="top",
- anchor="bottom center"
- if not data.VERTICAL and not data.BOTTOM_BAR
- else "left center"
- if data.VERTICAL_RIGHT_ALIGN
- else "right center",
- margin="-8px 0 -8px 0"
- if not data.VERTICAL and not data.BOTTOM_BAR
- else "-8px 0 -80px 0"
- if data.VERTICAL_RIGHT_ALIGN
- else "0 -80px 0 -8px",
+ anchor="bottom center",
+ margin="-8px 0 -8px 0",
exclusivity="none",
**kwargs,
)
# Add this instance to the registry
Dock._instances.append(self)
-
+
# Initialize configuration
- self.config = data.read_config()
+ self.config = read_config()
self.conn = get_hyprland_connection()
self.icon = IconResolver()
self.pinned = self.config.get("pinned_apps", [])
- self.config_path = get_relative_path("../config/assets/modus.json")
+ self.config_path = get_relative_path("../config/dock.json")
self.app_map = {} # Initialize the app map
- self._all_apps = get_desktop_applications() # Get all apps for lookup
+ self._all_apps = get_desktop_applications() # Get all apps for lookup
# Create app identifiers mapping
self.app_identifiers = self._build_app_identifiers_map()
self.is_hidden = False
@@ -80,20 +102,10 @@ class Dock(Window):
self._arranger_handler = None
self._drag_in_progress = False # Drag lock flag
self.is_hovered = False
- self.always_occluded = (
- data.DOCK_ALWAYS_OCCLUDED
- ) # Track the always occluded setting
+ self.always_occluded = data.DOCK_ALWAYS_OCCLUDED # Track the always occluded setting
# Set up UI containers
- self.view = Box(
- name="viewport",
- orientation="h"
- if not data.VERTICAL
- and not data.VERTICAL_RIGHT_ALIGN
- and not data.BOTTOM_BAR
- else "v",
- spacing=4,
- )
+ self.view = Box(name="viewport", orientation="h", spacing=4)
self.wrapper = Box(name="dock", orientation="v", children=[self.view])
# Main dock container with hover handling
@@ -102,146 +114,61 @@ class Dock(Window):
self.dock_eventbox.connect("enter-notify-event", self._on_dock_enter)
self.dock_eventbox.connect("leave-notify-event", self._on_dock_leave)
- # Helper to create corner boxes
- def create_corner_box(name, orientation, align, corner_position):
- return Box(
- name=name,
- orientation=orientation,
- **align,
- children=[
- Box(
- **(
- {"v_expand": True, "v_align": "fill"}
- if orientation == "v"
- else {"h_expand": True, "h_align": "fill"}
- )
- ),
- MyCorner(corner_position),
- ],
- )
+ # Create corners in the orientation-appropriate direction
+
+ self.corner_left = Box(
+ name="dock-corner-left",
+ orientation="v",
+ h_align="start",
+ children=[
+ Box(v_expand=True, v_align="fill"),
+ MyCorner("bottom-right"),
+ ]
+ )
+ self.corner_right = Box(
+ name="dock-corner-right",
+ orientation="v",
+ h_align="end",
+ children=[
+ Box(v_expand=True, v_align="fill"),
+ MyCorner("bottom-left"),
+ ]
+ )
+ # Create a horizontal box that contains the corners and the dock
+ self.dock_full = Box(
+ name="dock-full",
+ orientation="h",
+ h_expand=True,
+ h_align="fill",
+ children=[
+ self.corner_left,
+ self.dock_eventbox,
+ self.corner_right,
+ ]
+ )
+
- # Determine dock layout based on orientation
- if not data.VERTICAL and not data.BOTTOM_BAR:
- # Horizontal layout: corners at the bottom
- self.corner_left = create_corner_box(
- name="dock-corner-left",
- orientation="v",
- align={"h_align": "start"},
- corner_position="bottom-right",
- )
-
- self.corner_right = create_corner_box(
- name="dock-corner-right",
- orientation="v",
- align={"h_align": "end"},
- corner_position="bottom-left",
- )
-
- self.dock_full = Box(
- name="dock-full",
- orientation="h",
- h_expand=True,
- h_align="fill",
- children=[
- self.corner_left,
- self.dock_eventbox,
- self.corner_right,
- ],
- )
-
- elif data.VERTICAL_RIGHT_ALIGN:
- # Vertical right-aligned layout: corners at top and bottom (left-angled corners)
- self.corner_top = create_corner_box(
- name="dock-corner-top",
- orientation="h",
- align={"v_align": "start"},
- corner_position="bottom-left",
- )
-
- self.corner_bottom = create_corner_box(
- name="dock-corner-bottom",
- orientation="h",
- align={"v_align": "end"},
- corner_position="top-left",
- )
-
- self.dock_full = Box(
- name="dock-full",
- orientation="v",
- v_expand=True,
- v_align="fill",
- children=[
- self.corner_top,
- self.dock_eventbox,
- self.corner_bottom,
- ],
- )
-
- else:
- # Vertical left/default alignment: corners at top and bottom (right-angled corners)
- self.corner_top = create_corner_box(
- name="dock-corner-top",
- orientation="h",
- align={"v_align": "start"},
- corner_position="bottom-right",
- )
-
- self.corner_bottom = create_corner_box(
- name="dock-corner-bottom",
- orientation="h",
- align={"v_align": "end"},
- corner_position="top-right",
- )
-
- self.dock_full = Box(
- name="dock-full",
- orientation="v",
- v_expand=True,
- v_align="fill",
- children=[
- self.corner_top,
- self.dock_eventbox,
- self.corner_bottom,
- ],
- )
# Bottom/side hover activation area
self.hover_activator = EventBox()
- self.hover_activator.set_size_request(
- -1
- if not data.VERTICAL
- and not data.VERTICAL_RIGHT_ALIGN
- and not data.BOTTOM_BAR
- else 1,
- 1
- if not data.VERTICAL
- and not data.VERTICAL_RIGHT_ALIGN
- and not data.BOTTOM_BAR
- else -1,
- )
+ self.hover_activator.set_size_request(-1, 1)
self.hover_activator.connect("enter-notify-event", self._on_hover_enter)
self.hover_activator.connect("leave-notify-event", self._on_hover_leave)
# Update main_box to include the dock_full and activator
- self.main_box = Box(
- orientation="v"
- if not data.VERTICAL
- and not data.VERTICAL_RIGHT_ALIGN
- and not data.BOTTOM_BAR
- else "h",
- children=[self.dock_full, self.hover_activator],
- )
+ self.main_box = Box(orientation="v",
+ children=[self.dock_full, self.hover_activator])
self.add(self.main_box)
# Drag-and-drop setup for the viewport
self.view.drag_source_set(
Gdk.ModifierType.BUTTON1_MASK,
[Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)],
- Gdk.DragAction.MOVE,
+ Gdk.DragAction.MOVE
)
self.view.drag_dest_set(
Gtk.DestDefaults.ALL,
[Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)],
- Gdk.DragAction.MOVE,
+ Gdk.DragAction.MOVE
)
self.view.connect("drag-data-get", self.on_drag_data_get)
self.view.connect("drag-data-received", self.on_drag_data_received)
@@ -259,34 +186,20 @@ class Dock(Window):
for ev in ("activewindow", "openwindow", "closewindow", "changefloatingmode"):
self.conn.connect(f"event::{ev}", self.update_dock)
self.conn.connect("event::workspace", self.check_hide)
-
- if data.VERTICAL:
- self.wrapper.add_style_class("vertical")
- else:
- self.wrapper.remove_style_class("vertical")
-
- if data.BOTTOM_BAR:
- self.wrapper.add_style_class("vertical-right")
- else:
- self.wrapper.remove_style_class("vertical-right")
-
- if data.VERTICAL_RIGHT_ALIGN:
- self.wrapper.add_style_class("vertical-left")
- else:
- self.wrapper.remove_style_class("vertical-left")
+
GLib.timeout_add(250, self.check_occlusion_state)
- # Monitor modus.json for changes
+ # Monitor dock.json for changes
GLib.timeout_add_seconds(1, self.check_config_change)
-
+
# Check if dock should be visible based on configuration
if not data.DOCK_ENABLED:
self.set_visible(False)
-
+
# Apply always occluded state if enabled in settings
if self.always_occluded:
self.dock_full.add_style_class("occluded")
-
+
def _build_app_identifiers_map(self):
"""Build a mapping of app identifiers (class names, executables, names) to DesktopApp objects"""
identifiers = {}
@@ -294,55 +207,55 @@ class Dock(Window):
# Map by name (lowercase)
if app.name:
identifiers[app.name.lower()] = app
-
+
# Map by display name
if app.display_name:
identifiers[app.display_name.lower()] = app
-
+
# Map by window class if available
if app.window_class:
identifiers[app.window_class.lower()] = app
-
+
# Map by executable name if available
if app.executable:
- exe_basename = app.executable.split("/")[-1].lower()
+ exe_basename = app.executable.split('/')[-1].lower()
identifiers[exe_basename] = app
-
+
# Map by command line if available (without parameters)
if app.command_line:
- cmd_base = app.command_line.split()[0].split("/")[-1].lower()
+ cmd_base = app.command_line.split()[0].split('/')[-1].lower()
identifiers[cmd_base] = app
-
+
return identifiers
def _normalize_window_class(self, class_name):
"""Normalize window class by removing common suffixes and lowercase."""
if not class_name:
return ""
-
+
normalized = class_name.lower()
-
+
# Remove common suffixes
suffixes = [".bin", ".exe", ".so", "-bin", "-gtk"]
for suffix in suffixes:
if normalized.endswith(suffix):
- normalized = normalized[: -len(suffix)]
-
+ normalized = normalized[:-len(suffix)]
+
return normalized
-
+
def _classes_match(self, class1, class2):
"""Check if two window class names match with stricter comparison."""
if not class1 or not class2:
return False
-
+
# Normalize both classes
norm1 = self._normalize_window_class(class1)
norm2 = self._normalize_window_class(class2)
-
+
# Direct match after normalization
if norm1 == norm2:
return True
-
+
# Don't do substring matching as it's too error-prone
# This avoids incorrectly matching flatpak apps and others
return False
@@ -381,61 +294,44 @@ class Dock(Window):
self.is_hovered = False
self.delay_hide()
# Immediate occlusion check on true leave using simplified format
- occlusion_region = (
- ("bottom", OCCLUSION)
- if not data.VERTICAL
- and not data.VERTICAL_RIGHT_ALIGN
- and not data.BOTTOM_BAR
- else ("left", OCCLUSION)
- if data.VERTICAL_RIGHT_ALIGN
- else ("right", OCCLUSION)
- )
+ occlusion_region = ("bottom", OCCLUSION)
# Add occlusion style if not dragging an icon or if always_occluded is enabled
- if self.always_occluded or (
- not self._drag_in_progress
- and (check_occlusion(occlusion_region) or not self.view.get_children())
- ):
+ if (self.always_occluded or (not self._drag_in_progress and (check_occlusion(occlusion_region) or not self.view.get_children()))):
self.dock_full.add_style_class("occluded")
return True
# Enhanced app lookup methods
def find_app(self, app_identifier):
"""Return the DesktopApp object by matching any app identifier.
-
+
If app_identifier is a dict, it will use all available keys for matching.
"""
if not app_identifier:
return None
-
+
# If we got a dict object with app data (new format)
if isinstance(app_identifier, dict):
# Try to find by all available identifiers in priority order
- for key in [
- "window_class",
- "executable",
- "command_line",
- "name",
- "display_name",
- ]:
+ for key in ["window_class", "executable", "command_line", "name", "display_name"]:
if key in app_identifier and app_identifier[key]:
app = self.find_app_by_key(app_identifier[key])
if app:
return app
return None
-
+
# Simple string identifier (backward compatibility)
return self.find_app_by_key(app_identifier)
-
+
def find_app_by_key(self, key_value):
"""Find app by a single identifier value"""
if not key_value:
return None
-
+
# Try direct lookup in our identifiers map
normalized_id = str(key_value).lower()
if normalized_id in self.app_identifiers:
return self.app_identifiers[normalized_id]
-
+
# Try fuzzy matching - find apps where the identifier is part of their names
for app in self._all_apps:
if app.name and normalized_id in app.name.lower():
@@ -448,54 +344,40 @@ class Dock(Window):
return app
if app.command_line and normalized_id in app.command_line.lower():
return app
-
+
return None
# Update the dock's app map using DesktopApp objects from the system.
def update_app_map(self):
"""Updates the mapping of commands to DesktopApp objects."""
- self._all_apps = get_desktop_applications() # Refresh all apps
- self.app_map = {
- app.name: app for app in self._all_apps if app.name
- } # Map app names to DesktopApp objects
- self.app_identifiers = (
- self._build_app_identifiers_map()
- ) # Rebuild identifiers map
+ self._all_apps = get_desktop_applications() # Refresh all apps
+ self.app_map = {app.name: app for app in self._all_apps if app.name} # Map app names to DesktopApp objects
+ self.app_identifiers = self._build_app_identifiers_map() # Rebuild identifiers map
def create_button(self, app_identifier, instances):
"""Create dock application button"""
- desktop_app = self.find_app(app_identifier) # Find app by identifier
+ desktop_app = self.find_app(app_identifier) # Find app by identifier
icon_img = None
display_name = None
-
+
if desktop_app:
icon_img = desktop_app.get_icon_pixbuf(size=data.DOCK_ICON_SIZE)
display_name = desktop_app.display_name or desktop_app.name
-
+
# Extract identifier for fallback
- id_value = (
- app_identifier["name"]
- if isinstance(app_identifier, dict)
- else app_identifier
- )
-
+ id_value = app_identifier["name"] if isinstance(app_identifier, dict) else app_identifier
+
if not icon_img:
# Fallback to IconResolver with the app command
- icon_img = self.icon.get_icon_pixbuf(
- id_value, data.DOCK_ICON_SIZE
- ) # Use identifier for fallback
-
- if not icon_img: # Double check after exec path try
+ icon_img = self.icon.get_icon_pixbuf(id_value, data.DOCK_ICON_SIZE) # Use identifier for fallback
+
+ if not icon_img: # Double check after exec path try
# Fallback icon if no DesktopApp is found
- icon_img = self.icon.get_icon_pixbuf(
- "application-x-executable-symbolic", data.DOCK_ICON_SIZE
- )
+ icon_img = self.icon.get_icon_pixbuf("application-x-executable-symbolic", data.DOCK_ICON_SIZE)
# Final fallback
if not icon_img:
- icon_img = self.icon.get_icon_pixbuf(
- "image-missing", data.DOCK_ICON_SIZE
- )
-
+ icon_img = self.icon.get_icon_pixbuf("image-missing", data.DOCK_ICON_SIZE)
+
items = [Image(pixbuf=icon_img)]
tooltip = display_name or (id_value if isinstance(id_value, str) else "Unknown")
@@ -503,15 +385,13 @@ class Dock(Window):
tooltip = instances[0]["title"]
button = Button(
- child=Box(
+ child= Box(
name="dock-icon",
orientation="v",
h_align="center",
children=items,
),
- on_clicked=lambda *a: self.handle_app(
- app_identifier, instances, desktop_app
- ), # Pass desktop_app as well
+ on_clicked=lambda *a: self.handle_app(app_identifier, instances, desktop_app), # Pass desktop_app as well
tooltip_text=tooltip,
name="dock-app-button",
)
@@ -522,22 +402,22 @@ class Dock(Window):
button.instances = instances
if instances:
- button.add_style_class("instance") # Style running apps
+ button.add_style_class("instance") # Style running apps
# Enable DnD for ALL apps
button.drag_source_set(
Gdk.ModifierType.BUTTON1_MASK,
[Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)],
- Gdk.DragAction.MOVE,
+ Gdk.DragAction.MOVE
)
button.connect("drag-begin", self.on_drag_begin)
button.connect("drag-end", self.on_drag_end) # Connect drag-end to ALL buttons
-
+
# Make all buttons drop targets for better reordering
button.drag_dest_set(
Gtk.DestDefaults.ALL,
[Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)],
- Gdk.DragAction.MOVE,
+ Gdk.DragAction.MOVE
)
button.connect("drag-data-get", self.on_drag_data_get)
button.connect("drag-data-received", self.on_drag_data_received)
@@ -552,11 +432,11 @@ class Dock(Window):
# Try to launch the app with multiple fallback methods
if not desktop_app:
desktop_app = self.find_app(app_identifier)
-
+
if desktop_app:
# Try the standard launch method first
launch_success = desktop_app.launch()
-
+
# If that fails, try command line or executable
if not launch_success:
if desktop_app.command_line:
@@ -567,19 +447,10 @@ class Dock(Window):
# No desktop entry found, try direct execution
if isinstance(app_identifier, dict):
# Try command_line first, then executable, then name as last resort
- if (
- "command_line" in app_identifier
- and app_identifier["command_line"]
- ):
- exec_shell_command_async(
- f"nohup {app_identifier['command_line']}"
- )
- elif (
- "executable" in app_identifier and app_identifier["executable"]
- ):
- exec_shell_command_async(
- f"nohup {app_identifier['executable']}"
- )
+ if "command_line" in app_identifier and app_identifier["command_line"]:
+ exec_shell_command_async(f"nohup {app_identifier['command_line']}")
+ elif "executable" in app_identifier and app_identifier["executable"]:
+ exec_shell_command_async(f"nohup {app_identifier['executable']}")
elif "name" in app_identifier and app_identifier["name"]:
exec_shell_command_async(f"nohup {app_identifier['name']}")
elif isinstance(app_identifier, str):
@@ -653,96 +524,84 @@ class Dock(Window):
if arranger_handler:
remove_handler(arranger_handler)
clients = self.get_clients()
-
+
# Create a mapping of window class to instances
running_windows = {}
for c in clients:
# Try multiple identification methods in order of reliability
window_id = None
-
+
# Try initialClass first (most reliable)
if class_name := c.get("initialClass", "").lower():
window_id = class_name
-
+
# Try class second (fallback)
elif class_name := c.get("class", "").lower():
window_id = class_name
-
+
# Use title as last resort if both class identifiers are missing
elif title := c.get("title", "").lower():
# Extract app name from title (common format: "App Name - Document")
possible_name = title.split(" - ")[0].strip()
- if (
- possible_name and len(possible_name) > 1
- ): # Avoid single letter app names
+ if possible_name and len(potential_name) > 1: # Avoid single letter app names
window_id = possible_name
else:
window_id = title # Use full title if we can't extract a good name
-
+
# Use generic identifier as absolute fallback
if not window_id:
window_id = "unknown-app"
-
+
# Log window for debugging purposes
- logging.debug(
- f"Window detected: {window_id} (from {c.get('initialClass', '')}/{c.get('class', '')}/{c.get('title', '')})"
- )
-
+ logging.debug(f"Window detected: {window_id} (from {c.get('initialClass', '')}/{c.get('class', '')}/{c.get('title', '')})")
+
# Add to running windows - store with both original and normalized keys
running_windows.setdefault(window_id, []).append(c)
-
+
# Also store with normalized key for more flexible matching
normalized_id = self._normalize_window_class(window_id)
if normalized_id != window_id:
- running_windows.setdefault(normalized_id, []).extend(
- running_windows[window_id]
- )
-
+ running_windows.setdefault(normalized_id, []).extend(running_windows[window_id])
+
# Map pinned apps to their running instances
pinned_buttons = []
used_window_classes = set() # Track which window classes we've already assigned
-
+
for app_data in self.pinned:
app = self.find_app(app_data)
-
+
# Try to find running instances for this pinned app
instances = []
matched_class = None
-
+
# Extract all possible identifiers from app_data for matching
possible_identifiers = []
-
+
if isinstance(app_data, dict):
- for key in [
- "window_class",
- "executable",
- "command_line",
- "name",
- "display_name",
- ]:
+ for key in ["window_class", "executable", "command_line", "name", "display_name"]:
if key in app_data and app_data[key]:
possible_identifiers.append(app_data[key].lower())
elif isinstance(app_data, str):
possible_identifiers.append(app_data.lower())
-
+
# Add identifiers from DesktopApp if available
if app:
- if app.window_class:
+ if app.window_class:
possible_identifiers.append(app.window_class.lower())
if app.executable:
- possible_identifiers.append(app.executable.split("/")[-1].lower())
+ possible_identifiers.append(app.executable.split('/')[-1].lower())
if app.command_line:
cmd_parts = app.command_line.split()
if cmd_parts:
- possible_identifiers.append(cmd_parts[0].split("/")[-1].lower())
+ possible_identifiers.append(cmd_parts[0].split('/')[-1].lower())
if app.name:
possible_identifiers.append(app.name.lower())
if app.display_name:
possible_identifiers.append(app.display_name.lower())
-
+
# Remove duplicates
possible_identifiers = list(set(possible_identifiers))
-
+
# Try each identifier for matching
for identifier in possible_identifiers:
# Try direct match
@@ -750,14 +609,14 @@ class Dock(Window):
instances = running_windows[identifier]
matched_class = identifier
break
-
+
# Try normalized version
normalized = self._normalize_window_class(identifier)
if normalized in running_windows:
instances = running_windows[normalized]
matched_class = normalized
break
-
+
# Try substring matching for window classes (less reliable but helpful)
for window_class in running_windows:
# For substring matching, ensure the identifier is at least 3 chars
@@ -765,54 +624,50 @@ class Dock(Window):
if len(identifier) >= 3 and identifier in window_class:
instances = running_windows[window_class]
matched_class = window_class
- logging.debug(
- f"Substring match: {identifier} in {window_class}"
- )
+ logging.debug(f"Substring match: {identifier} in {window_class}")
break
-
+
if matched_class:
break
-
+
# Mark the matched class as used
if matched_class:
used_window_classes.add(matched_class)
# Also mark the normalized version as used
used_window_classes.add(self._normalize_window_class(matched_class))
- logging.debug(
- f"Matched pinned app {app_data} to running instances via {matched_class}"
- )
-
+ logging.debug(f"Matched pinned app {app_data} to running instances via {matched_class}")
+
# Create button for this pinned app with any found instances
pinned_buttons.append(self.create_button(app_data, instances))
-
+
# For any remaining window classes that aren't assigned to pinned apps
open_buttons = []
for class_name, instances in running_windows.items():
if class_name not in used_window_classes:
# Enhanced app identification for running windows
app = None
-
+
# Try multiple methods to find the correct app
# 1. Direct lookup by class name
app = self.app_identifiers.get(class_name)
-
+
# 2. Try with normalized class name
if not app:
norm_class = self._normalize_window_class(class_name)
app = self.app_identifiers.get(norm_class)
-
+
# 3. Try with our more robust find_app method
if not app:
app = self.find_app_by_key(class_name)
-
+
# 4. Try using window title which often contains app name
if not app and instances and instances[0].get("title"):
title = instances[0].get("title", "")
# Extract potential app name from title (common format: "App Name - Document")
- possible_name = title.split(" - ")[0].strip()
- if len(possible_name) > 2: # Avoid very short names
- app = self.find_app_by_key(possible_name)
-
+ potential_name = title.split(" - ")[0].strip()
+ if len(potential_name) > 2: # Avoid very short names
+ app = self.find_app_by_key(potential_name)
+
# Create comprehensive app data if app was found
if app:
app_data = {
@@ -820,37 +675,22 @@ class Dock(Window):
"display_name": app.display_name,
"window_class": app.window_class,
"executable": app.executable,
- "command_line": app.command_line,
+ "command_line": app.command_line
}
identifier = app_data
else:
# Fallback to just class name
identifier = class_name
-
+
open_buttons.append(self.create_button(identifier, instances))
# Assemble dock layout
children = pinned_buttons
# Only add separator if both pinned and open buttons exist
if pinned_buttons and open_buttons:
- children += [
- Box(
- orientation="v"
- if not data.VERTICAL
- and not data.VERTICAL_RIGHT_ALIGN
- and not data.BOTTOM_BAR
- else "h",
- v_expand=not data.VERTICAL
- and not data.VERTICAL_RIGHT_ALIGN
- and not data.BOTTOM_BAR,
- h_expand=data.VERTICAL
- and data.VERTICAL_RIGHT_ALIGN
- and data.BOTTOM_BAR,
- name="dock-separator",
- )
- ]
+ children += [Box(orientation="v", v_expand=True, name="dock-separator")]
children += open_buttons
-
+
self.view.children = children
idle_add(self._update_size)
self._drag_in_progress = False # Clear the drag lock
@@ -892,21 +732,13 @@ class Dock(Window):
if self.is_hovered or self._drag_in_progress:
self.dock_full.remove_style_class("occluded")
return True
-
+
# If always_occluded is enabled, keep the dock occluded
if self.always_occluded:
self.dock_full.add_style_class("occluded")
return True
-
- occlusion_region = (
- ("bottom", OCCLUSION)
- if not data.VERTICAL
- and not data.VERTICAL_RIGHT_ALIGN
- and not data.BOTTOM_BAR
- else ("left", OCCLUSION)
- if data.VERTICAL_RIGHT_ALIGN
- else ("right", OCCLUSION)
- )
+
+ occlusion_region = ("bottom", OCCLUSION)
if check_occlusion(occlusion_region) or not self.view.get_children():
self.dock_full.add_style_class("occluded")
else:
@@ -922,18 +754,14 @@ class Dock(Window):
def on_drag_data_get(self, widget, drag_context, data, info, time):
"""Handle drag start"""
- target = self._find_drag_target(
- widget.get_parent() if isinstance(widget, Box) else widget
- )
+ target = self._find_drag_target(widget.get_parent() if isinstance(widget, Box) else widget)
if target is not None:
index = self.view.get_children().index(target)
data.set_text(str(index), -1)
def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
"""Handle drop event"""
- target = self._find_drag_target(
- widget.get_parent() if isinstance(widget, Box) else widget
- )
+ target = self._find_drag_target(widget.get_parent() if isinstance(widget, Box) else widget)
if target is None:
return
try:
@@ -954,21 +782,20 @@ class Dock(Window):
if child.get_name() == "dock-separator":
separator_index = i
break
-
+
# Detect if we're dragging across the separator (between pinned/unpinned)
- cross_section_drag = separator_index != -1 and (
- (source_index < separator_index and target_index > separator_index)
- or (source_index > separator_index and target_index < separator_index)
- )
-
+ cross_section_drag = (separator_index != -1 and
+ ((source_index < separator_index and target_index > separator_index) or
+ (source_index > separator_index and target_index < separator_index)))
+
# Move the item in the children list
child = children.pop(source_index)
children.insert(target_index, child)
self.view.children = children # Update view immediately
-
+
# Update pinned apps configuration
self.update_pinned_apps(skip_update=not cross_section_drag)
-
+
# Force immediate update if we dragged across sections to update separator visibility
if cross_section_drag:
GLib.idle_add(self.update_dock)
@@ -988,13 +815,11 @@ class Dock(Window):
window = self.get_window()
if window: # Check if we got a window
win_x, win_y, width, height = window.get_geometry()
- if not (
- win_x <= x <= win_x + width and win_y <= y <= win_y + height
- ):
+ if not (win_x <= x <= win_x + width and win_y <= y <= win_y + height):
# Drag ended outside the dock - get app data from the button
app_id = widget.app_identifier
instances = widget.instances
-
+
# Find this app in pinned list
app_index = -1
for i, pinned_app in enumerate(self.pinned):
@@ -1003,26 +828,22 @@ class Dock(Window):
app_index = i
break
# For dict format, match by name
- elif isinstance(pinned_app, dict) and isinstance(
- app_id, dict
- ):
+ elif isinstance(pinned_app, dict) and isinstance(app_id, dict):
if pinned_app.get("name") == app_id.get("name"):
app_index = i
break
-
+
# If it's a pinned app, remove from pinned
if app_index >= 0:
self.pinned.pop(app_index)
self.config["pinned_apps"] = self.pinned
self.update_pinned_apps_file()
self.update_dock() # Update dock after file save
- elif instances: # Close if running and unpinned
+ elif instances: # Close if running and unpinned
# Close running app (if not pinned)
address = instances[0].get("address")
if address:
- exec_shell_command(
- f"hyprctl dispatch closewindow address:{address}"
- )
+ exec_shell_command(f"hyprctl dispatch closewindow address:{address}")
self.update_dock() # Update dock after closing window
self._drag_in_progress = False # Clear the drag lock
@@ -1030,7 +851,7 @@ class Dock(Window):
def check_config_change(self):
"""Check if the config file has been modified."""
- new_config = data.read_config()
+ new_config = read_config()
if new_config.get("pinned_apps", []) != self.config.get("pinned_apps", []):
self.config = new_config
self.pinned = self.config.get("pinned_apps", [])
@@ -1039,8 +860,8 @@ class Dock(Window):
return True # Continue the timeout
def update_pinned_apps_file(self):
- """Writes the updated pinned apps list to modus.json"""
- config_path = get_relative_path("../config/assets/modus.json")
+ """Writes the updated pinned apps list to dock.json"""
+ config_path = get_relative_path("../config/dock.json")
try:
with open(config_path, "w") as file:
json.dump(self.config, file, indent=4)
@@ -1055,7 +876,7 @@ class Dock(Window):
for child in self.view.get_children():
if child.get_name() == "dock-separator":
break # Stop at separator
-
+
# Use stored app identifier from the button
if hasattr(child, "app_identifier"):
# If the button has a desktop_app, use it to create comprehensive data
@@ -1066,7 +887,7 @@ class Dock(Window):
"display_name": app.display_name,
"window_class": app.window_class,
"executable": app.executable,
- "command_line": app.command_line,
+ "command_line": app.command_line
}
pinned_children.append(app_data)
else:
@@ -1079,7 +900,7 @@ class Dock(Window):
# Write to file first to ensure persistence
file_updated = self.update_pinned_apps_file()
-
+
# Only update the dock if needed and file was successfully written
if file_updated and not skip_update:
self.update_dock() # Refresh dock only after file is saved
@@ -1091,33 +912,27 @@ class Dock(Window):
for dock in Dock._instances:
# Use idle_add to avoid potential issues with direct calls
GLib.idle_add(dock.check_config_change_immediate)
-
+
def check_config_change_immediate(self):
"""Immediately check for configuration changes and update if needed."""
- new_config = data.read_config()
-
+ new_config = read_config()
+
# Check if always_occluded setting has changed
previous_always_occluded = self.always_occluded
self.always_occluded = data.DOCK_ALWAYS_OCCLUDED
-
+
# If always_occluded changed, update the dock appearance
if previous_always_occluded != self.always_occluded:
if self.always_occluded and not self.is_hovered:
self.dock_full.add_style_class("occluded")
elif not self.always_occluded and not self.is_hovered:
# Perform a normal occlusion check
- occlusion_region = (
- ("bottom", OCCLUSION)
- if not data.VERTICAL and not data.BOTTOM_BAR
- else ("left", OCCLUSION)
- if data.VERTICAL_RIGHT_ALIGN
- else ("right", OCCLUSION)
- )
+ occlusion_region = ("bottom", OCCLUSION)
if check_occlusion(occlusion_region) or not self.view.get_children():
self.dock_full.add_style_class("occluded")
else:
self.dock_full.remove_style_class("occluded")
-
+
if new_config.get("pinned_apps", []) != self.config.get("pinned_apps", []):
self.config = new_config
self.pinned = self.config.get("pinned_apps", [])
@@ -1130,4 +945,4 @@ class Dock(Window):
def update_visibility(visible):
"""Update visibility for all dock instances."""
for dock in Dock._instances:
- dock.set_visible(visible)
+ dock.set_visible(visible)
\ No newline at end of file
diff --git a/modules/launcher/components/__init__.py b/modules/launcher/components/__init__.py
deleted file mode 100644
index da2ef07..0000000
--- a/modules/launcher/components/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from .applauncher import *
-from .bluetooth import *
-from .cliphist import *
-from .dashboard import *
-from .emoji import *
-from .power import *
-from .sh import *
-from .wallpapers import *
-from .wifi import *
-from .calendar import *
-from .dashboard import *
-from .tools import *
-from .kanban import *
-from .tmux import *
-from .window_switcher import *
-from .notification_center import *
diff --git a/modules/launcher/components/applauncher.py b/modules/launcher/components/applauncher.py
deleted file mode 100644
index 83380de..0000000
--- a/modules/launcher/components/applauncher.py
+++ /dev/null
@@ -1,630 +0,0 @@
-import operator
-from collections.abc import Iterator
-from fabric.widgets.box import Box
-from fabric.widgets.label import Label
-from fabric.widgets.button import Button
-from fabric.widgets.entry import Entry
-from fabric.widgets.image import Image
-from fabric.widgets.scrolledwindow import ScrolledWindow
-from fabric.utils import (
- DesktopApp,
- get_desktop_applications,
- idle_add,
- remove_handler,
- exec_shell_command_async,
-)
-from fabric.utils.helpers import get_relative_path
-from gi.repository import GLib, Gdk
-import utils.icons as icons
-import config.data as data
-import json
-import os
-import re
-import math
-import numpy as np
-import subprocess
-from modules.dock import Dock # Import the Dock class
-
-
-class AppLauncher(Box):
- def __init__(self, **kwargs):
- super().__init__(
- name="app-launcher",
- visible=False,
- all_visible=False,
- **kwargs,
- )
-
- self.launcher = kwargs["launcher"]
- self.selected_index = -1 # Track the selected item index
-
- self._arranger_handler: int = 0
- self._all_apps = get_desktop_applications()
- self.config = data.read_config()
-
- # Calculator history initialization
- self.calc_history_path = f"{data.CACHE_DIR}/calc.json"
- if os.path.exists(self.calc_history_path):
- with open(self.calc_history_path, "r") as f:
- self.calc_history = json.load(f)
- else:
- self.calc_history = []
-
- self.viewport = Box(name="viewport", spacing=4, orientation="v")
- self.search_entry = Entry(
- name="search-entry",
- placeholder="Search Applications...",
- h_expand=True,
- notify_text=self.notify_text, # Use the method instead of lambda
- on_activate=lambda entry, *_: self.on_search_entry_activate(
- entry.get_text()
- ),
- on_key_press_event=self.on_search_entry_key_press, # Handle key presses
- )
- self.search_entry.props.xalign = 0.5
- self.scrolled_window = ScrolledWindow(
- name="scrolled-window",
- spacing=10,
- min_content_size=(450, 105),
- max_content_size=(450, 105),
- child=self.viewport,
- )
-
- self.header_box = Box(
- name="header_box",
- spacing=10,
- orientation="h",
- children=[
- Button(
- name="config-button",
- child=Label(name="config-label", markup=icons.config),
- on_clicked=lambda *_: (
- exec_shell_command_async(
- f"python {get_relative_path('../../../config/config.py')}"
- ),
- self.close_launcher(),
- ),
- ),
- self.search_entry,
- ],
- )
-
- self.launcher_box = Box(
- name="launcher-box",
- spacing=10,
- h_expand=True,
- orientation="v",
- children=[
- self.header_box,
- self.scrolled_window,
- ],
- )
-
- self.resize_viewport()
-
- self.add(self.launcher_box)
- self.show_all()
-
- def close_launcher(self):
- self.viewport.children = []
- self.selected_index = -1 # Reset selection
- self.launcher.close()
-
- def open_launcher(self):
- self._all_apps = get_desktop_applications()
- self.arrange_viewport()
-
- def ensure_initialized(self):
- """Make sure the launcher is initialized with apps list before opening"""
- if not hasattr(self, "_initialized"):
- # Force pre-loading apps
- self._all_apps = get_desktop_applications()
- self._initialized = True
- return True # Was initialized for first time
- return False # Was already initialized
-
- def arrange_viewport(self, query: str = ""):
- if query.startswith("="):
- # In calculator mode, update history view once (not per keystroke)
- self.show_scrolled_window()
- self.launcher.dashboard.hide()
- self.update_calculator_viewport()
- return
- remove_handler(self._arranger_handler) if self._arranger_handler else None
- self.viewport.children = []
- self.selected_index = -1 # Clear selection when viewport changes
-
- if query.startswith("/"):
- self.launcher.dashboard.hide()
- return
-
- if query.strip():
- self.launcher.dashboard.hide()
- self.show_scrolled_window()
- else:
- self.launcher.dashboard.show()
- self.hide_scrolled_window()
- return False
-
- filtered_apps_iter = iter(
- sorted(
- [
- app
- for app in self._all_apps
- if query.casefold()
- in (
- (app.display_name or "")
- + (" " + app.name + " ")
- + (app.generic_name or "")
- ).casefold()
- ],
- key=lambda app: (app.display_name or "").casefold(),
- )
- )
- should_resize = operator.length_hint(filtered_apps_iter) == len(self._all_apps)
-
- self._arranger_handler = idle_add(
- lambda apps_iter: self.add_next_application(apps_iter)
- or self.handle_arrange_complete(should_resize, query),
- filtered_apps_iter,
- pin=True,
- )
-
- def handle_arrange_complete(self, should_resize, query):
- if should_resize:
- self.resize_viewport()
- # Only auto-select first item if query exists
- if query.strip() != "" and self.viewport.get_children():
- self.update_selection(0)
- return False
-
- def add_next_application(self, apps_iter: Iterator[DesktopApp]):
- if not (app := next(apps_iter, None)):
- return False
- self.viewport.add(self.bake_application_slot(app))
- return True
-
- def show_scrolled_window(self):
- if hasattr(self, "scrolled_window") and self.scrolled_window:
- self.scrolled_window.show_all()
-
- def hide_scrolled_window(self):
- if hasattr(self, "scrolled_window") and self.scrolled_window:
- self.scrolled_window.hide()
-
- def resize_viewport(self):
- self.scrolled_window.set_min_content_width(
- self.viewport.get_allocation().width # type: ignore
- )
- return False
-
- def bake_application_slot(self, app: DesktopApp, **kwargs) -> Button:
- button = Button(
- name="slot-button",
- child=Box(
- name="slot-box",
- orientation="h",
- spacing=10,
- children=[
- Image(
- name="app-icon",
- pixbuf=app.get_icon_pixbuf(size=24),
- h_align="start",
- ),
- Label(
- name="app-label",
- label=app.display_name or "Unknown",
- ellipsization="end",
- v_align="center",
- h_align="center",
- ),
- Label(
- name="app-desc",
- label=app.description or "",
- ellipsization="end",
- v_align="center",
- h_align="start",
- h_expand=True,
- ),
- ],
- ),
- tooltip_text=app.description,
- on_clicked=lambda *_: (app.launch(), self.close_launcher()),
- **kwargs,
- )
- return button
-
- def update_selection(self, new_index: int):
- # Unselect current
- if self.selected_index != -1 and self.selected_index < len(
- self.viewport.get_children()
- ):
- current_button = self.viewport.get_children()[self.selected_index]
- current_button.get_style_context().remove_class("selected")
- # Select new
- if new_index != -1 and new_index < len(self.viewport.get_children()):
- new_button = self.viewport.get_children()[new_index]
- new_button.get_style_context().add_class("selected")
- self.selected_index = new_index
- self.scroll_to_selected(new_button)
- else:
- self.selected_index = -1
-
- def scroll_to_selected(self, button):
- def scroll():
- adj = self.scrolled_window.get_vadjustment()
- alloc = button.get_allocation()
- if alloc.height == 0:
- return False # Retry if allocation isn't ready
-
- y = alloc.y
- height = alloc.height
- page_size = adj.get_page_size()
- current_value = adj.get_value()
-
- # Calculate visible boundaries
- visible_top = current_value
- visible_bottom = current_value + page_size
-
- if y < visible_top:
- # Item above viewport - align to top
- adj.set_value(y)
- elif y + height > visible_bottom:
- # Item below viewport - align to bottom
- new_value = y + height - page_size
- adj.set_value(new_value)
- # No action if already fully visible
- return False
-
- GLib.idle_add(scroll)
-
- def on_search_entry_activate(self, text):
- # This method is now only used for non-Enter key activations
- commands = self.config.get("commands", {})
-
- # Check if text is a mathematical expression
- if text and (text[0].isdigit()):
- self.evaluate_calculator_expression(text)
- return
-
- if text in commands:
- command = commands[text]
- eval(command, {"launcher": self.launcher})
- else:
- children = self.viewport.get_children()
- if children:
- # Only activate if we have selection or non-empty query
- if text.strip() == "" and self.selected_index == -1:
- return # Prevent accidental activation when empty
- selected_index = self.selected_index if self.selected_index != -1 else 0
- if 0 <= selected_index < len(children):
- children[selected_index].clicked()
-
- def on_search_entry_key_press(self, widget, event):
- text = widget.get_text()
-
- # Check if we're in calculator mode
- if text.startswith("="):
- if event.keyval == Gdk.KEY_Down:
- self.move_selection(1)
- return True
- elif event.keyval == Gdk.KEY_Up:
- self.move_selection(-1)
- return True
- elif event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
- # In calculator mode, if a history item is highlighted:
- if self.selected_index != -1 and self.selected_index < len(
- self.calc_history
- ):
- if event.state & Gdk.ModifierType.SHIFT_MASK:
- # Shift+Enter deletes the selected calculator history item
- self.delete_selected_calc_history()
- else:
- # Normal Enter copies the result
- selected_text = self.calc_history[self.selected_index]
- self.copy_text_to_clipboard(selected_text)
- # Clear selection so new expressions are evaluated on further Return presses
- self.selected_index = -1
- else:
- # Force reset selection index
- self.selected_index = -1
- # No item selected, evaluate the expression
- self.evaluate_calculator_expression(text)
- return True
- elif event.keyval == Gdk.KEY_Escape:
- self.close_launcher()
- return True
- return False
- else:
- # Normal app mode behavior
- if event.keyval == Gdk.KEY_Down:
- self.move_selection(1)
- return True
- elif event.keyval == Gdk.KEY_Up:
- self.move_selection(-1)
- return True
- elif event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter) and (
- event.state & Gdk.ModifierType.SHIFT_MASK
- ):
- # Only add to dock when in app mode (not calculator mode)
- self.add_selected_app_to_dock()
- return True
- elif event.keyval == Gdk.KEY_Escape:
- self.close_launcher()
- return True
- return False
-
- def notify_text(self, entry, *_):
- """Handle text changes in the search entry"""
- text = entry.get_text()
- if text.startswith("="):
- # In calculator mode, show scrolled window and update viewport
- self.show_scrolled_window()
- self.launcher.dashboard.hide()
- self.update_calculator_viewport()
- # Always reset selection when typing a new expression
- self.selected_index = -1
- else:
- self.arrange_viewport(text)
-
- def add_selected_app_to_dock(self):
- """Adds the currently selected application to the modus.json file with comprehensive metadata."""
- children = self.viewport.get_children()
- if (
- not children
- or self.selected_index == -1
- or self.selected_index >= len(children)
- ):
- return # No app selected
-
- selected_button = children[self.selected_index]
- # Extract the app's display name from the label to find the DesktopApp object
- selected_app = next(
- (
- app
- for app in self._all_apps
- if app.display_name
- == selected_button.get_child().get_children()[1].props.label
- ),
- None,
- )
- if not selected_app:
- return
-
- # Create comprehensive app data dictionary - Include all available properties
- # Filter out None values to keep the JSON clean
- app_data = {
- k: v
- for k, v in {
- "name": selected_app.name,
- "display_name": selected_app.display_name,
- "window_class": selected_app.window_class,
- "executable": selected_app.executable,
- "command_line": selected_app.command_line,
- "icon_name": selected_app.icon_name,
- }.items()
- if v is not None
- }
-
- config_path = get_relative_path("../../../config/assets/modus.json")
- try:
- with open(config_path, "r") as file:
- data = json.load(file)
- except (FileNotFoundError, json.JSONDecodeError):
- data = {"pinned_apps": []}
-
- # Check if app is already pinned (by name)
- already_pinned = False
- for pinned_app in data.get("pinned_apps", []):
- if (
- isinstance(pinned_app, dict)
- and pinned_app.get("name") == app_data["name"]
- ):
- already_pinned = True
- # Update existing entry with latest app data
- pinned_app.update(app_data)
- break
- elif isinstance(pinned_app, str) and pinned_app == app_data["name"]:
- already_pinned = True
- # Replace string format with new comprehensive format
- data["pinned_apps"].remove(pinned_app)
- data["pinned_apps"].append(app_data)
- break
-
- # Add to pinned apps if not already there
- if not already_pinned:
- data.setdefault("pinned_apps", []).append(app_data)
-
- # Write the updated config
- with open(config_path, "w") as file:
- json.dump(data, file, indent=4)
-
- # Notify dock instances of the configuration change immediately
- Dock.notify_config_change()
-
- def move_selection(self, delta: int):
- children = self.viewport.get_children()
- if not children:
- return
- # Allow starting selection from nothing when empty
- if self.selected_index == -1 and delta == 1:
- new_index = 0
- else:
- new_index = self.selected_index + delta
- new_index = max(0, min(new_index, len(children) - 1))
- self.update_selection(new_index)
-
- def save_calc_history(self):
- with open(self.calc_history_path, "w") as f:
- json.dump(self.calc_history, f)
-
- def evaluate_calculator_expression(self, text: str):
- # Add debug print
- print(f"Evaluating calculator expression: {text}")
-
- # Remove the '=' prefix and extra spaces
- expr = text.lstrip("=").strip()
- if not expr:
- return
-
- # Common replacements to make expressions more natural
- replacements = {
- "^": "**", # Power operator
- "×": "*", # Multiplication
- "÷": "/", # Division
- "π": "np.pi", # Pi constant
- "pi": "np.pi", # Pi constant (text)
- "e": "np.e", # Euler's number
- "sin(": "np.sin(", # Sine
- "cos(": "np.cos(", # Cosine
- "tan(": "np.tan(", # Tangent
- "log(": "np.log10(", # Log base 10
- "ln(": "np.log(", # Natural log
- "sqrt(": "np.sqrt(", # Square root
- "abs(": "np.abs(", # Absolute value
- "exp(": "np.exp(", # Exponential
- }
-
- # Apply all replacements
- for old, new in replacements.items():
- expr = expr.replace(old, new)
-
- # Replace factorial: e.g. 5! -> np.factorial(5)
- expr = re.sub(r"(\d+)!", r"np.factorial(\1)", expr)
-
- # Replace brackets: allow [] and {} as ()
- for old, new in [("[", "("), ("]", ")"), ("{", "("), ("}", ")")]:
- expr = expr.replace(old, new)
-
- # Define the safe execution environment
- safe_dict = {
- "np": np,
- "math": math,
- "arange": np.arange,
- "linspace": np.linspace,
- "array": np.array,
- }
-
- try:
- # Evaluate the expression in the safe environment
- result = eval(expr, {"__builtins__": None}, safe_dict)
-
- # Format the result based on its type
- if isinstance(result, np.ndarray):
- if result.size > 10: # Truncate large arrays
- result_str = f"Array of shape {result.shape}"
- else:
- result_str = str(result)
- elif isinstance(result, (int, float, np.number)):
- # Format numbers nicely - integers as integers, floats with limited precision
- if isinstance(result, (int, np.integer)) or result.is_integer():
- result_str = str(int(result))
- else:
- result_str = f"{float(result):.10g}" # Remove trailing zeros
- else:
- result_str = str(result)
-
- except Exception as e:
- result_str = f"Error: {str(e)}"
-
- # Prepend to history (newest first)
- self.calc_history.insert(0, f"{text} => {result_str}")
- self.save_calc_history()
- self.update_calculator_viewport()
-
- def update_calculator_viewport(self):
- self.viewport.children = []
- for item in self.calc_history:
- btn = self.create_calc_history_button(item)
- self.viewport.add(btn)
- # Don't reset selection index here automatically
- # Ensure selection state stays valid
- if self.selected_index >= len(self.calc_history):
- self.selected_index = -1
-
- def create_calc_history_button(self, text: str) -> Button:
- # Parse the result to create a more readable display
- if "=>" in text:
- parts = text.split("=>")
- expression = parts[0].strip()
- result = parts[1].strip()
-
- # For very long results, truncate for display but keep full in tooltip
- display_text = text
- if len(result) > 50: # Truncate long results
- display_text = f"{expression} => {result[:47]}..."
-
- btn = Button(
- name="slot-button", # reuse existing CSS styling
- child=Box(
- name="calc-slot-box",
- orientation="h",
- spacing=10,
- children=[
- Label(
- name="calc-label",
- label=display_text,
- ellipsization="end",
- v_align="center",
- h_align="center",
- ),
- ],
- ),
- tooltip_text=text,
- on_clicked=lambda *_: self.copy_text_to_clipboard(text),
- )
- else:
- # Fallback for non-calculation entries
- btn = Button(
- name="slot-button",
- child=Box(
- name="calc-slot-box",
- orientation="h",
- spacing=10,
- children=[
- Label(
- name="calc-label",
- label=text,
- ellipsization="end",
- v_align="center",
- h_align="center",
- ),
- ],
- ),
- tooltip_text=text,
- on_clicked=lambda *_: self.copy_text_to_clipboard(text),
- )
- return btn
-
- def copy_text_to_clipboard(self, text: str):
- # Split the text on "=>" and copy only the result part if available
- parts = text.split("=>", 1)
- copy_text = parts[1].strip() if len(parts) > 1 else text
- try:
- subprocess.run(["wl-copy"], input=copy_text.encode(), check=True)
- except subprocess.CalledProcessError as e:
- print(f"Clipboard copy failed: {e}")
-
- def delete_selected_calc_history(self):
- if self.selected_index != -1 and self.selected_index < len(self.calc_history):
- # Store the current index before deletion
- current_index = self.selected_index
-
- # Delete the item
- del self.calc_history[current_index]
- self.save_calc_history()
-
- # Determine the new selection index
- # If we deleted the first item, stay at index 0
- # Otherwise, move to the previous item
- new_index = 0 if current_index == 0 else current_index - 1
-
- # Reset selection before updating viewport
- self.selected_index = -1
-
- # Update the viewport
- self.update_calculator_viewport()
-
- # If we still have items, select the determined index
- if len(self.calc_history) > 0:
- self.update_selection(min(new_index, len(self.calc_history) - 1))
diff --git a/modules/launcher/components/bluetooth.py b/modules/launcher/components/bluetooth.py
deleted file mode 100644
index c3f6930..0000000
--- a/modules/launcher/components/bluetooth.py
+++ /dev/null
@@ -1,170 +0,0 @@
-from fabric.widgets.box import Box
-from fabric.widgets.label import Label
-from fabric.widgets.image import Image
-from fabric.widgets.button import Button
-from fabric.widgets.centerbox import CenterBox
-from fabric.widgets.scrolledwindow import ScrolledWindow
-from fabric.bluetooth import BluetoothClient, BluetoothDevice
-import utils.icons as icons
-
-
-class BluetoothDeviceSlot(CenterBox):
- def __init__(self, device: BluetoothDevice, **kwargs):
- super().__init__(name="bluetooth-device", **kwargs)
- self.device = device
- self.device.connect("changed", self.on_changed)
- self.device.connect(
- "notify::closed", lambda *_: self.device.closed and self.destroy()
- )
-
- self.connection_label = Label(
- name="bluetooth-connection", markup=icons.bluetooth_disconnected
- )
- self.connect_button = Button(
- name="bluetooth-connect",
- label="Connect",
- on_clicked=lambda *_: self.device.set_connecting(not self.device.connected),
- )
-
- self.start_children = [
- Box(
- spacing=8,
- children=[
- Image(icon_name=device.icon_name + "-symbolic", size=32),
- Label(label=device.name),
- self.connection_label,
- ],
- )
- ]
- self.end_children = self.connect_button
-
- self.device.emit("changed") # to update display status
-
- def on_changed(self, *_):
- self.connection_label.set_markup(
- icons.bluetooth_connected
- if self.device.connected
- else icons.bluetooth_disconnected
- )
- if self.device.connecting:
- self.connect_button.set_label(
- "Connecting..." if not self.device.connecting else "Disconnecting..."
- )
- else:
- self.connect_button.set_label(
- "Connect" if not self.device.connected else "Disconnect"
- )
- return
-
-
-class BluetoothConnections(Box):
- def __init__(self, **kwargs):
- super().__init__(
- name="bluetooth",
- spacing=4,
- orientation="vertical",
- **kwargs,
- )
-
- self.launcher = kwargs["launcher"]
-
- self.buttons = self.launcher.dashboard.bluetooth_button
- self.bt_status_text = self.buttons.bluetooth_status_text
- self.bt_status_button = self.buttons.bluetooth_status_button
- self.bt_icon = self.buttons.bluetooth_icon
- self.bt_label = self.buttons.bluetooth_label
- self.bt_menu_button = self.buttons.bluetooth_menu_button
- self.bt_menu_label = self.buttons.bluetooth_menu_label
-
- self.client = BluetoothClient(on_device_added=self.on_device_added)
- self.scan_label = Label(name="bluetooth-scan-label", markup=icons.radar)
- self.scan_button = Button(
- name="bluetooth-scan",
- child=self.scan_label,
- tooltip_text="Scan for Bluetooth devices",
- on_clicked=lambda *_: self.client.toggle_scan(),
- )
- self.back_button = Button(
- name="bluetooth-back",
- child=Label(name="bluetooth-back-label", markup=icons.chevron_left),
- on_clicked=lambda *_: self.launcher.open("launcher"),
- )
-
- self.client.connect("notify::enabled", lambda *_: self.status_label())
- self.client.connect("notify::scanning", lambda *_: self.update_scan_label())
-
- self.paired_box = Box(spacing=2, orientation="vertical")
- self.available_box = Box(spacing=2, orientation="vertical")
-
- # Create a single content container with the required structure:
- # [Paired label] [Paired devices list] [Available label] [Available devices list]
- content_box = Box(spacing=4, orientation="vertical")
- content_box.add(self.paired_box)
- content_box.add(Label(name="bluetooth-section", label="Available"))
- content_box.add(self.available_box)
-
- self.children = [
- CenterBox(
- name="bluetooth-header",
- start_children=self.back_button,
- center_children=Label(name="bluetooth-text", label="Bluetooth Devices"),
- end_children=self.scan_button,
- ),
- ScrolledWindow(
- name="bluetooth-devices",
- min_content_size=(-1, -1),
- child=content_box,
- v_expand=True,
- ),
- ]
-
- # Trigger initial notifications to update status without delay.
- self.client.notify("scanning")
- self.client.notify("enabled")
-
- def status_label(self):
- print(self.client.enabled)
- if self.client.enabled:
- self.bt_status_text.set_label("Enabled")
- for i in [
- self.bt_status_button,
- self.bt_status_text,
- self.bt_icon,
- self.bt_label,
- self.bt_menu_button,
- self.bt_menu_label,
- ]:
- i.remove_style_class("disabled")
- self.bt_icon.set_markup(icons.bluetooth)
- else:
- self.bt_status_text.set_label("Disabled")
- for i in [
- self.bt_status_button,
- self.bt_status_text,
- self.bt_icon,
- self.bt_label,
- self.bt_menu_button,
- self.bt_menu_label,
- ]:
- i.add_style_class("disabled")
- self.bt_icon.set_markup(icons.bluetooth_off)
-
- def on_device_added(self, client: BluetoothClient, address: str):
- if not (device := client.get_device(address)):
- return
- slot = BluetoothDeviceSlot(device)
-
- if device.paired:
- return self.paired_box.add(slot)
- return self.available_box.add(slot)
-
- def update_scan_label(self):
- if self.client.scanning:
- self.scan_label.add_style_class("scanning")
- self.scan_button.add_style_class("scanning")
- self.scan_button.set_tooltip_text("Stop scanning for Bluetooth devices")
- else:
- self.scan_label.remove_style_class("scanning")
- self.scan_button.remove_style_class("scanning")
- self.scan_button.set_tooltip_text("Scan for Bluetooth devices")
-
diff --git a/modules/launcher/components/calendar.py b/modules/launcher/components/calendar.py
deleted file mode 100644
index 263e572..0000000
--- a/modules/launcher/components/calendar.py
+++ /dev/null
@@ -1,217 +0,0 @@
-import calendar
-from datetime import datetime
-import gi
-import utils.icons as icons
-from fabric.widgets.label import Label
-from fabric.widgets.box import Box
-from fabric.widgets.centerbox import CenterBox
-
-gi.require_version("Gtk", "3.0")
-from gi.repository import Gtk, GLib
-
-
-class Calendar(Gtk.Box):
- def __init__(self):
- super().__init__(
- orientation=Gtk.Orientation.VERTICAL, spacing=8, name="calendar"
- )
-
- self.current_year = datetime.now().year
- self.current_month = datetime.now().month
- self.current_day = datetime.now().day
- # Store previous month/year to determine transition direction.
- self.previous_key = (self.current_year, self.current_month)
-
- # Cache threshold in number of months away from the current view.
- self.cache_threshold = 1 # Allow current month +/- 1 month
-
- # Dictionary to store built views for each month.
- self.month_views = {}
-
- self.prev_month_button = Gtk.Button(
- name="prev-month-button",
- child=Label(name="month-button-label", markup=icons.chevron_left),
- )
- self.prev_month_button.connect("clicked", self.on_prev_month_clicked)
-
- self.month_label = Gtk.Label(name="month-label")
-
- self.next_month_button = Gtk.Button(
- name="next-month-button",
- child=Label(name="month-button-label", markup=icons.chevron_right),
- )
- self.next_month_button.connect("clicked", self.on_next_month_clicked)
-
- self.header = CenterBox(
- spacing=4,
- name="header",
- start_children=[self.prev_month_button],
- center_children=[
- self.month_label,
- ],
- end_children=[self.next_month_button],
- )
-
- self.add(self.header)
-
- self.weekday_row = Gtk.Box(spacing=4, name="weekday-row")
- self.pack_start(self.weekday_row, False, False, 0)
-
- # Create a stack to hold month days views.
- self.stack = Gtk.Stack(name="calendar-stack")
- self.stack.set_transition_duration(250)
- self.pack_start(self.stack, True, True, 0)
-
- self.update_header()
- self.update_calendar()
- GLib.timeout_add_seconds(60, self.check_date_change)
-
- def update_header(self):
- # Update header month label and weekday row.
- self.month_label.set_text(
- datetime(self.current_year, self.current_month, 1)
- .strftime("%B %Y")
- .capitalize()
- )
- # Clear existing children from weekday_row.
- for child in self.weekday_row.get_children():
- self.weekday_row.remove(child)
- days = self.get_weekday_initials()
- for day in days:
- label = Gtk.Label(label=day.upper(), name="weekday-label")
- self.weekday_row.pack_start(label, True, True, 0)
- self.weekday_row.show_all()
-
- def update_calendar(self):
- new_key = (self.current_year, self.current_month)
- # Set the transition type based on whether we're moving to a later or earlier month.
- if new_key > self.previous_key:
- self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT)
- elif new_key < self.previous_key:
- self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_RIGHT)
- # Update previous key for next comparison.
- self.previous_key = new_key
-
- # Create the month view if it doesn't exist.
- if new_key not in self.month_views:
- month_view = self.create_month_view(self.current_year, self.current_month)
- self.month_views[new_key] = month_view
- self.stack.add_titled(
- month_view,
- f"{self.current_year}_{self.current_month}",
- f"{self.current_year}_{self.current_month}",
- )
- # Switch the visible child in the stack.
- self.stack.set_visible_child_name(f"{self.current_year}_{self.current_month}")
- self.update_header()
- self.stack.show_all()
-
- # Purge any cached month views that are too far away.
- self.prune_cache()
-
- def prune_cache(self):
- # Compute a numerical value for a (year, month) key.
- def month_index(key):
- year, month = key
- return year * 12 + (month - 1)
-
- current_index = month_index((self.current_year, self.current_month))
- keys_to_remove = []
- for key in self.month_views:
- if abs(month_index(key) - current_index) > self.cache_threshold:
- keys_to_remove.append(key)
- for key in keys_to_remove:
- widget = self.month_views.pop(key)
- self.stack.remove(widget)
-
- def create_month_view(self, year, month):
- grid = Gtk.Grid(
- column_homogeneous=True, row_homogeneous=False, name="calendar-grid"
- )
-
- cal = calendar.Calendar(firstweekday=6)
- month_days = cal.monthdayscalendar(year, month)
-
- prev_month = month - 1 if month > 1 else 12
- next_month = month + 1 if month < 12 else 1
- prev_year = year if month > 1 else year - 1
- next_year = year if month < 12 else year + 1
-
- prev_month_days_count = calendar.monthrange(prev_year, prev_month)[1]
- next_month_day = 1
-
- for row, week in enumerate(month_days):
- for col, day in enumerate(week):
- day_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, name="day-box")
- top_spacer = Gtk.Box(hexpand=True, vexpand=True)
- middle_box = Gtk.Box(hexpand=True, vexpand=True)
- bottom_spacer = Gtk.Box(hexpand=True, vexpand=True)
-
- if day == 0:
- if row == 0:
- day_num = prev_month_days_count - week[:col].count(0)
- label = Gtk.Label(label=str(day_num), name="prev-month-day")
- else:
- label = Gtk.Label(
- label=str(next_month_day), name="next-month-day"
- )
- next_month_day += 1
- else:
- label = Gtk.Label(label=str(day), name="day-label")
- if (
- day == self.current_day
- and month == datetime.now().month
- and year == datetime.now().year
- ):
- label.get_style_context().add_class("current-day")
-
- middle_box.pack_start(
- Gtk.Box(hexpand=True, vexpand=True), True, True, 0
- )
- middle_box.pack_start(label, False, False, 0)
- middle_box.pack_start(
- Gtk.Box(hexpand=True, vexpand=True), True, True, 0
- )
-
- day_box.pack_start(top_spacer, True, True, 0)
- day_box.pack_start(middle_box, True, True, 0)
- day_box.pack_start(bottom_spacer, True, True, 0)
-
- grid.attach(day_box, col, row, 1, 1)
-
- grid.show_all()
- return grid
-
- def get_weekday_initials(self):
- # Returns localized weekday initials.
- return [datetime(2023, 1, i + 1).strftime("%a")[:1] for i in range(7)]
-
- def on_prev_month_clicked(self, widget):
- if self.current_month == 1:
- self.current_month = 12
- self.current_year -= 1
- else:
- self.current_month -= 1
- self.update_calendar()
-
- def on_next_month_clicked(self, widget):
- if self.current_month == 12:
- self.current_month = 1
- self.current_year += 1
- else:
- self.current_month += 1
- self.update_calendar()
-
- def check_date_change(self):
- now = datetime.now()
- if (
- now.day != self.current_day
- or now.month != self.current_month
- or now.year != self.current_year
- ):
- self.current_day = now.day
- self.current_month = now.month
- self.current_year = now.year
- self.update_calendar()
- return True
-
diff --git a/modules/launcher/components/cliphist.py b/modules/launcher/components/cliphist.py
deleted file mode 100644
index 5747747..0000000
--- a/modules/launcher/components/cliphist.py
+++ /dev/null
@@ -1,504 +0,0 @@
-from fabric.widgets.box import Box
-from fabric.widgets.label import Label
-from fabric.widgets.button import Button
-from fabric.widgets.entry import Entry
-from fabric.widgets.image import Image
-from fabric.widgets.scrolledwindow import ScrolledWindow
-from fabric.utils import idle_add, remove_handler
-from fabric.utils.helpers import get_relative_path
-from gi.repository import GLib, Gdk, GdkPixbuf
-import subprocess
-import os
-import re
-import sys
-import tempfile
-
-import utils.icons as icons
-
-
-class ClipHistory(Box):
- def __init__(self, **kwargs):
- super().__init__(
- name="clip-history",
- visible=False,
- all_visible=False,
- **kwargs,
- )
-
- # Create a temporary directory for image icons
- self.tmp_dir = tempfile.mkdtemp(prefix="cliphist-")
- self.image_cache = {} # Cache for image previews
-
- self.launcher = kwargs["launcher"]
- self.selected_index = -1 # Track the selected item index
- self._arranger_handler = 0
- self.clipboard_items = []
- self._loading = False
- self._pending_updates = False
-
- self.viewport = Box(name="viewport", spacing=4, orientation="v")
- self.search_entry = Entry(
- name="search-entry",
- placeholder="Search Clipboard History...",
- h_expand=True,
- notify_text=self.filter_items,
- on_activate=lambda entry, *_: self.use_selected_item(),
- on_key_press_event=self.on_search_entry_key_press,
- )
- self.search_entry.props.xalign = 0.5
-
- self.scrolled_window = ScrolledWindow(
- name="scrolled-window",
- spacing=10,
- min_content_size=(450, 105),
- max_content_size=(450, 105),
- child=self.viewport,
- )
-
- self.header_box = Box(
- name="header_box",
- spacing=10,
- orientation="h",
- children=[
- Button(
- name="clear-button",
- child=Label(name="clear-label", markup=icons.trash),
- on_clicked=lambda *_: self.clear_history(),
- ),
- self.search_entry,
- ],
- )
-
- self.history_box = Box(
- name="launcher-box",
- spacing=10,
- h_expand=True,
- orientation="v",
- children=[
- self.header_box,
- self.scrolled_window,
- ],
- )
-
- self.add(self.history_box)
- self.show_all()
-
- def close(self):
- """Close the clipboard history panel"""
- self.viewport.children = []
- self.selected_index = -1 # Reset selection
- self.launcher.close()
-
- def open(self):
- """Open the clipboard history panel and load items"""
- self.viewport.set_visible(True)
- if self._loading:
- return
- self._loading = True
- self.search_entry.set_text("") # Clear search
- self.search_entry.grab_focus()
- # Start loading in background using GLib.idle_add
- GLib.idle_add(self._load_clipboard_items_thread)
-
- def _load_clipboard_items_thread(self):
- """Worker for loading clipboard items, now runs in main loop via idle_add"""
- try:
- result = subprocess.run(
- ["cliphist", "list"], capture_output=True, text=True, check=True
- )
- lines = result.stdout.strip().split("\n")
- new_items = []
- for line in lines:
- if not line or " 1 else "0"
- content = parts[1] if len(parts) > 1 else item
-
- # Truncate content for display
- display_text = content.strip()
- if len(display_text) > 100:
- display_text = display_text[:97] + "..."
-
- # Check if this is an image by examining the content
- is_image = self.is_image_data(content)
-
- if is_image:
- # For images, create item with image preview
- button = Button(
- name="slot-button",
- child=Box(
- name="slot-box",
- orientation="h",
- spacing=10,
- children=[
- Image(name="clip-icon", h_align="start"), # Placeholder
- Label(
- name="clip-label",
- label="[Image]",
- ellipsization="end",
- v_align="center",
- h_align="start",
- h_expand=True,
- ),
- ],
- ),
- tooltip_text="Image in clipboard",
- on_clicked=lambda *_, id=item_id: self.paste_item(id),
- )
- # Load image preview in background
- self._load_image_preview_async(item_id, button)
- else:
- # For text, create regular item
- button = self.create_text_item_button(item_id, display_text)
-
- # Add key press event handler for Enter key
- button.connect(
- "key-press-event",
- lambda widget, event, id=item_id: self.on_item_key_press(widget, event, id),
- )
-
- # Make sure button can receive focus and key events
- button.set_can_focus(True)
- button.add_events(Gdk.EventMask.KEY_PRESS_MASK)
-
- return button
-
- def _load_image_preview_async(self, item_id, button):
- """Load image preview using GLib.idle_add instead of threads"""
-
- def load_image():
- try:
- if item_id in self.image_cache:
- pixbuf = self.image_cache[item_id]
- else:
- result = subprocess.run(
- ["cliphist", "decode", item_id], capture_output=True, check=True
- )
- loader = GdkPixbuf.PixbufLoader()
- loader.write(result.stdout)
- loader.close()
- pixbuf = loader.get_pixbuf()
- width, height = pixbuf.get_width(), pixbuf.get_height()
- max_size = 72
- if width > height:
- new_width = max_size
- new_height = int(height * (max_size / width))
- else:
- new_height = max_size
- new_width = int(width * (max_size / height))
- pixbuf = pixbuf.scale_simple(
- new_width, new_height, GdkPixbuf.InterpType.BILINEAR
- )
- self.image_cache[item_id] = pixbuf
- self._update_image_button(button, pixbuf)
- except Exception as e:
- print(f"Error loading image preview: {e}", file=sys.stderr)
- return False
-
- GLib.idle_add(load_image)
-
- def _update_image_button(self, button, pixbuf):
- """Update the button with the loaded image preview"""
- box = button.get_child()
- if box and len(box.get_children()) > 0:
- image_widget = box.get_children()[0]
- if isinstance(image_widget, Image):
- image_widget.set_from_pixbuf(pixbuf)
-
- def create_text_item_button(self, item_id, display_text):
- """Create a button for a text clipboard item"""
- return Button(
- name="slot-button",
- child=Box(
- name="slot-box",
- orientation="h",
- spacing=10,
- children=[
- Label(
- name="clip-icon",
- markup=icons.clip_text,
- h_align="start",
- ),
- Label(
- name="clip-label",
- label=display_text,
- ellipsization="end",
- v_align="center",
- h_align="start",
- h_expand=True,
- ),
- ],
- ),
- tooltip_text=display_text,
- on_clicked=lambda *_: self.paste_item(item_id),
- )
-
- def is_image_data(self, content):
- """Determine if clipboard content is likely an image"""
- # Check for common image data patterns
- return (
- content.startswith("data:image/")
- or content.startswith("\x89PNG")
- or content.startswith("GIF8")
- or content.startswith("\xff\xd8\xff") # JPEG
- or re.match(r"^\s* visible_bottom:
- # Item below viewport - align to bottom
- new_value = y + height - page_size
- adj.set_value(new_value)
- return False
-
- GLib.idle_add(scroll)
-
- def use_selected_item(self):
- """Use (paste) the selected clipboard item"""
- children = self.viewport.get_children()
- if (
- not children
- or self.selected_index == -1
- or self.selected_index >= len(self.clipboard_items)
- ):
- return
-
- # Get the item ID from the first part before the tab
- item_line = self.clipboard_items[self.selected_index]
- item_id = item_line.split("\t", 1)[0]
- self.paste_item(item_id)
-
- def delete_selected_item(self):
- """Delete the selected clipboard item"""
- children = self.viewport.get_children()
- if not children or self.selected_index == -1:
- return
-
- # Get the item ID from the first part before the tab
- item_line = self.clipboard_items[self.selected_index]
- item_id = item_line.split("\t", 1)[0]
- self.delete_item(item_id)
-
- def on_item_key_press(self, widget, event, item_id):
- """Handle key press events on clipboard items"""
- if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
- # Copy item to clipboard and close
- self.paste_item(item_id)
- return True
- return False
-
- def __del__(self):
- """Clean up temporary files on destruction"""
- try:
- if hasattr(self, "tmp_dir") and os.path.exists(self.tmp_dir):
- import shutil
-
- shutil.rmtree(self.tmp_dir)
- self.image_cache.clear()
- except Exception as e:
- print(f"Error cleaning up temporary files: {e}", file=sys.stderr)
diff --git a/modules/launcher/components/dashboard.py b/modules/launcher/components/dashboard.py
deleted file mode 100644
index 49b8ad8..0000000
--- a/modules/launcher/components/dashboard.py
+++ /dev/null
@@ -1,605 +0,0 @@
-import subprocess
-from fabric.widgets.box import Box
-from fabric.widgets.label import Label
-from fabric.widgets.button import Button
-from fabric.utils.helpers import exec_shell_command_async
-import gi
-from gi.repository import Gtk, Gdk, GLib
-from services import network_client
-from services.powerprofile import PowerProfiles
-from fabric import Fabricator
-
-gi.require_version("Gtk", "3.0")
-import utils.icons as icons
-
-
-def add_hover_cursor(widget):
- # Add enter/leave events to change the cursor
- widget.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK)
- widget.connect(
- "enter-notify-event",
- lambda w, e: w.get_window().set_cursor(
- Gdk.Cursor.new_from_name(w.get_display(), "pointer")
- )
- if w.get_window()
- else None,
- )
- widget.connect(
- "leave-notify-event",
- lambda w, e: w.get_window().set_cursor(None) if w.get_window() else None,
- )
-
-
-class NetworkButton(Box):
- def __init__(self):
- self.network_client = network_client
- self._animation_timeout_id = None
- self._animation_step = 0
- self._animation_direction = 1
-
- self.network_icon = Label(
- name="network-icon",
- markup=None,
- )
- self.network_label = Label(
- name="network-label",
- label="Wi-Fi",
- justification="left",
- )
- self.network_label_box = Box(children=[self.network_label, Box(h_expand=True)])
- self.network_ssid = Label(
- name="network-ssid",
- justification="left",
- )
- self.network_ssid_box = Box(children=[self.network_ssid, Box(h_expand=True)])
- self.network_text = Box(
- name="network-text",
- orientation="v",
- h_align="start",
- v_align="center",
- children=[self.network_label_box, self.network_ssid_box],
- )
- self.network_status_box = Box(
- h_align="start",
- v_align="center",
- spacing=10,
- children=[self.network_icon, self.network_text],
- )
- self.network_status_button = Button(
- name="network-status-button",
- h_expand=True,
- child=self.network_status_box,
- on_clicked=lambda *_: self.network_client.wifi_device.toggle_wifi()
- if self.network_client.wifi_device
- else None,
- )
- add_hover_cursor(self.network_status_button) # <-- Added hover
-
- self.network_menu_label = Label(
- name="network-menu-label",
- markup=icons.chevron_right,
- )
- self.network_menu_button = Button(
- name="network-menu-button",
- child=self.network_menu_label,
- )
- add_hover_cursor(self.network_menu_button) # <-- Added hover
-
- super().__init__(
- name="network-button",
- orientation="h",
- h_align="fill",
- v_align="fill",
- h_expand=True,
- v_expand=True,
- spacing=0,
- children=[self.network_status_button, self.network_menu_button],
- )
-
- self.widgets = [
- self,
- self.network_icon,
- self.network_label,
- self.network_ssid,
- self.network_status_button,
- self.network_menu_button,
- self.network_menu_label,
- ]
-
- # Connect to wifi device signals when ready
- self.network_client.connect("device-ready", self._on_wifi_ready)
-
- # Check initial state using idle_add to defer until GTK loop is running
- GLib.idle_add(self._initial_update)
-
- def _initial_update(self):
- self.update_state()
- return False # Run only once
-
- def _on_wifi_ready(self, *args):
- if self.network_client.wifi_device:
- self.network_client.wifi_device.connect(
- "notify::enabled", self.update_state
- )
- self.network_client.wifi_device.connect("notify::ssid", self.update_state)
- self.update_state()
-
- def _animate_searching(self):
- """Animate wifi icon when searching for networks"""
- wifi_icons = [
- icons.wifi_0,
- icons.wifi_1,
- icons.wifi_2,
- icons.wifi_3,
- icons.wifi_2,
- icons.wifi_1,
- ]
-
- # Si el widget no existe o el WiFi está desactivado, detener la animación
- wifi = self.network_client.wifi_device
- if not self.network_icon or not wifi or not wifi.enabled:
- self._stop_animation()
- return False
-
- # Si estamos conectados, detener la animación
- if wifi.state == "activated" and wifi.ssid != "Disconnected":
- self._stop_animation()
- return False
-
- GLib.idle_add(self.network_icon.set_markup, wifi_icons[self._animation_step])
-
- # Reiniciar al principio cuando llegamos al final
- self._animation_step = (self._animation_step + 1) % len(wifi_icons)
-
- return True # Mantener la animación activa
-
- def _start_animation(self):
- if self._animation_timeout_id is None:
- self._animation_step = 0
- self._animation_direction = 1
- # Ejecuta la animación cada 500ms sin usar idle_add
- self._animation_timeout_id = GLib.timeout_add(500, self._animate_searching)
-
- def _stop_animation(self):
- if self._animation_timeout_id is not None:
- GLib.source_remove(self._animation_timeout_id)
- self._animation_timeout_id = None
-
- def update_state(self, *args):
- """Update the button state based on network status"""
- wifi = self.network_client.wifi_device
- ethernet = self.network_client.ethernet_device
-
- # Update enabled/disabled state
- if wifi and not wifi.enabled:
- self._stop_animation()
- self.network_icon.set_markup(icons.wifi_off)
- for widget in self.widgets:
- widget.add_style_class("disabled")
- self.network_ssid.set_label("Disabled")
- return
-
- # Remove disabled class if we got here
- for widget in self.widgets:
- widget.remove_style_class("disabled")
-
- # Update text and animation based on state
- if wifi and wifi.enabled:
- if wifi.state == "activated" and wifi.ssid != "Disconnected":
- self._stop_animation()
- self.network_ssid.set_label(wifi.ssid)
- # Update icon based on signal strength
- if wifi.strength > 0:
- strength = wifi.strength
- if strength < 25:
- self.network_icon.set_markup(icons.wifi_0)
- elif strength < 50:
- self.network_icon.set_markup(icons.wifi_1)
- elif strength < 75:
- self.network_icon.set_markup(icons.wifi_2)
- else:
- self.network_icon.set_markup(icons.wifi_3)
- else:
- self.network_ssid.set_label("Enabled")
- self._start_animation()
-
- # Handle primary device check safely
- try:
- primary_device = self.network_client.primary_device
- except AttributeError:
- primary_device = "wireless" # Default to wireless if error occurs
-
- # Handle wired connection case
- if primary_device == "wired":
- self._stop_animation()
- if ethernet and ethernet.internet == "activated":
- self.network_icon.set_markup(icons.world)
- else:
- self.network_icon.set_markup(icons.world_off)
- else:
- if not wifi:
- self._stop_animation()
- self.network_icon.set_markup(icons.wifi_off)
- elif (
- wifi.state == "activated"
- and wifi.ssid != "Disconnected"
- and wifi.strength > 0
- ):
- self._stop_animation()
- strength = wifi.strength
- if strength < 25:
- self.network_icon.set_markup(icons.wifi_0)
- elif strength < 50:
- self.network_icon.set_markup(icons.wifi_1)
- elif strength < 75:
- self.network_icon.set_markup(icons.wifi_2)
- else:
- self.network_icon.set_markup(icons.wifi_3)
- else:
- self._start_animation()
-
-
-class BluetoothButton(Box):
- def __init__(self, launcher):
- super().__init__(
- name="bluetooth-button",
- orientation="h",
- h_align="fill",
- v_align="fill",
- h_expand=True,
- v_expand=True,
- )
- self.launcher = launcher
-
- self.bluetooth_icon = Label(
- name="bluetooth-icon",
- markup=icons.bluetooth,
- )
- self.bluetooth_label = Label(
- name="bluetooth-label",
- label="Bluetooth",
- justification="left",
- )
- self.bluetooth_label_box = Box(
- children=[self.bluetooth_label, Box(h_expand=True)]
- )
- self.bluetooth_status_text = Label(
- name="bluetooth-status",
- label="Disabled",
- justification="left",
- )
- self.bluetooth_status_box = Box(
- children=[self.bluetooth_status_text, Box(h_expand=True)]
- )
- self.bluetooth_text = Box(
- orientation="v",
- h_align="start",
- v_align="center",
- children=[self.bluetooth_label_box, self.bluetooth_status_box],
- )
- self.bluetooth_status_container = Box(
- h_align="start",
- v_align="center",
- spacing=10,
- children=[self.bluetooth_icon, self.bluetooth_text],
- )
- self.bluetooth_status_button = Button(
- name="bluetooth-status-button",
- h_expand=True,
- child=self.bluetooth_status_container,
- on_clicked=lambda *_: self.launcher.bluetooth.client.toggle_power(),
- )
- add_hover_cursor(self.bluetooth_status_button) # <-- Added hover
- self.bluetooth_menu_label = Label(
- name="bluetooth-menu-label",
- markup=icons.chevron_right,
- )
- self.bluetooth_menu_button = Button(
- name="bluetooth-menu-button",
- on_clicked=lambda *_: self.launcher.open("bluetooth"),
- child=self.bluetooth_menu_label,
- )
- add_hover_cursor(self.bluetooth_menu_button) # <-- Added hover
-
- self.add(self.bluetooth_status_button)
- self.add(self.bluetooth_menu_button)
-
-
-class NightModeButton(Button):
- def __init__(self):
- self.night_mode_icon = Label(
- name="night-mode-icon",
- markup=icons.night,
- )
- self.night_mode_label = Label(
- name="night-mode-label",
- label="Night Mode",
- justification="left",
- )
- self.night_mode_label_box = Box(
- children=[self.night_mode_label, Box(h_expand=True)]
- )
- self.night_mode_status = Label(
- name="night-mode-status",
- label="Enabled",
- justification="left",
- )
- self.night_mode_status_box = Box(
- children=[self.night_mode_status, Box(h_expand=True)]
- )
- self.night_mode_text = Box(
- name="night-mode-text",
- orientation="v",
- h_align="start",
- v_align="center",
- children=[self.night_mode_label_box, self.night_mode_status_box],
- )
- self.night_mode_box = Box(
- h_align="start",
- v_align="center",
- spacing=10,
- children=[self.night_mode_icon, self.night_mode_text],
- )
-
- super().__init__(
- name="night-mode-button",
- h_expand=True,
- child=self.night_mode_box,
- on_clicked=self.toggle_hyprsunset,
- )
- add_hover_cursor(self) # <-- Added hover
-
- self.widgets = [
- self,
- self.night_mode_label,
- self.night_mode_status,
- self.night_mode_icon,
- ]
- # self.check_hyprsunset()
- # **Monitor external changes every 3 seconds**
- self.hyprsunset_watcher = Fabricator(
- self.is_hyprsunset_running,
- interval=3000,
- stream=False,
- default_value=False,
- )
- self.hyprsunset_watcher.changed.connect(self.update_night_mode_status)
- self.update_night_mode_status(None, self.is_hyprsunset_running())
-
- def toggle_hyprsunset(self, *args):
- """
- Toggle the 'hyprsunset' process:
- - If running, kill it and mark as 'Disabled'.
- - If not running, start it and mark as 'Enabled'.
- """
- try:
- subprocess.check_output(["pgrep", "hyprsunset"])
- exec_shell_command_async("pkill hyprsunset")
- self.night_mode_status.set_label("Disabled")
- for widget in self.widgets:
- widget.add_style_class("disabled")
- except subprocess.CalledProcessError:
- exec_shell_command_async("hyprsunset -t 3500")
- self.night_mode_status.set_label("Enabled")
- for widget in self.widgets:
- widget.remove_style_class("disabled")
-
- def is_hyprsunset_running(self, *args):
- """Returns True if hyprsunset is running."""
- try:
- subprocess.check_output(["pgrep", "hyprsunset"])
- return True
- except subprocess.CalledProcessError:
- return False
-
- def update_night_mode_status(self, _, is_running):
- """Update UI based on hyprsunset status."""
- if is_running:
- self.night_mode_status.set_label("Enabled")
- for widget in self.widgets:
- widget.remove_style_class("disabled")
- else:
- self.night_mode_status.set_label("Disabled")
- for widget in self.widgets:
- widget.add_style_class("disabled")
-
-
-class CaffeineButton(Button):
- def __init__(self):
- self.caffeine_icon = Label(
- name="caffeine-icon",
- markup=icons.coffee,
- )
- self.caffeine_label = Label(
- name="caffeine-label",
- label="Caffeine",
- justification="left",
- )
- self.caffeine_label_box = Box(
- children=[self.caffeine_label, Box(h_expand=True)]
- )
- self.caffeine_status = Label(
- name="caffeine-status",
- label="Enabled",
- justification="left",
- )
- self.caffeine_status_box = Box(
- children=[self.caffeine_status, Box(h_expand=True)]
- )
- self.caffeine_text = Box(
- name="caffeine-text",
- orientation="v",
- h_align="start",
- v_align="center",
- children=[self.caffeine_label_box, self.caffeine_status_box],
- )
- self.caffeine_box = Box(
- h_align="start",
- v_align="center",
- spacing=10,
- children=[self.caffeine_icon, self.caffeine_text],
- )
- super().__init__(
- name="caffeine-button",
- h_expand=True,
- child=self.caffeine_box,
- on_clicked=self.toggle_wlinhibit,
- )
- add_hover_cursor(self) # <-- Added hover
-
- self.widgets = [
- self,
- self.caffeine_label,
- self.caffeine_status,
- self.caffeine_icon,
- ]
- # **Monitor external changes every 3 seconds**
- self.wlinhibit_watcher = Fabricator(
- self.is_wlinhibit_running,
- interval=3000,
- stream=False,
- default_value=False,
- )
- self.wlinhibit_watcher.changed.connect(self.update_wlinhibit_status)
- self.update_wlinhibit_status(None, self.is_wlinhibit_running())
-
- def toggle_wlinhibit(self, *args):
- """Toggle the 'wlinhibit' process."""
- if self.is_wlinhibit_running():
- exec_shell_command_async("pkill wlinhibit")
- else:
- exec_shell_command_async("wlinhibit")
-
- def is_wlinhibit_running(self, *args):
- """Returns True if wlinhibit is running. Accepts extra arguments to avoid Fabricator errors."""
- try:
- subprocess.check_output(["pgrep", "wlinhibit"])
- return True
- except subprocess.CalledProcessError:
- return False
-
- def update_wlinhibit_status(self, _, is_running):
- """Update UI based on wlinhibit status."""
- if is_running:
- self.caffeine_status.set_label("Enabled")
- for widget in self.widgets:
- widget.remove_style_class("disabled")
- else:
- self.caffeine_status.set_label("Disabled")
- for widget in self.widgets:
- widget.add_style_class("disabled")
-
-
-class PowerProfileButton(Button):
- def __init__(self):
- self.power_profile_service = PowerProfiles.get_default()
-
- self.power_profile_icon = Label(
- name="power-profile-icon",
- markup=icons.power_balanced, # Default icon
- )
- self.power_profile_label = Label(
- name="power-profile-label",
- label="Power Profile",
- justification="left",
- )
- self.power_profile_label_box = Box(
- children=[self.power_profile_label, Box(h_expand=True)]
- )
- self.power_profile_status = Label(
- name="power-profile-status",
- label="Balanced",
- justification="left",
- )
- self.power_profile_status_box = Box(
- children=[self.power_profile_status, Box(h_expand=True)]
- )
- self.power_profile_text = Box(
- name="power-profile-text",
- orientation="v",
- h_align="start",
- v_align="center",
- children=[self.power_profile_label_box, self.power_profile_status_box],
- )
- self.power_profile_box = Box(
- h_align="start",
- v_align="center",
- spacing=10,
- children=[self.power_profile_icon, self.power_profile_text],
- )
-
- super().__init__(
- name="power-profile-button",
- h_expand=True,
- child=self.power_profile_box,
- on_clicked=self.toggle_power_profile,
- )
- add_hover_cursor(self)
-
- self.widgets = [
- self,
- self.power_profile_label,
- self.power_profile_status,
- self.power_profile_icon,
- ]
-
- # Connect to power profile service signals
- self.power_profile_service.connect("profile", self.update_power_profile_status)
-
- # Initial update
- self.update_power_profile_status(None, self.power_profile_service.get_current_profile())
-
- def toggle_power_profile(self, *args):
- current_profile = self.power_profile_service.get_current_profile()
- profiles = ["power-saver", "balanced", "performance"]
- current_index = profiles.index(current_profile)
- next_profile = profiles[(current_index + 1) % len(profiles)]
- self.power_profile_service.set_power_profile(next_profile)
-
- def update_power_profile_status(self, _, profile):
- # Map profiles to their display names and icons
- profile_map = {
- "power-saver": {"name": "Power Saver", "icon": icons.power_saving},
- "balanced": {"name": "Balanced", "icon": icons.power_balanced},
- "performance": {"name": "Performance", "icon": icons.power_performance},
- }
-
- profile_info = profile_map.get(profile, profile_map["balanced"])
- self.power_profile_status.set_label(profile_info["name"])
- self.power_profile_icon.set_markup(profile_info["icon"])
-
- if profile == "balanced":
- for widget in self.widgets:
- widget.add_style_class("disabled")
- else:
- for widget in self.widgets:
- widget.remove_style_class("disabled")
-
-
-
-class Dashboard(Gtk.Grid):
- def __init__(self, **kwargs):
- super().__init__(name="buttons-grid")
- self.set_row_homogeneous(True)
- self.set_column_homogeneous(True)
- self.set_row_spacing(8)
- self.set_column_spacing(8)
- self.set_vexpand(True) # Prevent vertical expansion
-
- self.launcher = kwargs["launcher"]
-
- # Instantiate each button
- self.network_button = NetworkButton()
- self.bluetooth_button = BluetoothButton(self.launcher)
- self.night_mode_button = NightModeButton()
- self.caffeine_button = CaffeineButton()
- self.power_profile_button = PowerProfileButton()
-
- # Attach buttons into the grid (two rows, three columns)
- self.attach(self.network_button, 0, 0, 1, 1)
- self.attach(self.bluetooth_button, 1, 0, 1, 1)
- self.attach(self.power_profile_button, 2, 0, 1, 1)
- self.attach(self.night_mode_button, 0, 1, 1, 1)
- self.attach(self.caffeine_button, 1, 1, 1, 1)
-
- self.show_all()
diff --git a/modules/launcher/components/emoji.py b/modules/launcher/components/emoji.py
deleted file mode 100644
index b69355e..0000000
--- a/modules/launcher/components/emoji.py
+++ /dev/null
@@ -1,330 +0,0 @@
-from fabric.widgets.box import Box
-from fabric.widgets.label import Label
-from fabric.widgets.button import Button
-from fabric.widgets.entry import Entry
-from fabric.widgets.stack import Stack
-from fabric.utils import remove_handler, get_relative_path
-from gi.repository import Gdk
-import utils.icons as icons
-import os
-import subprocess
-import ijson
-
-
-class EmojiPicker(Box):
- def __init__(self, **kwargs):
- super().__init__(
- name="emoji",
- visible=False,
- all_visible=False,
- **kwargs,
- )
-
- self.launcher = kwargs["launcher"]
- self.selected_index = -1
- self.emojis_per_page = 9 * 3
- self.current_page_index = 0
- self.filtered_emojis = []
- self.total_pages = 0
-
- self._arranger_handler: int = 0
- self._all_emojis = self._load_emoji_data()
-
- self.stack = Stack(
- name="viewport",
- spacing=4,
- orientation="v",
- transition_type="slide-up-down",
- transition_duration=200,
- )
- self.search_entry = Entry(
- name="search-entry",
- placeholder="Search Emojis...",
- h_expand=True,
- notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()),
- on_activate=lambda entry, *_: self.on_search_entry_activate(
- entry.get_text()
- ),
- on_key_press_event=self.on_search_entry_key_press,
- )
- self.search_entry.props.xalign = 0.5
- self.header_box = Box(
- name="header_box",
- spacing=10,
- orientation="h",
- children=[
- self.search_entry,
- # Button(
- # name="close-button",
- # child=Label(name="close-label", markup=icons.cancel),
- # tooltip_text="Exit",
- # on_clicked=lambda *_: self.close_picker(),
- # ),
- ],
- )
-
- self.picker_box = Box(
- name="picker-box",
- spacing=10,
- h_expand=True,
- orientation="v",
- children=[
- self.header_box,
- self.stack,
- ],
- )
-
- self.resize_viewport()
-
- self.add(self.picker_box)
- self.show_all()
-
- def _load_emoji_data(self):
- emoji_data = {}
- emoji_file_path = get_relative_path("../../../config/assets/emoji.json")
- if not os.path.exists(emoji_file_path):
- print(f"Emoji JSON file not found at: {emoji_file_path}")
- return {}
-
- with open(emoji_file_path, "r") as f:
- for emoji_char, emoji_info in ijson.kvitems(f, ""):
- emoji_data[emoji_char] = emoji_info
- return emoji_data
-
- def close_picker(self):
- self.stack.children = []
- self.selected_index = -1
- self.launcher.close()
-
- def open_picker(self):
- self.search_entry.set_text("")
- self.current_page_index = 0
- self.arrange_viewport()
- self.search_entry.grab_focus()
-
- def arrange_viewport(self, query: str = ""):
- remove_handler(self._arranger_handler) if self._arranger_handler else None
- self.stack.children = []
- self.selected_index = -1
- self.current_page_index = 0
-
- self.filtered_emojis = [
- (emoji_char, emoji_info)
- for emoji_char, emoji_info in self._all_emojis.items()
- if query.casefold()
- in (
- emoji_info.get("name", "") + " " + emoji_info.get("group", "")
- ).casefold()
- ]
- self.total_pages = (
- (len(self.filtered_emojis) + self.emojis_per_page - 1)
- // self.emojis_per_page
- if self.filtered_emojis
- else 0
- )
-
- self.load_page(self.current_page_index)
-
- should_resize = not query
-
- if should_resize:
- self.resize_viewport()
- if query.strip() != "" and self.get_all_emoji_buttons():
- self.update_selection(0)
-
- def load_page(self, page_index):
- self.update_selection(-1)
- page_box = Box(name=f"page-box-{page_index}", orientation="v", spacing=4)
- start_index = page_index * self.emojis_per_page
- end_index = min(
- (page_index + 1) * self.emojis_per_page, len(self.filtered_emojis)
- )
- page_emojis = self.filtered_emojis[start_index:end_index]
-
- grid_box = Box(name="emoji-grid-box", orientation="v", spacing=2)
-
- row_box = None
- for i, (emoji_char, emoji_info) in enumerate(page_emojis):
- if i % 9 == 0:
- row_box = Box(name="emoji-row-box", orientation="h", spacing=2)
- grid_box.add(row_box)
- if row_box is not None:
- row_box.add(self.bake_emoji_slot(emoji_char, emoji_info))
- page_box.add(grid_box)
- self.stack.add_named(page_box, f"page-{page_index}")
- self.stack.set_visible_child_name(f"page-{page_index}")
- page_box.show_all()
-
- buttons = self.get_all_emoji_buttons()
- if buttons and self.selected_index != -1:
- page_relative_index = self.selected_index % self.emojis_per_page
- if page_relative_index < len(buttons):
- self.update_selection(page_relative_index)
- else:
- self.update_selection(len(buttons) - 1)
-
- def resize_viewport(self):
- return False
-
- def bake_emoji_slot(self, emoji_char: str, emoji_info: dict, **kwargs) -> Button:
- button = Button(
- name="emoji-slot-button",
- child=Box(
- name="emoji-slot-box",
- orientation="horizontal",
- halign="center",
- valign="center",
- children=[
- Label(
- name="emoji-char-label",
- label=emoji_char,
- use_markup=True,
- v_align="center",
- h_align="center",
- css_name="emoji-char-label",
- ),
- ],
- ),
- tooltip_text=emoji_info.get("name", "Unknown"),
- on_clicked=lambda *_: (
- self.copy_emoji_to_clipboard(emoji_char),
- self.close_picker(),
- ),
- **kwargs,
- )
- return button
-
- def update_selection(self, new_index: int):
- buttons = self.get_all_emoji_buttons()
- if not buttons:
- self.selected_index = -1
- return
-
- if self.selected_index != -1 and self.selected_index < len(buttons):
- current_button = buttons[self.selected_index]
- current_button.get_style_context().remove_class("selected")
- if not buttons or current_button not in buttons:
- self.selected_index = -1
-
- if 0 <= new_index < len(buttons):
- new_button = buttons[new_index]
- new_button.get_style_context().add_class("selected")
- self.selected_index = new_index
- else:
- self.selected_index = -1
-
- def get_all_emoji_buttons(self):
- buttons = []
- current_page = self.stack.get_visible_child()
- if current_page and current_page.get_children():
- if current_page.get_children()[0].get_children():
- for row_box in current_page.get_children()[0].get_children():
- buttons.extend(row_box.get_children())
- return buttons
-
- def on_search_entry_activate(self, text):
- buttons = self.get_all_emoji_buttons()
- if buttons:
- if self.selected_index != -1:
- buttons[self.selected_index].clicked()
- elif buttons and text.strip() != "":
- buttons[0].clicked()
-
- def on_search_entry_key_press(self, widget, event):
- if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down, Gdk.KEY_Left, Gdk.KEY_Right):
- self.move_selection_2d(event.keyval)
- return True
- elif event.keyval == Gdk.KEY_Escape:
- self.close_picker()
- return True
- return False
-
- def move_selection_2d(self, keyval):
- buttons = self.get_all_emoji_buttons()
- total_items_current_page = len(buttons)
- if total_items_current_page == 0:
- return
-
- rows = 3
- columns = 9
-
- if self.selected_index == -1:
- if keyval in (Gdk.KEY_Down, Gdk.KEY_Right):
- new_index = 0
- elif keyval in (Gdk.KEY_Up, Gdk.KEY_Left):
- new_index = total_items_current_page - 1
- else:
- return
- else:
- current_index_page = self.selected_index
- row = current_index_page // columns
- col = current_index_page % columns
-
- if keyval == Gdk.KEY_Right:
- new_col = (col + 1) % columns
- new_row = row
- if new_col == 0:
- new_row = row + 1
- elif keyval == Gdk.KEY_Left:
- new_col = (col - 1) % columns
- new_row = row
- if new_col == (columns - 1):
- new_row = row - 1
- elif keyval == Gdk.KEY_Down:
- new_row = row + 1
- new_col = col
- elif keyval == Gdk.KEY_Up:
- new_row = row - 1
- new_col = col
- else:
- return
-
- if new_row >= rows:
- if self.current_page_index < self.total_pages - 1:
- current_col = col # Keep track of current column
- self.current_page_index += 1
- self.load_page(self.current_page_index)
- new_index = current_col # Try to keep the same column
- if (
- new_index >= total_items_current_page
- ): # if column is out of bound, select last
- new_index = total_items_current_page - 1
- self.selected_index = -1
- self.update_selection(new_index)
- return
- else:
- new_index = total_items_current_page - 1
- elif new_row < 0:
- if self.current_page_index > 0:
- current_col = col # Keep track of current column
- self.current_page_index -= 1
- self.load_page(self.current_page_index)
- new_index = (
- rows - 1
- ) * columns + current_col # Select last row, same column
- if (
- new_index >= total_items_current_page
- ): # if column is out of bound, select last
- new_index = total_items_current_page - 1
- self.selected_index = -1
- self.update_selection(new_index)
- return
- else:
- new_index = 0
- else:
- new_index = new_row * columns + new_col
- if new_index >= total_items_current_page:
- new_index = total_items_current_page - 1
-
- if new_index < 0:
- new_index = 0
- elif new_index >= total_items_current_page:
- new_index = total_items_current_page - 1
-
- self.update_selection(new_index)
-
- def copy_emoji_to_clipboard(self, emoji_char: str):
- try:
- subprocess.run(["wl-copy"], input=emoji_char.encode("utf-8"), check=True)
- except subprocess.CalledProcessError as e:
- print(f"Clipboard copy failed: {e}")
diff --git a/modules/launcher/components/kanban.py b/modules/launcher/components/kanban.py
deleted file mode 100644
index 8c2bf8a..0000000
--- a/modules/launcher/components/kanban.py
+++ /dev/null
@@ -1,365 +0,0 @@
-import gi
-import json
-import os
-from pathlib import Path
-import cairo # For rendering the drag preview
-
-from fabric.widgets.label import Label
-from fabric.widgets.box import Box
-from fabric.widgets.centerbox import CenterBox
-import utils.icons as icons
-
-gi.require_version("Gtk", "3.0")
-from gi.repository import Gtk, Gdk, GObject, GLib
-
-
-def createSurfaceFromWidget(widget: Gtk.Widget) -> cairo.ImageSurface:
- alloc = widget.get_allocation()
- surface = cairo.ImageSurface(cairo.Format.ARGB32, alloc.width, alloc.height)
- cr = cairo.Context(surface)
- # Use a transparent background.
- cr.set_source_rgba(0, 0, 0, 0)
- cr.rectangle(0, 0, alloc.width, alloc.height)
- cr.fill()
- widget.draw(cr)
- return surface
-
-
-class InlineEditor(Gtk.Box):
- __gsignals__ = {
- "confirmed": (GObject.SignalFlags.RUN_LAST, None, (str,)),
- "canceled": (GObject.SignalFlags.RUN_LAST, None, ()),
- }
-
- def __init__(self, initial_text=""):
- super().__init__(name="inline-editor", spacing=4)
- # Replace Gtk.Entry with a Gtk.TextView for multiline editing.
- self.text_view = Gtk.TextView()
- self.text_view.set_wrap_mode(Gtk.WrapMode.WORD)
- buffer = self.text_view.get_buffer()
- buffer.set_text(initial_text)
- # Connect key press events to handle Return and SHIFT+Return.
- self.text_view.connect("key-press-event", self.on_key_press)
-
- confirm_btn = Gtk.Button(
- name="kanban-btn", child=Label(name="kanban-btn-label", markup=icons.accept)
- )
- confirm_btn.connect("clicked", self.on_confirm)
- confirm_btn.get_style_context().add_class("flat")
-
- cancel_btn = Gtk.Button(
- name="kanban-btn", child=Label(name="kanban-btn-neg", markup=icons.cancel)
- )
- cancel_btn.connect("clicked", self.on_cancel)
- cancel_btn.get_style_context().add_class("flat")
-
- # Pack the TextView inside a ScrolledWindow for better appearance.
- sw = Gtk.ScrolledWindow()
- sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
- sw.set_min_content_height(50)
- sw.add(self.text_view)
-
- self.button_box = Box(children=[confirm_btn, cancel_btn], spacing=4)
- self.center_box = CenterBox(center_children=[self.button_box], orientation="v")
-
- self.pack_start(sw, True, True, 0)
- self.pack_start(self.center_box, False, False, 0)
- self.show_all()
-
- def on_confirm(self, widget):
- buffer = self.text_view.get_buffer()
- start, end = buffer.get_bounds()
- text = buffer.get_text(start, end, True).strip()
- if text:
- self.emit("confirmed", text)
- else:
- self.emit("canceled")
-
- def on_cancel(self, widget):
- self.emit("canceled")
-
- def on_key_press(self, widget, event):
- # Check for Escape to cancel.
- if event.keyval == Gdk.KEY_Escape:
- self.emit("canceled")
- return True
-
- # If Return is pressed...
- if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
- state = event.get_state()
- if state & Gdk.ModifierType.SHIFT_MASK:
- # SHIFT+Return: insert a newline.
- buffer = self.text_view.get_buffer()
- cursor_iter = buffer.get_iter_at_mark(buffer.get_insert())
- buffer.insert(cursor_iter, "\n")
- return True # Prevent further handling.
- else:
- # Plain Return: confirm the edit.
- self.on_confirm(widget)
- return True
- return False
-
-
-class KanbanNote(Gtk.EventBox):
- __gsignals__ = {
- "changed": (GObject.SignalFlags.RUN_LAST, None, ()),
- }
-
- def __init__(self, text):
- super().__init__()
- self.text = text
- # Variables to store the click offset for drag preview.
- self.setup_ui()
- self.setup_dnd()
- self.connect("button-press-event", self.on_button_press)
-
- def setup_ui(self):
- self.box = Gtk.Box(name="kanban-note", spacing=4)
- self.label = Gtk.Label(label=self.text)
- self.label.set_line_wrap(True)
- # Wrap long lines.
- self.label.set_line_wrap_mode(Gtk.WrapMode.WORD)
-
- self.delete_btn = Gtk.Button(
- name="kanban-btn", child=Label(name="kanban-btn-neg", markup=icons.trash)
- )
- self.delete_btn.connect("clicked", self.on_delete_clicked)
-
- self.center_btn = CenterBox(orientation="v", start_children=[self.delete_btn])
-
- self.box.pack_start(self.label, True, True, 0)
- self.box.pack_start(self.center_btn, False, False, 0)
- self.add(self.box)
- self.show_all()
-
- def setup_dnd(self):
- self.drag_source_set(
- Gdk.ModifierType.BUTTON1_MASK,
- [Gtk.TargetEntry.new("UTF8_STRING", Gtk.TargetFlags.SAME_APP, 0)],
- Gdk.DragAction.MOVE,
- )
- self.connect("drag-data-get", self.on_drag_data_get)
- self.connect("drag-data-delete", self.on_drag_data_delete)
- # Set up drag-begin to display a preview of the card.
- self.connect("drag-begin", self.on_drag_begin)
-
- def on_button_press(self, widget, event):
- if event.type != Gdk.EventType._2BUTTON_PRESS:
- return True
- self.start_edit()
- return False
-
- def on_drag_begin(self, widget, context):
- surface = createSurfaceFromWidget(self)
- Gtk.drag_set_icon_surface(context, surface)
-
- def on_drag_data_get(self, widget, drag_context, data, info, time):
- data.set_text(self.label.get_text(), -1)
-
- def on_drag_data_delete(self, widget, drag_context):
- self.get_parent().destroy()
-
- def on_delete_clicked(self, button):
- self.get_parent().destroy()
-
- def start_edit(self):
- row = self.get_parent()
- editor = InlineEditor(self.label.get_text())
-
- def on_confirmed(editor, text):
- self.label.set_text(text)
- row.remove(editor)
- row.add(self)
- row.show_all()
- self.emit("changed")
-
- def on_canceled(editor):
- row.remove(editor)
- row.add(self)
- row.show_all()
-
- editor.connect("confirmed", on_confirmed)
- editor.connect("canceled", on_canceled)
-
- row.remove(self)
- row.add(editor)
- row.show_all()
- # Usamos un retardo de 50 milisegundos:
- GLib.timeout_add(50, lambda: (editor.text_view.grab_focus(), False))
-
-
-class KanbanColumn(Gtk.Frame):
- __gsignals__ = {
- "changed": (GObject.SignalFlags.RUN_LAST, None, ()),
- }
-
- def __init__(self, title):
- super().__init__(name="kanban-column")
- self.title = title
- self.setup_ui()
- self.setup_dnd()
- self.set_hexpand(True)
- self.set_vexpand(True)
-
- def setup_ui(self):
- self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
- self.listbox = Gtk.ListBox()
- self.listbox.set_selection_mode(Gtk.SelectionMode.NONE)
-
- self.add_btn = Gtk.Button(
- name="kanban-btn-add",
- child=Label(name="kanban-btn-label", markup=icons.add),
- )
- header = CenterBox(
- name="kanban-header",
- center_children=[Label(name="column-header", label=self.title)],
- end_children=[self.add_btn],
- )
- self.box.pack_start(header, False, False, 0)
-
- self.add_btn.connect("clicked", self.on_add_clicked)
-
- scrolled = Gtk.ScrolledWindow(name="kanban-scroll")
- scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
- scrolled.add(self.listbox)
- scrolled.set_vexpand(True)
-
- self.box.pack_start(scrolled, True, True, 0)
- self.box.pack_start(self.add_btn, False, False, 0)
- self.add(self.box)
- self.show_all()
-
- def setup_dnd(self):
- self.listbox.drag_dest_set(
- Gtk.DestDefaults.ALL,
- [Gtk.TargetEntry.new("UTF8_STRING", Gtk.TargetFlags.SAME_APP, 0)],
- Gdk.DragAction.MOVE,
- )
-
- self.listbox.connect("drag-data-received", self.on_drag_data_received)
- self.listbox.connect("drag-motion", self.on_drag_motion)
- self.listbox.connect("drag-leave", self.on_drag_leave)
-
- def on_add_clicked(self, button):
- editor = InlineEditor()
- row = Gtk.ListBoxRow(name="kanban-row")
- row.add(editor)
- self.listbox.add(row)
- self.listbox.show_all()
- editor.text_view.grab_focus()
-
- def on_confirmed(editor, text):
- note = KanbanNote(text)
- note.connect("changed", lambda x: self.emit("changed"))
- row.remove(editor)
- row.add(note)
- self.listbox.show_all()
- self.emit("changed") # Emit on add
-
- def on_canceled(editor):
- row.destroy()
-
- editor.connect("confirmed", on_confirmed)
- editor.connect("canceled", on_canceled)
-
- def add_note(self, text, suppress_signal=False):
- note = KanbanNote(text)
- note.connect("changed", lambda x: self.emit("changed"))
- row = Gtk.ListBoxRow(name="kanban-row")
- row.add(note)
- row.connect("destroy", lambda x: self.emit("changed"))
- self.listbox.add(row)
- self.listbox.show_all()
- if not suppress_signal:
- self.emit("changed")
-
- def get_notes(self):
- return [
- row.get_children()[0].label.get_text()
- for row in self.listbox.get_children()
- if isinstance(row.get_children()[0], KanbanNote)
- ]
-
- def clear_notes(self, suppress_signal=False):
- for row in self.listbox.get_children():
- row.destroy()
- if not suppress_signal:
- self.emit("changed")
-
- def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
- text = data.get_text()
- if text:
- row = self.listbox.get_row_at_y(y)
- new_note = KanbanNote(text)
- new_note.connect("changed", lambda x: self.emit("changed"))
- new_row = Gtk.ListBoxRow(name="kanban-row")
- new_row.add(new_note)
- new_row.connect("destroy", lambda x: self.emit("changed"))
-
- if row:
- self.listbox.insert(new_row, row.get_index())
- else:
- self.listbox.add(new_row)
-
- self.listbox.show_all()
- drag_context.finish(True, False, time)
- self.emit("changed") # Emit on move
-
- def on_drag_motion(self, widget, drag_context, x, y, time):
- Gdk.drag_status(drag_context, Gdk.DragAction.MOVE, time)
- return True
-
- def on_drag_leave(self, widget, drag_context, time):
- widget.get_parent().get_parent().drag_unhighlight()
-
-
-class Kanban(Gtk.Box):
- STATE_FILE = Path(os.path.expanduser("~/.local/state/modus/kanban.json"))
-
- def __init__(self):
- super().__init__(name="kanban")
-
- self.grid = Gtk.Grid(column_spacing=4, column_homogeneous=True)
- self.grid.set_vexpand(True)
- self.add(self.grid)
-
- self.columns = [
- KanbanColumn("To Do"),
- KanbanColumn("In Progress"),
- KanbanColumn("Done"),
- ]
-
- for i, column in enumerate(self.columns):
- self.grid.attach(column, i, 0, 1, 1)
- column.connect("changed", lambda x: self.save_state())
-
- self.load_state()
- self.show_all()
-
- def save_state(self):
- state = {
- "columns": [
- {"title": col.title, "notes": col.get_notes()} for col in self.columns
- ]
- }
- try:
- with open(self.STATE_FILE, "w") as f:
- json.dump(state, f, indent=2)
- except Exception as e:
- print(f"Error saving state: {e}")
-
- def load_state(self):
- try:
- with open(self.STATE_FILE, "r") as f:
- state = json.load(f)
- for col_data in state["columns"]:
- for column in self.columns:
- if column.title == col_data["title"]:
- column.clear_notes(suppress_signal=True)
- for note_text in col_data["notes"]:
- column.add_note(note_text, suppress_signal=True)
- break
- except FileNotFoundError:
- pass
- except Exception as e:
- print(f"Error loading state: {e}")
diff --git a/modules/launcher/components/notification_center.py b/modules/launcher/components/notification_center.py
deleted file mode 100644
index 86ade48..0000000
--- a/modules/launcher/components/notification_center.py
+++ /dev/null
@@ -1,409 +0,0 @@
-from fabric.notifications import Notification
-from services import notification_service
-from fabric.widgets.box import Box
-from fabric.widgets.button import Button
-from fabric.widgets.label import Label
-from fabric.widgets.image import Image
-from fabric.widgets.scrolledwindow import ScrolledWindow
-from fabric.widgets.revealer import Revealer
-from loguru import logger
-import utils.icons as icons
-from gi.repository import GdkPixbuf, GLib
-from utils import CustomImage
-import os
-from gi.repository import Gtk
-from fabric.widgets.centerbox import CenterBox
-
-
-class NotificationCenter(Box):
- def __init__(self, **kwargs):
- super().__init__(
- name="notification-center",
- visible=False,
- all_visible=False,
- **kwargs,
- )
-
- self.launcher = kwargs["launcher"]
- self.notification_service = notification_service
-
- # Main container for notifications
- self.notifications = Box(
- v_expand=True,
- h_expand=True,
- orientation="v",
- spacing=8,
- )
- self.header_switch = Gtk.Switch(name="matugen-switcher")
- self.header_switch.set_vexpand(False)
- self.header_switch.set_valign(Gtk.Align.CENTER)
- self.header_switch.set_active(self.notification_service.dont_disturb)
- self.do_not_disturb_enabled = self.notification_service.dont_disturb
- self.header_switch.connect("notify::active", self.on_do_not_disturb_changed)
- self.dnd_label = Label(name="dnd-label", markup=icons.notifications_off)
- # Header with clear all button
- header = CenterBox(
- h_expand=True,
- start_children=[self.header_switch, self.dnd_label],
- center_children=[
- Label(
- label="Notifications",
- name="nhh",
- )
- ],
- end_children=[
- Button(
- name="nhh-button",
- child=Label(name="nhh-button-label", markup=icons.trash),
- on_clicked=lambda *args: self.clear_all_notifications(),
- )
- ],
- )
-
- self.scrolledwindow = ScrolledWindow(
- name="scrolled-window",
- child=self.notifications,
- min_content_size=(400, 300),
- max_content_size=(400, 300),
- v_expand=True,
- h_expand=True,
- )
-
- # Main container
- main_container = Box(
- v_expand=True,
- h_expand=True,
- orientation="v",
- children=[header, self.scrolledwindow],
- )
-
- self.add(main_container)
-
- # Connect to notification service signals
- self.notification_service.connect(
- "notification-added", self.on_new_notification
- )
- self.notification_service.connect("clear_all", self.on_clear_all)
-
- # Load existing notifications
- self.load_notifications()
-
- def load_notifications(self):
- """Load existing notifications from the service."""
- notifications = self.notification_service.get_deserialized()
- if not notifications:
- # Create a container box to better center the message
- container = Box(
- name="no-notifications-container",
- orientation="v",
- h_align="center",
- v_align="center",
- h_expand=True,
- v_expand=True,
- )
-
- # Show a message if no notifications
- label = Label(
- name="no-notifications-label",
- markup=icons.notifications_clear,
- v_align="fill",
- h_align="fill",
- v_expand=True,
- h_expand=True,
- justification="center",
- )
-
- container.add(label)
- self.notifications.add(container)
- return
-
- for notification in notifications:
- self.add_notification(notification)
-
- def toggle_notification_body(self, revealer, chevron_label):
- """Toggle the visibility of notification body."""
- is_revealed = revealer.get_reveal_child()
- revealer.set_reveal_child(not is_revealed)
- # Update chevron icon
- chevron_label.set_markup(
- icons.chevron_up if not is_revealed else icons.chevron_down
- )
-
- def add_notification(self, notification: Notification):
- """Add a notification to the center."""
- # Remove empty state container if it exists
- for child in self.notifications.get_children():
- if child.get_name() == "no-notifications-container":
- self.notifications.remove(child)
- break
-
- # Get timestamp from notification object
- timestamp = getattr(notification, 'timestamp', GLib.get_real_time() / 1000000)
- time_str = GLib.DateTime.new_from_unix_local(timestamp).format("%I:%M %p")
-
- # Create the notification header (summary) box
- header_box = Box(
- name="notification-header",
- # h_expand=True,
- spacing=8,
- children=[
- Box(
- orientation="h",
- h_expand=True,
- spacing=8,
- style="min-width: 0;",
- children=[
- # Summary
- Label(
- name="notification-app-name",
- markup=notification.app_name + " | ",
- h_align="start",
- ellipsization="end",
- ),
- Label(
- markup=notification.summary.replace("\n", " "),
- h_align="start",
- ellipsization="end",
- ),
- Label(
- name="notification-time",
- markup=f" | {time_str}",
- h_align="end",
- style="color: var(--outline);",
- ),
- ],
- ),
- # Add chevron button if there's content to show
- *(
- [
- Button(
- name="chevron-button",
- child=Label(
- name="chevron-label", markup=icons.chevron_down
- ),
- )
- ]
- if notification.body or notification.image_pixbuf
- else []
- ),
- Button(
- name="notif-close-button",
- child=Label(name="notif-close-label", markup=icons.cancel),
- on_clicked=lambda *args: self.remove_notification(notification),
- ),
- ],
- )
-
- # Create content box that will be revealed
- # Create content box in vertical layout for body + actions
- content_box = Box(
- name="notification-content",
- spacing=8,
- orientation="v",
- )
-
- # Create horizontal box for image and body
- body_box = Box(
- name="notification-body-box",
- spacing=8,
- orientation="h",
- )
-
- # Add image if available
- if notification.image_pixbuf:
- image_box = Box(
- name="notification-image",
- children=[
- CustomImage(
- pixbuf=notification.image_pixbuf.scale_simple(
- 48, 48, GdkPixbuf.InterpType.BILINEAR
- )
- if notification.image_pixbuf
- else self.get_pixbuf(notification.app_icon, 48, 48)
- ),
- ],
- )
- body_box.add(image_box)
-
- # Add body text if available
- if notification.body:
- body_box.add(
- Label(
- markup=notification.body.replace("\n", " "),
- name="notification-body",
- h_align="start",
- ellipsization="end",
- )
- )
-
- # Add body box to content
- content_box.add(body_box)
-
- # Add action buttons if available
- if notification.actions:
- action_box = Box(
- name="notification-action-buttons",
- spacing=4,
- h_expand=True,
- )
- for i, action in enumerate(notification.actions):
- style_class = (
- "start-action"
- if i == 0
- else "end-action"
- if i == len(notification.actions) - 1
- else "middle-action"
- )
- button = Button(
- name="action-button",
- h_expand=True,
- child=Label(name="button-label", label=action.label),
- on_clicked=lambda btn, act=action, notif=notification: [
- act.invoke(),
- self.notification_service.remove_notification(notif.id),
- self.remove_notification(notif)
- ],
- )
- button.add_style_class(style_class)
- action_box.add(button)
- content_box.add(action_box)
-
- # Create revealer for content
- body_revealer = None
- if notification.body or notification.image_pixbuf:
- body_revealer = Revealer(
- name="body-revealer",
- transition_type="slide-down",
- reveal_child=False,
- child=content_box,
- )
-
- # Create main notification box
- notification_box = Box(
- name="notification-box",
- orientation="v",
- h_expand=True,
- style="margin: 4px; padding: 16px; border-radius: 20px; background-color: var(--surface-container-low); min-width: 200px;",
- children=[header_box],
- )
-
- # Add body revealer if exists
- if body_revealer:
- notification_box.add(body_revealer)
- # Get the chevron button from header_box children
- chevron_button = [
- c for c in header_box.get_children() if c.get_name() == "chevron-button"
- ][0]
- chevron_label = chevron_button.get_child()
- chevron_button.connect(
- "clicked",
- lambda *args: self.toggle_notification_body(
- body_revealer, chevron_label
- ),
- )
-
- self.notifications.add(notification_box)
-
- def on_do_not_disturb_changed(self, switch, pspec):
- """Handle Do Not Disturb toggle."""
- self.do_not_disturb_enabled = switch.get_active()
- self.notification_service.dont_disturb = self.do_not_disturb_enabled
-
- def on_new_notification(self, fabric_notif, id):
- """Handle new notification event."""
- notification = fabric_notif.get_notification_from_id(id)
- self.add_notification(notification)
-
- def remove_notification(self, notification: Notification):
- """Remove a notification from the center."""
- self.notification_service.remove_notification(notification.id)
- # Find and remove the notification box
- for child in self.notifications.get_children():
- if child.get_name() == "notification-box":
- # Get the header box
- header_box = child.get_children()[0]
- # Get the text container box
- text_box = header_box.get_children()[0]
- # Get the summary label (second child of text box)
- summary_label = text_box.get_children()[1]
- if summary_label.get_text() == notification.summary:
- self.notifications.remove(child)
- break
-
- def clear_all_notifications(self, *args):
- """Clear all notifications."""
- self.notification_service.clear_all_notifications()
- # Remove all child widgets
- for child in self.notifications.get_children():
- self.notifications.remove(child)
-
- # Add empty state
- container = Box(
- name="no-notifications-container",
- orientation="v",
- h_align="center",
- v_align="center",
- h_expand=True,
- v_expand=True,
- )
-
- label = Label(
- name="no-notifications-label",
- markup=icons.notifications_clear,
- v_align="fill",
- h_align="fill",
- v_expand=True,
- h_expand=True,
- justification="center",
- )
-
- container.add(label)
- self.notifications.add(container)
-
- def on_clear_all(self, *args):
- """Handle clear all event from service."""
- # Remove all child widgets
- for child in self.notifications.get_children():
- self.notifications.remove(child)
-
- # Add empty state
- container = Box(
- name="no-notifications-container",
- orientation="v",
- h_align="center",
- v_align="center",
- h_expand=True,
- v_expand=True,
- )
-
- label = Label(
- name="no-notifications-label",
- markup=icons.notifications_clear,
- v_align="fill",
- h_align="fill",
- v_expand=True,
- h_expand=True,
- justification="center",
- )
-
- container.add(label)
- self.notifications.add(container)
-
- def get_pixbuf(self, icon_path, width, height):
- if icon_path.startswith("file://"):
- icon_path = icon_path[7:]
-
- if not os.path.exists(icon_path):
- logger.warning(f"Icon path does not exist: {icon_path}")
- return None
-
- try:
- pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path)
- return pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
- except Exception as e:
- logger.error(f"Failed to load or scale icon: {e}")
- return None
-
- def open_center(self):
- """Open the notification center."""
- self.show_all()
diff --git a/modules/launcher/components/power.py b/modules/launcher/components/power.py
deleted file mode 100644
index b441e25..0000000
--- a/modules/launcher/components/power.py
+++ /dev/null
@@ -1,88 +0,0 @@
-from fabric.widgets.box import Box
-from fabric.widgets.label import Label
-from fabric.widgets.button import Button
-from fabric.utils.helpers import exec_shell_command_async
-import utils.icons as icons
-
-
-class PowerMenu(Box):
- def __init__(self, **kwargs):
- super().__init__(
- name="power-menu",
- orientation="h",
- spacing=10,
- v_align="center",
- h_align="center",
- v_expand=True,
- h_expand=True,
- visible=True,
- **kwargs,
- )
-
- self.launcher = kwargs["launcher"]
-
- self.btn_lock = Button(
- name="power-menu-button",
- child=Label(name="button-label", markup=icons.lock),
- on_clicked=self.lock,
- )
-
- self.btn_suspend = Button(
- name="power-menu-button",
- child=Label(name="button-label", markup=icons.suspend),
- on_clicked=self.suspend,
- )
-
- self.btn_logout = Button(
- name="power-menu-button",
- child=Label(name="button-label", markup=icons.logout),
- on_clicked=self.logout,
- )
-
- self.btn_reboot = Button(
- name="power-menu-button",
- child=Label(name="button-label", markup=icons.reboot),
- on_clicked=self.reboot,
- )
-
- self.btn_shutdown = Button(
- name="power-menu-button",
- child=Label(name="button-label", markup=icons.shutdown),
- on_clicked=self.poweroff,
- )
-
- self.buttons = [
- self.btn_lock,
- self.btn_suspend,
- self.btn_logout,
- self.btn_reboot,
- self.btn_shutdown,
- ]
-
- for button in self.buttons:
- self.add(button)
-
- self.show_all()
-
- def close_menu(self):
- self.launcher.close()
-
- def lock(self, *args):
- exec_shell_command_async("hyprlock --immediate")
- self.close_menu()
-
- def suspend(self, *args):
- exec_shell_command_async("systemctl suspend")
- self.close_menu()
-
- def logout(self, *args):
- exec_shell_command_async("hyprctl dispatch exit")
- self.close_menu()
-
- def reboot(self, *args):
- exec_shell_command_async("systemctl reboot")
- self.close_menu()
-
- def poweroff(self, *args):
- exec_shell_command_async("systemctl poweroff")
- self.close_menu()
diff --git a/modules/launcher/components/sh.py b/modules/launcher/components/sh.py
deleted file mode 100644
index 6fef79a..0000000
--- a/modules/launcher/components/sh.py
+++ /dev/null
@@ -1,139 +0,0 @@
-import os
-import subprocess
-from typing import List
-from fabric.widgets.box import Box
-from fabric.widgets.button import Button
-from fabric.widgets.entry import Entry
-from fabric.widgets.label import Label
-from fabric.widgets.scrolledwindow import ScrolledWindow
-import config.data as data
-
-BINS = f"{data.CACHE_DIR}/binaries"
-
-
-class Sh(Box):
- def __init__(self, **kwargs):
- super().__init__(
- all_visible=False,
- visible=False,
- **kwargs,
- )
-
- self.launcher = kwargs["launcher"]
- self._arranger_handler: int = 0
- self.shell_command_manager = ShellCommandManager(self)
- self.viewport = None
-
- self.search_entry = Entry(
- name="search-entry",
- placeholder="Search Shell Commands...",
- h_expand=True,
- notify_text=lambda entry, *_: self.handle_search_input(entry.get_text()),
- on_activate=lambda entry, *_: self.handle_search_input(entry.get_text()),
- )
- self.search_entry.props.xalign = 0.5
-
- self.header_box = Box(
- spacing=10,
- orientation="h",
- children=[self.search_entry],
- )
-
- self.launcher_box = Box(
- spacing=10,
- orientation="v",
- h_expand=True,
- children=[self.header_box],
- )
-
- self.add(self.launcher_box)
-
- def open_sh(self):
- if not self.viewport:
- self.viewport = Box(name="viewport", spacing=10, orientation="v")
- self.scrolled_window = ScrolledWindow(
- name="scrolled-window",
- spacing=10,
- h_scrollbar_policy="never",
- v_scrollbar_policy="never",
- # min_content_size=(-1, -1),
- child=self.viewport,
- )
- self.launcher_box.add(self.scrolled_window)
-
- self.viewport.children = []
- self.shell_command_manager.show_shell_commands(self.viewport, "")
-
- self.viewport.show()
- self.search_entry.grab_focus()
-
- def handle_search_input(self, text: str):
- self.shell_command_manager.show_shell_commands(self.viewport, text)
-
-
-class ShellCommandManager:
- def __init__(self, launcher):
- self.launcher = launcher
- self.binaries = set()
- self.reload_binaries()
-
- def reload_binaries(self):
- paths = os.getenv("PATH", "").split(":")
- self.binaries = set()
-
- for path in paths:
- self.binaries.update(self.list_binaries_in_path(path))
-
- with open(BINS, "w") as f:
- f.write("\n".join(self.binaries))
-
- def list_binaries_in_path(self, path):
- if not os.path.exists(path):
- return []
- try:
- return [
- entry
- for entry in os.listdir(path)
- if os.access(os.path.join(path, entry), os.X_OK)
- ]
- except OSError:
- return []
-
- def show_shell_commands(self, viewport, search_query: str):
- if not search_query:
- return
-
- viewport.children = []
-
- results = self.query_binaries(search_query)
- self.display_results(viewport, results)
-
- def query_binaries(self, filter_str: str) -> List[str]:
- filter_str = filter_str.lower()
- filtered_bins = (cmd for cmd in self.binaries if filter_str in cmd)
- return list(dict.fromkeys(filtered_bins))[:8]
-
- def display_results(self, viewport, results: List[str]):
- for command in results:
- button = Button(
- child=Label(
- label=command,
- v_align="center",
- h_align="start",
- ),
- on_clicked=lambda _, cmd=command: self.execute_command(cmd),
- name="app-slot-button",
- )
- viewport.add(button)
-
- def close_menu(self):
- self.launcher.close()
-
- def execute_command(self, cmd: str):
- try:
- output = subprocess.check_output(cmd, shell=True, text=True)
- print(output)
- except subprocess.CalledProcessError as e:
- print(e.output)
- finally:
- self.close_menu()
diff --git a/modules/launcher/components/tmux.py b/modules/launcher/components/tmux.py
deleted file mode 100644
index 583e477..0000000
--- a/modules/launcher/components/tmux.py
+++ /dev/null
@@ -1,540 +0,0 @@
-import subprocess
-import os
-from fabric.widgets.box import Box
-from fabric.widgets.label import Label
-from fabric.widgets.button import Button
-from fabric.widgets.entry import Entry
-from fabric.widgets.scrolledwindow import ScrolledWindow
-from fabric.utils import idle_add, remove_handler, exec_shell_command_async
-from gi.repository import GLib, Gdk, Gtk
-import utils.icons as icons
-import config.data as data
-
-class TmuxManager(Box):
- def __init__(self, **kwargs):
- super().__init__(
- name="tmux-manager",
- visible=False,
- all_visible=False,
- **kwargs,
- )
-
- self.launcher = kwargs["launcher"]
- self.selected_index = -1 # Track the selected item index
-
- self._arranger_handler: int = 0
-
- self.viewport = Box(name="viewport", spacing=4, orientation="v")
- self.session_name_entry = Entry(
- name="session-name-entry",
- placeholder="Create Tmux Session...",
- h_expand=True,
- on_activate=lambda entry, *_: self.create_session(entry.get_text()),
- on_key_press_event=self.on_entry_key_press,
- )
- self.session_name_entry.props.xalign = 0.5
- self.scrolled_window = ScrolledWindow(
- name="scrolled-window",
- spacing=10,
- min_content_size=(450, 105),
- max_content_size=(450, 105),
- child=self.viewport,
- )
-
- self.header_box = Box(
- name="header_box",
- spacing=10,
- orientation="h",
- children=[
- Button(
- name="new-session-button",
- child=Label(name="new-session-label", markup=icons.add),
- tooltip_text="Create New Session",
- on_clicked=lambda *_: self.create_session(self.session_name_entry.get_text()),
- ),
- self.session_name_entry,
- ],
- )
-
- self.tmux_box = Box(
- name="tmux-box",
- spacing=10,
- h_expand=True,
- orientation="v",
- children=[
- self.header_box,
- self.scrolled_window,
- ],
- )
-
- self.add(self.tmux_box)
- self.show_all()
-
- def close_manager(self):
- """Close the tmux manager"""
- self.viewport.children = []
- self.selected_index = -1 # Reset selection
- self.launcher.close()
-
- def open_manager(self):
- """Open the tmux manager and refresh sessions"""
- self.viewport.set_visible(True)
- self.refresh_sessions()
- self.session_name_entry.set_text("")
- self.session_name_entry.grab_focus()
-
- def refresh_sessions(self):
- """Get tmux sessions and populate the viewport"""
- remove_handler(self._arranger_handler) if self._arranger_handler else None
- self.viewport.children = []
- self.selected_index = -1 # Clear selection when viewport changes
-
- # Get tmux sessions
- sessions = self.get_tmux_sessions()
- if not sessions:
- # Create a container box to better center the message
- container = Box(
- name="no-tmux-container",
- orientation="v",
- h_align="center",
- v_align="center",
- h_expand=True,
- v_expand=True
- )
-
- # Show a message if no sessions
- label = Label(
- name="no-tmux",
- markup=icons.terminal,
- h_align="center",
- v_align="center",
- )
-
- container.add(label)
-
- self.viewport.add(container)
- return
-
- # Add session slots to viewport
- for session in sessions:
- self.viewport.add(self.create_session_slot(session))
-
- def get_tmux_sessions(self):
- """Get list of tmux sessions"""
- try:
- result = subprocess.run(
- ["tmux", "list-sessions", "-F", "#{session_name}"],
- capture_output=True,
- text=True
- )
- if result.returncode == 0:
- return [s.strip() for s in result.stdout.strip().split('\n') if s.strip()]
- return []
- except Exception as e:
- print(f"Error getting tmux sessions: {e}")
- return []
-
- def create_session_slot(self, session_name):
- """Create a button for a tmux session"""
- # Create an entry for inline editing (initially hidden)
- name_entry = Entry(
- name="session-name-entry",
- text=session_name,
- visible=False,
- on_activate=lambda entry, *_: self.finish_rename(button, session_name, entry),
- on_key_press_event=self.on_rename_key_press,
- )
-
- # Create the label showing the session name
- name_label = Label(
- name="app-label",
- label=session_name,
- ellipsization="end",
- v_align="center",
- h_align="center",
- )
-
- # Session slot content box
- slot_box = Box(
- name="slot-box",
- orientation="h",
- spacing=10,
- children=[
- Label(
- name="tmux-icon",
- markup=icons.terminal, # Use existing terminal icon
- ),
- name_label,
- name_entry,
- ],
- )
-
- button = Button(
- name="slot-button", # reuse existing CSS styling
- child=slot_box,
- tooltip_text=f"Attach to session: {session_name}",
- on_clicked=lambda *_: self.attach_to_session(session_name),
- can_focus=True, # Ensure the button can receive focus
- )
-
- # Add double-click handler to start renaming
- button.connect("button-press-event", self.on_session_click, session_name, name_label, name_entry)
-
- # Add key press handler for 'r' to rename
- button.connect("key-press-event", self.on_slot_key_press, session_name, name_label, name_entry)
-
- # Store reference to entry and label in button for later access
- button.name_entry = name_entry
- button.name_label = name_label
- button.session_name = session_name
-
- return button
-
- def on_session_click(self, button, event, session_name, label, entry):
- """Handle clicks on session buttons"""
- # Handle double-click to rename
- if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS and event.button == 1:
- self.start_rename(button, session_name, label, entry)
- return True
- # Handle right click for context menu
- elif event.button == 3:
- menu = Gtk.Menu()
-
- # Rename option
- rename_item = Gtk.MenuItem(label="Rename")
- rename_item.connect("activate", lambda _: self.start_rename(button, session_name, label, entry))
- menu.append(rename_item)
-
- # Kill option
- kill_item = Gtk.MenuItem(label="Kill Session")
- kill_item.connect("activate", lambda _: self.kill_session(session_name))
- menu.append(kill_item)
-
- menu.show_all()
- menu.popup_at_pointer(event)
- return True
-
- return False
-
- def start_rename(self, button, session_name, label, entry):
- """Start inline renaming of a session"""
- # Hide label, show entry
- label.set_visible(False)
- entry.set_visible(True)
-
- # Focus entry and select all text
- entry.grab_focus()
- entry.select_region(0, -1)
-
- # Mark button as being edited
- button.get_style_context().add_class("editing")
-
- def finish_rename(self, button, old_name, entry):
- """Finish renaming a session"""
- new_name = entry.get_text().strip()
-
- # Only rename if the name changed and isn't empty
- if new_name and new_name != old_name:
- self.rename_session(old_name, new_name)
-
- # Reset UI state
- self.cancel_rename(button)
-
- def cancel_rename(self, button):
- """Cancel renaming operation"""
- # Restore original view
- button.name_entry.set_visible(False)
- button.name_label.set_visible(True)
-
- # Remove editing style
- button.get_style_context().remove_class("editing")
-
- # Return focus to session name entry
- self.session_name_entry.grab_focus()
-
- def on_rename_key_press(self, entry, event):
- """Handle key presses in the rename entry"""
- if event.keyval == Gdk.KEY_Escape:
- # Find the parent button
- parent = entry.get_parent()
- while parent and not isinstance(parent, Button):
- parent = parent.get_parent()
-
- if parent:
- self.cancel_rename(parent)
- return True
-
- return False
-
- def on_session_right_click(self, button, event, session_name):
- """Handle right-click on a session button to show context menu"""
- if event.button == 3: # Right click
- menu = Gtk.Menu()
-
- # Rename option
- rename_item = Gtk.MenuItem(label="Rename")
- rename_item.connect("activate", lambda _: self.start_rename(
- button,
- session_name,
- button.name_label,
- button.name_entry
- ))
- menu.append(rename_item)
-
- # Kill option
- kill_item = Gtk.MenuItem(label="Kill Session")
- kill_item.connect("activate", lambda _: self.kill_session(session_name))
- menu.append(kill_item)
-
- menu.show_all()
- menu.popup_at_pointer(event)
- return True
-
- return False
-
- def on_entry_key_press(self, widget, event):
- """Handle key press events in the entry"""
- if event.keyval == Gdk.KEY_Escape:
- self.close_manager()
- return True
-
- # Custom navigation with UP/DOWN keys removed
- return False
-
- def scroll_to_selected(self, button):
- """Scroll to ensure the selected button is visible"""
- def scroll():
- adj = self.scrolled_window.get_vadjustment()
- alloc = button.get_allocation()
- if alloc.height == 0:
- return False # Retry if allocation isn't ready
-
- y = alloc.y
- height = alloc.height
- page_size = adj.get_page_size()
- current_value = adj.get_value()
-
- # Calculate visible boundaries
- visible_top = current_value
- visible_bottom = current_value + page_size
-
- if y < visible_top:
- # Item above viewport - align to top
- adj.set_value(y)
- elif y + height > visible_bottom:
- # Item below viewport - align to bottom
- new_value = y + height - page_size
- adj.set_value(new_value)
- # No action if already fully visible
- return False
- GLib.idle_add(scroll)
-
- def create_session(self, session_name):
- """Create a new tmux session"""
- if not session_name:
- # Get existing session names
- existing_sessions = self.get_tmux_sessions()
-
- # Find the next available number
- counter = 0
- while str(counter) in existing_sessions:
- counter += 1
-
- session_name = str(counter)
-
- try:
- # Clean the session name (replace spaces with underscores)
- clean_name = session_name.strip().replace(" ", "_")
-
- # Create session
- subprocess.run(
- ["tmux", "new-session", "-d", "-s", clean_name],
- check=True
- )
-
- # Refresh the session list
- self.refresh_sessions()
-
- # Clear entry
- self.session_name_entry.set_text("")
-
- # Launch a terminal and attach to this session
- terminal_cmd = self.get_terminal_command(f"tmux attach-session -t {clean_name}")
- exec_shell_command_async(terminal_cmd)
-
- # Close manager
- self.close_manager()
-
- except Exception as e:
- print(f"Error creating tmux session: {e}")
-
- def attach_to_session(self, session_name):
- """Attach to an existing tmux session"""
- try:
- # Launch a terminal and attach to this session
- terminal_cmd = self.get_terminal_command(f"tmux attach-session -t {session_name}")
- exec_shell_command_async(terminal_cmd)
- self.close_manager()
- except Exception as e:
- print(f"Error attaching to tmux session: {e}")
-
- def get_terminal_command(self, cmd):
- """Get terminal command based on configured terminal or available terminals"""
- # First try to use the configured terminal command
- if hasattr(data, 'TERMINAL_COMMAND') and data.TERMINAL_COMMAND:
- parts = data.TERMINAL_COMMAND.split()
- terminal = parts[0]
-
- try:
- # Check if the configured terminal is available
- subprocess.run(["which", terminal], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- return f"{data.TERMINAL_COMMAND} {cmd}"
- except subprocess.CalledProcessError:
- # If configured terminal is not available, fall back to defaults
- pass
-
- # Fallback to checking available terminals
- terminals = [
- ("kitty", f"kitty -e {cmd}"),
- ("alacritty", f"alacritty -e {cmd}"),
- ("foot", f"foot {cmd}"),
- ("gnome-terminal", f"gnome-terminal -- {cmd}"),
- ("konsole", f"konsole -e {cmd}"),
- ("xfce4-terminal", f"xfce4-terminal -e '{cmd}'"),
- ]
-
- for term, term_cmd in terminals:
- try:
- # Check if terminal is available
- subprocess.run(["which", term], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- return term_cmd
- except subprocess.CalledProcessError:
- continue
-
- # Default fallback
- return f"kitty -e {cmd}"
-
- def rename_session_dialog(self, old_name):
- """Show dialog to rename a session"""
- dialog = Gtk.Dialog(
- title="Rename Session",
- transient_for=None,
- flags=0
- )
-
- dialog.add_buttons(
- Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
- Gtk.STOCK_OK, Gtk.ResponseType.OK
- )
-
- content_area = dialog.get_content_area()
- entry = Gtk.Entry()
- entry.set_text(old_name)
- entry.set_activates_default(True)
- content_area.add(entry)
-
- dialog.set_default_response(Gtk.ResponseType.OK)
- dialog.show_all()
-
- response = dialog.run()
- if response == Gtk.ResponseType.OK:
- new_name = entry.get_text()
- if new_name and new_name != old_name:
- self.rename_session(old_name, new_name)
-
- dialog.destroy()
-
- def rename_session(self, old_name, new_name):
- """Rename a tmux session"""
- try:
- # Clean the session name (replace spaces with underscores)
- clean_name = new_name.strip().replace(" ", "_")
-
- # Rename session
- subprocess.run(
- ["tmux", "rename-session", "-t", old_name, clean_name],
- check=True
- )
-
- # Refresh the session list
- self.refresh_sessions()
-
- except Exception as e:
- print(f"Error renaming tmux session: {e}")
-
- def kill_session(self, session_name):
- """Kill a tmux session"""
- try:
- # Kill session
- subprocess.run(
- ["tmux", "kill-session", "-t", session_name],
- check=True
- )
-
- # Refresh the session list
- self.refresh_sessions()
-
- # Close the launcher after killing session
- self.close_manager()
-
- except Exception as e:
- print(f"Error killing tmux session: {e}")
-
- # Add new method to handle key presses on session slots
- def on_slot_key_press(self, button, event, session_name, label, entry):
- """Handle key presses on session buttons"""
- # Print debugging info
- print(f"Key pressed: {event.keyval}, State: {event.state}")
-
- # Check if 'r' key was pressed for renaming
- if event.keyval == Gdk.KEY_r:
- self.start_rename(button, session_name, label, entry)
- return True
- # Check for 'K' (capital K) which indicates Shift is pressed
- elif event.keyval == Gdk.KEY_K:
- print("Shift+K detected - killing session without confirmation")
- self.kill_session(session_name)
- return True
- # Check for lowercase 'k'
- elif event.keyval == Gdk.KEY_k:
- print("Regular k detected - showing confirmation")
- self.show_kill_confirmation_menu(button, session_name)
- return True
- # Check if Delete key was pressed for killing session
- elif event.keyval == Gdk.KEY_Delete:
- self.show_kill_confirmation_menu(button, session_name)
- return True
- return False
-
- def show_kill_confirmation_menu(self, button, session_name):
- """Show a confirmation menu for killing a session"""
- menu = Gtk.Menu()
-
- # Confirmation message as a disabled menu item
- msg_item = Gtk.MenuItem(label=f"Kill session '{session_name}'?")
- msg_item.set_sensitive(False)
- menu.append(msg_item)
-
- # Separator
- menu.append(Gtk.SeparatorMenuItem())
-
- # Confirm option
- confirm_item = Gtk.MenuItem(label="Confirm")
- confirm_item.connect("activate", lambda _: self.kill_session(session_name))
- menu.append(confirm_item)
-
- # Cancel option
- cancel_item = Gtk.MenuItem(label="Cancel")
- # Close launcher on cancel
- cancel_item.connect("activate", lambda _: self.close_manager())
- menu.append(cancel_item)
-
- menu.show_all()
-
- # Show the menu positioned at the button
- menu.popup_at_widget(
- button,
- Gdk.Gravity.SOUTH_WEST,
- Gdk.Gravity.NORTH_WEST,
- None
- )
\ No newline at end of file
diff --git a/modules/launcher/components/tools.py b/modules/launcher/components/tools.py
deleted file mode 100644
index d04c39d..0000000
--- a/modules/launcher/components/tools.py
+++ /dev/null
@@ -1,370 +0,0 @@
-from fabric.widgets.box import Box
-from fabric.widgets.label import Label
-from fabric.widgets.button import Button
-from fabric.utils.helpers import exec_shell_command_async, get_relative_path
-import utils.icons as icons
-from gi.repository import Gdk, GLib
-import os
-import subprocess
-import threading
-
-
-CAPTURE_SCRIPT = get_relative_path("../../../config/scripts/capture.sh")
-GAMEMODE_SCRIPT = get_relative_path("../../../config/scripts/gamemode.sh")
-
-
-class Toolbox(Box):
- def __init__(self, **kwargs):
- super().__init__(
- name="toolbox",
- orientation="h",
- spacing=4,
- v_align="center",
- h_align="center",
- visible=True,
- **kwargs,
- )
-
- self.launcher = kwargs["launcher"]
-
- self.btn_ssregion = Button(
- name="toolbox-button",
- child=Label(name="button-label", markup=icons.ssregion),
- on_clicked=self.ssregion,
- h_expand=False,
- v_expand=False,
- h_align="center",
- v_align="center",
- )
- # Enable keyboard focus and connect events
- self.btn_ssregion.set_can_focus(True)
- self.btn_ssregion.connect("button-press-event", self.on_ssregion_click)
- self.btn_ssregion.connect("key-press-event", self.on_ssregion_key)
-
- self.btn_ssfull = Button(
- name="toolbox-button",
- child=Label(name="button-label", markup=icons.ssfull),
- on_clicked=self.ssfull,
- h_expand=False,
- v_expand=False,
- h_align="center",
- v_align="center",
- )
- # Enable keyboard focus and connect events
- self.btn_ssfull.set_can_focus(True)
- self.btn_ssfull.connect("button-press-event", self.on_ssfull_click)
- self.btn_ssfull.connect("key-press-event", self.on_ssfull_key)
-
- self.btn_screenrecord = Button(
- name="toolbox-button",
- child=Label(name="button-label", markup=icons.screenrecord),
- on_clicked=self.screenrecord,
- h_expand=False,
- v_expand=False,
- h_align="center",
- v_align="center",
- )
-
- self.btn_ocr = Button(
- name="toolbox-button",
- child=Label(name="button-label", markup=icons.ocr),
- on_clicked=self.ocr,
- h_expand=False,
- v_expand=False,
- h_align="center",
- v_align="center",
- )
-
- self.btn_color = Button(
- name="toolbox-button",
- tooltip_text="Color Picker\nLeft Click: HEX\nMiddle Click: HSV\nRight Click: RGB\n\nKeyboard:\nEnter: HEX\nShift+Enter: RGB\nCtrl+Enter: HSV",
- child=Label(name="button-bar-label", markup=icons.colorpicker),
- h_expand=False,
- v_expand=False,
- h_align="center",
- v_align="center",
- )
-
- self.btn_gamemode = Button(
- name="toolbox-button",
- child=Label(name="button-label", markup=icons.gamemode),
- on_clicked=self.gamemode,
- h_expand=False,
- v_expand=False,
- h_align="center",
- v_align="center",
- )
-
- # Enable keyboard focus for the colorpicker button.
- self.btn_color.set_can_focus(True)
- # Connect both mouse and keyboard events.
- self.btn_color.connect("button-press-event", self.colorpicker)
- self.btn_color.connect("key_press_event", self.colorpicker_key)
-
- self.btn_emoji = Button(
- name="toolbox-button",
- child=Label(name="button-label", markup=icons.emoji),
- on_clicked=self.emoji,
- h_expand=False,
- v_expand=False,
- h_align="center",
- v_align="center",
- )
-
- self.btn_screenshots_folder = Button(
- name="toolbox-button",
- child=Label(name="button-label", markup=icons.screenshots),
- on_clicked=self.open_screenshots_folder,
- h_expand=False,
- v_expand=False,
- h_align="center",
- v_align="center",
- )
-
- self.btn_recordings_folder = Button(
- name="toolbox-button",
- child=Label(name="button-label", markup=icons.recordings),
- on_clicked=self.open_recordings_folder,
- h_expand=False,
- v_expand=False,
- h_align="center",
- v_align="center",
- )
-
- self.buttons = [
- self.btn_ssregion,
- self.btn_ssfull,
- self.btn_screenshots_folder,
- Box(
- name="tool-sep",
- h_expand=False,
- v_expand=False,
- h_align="center",
- v_align="center",
- ),
- self.btn_screenrecord,
- self.btn_recordings_folder,
- Box(
- name="tool-sep",
- h_expand=False,
- v_expand=False,
- h_align="center",
- v_align="center",
- ),
- self.btn_ocr,
- self.btn_color,
- Box(
- name="tool-sep",
- h_expand=False,
- v_expand=False,
- h_align="center",
- v_align="center",
- ),
- self.btn_gamemode,
- self.btn_emoji,
- ]
-
- for button in self.buttons:
- self.add(button)
-
- self.show_all()
-
- # Start polling for process state every second.
- self.recorder_timer_id = GLib.timeout_add_seconds(
- 1, self.update_screenrecord_state
- )
-
- def close_menu(self):
- self.launcher.close()
-
- # Action methods
- def ssfull(self, *args):
- exec_shell_command_async(f"bash {CAPTURE_SCRIPT} screenshot p")
- self.close_menu()
-
- def on_ssfull_click(self, button, event):
- if event.type == Gdk.EventType.BUTTON_PRESS:
- if event.button == 1: # Left click
- self.ssfull()
- elif event.button == 3: # Right click
- exec_shell_command_async(
- f"bash {CAPTURE_SCRIPT} screenshot p mockup")
- self.close_menu()
- return True
- return False
-
- def on_ssfull_key(self, widget, event):
- if event.keyval in {Gdk.KEY_Return, Gdk.KEY_KP_Enter}:
- modifiers = event.get_state()
- if modifiers & Gdk.ModifierType.SHIFT_MASK:
- exec_shell_command_async(
- f"bash {CAPTURE_SCRIPT} screenshot p mockup")
- self.close_menu()
- else:
- self.ssfull()
- return True
- return False
-
- def ssregion(self, *args):
- exec_shell_command_async(f"bash {CAPTURE_SCRIPT} screenshot sf")
- self.close_menu()
-
- def on_ssregion_click(self, button, event):
- if event.type == Gdk.EventType.BUTTON_PRESS:
- if event.button == 1: # Left click
- self.ssregion()
- elif event.button == 3: # Right click
- exec_shell_command_async(
- f"bash {CAPTURE_SCRIPT} screenshot sf mockup")
- self.close_menu()
- return True
- return False
-
- def on_ssregion_key(self, widget, event):
- if event.keyval in {Gdk.KEY_Return, Gdk.KEY_KP_Enter}:
- modifiers = event.get_state()
- if modifiers & Gdk.ModifierType.SHIFT_MASK:
- exec_shell_command_async(
- f"bash {CAPTURE_SCRIPT} screenshot sf mockup")
- self.close_menu()
- else:
- self.ssregion()
- return True
- return False
-
- def screenrecord(self, *args):
- # Launch screenrecord script in detached mode so that it remains running independently of this program.
- exec_shell_command_async(
- f"bash -c 'nohup bash {CAPTURE_SCRIPT} record > /dev/null 2>&1 & disown'"
- )
- self.close_menu()
-
- def ocr(self, *args):
- exec_shell_command_async(f"bash {CAPTURE_SCRIPT} ocr sf")
- self.close_menu()
-
- def gamemode(self, *args):
- exec_shell_command_async(f"bash {GAMEMODE_SCRIPT}")
- self.gamemode_check()
- self.close_menu()
-
- def gamemode_check(self):
- def check():
- try:
- result = subprocess.run(
- f"bash {GAMEMODE_SCRIPT} check",
- shell=True,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- )
- enabled = result.stdout == b"t\n"
- except Exception:
- enabled = False
-
- def update_ui():
- if enabled:
- self.btn_gamemode.get_child().set_markup(icons.gamemode_off)
- else:
- self.btn_gamemode.get_child().set_markup(icons.gamemode)
- return False
-
- GLib.idle_add(update_ui)
- return False
-
- GLib.idle_add(lambda: threading.Thread(target=check).start())
- return True
-
- def ssregion(self, *args):
- exec_shell_command_async(f"bash {CAPTURE_SCRIPT} screenshot sf")
- self.close_menu()
-
- def colorpicker(self, button, event):
- if event.type == Gdk.EventType.BUTTON_PRESS:
- cmd = {
- 1: "-hex", # Left click
- 2: "-hsv", # Middle click
- 3: "-rgb", # Right click
- }.get(event.button)
-
- if cmd:
- exec_shell_command_async(
- f"bash {
- get_relative_path(
- '../../../config/scripts/hyprpicker.sh')
- } {cmd}"
- )
- self.close_menu()
-
- def colorpicker_key(self, widget, event):
- if event.keyval in {Gdk.KEY_Return, Gdk.KEY_KP_Enter}:
- modifiers = event.get_state()
- cmd = "-hex" # Default
-
- match modifiers & (
- Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.CONTROL_MASK
- ):
- case Gdk.ModifierType.SHIFT_MASK:
- cmd = "-rgb"
- case Gdk.ModifierType.CONTROL_MASK:
- cmd = "-hsv"
-
- exec_shell_command_async(
- f"bash {get_relative_path('../../../config/scripts/hyprpicker.sh')} {
- cmd
- }"
- )
- self.close_menu()
- return True
- return False
-
- def update_screenrecord_state(self):
- def check():
- try:
- result = subprocess.run(
- "pgrep -f gpu-screen-recorder",
- shell=True,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- )
- running = result.returncode == 0
- except Exception:
- running = False
-
- def update_ui():
- if running:
- self.btn_screenrecord.get_child().set_markup(icons.stop)
- self.btn_screenrecord.add_style_class("recording")
- else:
- self.btn_screenrecord.get_child().set_markup(icons.screenrecord)
- self.btn_screenrecord.remove_style_class("recording")
- return False
-
- GLib.idle_add(update_ui)
- return False
-
- GLib.idle_add(lambda: threading.Thread(target=check).start())
- return True
-
- def open_screenshots_folder(self, *args):
- screenshots_dir = os.path.join(
- os.environ.get("XDG_PICTURES_DIR",
- os.path.expanduser("~/Pictures")),
- "Screenshots",
- )
- # Create directory if it doesn't exist
- os.makedirs(screenshots_dir, exist_ok=True)
- exec_shell_command_async(f"xdg-open {screenshots_dir}")
- self.close_menu()
-
- def open_recordings_folder(self, *args):
- recordings_dir = os.path.join(
- os.environ.get("XDG_VIDEOS_DIR", os.path.expanduser("~/Videos")),
- "Recordings",
- )
- # Create directory if it doesn't exist
- os.makedirs(recordings_dir, exist_ok=True)
- exec_shell_command_async(f"xdg-open {recordings_dir}")
- self.close_menu()
-
- def emoji(self, *args):
- self.launcher.open("emoji")
diff --git a/modules/launcher/components/wallpapers.py b/modules/launcher/components/wallpapers.py
deleted file mode 100644
index d8d2197..0000000
--- a/modules/launcher/components/wallpapers.py
+++ /dev/null
@@ -1,340 +0,0 @@
-import os
-import hashlib
-from gi.repository import GdkPixbuf, Gtk, GLib, Gio, Gdk
-from fabric.widgets.box import Box
-from fabric.widgets.centerbox import CenterBox
-from fabric.widgets.entry import Entry
-from fabric.widgets.scrolledwindow import ScrolledWindow
-from fabric.widgets.label import Label
-from fabric.utils.helpers import exec_shell_command_async
-from PIL import Image
-import concurrent.futures
-from concurrent.futures import ThreadPoolExecutor
-import utils.icons as icons
-import config.data as data
-
-
-class WallpaperSelector(Box):
- CACHE_DIR = f"{data.CACHE_DIR}/thumbnails"
-
- def __init__(self, **kwargs):
- super().__init__(
- name="wallpapers",
- spacing=4,
- orientation="v",
- h_expand=False,
- v_expand=False,
- **kwargs,
- )
- os.makedirs(self.CACHE_DIR, exist_ok=True)
-
- with os.scandir(data.WALLPAPERS_DIR) as entries:
- for entry in entries:
- if entry.is_file() and self._is_image(entry.name):
- # Check if the file needs renaming: file should be lowercase and have hyphens instead of spaces
- if entry.name != entry.name.lower() or " " in entry.name:
- new_name = entry.name.lower().replace(" ", "-")
- full_path = os.path.join(data.WALLPAPERS_DIR, entry.name)
- new_full_path = os.path.join(data.WALLPAPERS_DIR, new_name)
- try:
- os.rename(full_path, new_full_path)
- print(
- f"Renamed old wallpaper '{full_path}' to '{new_full_path}'"
- )
- except Exception as e:
- print(f"Error renaming file {full_path}: {e}")
-
- # Refresh the file list after possible renaming
- self.files = sorted(
- [f for f in os.listdir(data.WALLPAPERS_DIR) if self._is_image(f)]
- )
- self.thumbnails = []
- self.thumbnail_queue = []
- self.executor = ThreadPoolExecutor(max_workers=4) # Shared executor
-
- # Variable to control the selection (similar to AppLauncher)
- self.selected_index = -1
-
- # Initialize UI components
- self.viewport = Gtk.IconView(name="wallpaper-icons")
- self.viewport.set_model(Gtk.ListStore(GdkPixbuf.Pixbuf, str))
- self.viewport.set_pixbuf_column(0)
- # Hide text column so only the image is shown
- self.viewport.set_text_column(-1)
- self.viewport.set_item_width(0)
- self.viewport.connect("item-activated", self.on_wallpaper_selected)
-
- self.scrolled_window = ScrolledWindow(
- name="scrolled-window",
- spacing=10,
- h_expand=True,
- v_expand=True,
- child=self.viewport,
- )
-
- self.search_entry = Entry(
- name="search-entry-walls",
- placeholder="Search Wallpapers...",
- h_expand=True,
- notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()),
- on_key_press_event=self.on_search_entry_key_press,
- )
- self.search_entry.props.xalign = 0.5
- self.search_entry.connect("focus-out-event", self.on_search_entry_focus_out)
-
- self.schemes = {
- "scheme-tonal-spot": "Tonal Spot",
- "scheme-content": "Content",
- "scheme-expressive": "Expressive",
- "scheme-fidelity": "Fidelity",
- "scheme-fruit-salad": "Fruit Salad",
- "scheme-monochrome": "Monochrome",
- "scheme-neutral": "Neutral",
- "scheme-rainbow": "Rainbow",
- }
-
- self.scheme_dropdown = Gtk.ComboBoxText()
- self.scheme_dropdown.set_name("scheme-dropdown")
- self.scheme_dropdown.set_tooltip_text("Select color scheme")
- for key, display_name in self.schemes.items():
- self.scheme_dropdown.append(key, display_name)
- self.scheme_dropdown.set_active_id("scheme-tonal-spot")
- self.scheme_dropdown.connect("changed", self.on_scheme_changed)
-
- # Create a switcher to enable/disable Matugen (enabled by default)
- self.matugen_switcher = Gtk.Switch(name="matugen-switcher")
- self.matugen_switcher.set_vexpand(False)
- self.matugen_switcher.set_hexpand(False)
- self.matugen_switcher.set_valign(Gtk.Align.CENTER)
- self.matugen_switcher.set_halign(Gtk.Align.CENTER)
- self.matugen_switcher.set_active(True)
-
- self.mat_icon = Label(name="mat-label", markup=icons.palette)
-
- # Add the switcher to the header_box's start_children
- self.header_box = CenterBox(
- name="header-box",
- spacing=8,
- orientation="h",
- start_children=[self.matugen_switcher, self.mat_icon],
- center_children=[self.search_entry],
- end_children=[self.scheme_dropdown],
- )
-
- self.add(self.header_box)
- self.add(self.scrolled_window)
- self._start_thumbnail_thread()
- self.setup_file_monitor() # Initialize file monitoring
- self.show_all()
- # Ensure the search entry gets focus when starting
- self.search_entry.grab_focus()
-
- def setup_file_monitor(self):
- gfile = Gio.File.new_for_path(data.WALLPAPERS_DIR)
- self.file_monitor = gfile.monitor_directory(Gio.FileMonitorFlags.NONE, None)
- self.file_monitor.connect("changed", self.on_directory_changed)
-
- def on_directory_changed(self, monitor, file, other_file, event_type):
- file_name = file.get_basename()
- if event_type == Gio.FileMonitorEvent.DELETED:
- if file_name in self.files:
- self.files.remove(file_name)
- cache_path = self._get_cache_path(file_name)
- if os.path.exists(cache_path):
- try:
- os.remove(cache_path)
- except Exception as e:
- print(f"Error deleting cache {cache_path}: {e}")
- self.thumbnails = [(p, n) for p, n in self.thumbnails if n != file_name]
- GLib.idle_add(self.arrange_viewport, self.search_entry.get_text())
- elif event_type == Gio.FileMonitorEvent.CREATED:
- if self._is_image(file_name):
- # Convert filename to lowercase and replace spaces with "-"
- new_name = file_name.lower().replace(" ", "-")
- full_path = os.path.join(data.WALLPAPERS_DIR, file_name)
- new_full_path = os.path.join(data.WALLPAPERS_DIR, new_name)
- if new_name != file_name:
- try:
- os.rename(full_path, new_full_path)
- file_name = new_name
- print(f"Renamed file '{full_path}' to '{new_full_path}'")
- except Exception as e:
- print(f"Error renaming file {full_path}: {e}")
- if file_name not in self.files:
- self.files.append(file_name)
- self.files.sort()
- self.executor.submit(self._process_file, file_name)
- elif event_type == Gio.FileMonitorEvent.CHANGED:
- if self._is_image(file_name) and file_name in self.files:
- cache_path = self._get_cache_path(file_name)
- if os.path.exists(cache_path):
- try:
- os.remove(cache_path)
- except Exception as e:
- print(f"Error deleting cache for changed file {file_name}: {e}")
- self.executor.submit(self._process_file, file_name)
-
- def arrange_viewport(self, query: str = ""):
- model = self.viewport.get_model()
- model.clear()
- filtered_thumbnails = [
- (thumb, name)
- for thumb, name in self.thumbnails
- if query.casefold() in name.casefold()
- ]
- filtered_thumbnails.sort(key=lambda x: x[1].lower())
- for pixbuf, file_name in filtered_thumbnails:
- model.append([pixbuf, file_name])
- # If the search entry is empty, no icon is selected; otherwise, select the first one.
- if query.strip() == "":
- self.viewport.unselect_all()
- self.selected_index = -1
- elif len(model) > 0:
- self.update_selection(0)
-
- def on_wallpaper_selected(self, iconview, path):
- model = iconview.get_model()
- file_name = model[path][1]
- full_path = os.path.join(data.WALLPAPERS_DIR, file_name)
- selected_scheme = self.scheme_dropdown.get_active_id()
- if self.matugen_switcher.get_active():
- # Matugen is enabled: run the normal command.
- exec_shell_command_async(f"matugen image {full_path} -t {selected_scheme}")
- else:
- # Matugen is disabled: run the alternative swww command.
- exec_shell_command_async(
- f"swww img {full_path} -t outer --transition-duration 1.5 --transition-step 255 --transition-fps 60 -f Nearest"
- )
-
- def on_scheme_changed(self, combo):
- selected_scheme = combo.get_active_id()
- print(f"Color scheme selected: {selected_scheme}")
-
- def on_search_entry_key_press(self, widget, event):
- if event.state & Gdk.ModifierType.SHIFT_MASK:
- if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down):
- schemes_list = list(self.schemes.keys())
- current_id = self.scheme_dropdown.get_active_id()
- current_index = (
- schemes_list.index(current_id) if current_id in schemes_list else 0
- )
- new_index = (
- (current_index - 1) % len(schemes_list)
- if event.keyval == Gdk.KEY_Up
- else (current_index + 1) % len(schemes_list)
- )
- self.scheme_dropdown.set_active(new_index)
- return True
- elif event.keyval == Gdk.KEY_Right:
- self.scheme_dropdown.popup()
- return True
-
- if event.keyval in (Gdk.KEY_Up, Gdk.KEY_Down, Gdk.KEY_Left, Gdk.KEY_Right):
- self.move_selection_2d(event.keyval)
- return True
- elif event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
- if self.selected_index != -1:
- path = Gtk.TreePath.new_from_indices([self.selected_index])
- self.on_wallpaper_selected(self.viewport, path)
- return True
- return False
-
- def move_selection_2d(self, keyval):
- model = self.viewport.get_model()
- total_items = len(model)
- if total_items == 0:
- return
-
- if self.selected_index == -1:
- new_index = (
- 0 if keyval in (Gdk.KEY_Down, Gdk.KEY_Right) else total_items - 1
- )
- else:
- current_index = self.selected_index
- allocation = self.viewport.get_allocation()
- item_width = 108 # Approximate item width including margins
- columns = max(1, allocation.width // item_width)
- if keyval == Gdk.KEY_Right:
- new_index = current_index + 1
- elif keyval == Gdk.KEY_Left:
- new_index = current_index - 1
- elif keyval == Gdk.KEY_Down:
- new_index = current_index + columns
- elif keyval == Gdk.KEY_Up:
- new_index = current_index - columns
- if new_index < 0:
- new_index = 0
- if new_index >= total_items:
- new_index = total_items - 1
-
- self.update_selection(new_index)
-
- def update_selection(self, new_index: int):
- self.viewport.unselect_all()
- path = Gtk.TreePath.new_from_indices([new_index])
- self.viewport.select_path(path)
- self.viewport.scroll_to_path(
- path, False, 0.5, 0.5
- ) # Ensure the selected icon is visible
- self.selected_index = new_index
-
- def _start_thumbnail_thread(self):
- thread = GLib.Thread.new("thumbnail-loader", self._preload_thumbnails, None)
-
- def _preload_thumbnails(self, _data):
- futures = [
- self.executor.submit(self._process_file, file_name)
- for file_name in self.files
- ]
- concurrent.futures.wait(futures)
- GLib.idle_add(self._process_batch)
-
- def _process_file(self, file_name):
- full_path = os.path.join(data.WALLPAPERS_DIR, file_name)
- cache_path = self._get_cache_path(file_name)
- if not os.path.exists(cache_path):
- try:
- with Image.open(full_path) as img:
- width, height = img.size
- side = min(width, height)
- left = (width - side) // 2
- top = (height - side) // 2
- right = left + side
- bottom = top + side
- img_cropped = img.crop((left, top, right, bottom))
- img_cropped.thumbnail((96, 96), Image.Resampling.LANCZOS)
- img_cropped.save(cache_path, "PNG")
- except Exception as e:
- print(f"Error processing {file_name}: {e}")
- return
- self.thumbnail_queue.append((cache_path, file_name))
- GLib.idle_add(self._process_batch)
-
- def _process_batch(self):
- batch = self.thumbnail_queue[:10]
- del self.thumbnail_queue[:10]
- for cache_path, file_name in batch:
- try:
- pixbuf = GdkPixbuf.Pixbuf.new_from_file(cache_path)
- self.thumbnails.append((pixbuf, file_name))
- self.viewport.get_model().append([pixbuf, file_name])
- except Exception as e:
- print(f"Error loading thumbnail {cache_path}: {e}")
- if self.thumbnail_queue:
- GLib.idle_add(self._process_batch)
- return False
-
- def _get_cache_path(self, file_name: str) -> str:
- file_hash = hashlib.md5(file_name.encode("utf-8")).hexdigest()
- return os.path.join(self.CACHE_DIR, f"{file_hash}.png")
-
- @staticmethod
- def _is_image(file_name: str) -> bool:
- return file_name.lower().endswith(
- (".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp")
- )
-
- def on_search_entry_focus_out(self, widget, event):
- if self.get_mapped():
- widget.grab_focus()
- return False
diff --git a/modules/launcher/components/wifi.py b/modules/launcher/components/wifi.py
deleted file mode 100644
index 94e52d6..0000000
--- a/modules/launcher/components/wifi.py
+++ /dev/null
@@ -1,153 +0,0 @@
-from fabric.widgets.box import Box
-from fabric.widgets.label import Label
-from fabric.widgets.image import Image
-from fabric.widgets.button import Button
-from fabric.widgets.centerbox import CenterBox
-from fabric.widgets.scrolledwindow import ScrolledWindow
-from services import network_client
-import utils.icons as icons
-
-
-class WifiNetworkSlot(CenterBox):
- def __init__(self, network, client: network_client, **kwargs):
- super().__init__(name="wifi-device", **kwargs)
- self.network = network
- self.client = client
- self.connecting = False # Tracks if we are connecting/disconnecting
-
- self.connection_icon = Label(name="wifi-connection", markup=icons.wifi_off)
- self.connect_button = Button(
- name="wifi-connect",
- label="Connect",
- on_clicked=self.on_connect_clicked,
- )
-
- self.start_children = [
- Box(
- spacing=8,
- children=[
- Image(icon_name=network.get("icon-name"), size=32),
- Label(label=network.get("ssid")),
- self.connection_icon,
- ],
- )
- ]
- self.end_children = self.connect_button
-
- self.visible = True
- self.show_all()
-
- # Listen to WiFi state changes
- self.client.wifi_device.connect("changed", lambda *_: self.update_status())
-
- self.update_status()
-
- def on_connect_clicked(self, *_):
- connected = self.network.get("ssid") == self.client.wifi_device.ssid
- if connected:
- self.connect_button.set_label("Disconnecting...")
- self.client.wifi_device.enabled = False # Disables WiFi to disconnect
- else:
- self.connect_button.set_label("Connecting...")
- self.client.connect_wifi_bssid(self.network.get("bssid"))
-
- self.connecting = True
- self.connect_button.sensitive = False
-
- def update_status(self):
- state = self.client.wifi_device.state
- connected = self.network.get("ssid") == self.client.wifi_device.ssid
-
- if self.connecting:
- if state in ["activating"]:
- self.connect_button.set_label("Connecting...")
- elif state in ["deactivating"]:
- self.connect_button.set_label("Disconnecting...")
- else:
- self.connecting = False
- self.connect_button.sensitive = True
-
- if not self.connecting:
- self.connection_icon.set_markup(icons.wifi if connected else icons.wifi_off)
- self.connect_button.set_label("Disconnect" if connected else "Connect")
-
-
-class WifiManager(Box):
- def __init__(self, **kwargs):
- super().__init__(
- name="wifi",
- spacing=8,
- orientation="vertical",
- **kwargs,
- )
-
- self.client = network_client
- self.scan_icon = Label(name="wifi-scan-icon", markup=icons.scan)
- self.toggle_icon = Label(name="wifi-toggle-icon", markup=icons.wifi_off)
-
- self.scan_button = Button(
- name="wifi-scan",
- child=self.scan_icon,
- tooltip_text="Scan",
- on_clicked=lambda *_: self.client.wifi_device
- and self.client.wifi_device.scan(),
- )
-
- self.toggle_button = Button(
- name="wifi-toggle",
- tooltip_text="Toggle WiFi",
- child=self.toggle_icon,
- on_clicked=lambda *_: self.client.wifi_device
- and self.client.wifi_device.toggle_wifi(),
- )
-
- self.available_box = Box(spacing=2, orientation="vertical")
-
- self.children = [
- CenterBox(
- name="wifi-header",
- start_children=self.scan_button,
- center_children=Label(name="wifi-text", label="WiFi Networks"),
- end_children=self.toggle_button,
- ),
- ScrolledWindow(
- name="wifi-available",
- v_expand=True,
- h_scrollbar_policy="never",
- min_content_size=(-1, -1),
- child=self.available_box,
- ),
- ]
-
- self.client.connect("device-ready", lambda *_: self.on_device_ready())
-
- def on_device_ready(self):
- if not self.client.wifi_device:
- return
-
- self.update_toggle_icon()
-
- self.client.wifi_device.connect(
- "notify::enabled",
- lambda *_: self.update_toggle_icon(),
- )
-
- self.build_wifi_options()
-
- def update_toggle_icon(self):
- self.toggle_icon.set_markup(
- icons.wifi if self.client.wifi_device.enabled else icons.wifi_off
- )
-
- def build_wifi_options(self):
- self.available_box.children = []
- if not self.client.wifi_device:
- return
-
- aps = self.client.wifi_device.access_points
- seen_ssids = set()
- for ap in aps:
- ssid = ap.get("ssid")
- if ap.get("ssid") != "Unknown" and ssid not in seen_ssids:
- seen_ssids.add(ssid)
- self.available_box.add(WifiNetworkSlot(ap, self.client))
diff --git a/modules/launcher/components/window_switcher.py b/modules/launcher/components/window_switcher.py
deleted file mode 100644
index c2ed906..0000000
--- a/modules/launcher/components/window_switcher.py
+++ /dev/null
@@ -1,312 +0,0 @@
-import subprocess
-import json
-from fabric.widgets.box import Box
-from fabric.widgets.label import Label
-from fabric.widgets.button import Button
-from fabric.widgets.entry import Entry
-from fabric.widgets.scrolledwindow import ScrolledWindow
-from fabric.widgets.image import Image
-from fabric.utils import idle_add, remove_handler, exec_shell_command_async
-from utils import IconResolver
-from gi.repository import GLib, Gdk, Gtk
-import utils.icons as icons
-import config.data as data
-
-class WindowSwitcher(Box):
- def __init__(self, **kwargs):
- super().__init__(
- name="window-switcher",
- visible=False,
- all_visible=False,
- **kwargs,
- )
-
- self.launcher = kwargs["launcher"]
- self.selected_index = -1 # Track the selected item index
- self.icon_resolver = IconResolver()
-
- self.viewport = Box(name="viewport", spacing=4, orientation="v")
- self.search_entry = Entry(
- name="search-entry",
- placeholder="Search Windows...",
- h_expand=True,
- notify_text=lambda entry, *_: self.handle_search(entry.get_text()),
- on_activate=lambda entry, *_: self.handle_search(entry.get_text()),
- on_key_press_event=self.on_entry_key_press,
- )
- self.search_entry.props.xalign = 0.5
- self.scrolled_window = ScrolledWindow(
- name="scrolled-window",
- spacing=10,
- min_content_size=(450, 305),
- max_content_size=(450, 305),
- child=self.viewport,
- )
-
- self.header_box = Box(
- name="header_box",
- spacing=10,
- orientation="h",
- children=[
- self.search_entry,
- ],
- )
-
- self.window_switcher_box = Box(
- name="window-switcher-box",
- spacing=10,
- h_expand=True,
- orientation="v",
- children=[
- self.header_box,
- self.scrolled_window,
- ],
- )
-
- self.add(self.window_switcher_box)
- self.show_all()
-
- def close_switcher(self):
- """Close the window switcher"""
- self.viewport.hide()
- self.selected_index = -1 # Reset selection
- self.launcher.close()
-
- def open_switcher(self):
- """Open the window switcher and refresh windows"""
- self.refresh_windows()
- self.search_entry.set_text("")
- self.search_entry.grab_focus()
- self.viewport.show()
-
- def refresh_windows(self):
- """Get Hyprland windows and populate the viewport"""
- remove_handler(self._arranger_handler) if hasattr(self, '_arranger_handler') else None
- self.viewport.children = []
- self.selected_index = -1 # Clear selection when viewport changes
-
- # Get Hyprland windows
- windows = self.get_hyprland_windows()
- if not windows:
- # Create a container box to better center the message
- container = Box(
- name="no-windows-container",
- orientation="v",
- h_align="center",
- v_align="center",
- h_expand=True,
- v_expand=True
- )
-
- # Show a message if no windows
- label = Label(
- name="no-windows",
- markup=icons.window,
- h_align="center",
- v_align="center",
- )
-
- container.add(label)
-
- self.viewport.add(container)
- return
-
- # Add window slots to viewport
- for window in windows:
- self.viewport.add(self.create_window_slot(window))
-
- def get_hyprland_windows(self):
- """Get list of windows from Hyprland"""
- try:
- # Get clients info from Hyprland
- result = subprocess.run(
- ["hyprctl", "clients", "-j"],
- capture_output=True,
- text=True,
- check=True
- )
- clients = json.loads(result.stdout)
-
- # Filter out special windows and sort by workspace
- windows = []
- for client in clients:
- if client.get("class") and not client.get("class").startswith("special"):
- windows.append(client)
-
- # Sort windows by workspace number
- windows.sort(key=lambda x: int(x.get("workspace", {}).get("id", 0)))
- return windows
-
- except Exception as e:
- print(f"Error getting Hyprland windows: {e}")
- return []
-
- def create_window_slot(self, window):
- """Create a button slot for a window"""
- # Get window title or class as fallback
- title = window.get("title", window.get("class", "Unknown"))
- workspace = window.get("workspace", {}).get("id", 0)
- window_class = window.get("class", "")
-
- # Get window icon using IconResolver
- icon = self.get_window_icon(window_class)
-
- # Create the button with window info
- button = Button(
- name="slot-button",
- child=Box(
- name="window-slot-box",
- orientation="h",
- spacing=10,
- children=[
- Image(
- name="window-icon",
- pixbuf=icon,
- h_align="start",
- ),
- Label(
- name="workspace-label",
- label=f"Workspace {workspace}",
- h_align="start",
- ),
- Label(
- name="window-title",
- label=title,
- ellipsization="end",
- v_align="center",
- h_align="center",
- ),
- ],
- ),
- on_clicked=lambda *_: self.focus_window(window.get("address")),
- on_key_press_event=lambda btn, event, addr=window.get("address"): self.on_slot_key_press(btn, event, addr),
- )
- return button
-
- def get_window_icon(self, window_class):
- """Get the appropriate icon for a window class using IconResolver"""
- try:
- # Try to get icon from IconResolver
- icon = self.icon_resolver.get_icon_pixbuf(window_class, size=24)
- if icon:
- return icon
- except Exception as e:
- print(f"Error resolving icon for {window_class}: {e}")
-
- # Fallback to default window icon if resolution fails
- return icons.window
-
- def focus_window(self, address):
- """Focus a window by its address"""
- try:
- subprocess.run(
- ["hyprctl", "dispatch", "focuswindow", f"address:{address}"],
- check=True
- )
- self.close_switcher()
- except Exception as e:
- print(f"Error focusing window: {e}")
-
- def handle_search(self, query):
- """Handle search input"""
- if not query:
- self.refresh_windows()
- return
-
- # Filter windows based on search query
- windows = self.get_hyprland_windows()
- filtered_windows = [
- window for window in windows
- if query.lower() in window.get("title", "").lower() or
- query.lower() in window.get("class", "").lower()
- ]
-
- # Update viewport with filtered results
- self.viewport.children = []
- self.selected_index = -1
-
- for window in filtered_windows:
- self.viewport.add(self.create_window_slot(window))
-
- def on_entry_key_press(self, widget, event):
- """Handle key presses on the search entry"""
- if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
- if self.selected_index != -1:
- children = self.viewport.get_children()
- if 0 <= self.selected_index < len(children):
- children[self.selected_index].clicked()
- return True
- elif event.keyval == Gdk.KEY_Down:
- self.move_selection(1)
- return True
- elif event.keyval == Gdk.KEY_Up:
- self.move_selection(-1)
- return True
- elif event.keyval == Gdk.KEY_Escape:
- self.close_switcher()
- return True
- return False
-
- def on_slot_key_press(self, button, event, address):
- """Handle key presses on window slots"""
- if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
- self.focus_window(address)
- return True
- elif event.keyval == Gdk.KEY_Escape:
- self.close_switcher()
- return True
- return False
-
- def move_selection(self, direction):
- """Move the selection up or down"""
- children = self.viewport.get_children()
- if not children:
- return
-
- new_index = self.selected_index + direction
- if 0 <= new_index < len(children):
- self.update_selection(new_index)
-
- def update_selection(self, new_index):
- """Update the selected item"""
- # Unselect current
- if self.selected_index != -1 and self.selected_index < len(self.viewport.get_children()):
- current_button = self.viewport.get_children()[self.selected_index]
- current_button.get_style_context().remove_class("selected")
-
- # Select new
- if new_index != -1 and new_index < len(self.viewport.get_children()):
- new_button = self.viewport.get_children()[new_index]
- new_button.get_style_context().add_class("selected")
- self.selected_index = new_index
- self.scroll_to_selected(new_button)
- else:
- self.selected_index = -1
-
- def scroll_to_selected(self, button):
- """Scroll to ensure the selected button is visible"""
- def scroll():
- adj = self.scrolled_window.get_vadjustment()
- alloc = button.get_allocation()
- if alloc.height == 0:
- return False # Retry if allocation isn't ready
-
- y = alloc.y
- height = alloc.height
- page_size = adj.get_page_size()
- current_value = adj.get_value()
-
- # Calculate visible boundaries
- visible_top = current_value
- visible_bottom = current_value + page_size
-
- if y < visible_top:
- # Item above viewport - align to top
- adj.set_value(y)
- elif y + height > visible_bottom:
- # Item below viewport - align to bottom
- new_value = y + height - page_size
- adj.set_value(new_value)
- # No action if already fully visible
- return False
- GLib.idle_add(scroll)
\ No newline at end of file
diff --git a/modules/launcher/launcher.py b/modules/launcher/launcher.py
deleted file mode 100644
index 4704e75..0000000
--- a/modules/launcher/launcher.py
+++ /dev/null
@@ -1,201 +0,0 @@
-from fabric.widgets.centerbox import CenterBox
-from fabric.widgets.stack import Stack
-from fabric.widgets.wayland import WaylandWindow as Window
-from fabric.widgets.box import Box
-from modules.launcher.components import (
- AppLauncher,
- BluetoothConnections,
- EmojiPicker,
- PowerMenu,
- Sh,
- WallpaperSelector,
- WifiManager,
- Calendar,
- Dashboard,
- Toolbox,
- Kanban,
- TmuxManager,
- WindowSwitcher,
- NotificationCenter,
- ClipHistory,
-)
-
-
-class Launcher(Window):
- def __init__(self, **kwargs):
- super().__init__(
- layer="top",
- anchor="center",
- keyboard_mode="none",
- exclusivity="normal",
- visible=False,
- all_visible=False,
- **kwargs,
- )
-
- self.dashboard = Dashboard(launcher=self)
- self.wallpapers = WallpaperSelector(launcher=self)
- self.power = PowerMenu(launcher=self)
- self.emoji = EmojiPicker(launcher=self)
- self.bluetooth = BluetoothConnections(launcher=self)
- self.sh = Sh(launcher=self)
- self.wifi = WifiManager()
- self.calendar = Calendar()
- self.tools = Toolbox(launcher=self)
- self.kanban = Kanban()
- self.tmux = TmuxManager(launcher=self)
- self.window_switcher = WindowSwitcher(launcher=self)
- self.notification_center = NotificationCenter(launcher=self)
- self.cliphist = ClipHistory(launcher=self)
-
- # Wrap the dashboard in a Box container
- self.dashboard = Box(
- name="dashboard",
- orientation="h",
- spacing=10,
- children=[self.dashboard],
- )
- self.launcher = AppLauncher(launcher=self)
-
- self.stack = Stack(
- name="launcher-content",
- v_expand=True,
- h_expand=True,
- transition_type="crossfade",
- transition_duration=100,
- children=[
- self.launcher,
- self.wallpapers,
- self.power,
- self.emoji,
- self.cliphist,
- self.bluetooth,
- self.sh,
- self.wifi,
- self.calendar,
- self.tools,
- self.kanban,
- self.tmux,
- self.window_switcher,
- self.notification_center,
- ],
- )
-
- self.launcher_box = CenterBox(
- name="launcher",
- orientation="v",
- start_children=self.stack,
- end_children=self.dashboard,
- )
-
- self.add(self.launcher_box)
- self.show_all()
- self.hide()
- self.add_keybinding("Escape", lambda *_: self.close())
-
- def close(self):
- self.set_keyboard_mode("none")
- self.hide()
-
- for widget in [
- self.launcher,
- self.wallpapers,
- self.power,
- self.emoji,
- self.cliphist,
- self.bluetooth,
- self.sh,
- self.wifi,
- self.calendar,
- self.tools,
- self.kanban,
- self.tmux,
- self.window_switcher,
- self.notification_center,
- ]:
- if hasattr(widget, "viewport") and widget.viewport:
- widget.viewport.hide()
-
- for style in [
- "launcher",
- "wallpapers",
- "power",
- "emoji",
- "cliphist",
- "bluetooth",
- "sh",
- "wifi",
- "calendar",
- "tools",
- "kanban",
- "tmux",
- "window-switcher",
- "notification-center",
- ]:
- self.stack.remove_style_class(style)
-
- return True
-
- def open(self, widget):
- widgets = {
- "launcher": self.launcher,
- "wallpapers": self.wallpapers,
- "power": self.power,
- "emoji": self.emoji,
- "cliphist": self.cliphist,
- "bluetooth": self.bluetooth,
- "sh": self.sh,
- "wifi": self.wifi,
- "calendar": self.calendar,
- "tools": self.tools,
- "kanban": self.kanban,
- "tmux": self.tmux,
- "window-switcher": self.window_switcher,
- "notification-center": self.notification_center,
- }
- self.set_keyboard_mode("exclusive")
- self.show()
-
- for w in widgets.values():
- w.hide()
- self.dashboard.hide()
- for style in widgets.keys():
- self.stack.remove_style_class(style)
-
- if widget in widgets:
- self.stack.get_style_context().add_class(widget)
- self.stack.set_visible_child(widgets[widget])
- widgets[widget].show()
-
- if widget == "launcher":
- self.launcher.open_launcher()
- self.launcher.search_entry.set_text("")
- self.launcher.search_entry.grab_focus()
- self.dashboard.show()
-
- elif widget == "wallpapers":
- self.wallpapers.search_entry.set_text("")
- self.wallpapers.search_entry.grab_focus()
- self.wallpapers.viewport.show()
-
- elif widget == "tmux":
- self.tmux.open_manager()
- self.tmux.search_entry.set_text("")
- self.tmux.search_entry.grab_focus()
-
- elif widget == "emoji":
- self.emoji.open_picker()
- self.emoji.search_entry.set_text("")
- self.emoji.search_entry.grab_focus()
-
- elif widget == "cliphist":
- self.cliphist.open()
-
- elif widget == "sh":
- self.sh.open_sh()
-
- elif widget == "window-switcher":
- self.window_switcher.open_switcher()
-
- elif widget == "notification-center":
- self.notification_center.open_center()
diff --git a/modules/notification_popup.py b/modules/notification_popup.py
deleted file mode 100644
index 82dcb5d..0000000
--- a/modules/notification_popup.py
+++ /dev/null
@@ -1,302 +0,0 @@
-import os
-from gi.repository import GdkPixbuf, GLib, Gdk
-from fabric.notifications import (
- Notification,
- NotificationAction,
- NotificationCloseReason,
-)
-from loguru import logger
-from fabric.widgets.box import Box
-from fabric.widgets.button import Button
-from fabric.widgets.image import Image
-from fabric.widgets.centerbox import CenterBox
-from fabric.widgets.label import Label
-from fabric.widgets.revealer import Revealer
-from fabric.widgets.wayland import WaylandWindow as Window
-from utils import CustomImage
-import utils.icons as icons
-from services import notification_service
-
-
-class ActionButton(Button):
- def __init__(
- self, action: NotificationAction, index: int, total: int, notification_box
- ):
- super().__init__(
- name="action-button",
- h_expand=True,
- on_clicked=self.on_clicked,
- child=Label(name="button-label", label=action.label),
- )
- self.action = action
- self.notification_box = notification_box
- style_class = (
- "start-action"
- if index == 0
- else "end-action"
- if index == total - 1
- else "middle-action"
- )
- self.add_style_class(style_class)
- self.connect(
- "enter-notify-event", lambda *_: notification_box.hover_button(self)
- )
- self.connect(
- "leave-notify-event", lambda *_: notification_box.unhover_button(self)
- )
-
- def on_clicked(self, *_):
- self.action.invoke()
- self.action.parent.close("dismissed-by-user")
-
-
-class NotificationWidget(Box):
- def __init__(self, notification: Notification, timeout_ms=5000, **kwargs):
- super().__init__(
- name="notification-box",
- orientation="v",
- h_align="fill",
- h_expand=True,
- children=[
- self.create_content(notification),
- self.create_action_buttons(notification),
- ],
- )
- self.notification = notification
- self.timeout_ms = timeout_ms
- self._timeout_id = None
- self.start_timeout()
-
- def create_header(self, notification):
- app_icon = (
- Image(
- name="notification-icon",
- image_file=notification.app_icon[7:],
- size=24,
- )
- if "file://" in notification.app_icon
- else Image(
- name="notification-icon",
- icon_name="dialog-information-symbolic" or notification.app_icon,
- icon_size=24,
- )
- )
-
- return CenterBox(
- name="notification-title",
- start_children=[
- Box(
- spacing=4,
- children=[
- app_icon,
- Label(
- notification.app_name,
- name="notification-app-name",
- h_align="start",
- ),
- ],
- )
- ],
- end_children=[self.create_close_button()],
- )
-
- def create_content(self, notification):
- return Box(
- name="notification-content",
- spacing=8,
- children=[
- Box(
- name="notification-image",
- children=CustomImage(
- pixbuf=notification.image_pixbuf.scale_simple(
- 48, 48, GdkPixbuf.InterpType.BILINEAR
- )
- if notification.image_pixbuf
- else self.get_pixbuf(notification.app_icon, 48, 48)
- ),
- ),
- Box(
- name="notification-text",
- orientation="v",
- v_align="center",
- children=[
- Box(
- name="notification-summary-box",
- orientation="h",
- children=[
- Label(
- name="notification-summary",
- markup=notification.summary.replace("\n", " "),
- h_align="start",
- ellipsization="end",
- ),
- Label(
- name="notification-app-name",
- markup=" | " + notification.app_name,
- h_align="start",
- ellipsization="end",
- ),
- ],
- ),
- Label(
- markup=notification.body.replace("\n", " "),
- h_align="start",
- ellipsization="end",
- )
- if notification.body
- else Box(),
- ],
- ),
- Box(h_expand=True),
- Box(
- orientation="v",
- children=[
- self.create_close_button(),
- Box(v_expand=True),
- ],
- ),
- ],
- )
-
- def get_pixbuf(self, icon_path, width, height):
- if icon_path.startswith("file://"):
- icon_path = icon_path[7:]
-
- if not os.path.exists(icon_path):
- logger.warning(f"Icon path does not exist: {icon_path}")
- return None
-
- try:
- pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_path)
- return pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
- except Exception as e:
- logger.error(f"Failed to load or scale icon: {e}")
- return None
-
- def create_action_buttons(self, notification):
- return Box(
- name="notification-action-buttons",
- spacing=4,
- h_expand=True,
- children=[
- ActionButton(action, i, len(notification.actions), self)
- for i, action in enumerate(notification.actions)
- ],
- )
-
- def create_close_button(self):
- close_button = Button(
- name="notif-close-button",
- child=Label(name="notif-close-label", markup=icons.cancel),
- on_clicked=lambda *_: self.notification.close("dismissed-by-user"),
- )
- close_button.connect(
- "enter-notify-event", lambda *_: self.hover_button(close_button)
- )
- close_button.connect(
- "leave-notify-event", lambda *_: self.unhover_button(close_button)
- )
- return close_button
-
- def start_timeout(self):
- self.stop_timeout()
- self._timeout_id = GLib.timeout_add(self.timeout_ms, self.close_notification)
-
- def stop_timeout(self):
- if self._timeout_id is not None:
- GLib.source_remove(self._timeout_id)
- self._timeout_id = None
-
- def close_notification(self):
- self.notification.close("expired")
- self.stop_timeout()
- return False
-
- def pause_timeout(self):
- self.stop_timeout()
-
- def resume_timeout(self):
- self.start_timeout()
-
- def destroy(self):
- self.stop_timeout()
- super().destroy()
-
- # @staticmethod
- def set_pointer_cursor(self, widget, cursor_name):
- window = widget.get_window()
- if window:
- cursor = Gdk.Cursor.new_from_name(widget.get_display(), cursor_name)
- window.set_cursor(cursor)
-
- def hover_button(self, button):
- self.pause_timeout()
- self.set_pointer_cursor(button, "hand2")
-
- def unhover_button(self, button):
- self.resume_timeout()
- self.set_pointer_cursor(button, "arrow")
-
-
-class NotificationRevealer(Revealer):
- def __init__(self, notification: Notification, **kwargs):
- self.notif_box = NotificationWidget(notification)
- self._notification = notification
- super().__init__(
- child=Box(
- children=[self.notif_box],
- ),
- transition_duration=250,
- transition_type="slide-down",
- )
-
- self.connect(
- "notify::child-revealed",
- lambda *_: self.destroy() if not self.get_child_revealed() else None,
- )
-
- self._notification.connect("closed", self.on_resolved)
-
- def on_resolved(
- self,
- notification: Notification,
- reason: NotificationCloseReason,
- ):
- self.set_reveal_child(False)
-
-
-class NotificationPopup(Window):
- def __init__(self):
- self._server = notification_service
- self.cache_notification_service = notification_service
- self.notifications = Box(
- v_expand=True,
- h_expand=True,
- style="margin: 1px 0px 1px 1px;",
- orientation="v",
- spacing=5,
- )
- self._server.connect("notification-added", self.on_new_notification)
-
- super().__init__(
- anchor="top right",
- child=self.notifications,
- layer="overlay",
- all_visible=True,
- visible=True,
- exclusive=False,
- )
-
- def on_new_notification(self, fabric_notif, id):
- notification: Notification = fabric_notif.get_notification_from_id(id)
- # Always cache the notification regardless of DND status
- self.cache_notification_service.cache_notification(notification)
-
- # Only show popup if DND is not enabled
- if not self.cache_notification_service.dont_disturb:
- new_box = NotificationRevealer(fabric_notif.get_notification_from_id(id))
- self.notifications.add(new_box)
- new_box.set_reveal_child(True)
- logger.info(f"[Notification] New notification from {notification.app_name}")
- else:
- logger.info(f"[Notification] DND enabled, notification from {notification.app_name} not shown")
diff --git a/modules/osd.py b/modules/osd.py
deleted file mode 100644
index 6d79a60..0000000
--- a/modules/osd.py
+++ /dev/null
@@ -1,183 +0,0 @@
-import time
-from typing import ClassVar
-
-from fabric.audio import Audio
-from fabric.widgets.box import Box
-from fabric.widgets.scale import Scale, ScaleMark
-from fabric.widgets.wayland import WaylandWindow as Window
-from fabric.utils import invoke_repeater
-from gi.repository import GObject
-from services.brightness import Brightness
-from fabric.widgets.label import Label
-from utils import Animator
-
-
-class AnimatedScale(Scale):
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.animator = None # Lazily initialized
-
- def animate_value(self, value: float):
- if not self.animator:
- self.animator = Animator(
- bezier_curve=(0.34, 1.56, 0.64, 1.0),
- duration=0.8,
- min_value=self.min_value,
- max_value=self.max_value,
- tick_widget=self,
- notify_value=lambda p, *_: self.set_value(p.value),
- )
- self.animator.pause()
- self.animator.min_value = self.value
- self.animator.max_value = min(max(value, self.min_value), self.max_value)
- self.animator.play()
-
-
-class BaseOSDContainer(Box):
- def __init__(self, label_text, **kwargs):
- super().__init__(**kwargs, orientation="v", spacing=8, name="osd")
-
- self.header = Box(orientation="h")
- self.label = Label(
- label=label_text, name="osd-label", h_align="start", h_expand=True
- )
- self.value_label = Label(name="osd-label", h_align="end")
- self.header.add(self.label)
- self.header.add(self.value_label)
-
- self.scale = AnimatedScale(
- marks=(ScaleMark(value=i) for i in range(0, 100, 10)),
- value=70,
- min_value=0,
- max_value=100,
- increments=(1, 1),
- orientation="h",
- )
-
- self.add(self.header)
- self.add(self.scale)
-
- def update_value_label(self, value):
- self.value_label.set_label(str(round(value)))
-
-
-class BrightnessOSDContainer(BaseOSDContainer):
- def __init__(self, **kwargs):
- super().__init__(label_text="Brightness", **kwargs)
- self.brightness_service = Brightness()
- self.update_brightness()
-
- self.scale.connect("value-changed", lambda *_: self.update_brightness())
- self.brightness_service.connect("screen", self.on_brightness_changed)
-
- def update_brightness(self) -> None:
- current_brightness = self.brightness_service.screen_brightness
- if current_brightness != 0:
- normalized_brightness = self._normalize_brightness(current_brightness)
- self.scale.set_value(normalized_brightness)
- self.update_value_label(normalized_brightness)
-
- def on_brightness_changed(self, _sender: any, value: float, *_args) -> None:
- normalized_brightness = self._normalize_brightness(value)
- self.scale.animate_value(normalized_brightness)
- self.update_value_label(normalized_brightness)
-
- def _normalize_brightness(self, brightness: float) -> float:
- normalized = (brightness / self.brightness_service.max_screen) * 100
- return max(0, min(100, normalized)) # Ensure value stays within bounds
-
-
-class AudioOSDContainer(BaseOSDContainer):
- __gsignals__: ClassVar[dict] = {
- "volume-changed": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()),
- }
-
- def __init__(self, **kwargs):
- super().__init__(label_text="Volume", **kwargs)
- self.audio = Audio()
- self.sync_with_audio()
-
- self.scale.connect("value-changed", self.on_volume_changed)
- self.audio.connect("notify::speaker", self.on_audio_speaker_changed)
- self.audio.connect("speaker-changed", self.on_speaker_changed)
-
- def sync_with_audio(self):
- if self.audio.speaker:
- volume = round(self.audio.speaker.volume)
- self.scale.set_value(
- max(0, min(100, volume))
- ) # Ensure value stays within bounds
- self.update_value_label(volume)
-
- def on_volume_changed(self, *_):
- if self.audio.speaker:
- volume = self.scale.value
- if 0 <= volume <= 100:
- self.audio.speaker.set_volume(volume)
- self.update_value_label(volume)
- self.emit("volume-changed")
-
- def on_audio_speaker_changed(self, *_):
- if self.audio.speaker:
- self.audio.speaker.connect("changed", self.update_volume)
- self.update_volume()
-
- def on_speaker_changed(self, *_):
- self.update_volume()
-
- def update_volume(self, *_):
- if self.audio.speaker and not self.is_hovered():
- volume = round(self.audio.speaker.volume)
- self.scale.set_value(
- max(0, min(100, volume))
- ) # Ensure value stays within bounds
- self.update_value_label(volume)
-
-
-class OSD(Window):
- def __init__(self, **kwargs):
- self.audio_container = AudioOSDContainer()
- self.brightness_container = BrightnessOSDContainer()
-
- self.timeout = 1000
-
- self.controls_box = Box(
- orientation="h",
- spacing=20,
- children=[self.brightness_container, self.audio_container],
- )
-
- super().__init__(
- layer="overlay",
- anchor="top",
- child=self.controls_box,
- visible=False,
- pass_through=True,
- keyboard_mode="on-demand",
- **kwargs,
- )
-
- self.last_activity_time = time.time()
-
- self.audio_container.audio.connect("notify::speaker", self.show_controls)
- self.brightness_container.brightness_service.connect(
- "screen", self.show_controls
- )
- self.audio_container.connect("volume-changed", self.show_controls)
-
- invoke_repeater(100, self.check_inactivity, initial_call=True)
-
- def show_controls(self, *_):
- self.set_visible(True)
- self.reset_inactivity_timer()
-
- def start_hide_timer(self):
- self.set_visible(False)
-
- def reset_inactivity_timer(self):
- self.last_activity_time = time.time()
-
- def check_inactivity(self):
- if time.time() - self.last_activity_time >= (self.timeout / 1000):
- self.start_hide_timer()
- return True
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..ca1847f
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,16 @@
+[project]
+name = "modusv2"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.13"
+dependencies = [
+ "fabric",
+ "loguru>=0.7.3",
+ "psutil>=7.0.0",
+ "pydbus>=0.6.0",
+ "setproctitle>=1.3.6",
+]
+
+[tool.uv.sources]
+fabric = { git = "https://github.com/Fabric-Development/fabric.git" }
diff --git a/services/__init__.py b/services/__init__.py
index cdba666..fdf19b4 100644
--- a/services/__init__.py
+++ b/services/__init__.py
@@ -1,10 +1,9 @@
from fabric.audio import Audio
from .brightness import Brightness
from .network import NetworkClient
-from .notification import *
-from .powerprofile import *
+from .battery import *
network_client = NetworkClient()
brightness = Brightness()
audio = Audio()
-notification_service = CustomNotifications()
+
diff --git a/services/battery.py b/services/battery.py
new file mode 100644
index 0000000..1648f64
--- /dev/null
+++ b/services/battery.py
@@ -0,0 +1,112 @@
+from fabric.core import Service, Property, Signal
+from pydbus import SystemBus
+from loguru import logger
+
+DeviceState = {
+ 0: "UNKNOWN",
+ 1: "CHARGING",
+ 2: "DISCHARGING",
+ 3: "EMPTY",
+ 4: "FULLY_CHARGED",
+ 5: "PENDING_CHARGE",
+ 6: "PENDING_DISCHARGE",
+}
+
+
+class Battery(Service):
+ @staticmethod
+ def seconds_to_hours_minutes(seconds):
+ hours = seconds // 3600
+ minutes = (seconds % 3600) // 60
+ return f"{hours}h {minutes}m" if hours else f"{minutes}m"
+
+ @Signal
+ def changed(self) -> None: ...
+
+ @Signal
+ def profile_changed(self, value: str) -> None: ...
+
+ @Property(int, "readable")
+ def percentage(self):
+ return int(self._battery.Percentage)
+
+ @Property(str, "readable")
+ def temperature(self):
+ return f"{self._battery.Temperature}°C" if hasattr(self._battery, "Temperature") else "N/A"
+
+ @Property(str, "readable")
+ def time_to_empty(self):
+ return self.seconds_to_hours_minutes(getattr(self._battery, "TimeToEmpty", 0))
+
+ @Property(str, "readable")
+ def time_to_full(self):
+ return self.seconds_to_hours_minutes(getattr(self._battery, "TimeToFull", 0))
+
+ @Property(str, "readable")
+ def icon_name(self):
+ return self._battery.IconName
+
+ @Property(str, "readable")
+ def state(self):
+ return DeviceState.get(self._battery.State, "UNKNOWN")
+
+ @Property(str, "readable")
+ def capacity(self):
+ return f"{self._battery.Capacity}%"
+
+ @Property(bool, "readable", default_value=False)
+ def is_present(self):
+ return self._battery.IsPresent
+
+ @Property(str, "readable")
+ def power_profile(self):
+ return self._profile_proxy.ActiveProfile
+
+ def set_power_profile(self, profile: str) -> bool:
+ if profile not in [p.Profile for p in self._profile_proxy.Profiles]:
+ logger.warning(f"[Battery] Invalid profile: {profile}")
+ return False
+ try:
+ self._profile_proxy.ActiveProfile = profile
+ logger.info(f"[Battery] Power profile set to {profile}")
+ self.profile_changed.emit(profile)
+ self.changed.emit()
+ return True
+ except Exception as e:
+ logger.error(f"[Battery] Failed to set profile: {e}")
+ return False
+
+ def __init__(self):
+ super().__init__()
+ self._bus = SystemBus()
+
+ # Battery device
+ try:
+ self._battery = self._bus.get("org.freedesktop.UPower", "/org/freedesktop/UPower/devices/battery_BAT0")
+ self._battery.onPropertiesChanged = self.handle_battery_change
+ logger.info("[Battery] UPower device connected.")
+ except Exception as e:
+ logger.error(f"[Battery] Failed to connect to UPower battery: {e}")
+ return
+
+ # PowerProfiles
+ try:
+ self._profile_proxy = self._bus.get("net.hadess.PowerProfiles", "/net/hadess/PowerProfiles")
+ self._profile_proxy.onPropertiesChanged = self.handle_profile_change
+ logger.info("[Battery] PowerProfiles daemon connected.")
+ except Exception as e:
+ logger.error(f"[Battery] Failed to connect to PowerProfiles daemon: {e}")
+ self._profile_proxy = None
+
+ self.changed.emit()
+
+ def handle_battery_change(self, iface, changed, invalidated):
+ logger.debug(f"[Battery] UPower properties changed: {changed}")
+ self.changed.emit()
+
+ def handle_profile_change(self, iface, changed, invalidated):
+ if "ActiveProfile" in changed:
+ new_profile = changed["ActiveProfile"]
+ logger.info(f"[Battery] Power profile changed: {new_profile}")
+ self.profile_changed.emit(new_profile)
+ self.changed.emit()
diff --git a/services/brightness.py b/services/brightness.py
index 49e7040..2c07276 100644
--- a/services/brightness.py
+++ b/services/brightness.py
@@ -1,115 +1,81 @@
import os
-import subprocess
-
-from fabric.core.service import Property, Service, Signal
-from fabric.utils import exec_shell_command, monitor_file
-from gi.repository import GLib
-from loguru import logger # Import loguru logger
-
-
-# Helper function to execute brightnessctl asynchronously
-def exec_brightnessctl_async(args: str):
- # Executes brightnessctl command asynchronously, ensuring no resource leaks.
- try:
- # Use subprocess.Popen to run the command without blocking
- process = subprocess.Popen(
- f"brightnessctl {args}",
- shell=True,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- )
-
- # Wait for the process to complete
- stdout, stderr = process.communicate()
-
- if process.returncode != 0:
- logger.error(f"Error executing brightnessctl: {stderr.decode().strip()}")
- else:
- logger.debug(
- f"brightnessctl output: {stdout.decode().strip()}"
- ) # Optional: Log the output
- except Exception as e:
- logger.exception(f"Exception in exec_brightnessctl_async: {e}")
-
-
-# Discover screen backlight device
-try:
- screen_device = str(exec_shell_command("ls -w1 /sys/class/backlight")).split("\n")[
- 0
- ]
-except IndexError:
- screen_device = None
-
-
-class NoBrightnessError(ImportError):
- # Raised when brightness-related dependencies are missing.
- def __init__(self, *args):
- super().__init__(
- "Brightness control not available or missing dependencies", *args
- )
+from loguru import logger
+from fabric.core import Service, Property, Signal
+from fabric.utils import exec_shell_command_async, monitor_file
class Brightness(Service):
- # Service to manage screen brightness levels.
-
@Signal
- def screen(self, value: int) -> None:
- """Signal emitted when screen brightness changes."""
- pass # Implement as needed for your application
+ def changed(self) -> None: ...
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
-
- if not screen_device:
- raise NoBrightnessError()
-
- # Path for screen backlight control
- self.screen_backlight_path = f"/sys/class/backlight/{screen_device}"
-
- # Initialize maximum brightness level
- self.max_screen = self._read_max_brightness(self.screen_backlight_path)
-
- # Monitor screen brightness file
- self.screen_monitor = monitor_file(f"{self.screen_backlight_path}/brightness")
- self.screen_monitor.connect(
- "changed",
- lambda _, file, *args: self.emit(
- "screen", round(int(file.load_bytes()[0].get_data()))
- ),
- )
-
- # Log the initialization of the service
- logger.info(f"Brightness service initialized for device: {screen_device}")
-
- def _read_max_brightness(self, path: str) -> int:
- # Reads the maximum brightness value from the specified path.
- max_brightness_path = os.path.join(path, "max_brightness")
- if os.path.exists(max_brightness_path):
- with open(max_brightness_path) as f:
- return int(f.read().strip())
- return -1 # Return -1 if file doesn't exist, indicating an error.
+ @Property(int, "readable")
+ def max_brightness(self):
+ if self._cached_max_brightness is not None:
+ return self._cached_max_brightness
+ try:
+ with open(self._max_brightness_path) as f:
+ self._cached_max_brightness = int(f.read().strip())
+ return self._cached_max_brightness
+ except Exception as e:
+ logger.error(f"[Brightness] Failed to read max brightness: {e}")
+ return -1
@Property(int, "read-write")
- def screen_brightness(self) -> int:
- # Property to get or set the screen brightness.
- brightness_path = os.path.join(self.screen_backlight_path, "brightness")
- if os.path.exists(brightness_path):
- with open(brightness_path) as f:
+ def brightness(self):
+ try:
+ with open(self._brightness_path) as f:
return int(f.read().strip())
- logger.warning(f"Brightness file does not exist: {brightness_path}")
- return -1 # Return -1 if file doesn't exist, indicating error.
+ except Exception as e:
+ logger.error(f"[Brightness] Failed to read current brightness: {e}")
+ return -1
- @screen_brightness.setter
- def screen_brightness(self, value: int):
- # Setter for screen brightness property.
- if not (0 <= value <= self.max_screen):
- value = max(0, min(value, self.max_screen))
+ @brightness.setter
+ def brightness(self, value: int):
+ max_brightness = self.max_brightness
+ if max_brightness <= 0:
+ logger.error("[Brightness] Invalid max brightness; cannot set brightness.")
+ return
+
+ value = max(0, min(value, max_brightness))
+ try:
+ exec_shell_command_async(
+ f"brightnessctl --device '{self._device}' set {value}"
+ )
+ logger.info(f"[Brightness] Set screen brightness to {value}")
+ except Exception as e:
+ logger.exception(f"[Brightness] Failed to set brightness: {e}")
+
+ @Property(int, "readable")
+ def brightness_percentage(self):
+ max_brightness = self.max_brightness
+ current_brightness = self.brightness
+
+ if max_brightness <= 0:
+ return 0
+ return int((current_brightness / max_brightness) * 100)
+
+ def __init__(self):
+ super().__init__()
+ self._device = None
+ self._cached_max_brightness = None
+
+ self._backlight_path = "/sys/class/backlight"
try:
- exec_brightnessctl_async(f"--device '{screen_device}' set {value}")
- self.emit("screen", int((value / self.max_screen) * 100))
- logger.info(f"Set screen brightness to {value} (out of {self.max_screen})")
- except GLib.Error as e:
- logger.error(f"Error setting screen brightness: {e.message}")
+ devices = os.listdir(self._backlight_path)
+ if devices:
+ self._device = devices[0]
+ else:
+ raise FileNotFoundError
+ except FileNotFoundError:
+ logger.error("[Brightness] No backlight device found; brightness control disabled.")
+ return
+
+ self._brightness_path = f"{self._backlight_path}/{self._device}/brightness"
+ self._max_brightness_path = f"{self._backlight_path}/{self._device}/max_brightness"
+
+ try:
+ self._screen_monitor = monitor_file(self._brightness_path)
+ self._screen_monitor.connect("changed", lambda *args: self.changed.emit())
except Exception as e:
- logger.exception(f"Unexpected error setting screen brightness: {e}")
+ logger.error(f"[Brightness] Failed to monitor brightness file: {e}")
diff --git a/services/notification.py b/services/notification.py
deleted file mode 100644
index 4da918b..0000000
--- a/services/notification.py
+++ /dev/null
@@ -1,226 +0,0 @@
-import json
-import os
-from typing import List
-
-from fabric import Signal
-from fabric.notifications import Notification, Notifications
-from loguru import logger
-import gi
-gi.require_version('GLib', '2.0')
-from gi.repository import GLib
-
-import config.data as data
-
-NOTIFICATION_CACHE_FILE = f"{data.CACHE_DIR}/notifications.json"
-
-
-class CustomNotifications(Notifications):
- """A service to manage the notifications."""
-
- @Signal
- def clear_all(self, value: bool) -> None:
- """Signal emitted when notifications are emptied."""
- # Implement as needed for your application
-
- @Signal
- def notification_count(self, value: int) -> None:
- """Signal emitted when a new notification is added."""
- # Implement as needed for your application
-
- @Signal
- def dnd(self, value: bool) -> None:
- """Signal emitted when dnd is toggled."""
- # Implement as needed for your application
-
- @property
- def count(self) -> int:
- """Return the count of notifications."""
- return len(self.all_notifications)
-
- @property
- def dont_disturb(self) -> bool:
- """Return the pause status."""
- return self._dont_disturb
-
- @dont_disturb.setter
- def dont_disturb(self, value: bool):
- """Set the pause status."""
- self._dont_disturb = value
- self.emit("dnd", value)
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.all_notifications = []
- self._count = 0 # Will be updated to highest ID when loading
- self.deserialized_notifications = []
- self._dont_disturb = False
- self._load_notifications()
-
- def _load_notifications(self):
- """Read notifications from the cache file."""
- if os.path.exists(NOTIFICATION_CACHE_FILE):
- try:
- with open(NOTIFICATION_CACHE_FILE, "r") as file:
- notifications = json.load(file)
-
- def validate_with_id(notif):
- """Helper to validate and return ID if valid."""
- try:
- self._deserialize_notification(notif)
- return (True, notif, notif.get("id", 0))
- except Exception as e:
- msg = f"[Notification] Invalid: {str(e)[:50]}"
- logger.error(msg)
- return (False, None, 0)
-
- # Validate all notifications at once
- results = [validate_with_id(n) for n in notifications]
-
- # Process results and find highest ID
- valid_notifications = []
- highest_id = self._count # Start with current count
- for is_valid, notif, notif_id in results:
- if is_valid:
- valid_notifications.append(notif)
- highest_id = max(highest_id, notif_id)
-
- self.all_notifications = valid_notifications
- self._count = highest_id # Update to highest ID seen
- self._write_notifications(self.all_notifications)
-
- except (json.JSONDecodeError, KeyError, ValueError, IndexError) as e:
- logger.error(f"[Notification] {e}")
- self.all_notifications = []
- self._count = 0
-
- def remove_notification(self, id: int):
- """Remove the notification of given id."""
- item = next((p for p in self.all_notifications if p["id"] == id), None)
- if item:
- self.all_notifications.remove(item)
- self._write_notifications(self.all_notifications)
- self.emit("notification_count", len(self.all_notifications))
-
- # Emit clear_all signal if there are no notifications left
- if len(self.all_notifications) == 0:
- self.emit("clear_all", True)
-
- def cache_notification(self, data: Notification):
- """Cache the notification without widget_config and max_count."""
-
- # First clean up any invalid notifications
- self._cleanup_invalid_notifications()
-
- # Create the new notification
- self._count += 1
- new_id = self._count
- timestamp = GLib.get_real_time() / 1000000 # Convert microseconds to seconds
- serialized_data = data.serialize()
- serialized_data.update({
- "id": new_id,
- "app_name": data.app_name,
- "timestamp": timestamp
- })
-
- # Append the new notification
- self.all_notifications.append(serialized_data)
-
- self._write_notifications(self.all_notifications)
- self.emit("notification_count", len(self.all_notifications))
-
- def _cleanup_invalid_notifications(self):
- """Remove any invalid notifications."""
-
- def validate_with_id(notif):
- """Helper to validate and return result with ID."""
- try:
- self._deserialize_notification(notif)
- return (True, notif, None)
- except Exception as e:
- msg = f"[Notification] Removing invalid: {str(e)[:50]}"
- logger.debug(msg)
- return (False, None, notif.get("id", 0))
-
- # Validate all notifications at once
- results = [validate_with_id(n) for n in self.all_notifications]
-
- # Process results
- valid_notifications = []
- invalid_count = 0
- for is_valid, notif, invalid_id in results:
- if is_valid:
- valid_notifications.append(notif)
- else:
- invalid_count += 1
- self.emit("notification-closed", invalid_id, "dismissed-by-limit")
-
- if invalid_count > 0:
- self.all_notifications = valid_notifications
- self._write_notifications(self.all_notifications)
- self.emit("notification_count", len(self.all_notifications))
-
- def _deserialize_notification(self, notification):
- """Deserialize a notification."""
- try:
- notif = Notification.deserialize(notification)
- # Store the timestamp in the notification object
- notif.timestamp = notification.get("timestamp", GLib.get_real_time() / 1000000)
- return notif
- except Exception as e:
- logger.error(
- f"[Notification] Deserialize failed for: {notification}, Error: {e}"
- )
- raise
-
- def clear_all_notifications(self):
- """Empty the notifications."""
- logger.info("[Notification] Clearing all notifications")
- # Clear notifications but preserve the highest ID we've seen
- highest_id = self._count
- self.all_notifications = []
- self._write_notifications(self.all_notifications)
- self.emit("notification_count", 0)
- self.emit("clear_all", True)
- # Restore the ID counter so new notifications get unique IDs
- self._count = highest_id
-
- def _write_notifications(self, data):
- """Write the notifications to the cache file."""
- try:
- with open(NOTIFICATION_CACHE_FILE, "w") as f:
- json.dump(data, f, indent=4, ensure_ascii=False)
- logger.info("[Notification] Notifications written successfully.")
- except Exception as e:
- logger.error(f"[Notification] Failed to write notifications: {e}")
-
- def get_deserialized(self) -> List[Notification]:
- """Return the notifications."""
-
- def deserialize_with_id(notif):
- """Helper to deserialize and return result with ID."""
- try:
- return (self._deserialize_notification(notif), None)
- except Exception as e:
- msg = f"[Notification] Deserialize failed: {str(e)[:50]}"
- logger.error(msg)
- return (None, notif.get("id"))
-
- # Process all notifications at once
- results = [
- deserialize_with_id(notification) for notification in self.all_notifications
- ]
-
- # Split into successful and failed
- deserialized = []
- invalid_ids = []
- for result, error_id in results:
- if result is not None:
- deserialized.append(result)
- elif error_id is not None:
- invalid_ids.append(error_id)
-
- # Clean up invalid notifications
- for invalid_id in invalid_ids:
- self.remove_notification(invalid_id)
-
- return deserialized
diff --git a/services/powerprofile.py b/services/powerprofile.py
deleted file mode 100644
index cccf03e..0000000
--- a/services/powerprofile.py
+++ /dev/null
@@ -1,88 +0,0 @@
-import dbus
-from dbus.mainloop.glib import DBusGMainLoop
-from fabric import Service, Signal
-from loguru import logger
-
-
-class PowerProfiles(Service):
- """Service to interact with the PowerProfiles service."""
-
- @Signal
- def profile(self, value: str) -> None:
- """Signal emitted when profile changes."""
-
- instance = None
-
- @staticmethod
- def get_default():
- if PowerProfiles.instance is None:
- PowerProfiles.instance = PowerProfiles()
-
- return PowerProfiles.instance
-
- def __init__(
- self,
- **kwargs,
- ):
- super().__init__(
- **kwargs,
- )
-
- self.bus_name = "net.hadess.PowerProfiles"
- self.object_path = "/net/hadess/PowerProfiles"
-
- self.power_profiles = {
- "power-saver": {
- "name": "Power Saver",
- "icon_name": "power-profile-power-saver-symbolic",
- },
- "balanced": {
- "name": "Balanced",
- "icon_name": "power-profile-balanced-symbolic",
- },
- "performance": {
- "name": "Performance",
- "icon_name": "power-profile-performance-symbolic",
- },
- }
-
- # Set up the dbus main loop
- DBusGMainLoop(set_as_default=True)
-
- self.bus = dbus.SystemBus()
-
- self.power_profiles_obj = self.bus.get_object(self.bus_name, self.object_path)
-
- self.iface = dbus.Interface(
- self.power_profiles_obj, "org.freedesktop.DBus.Properties"
- )
-
- # Connect the 'g-properties-changed' signal to the handler
- self.iface.connect_to_signal("PropertiesChanged", self.handle_property_change)
-
- def get_current_profile(self):
- try:
- return self.iface.Get("net.hadess.PowerProfiles", "ActiveProfile").strip()
-
- except dbus.DBusException as e:
- logger.error(f"[PowerProfile] Error retrieving current power profile: {e}")
- return "balanced"
-
- def set_power_profile(self, profile):
- try:
- self.iface.Set(self.bus_name, "ActiveProfile", dbus.String(profile))
- logger.info(f"[PowerProfile] Power profile set to {profile}")
- except dbus.DBusException as e:
- logger.error(
- f"[PowerProfile] Could not change power level to {profile}: {e}"
- )
-
- # Function to handle properties change signals
- def handle_property_change(self, proxy, changed, invalidated):
- # Print the changed ActiveProfile
- if "ActiveProfile" in changed:
- logger.info(f"Profile changed: {changed['ActiveProfile']}")
- self.emit("profile", changed["ActiveProfile"])
-
- def get_profile_icon(self, profile):
- return self.power_profiles.get(profile, "balanced").get("icon_name")
diff --git a/services/screen_record.py b/services/screen_record.py
deleted file mode 100644
index 7fbf6d5..0000000
--- a/services/screen_record.py
+++ /dev/null
@@ -1,142 +0,0 @@
-import datetime
-import subprocess
-
-from fabric.core.service import Property, Service, Signal
-from fabric.utils import exec_shell_command, exec_shell_command_async
-from gi.repository import Gio, GLib
-from loguru import logger
-
-
-class ScreenRecorder(Service):
- @Signal
- def recording(self, value: bool) -> None: ...
-
- def __init__(self, **kwargs):
- self.screenshot_path = GLib.get_home_dir() + "/Pictures/Screenshots/"
- self.screenrecord_path = GLib.get_home_dir() + "/Videos/Screencasting/"
-
- super().__init__(**kwargs)
-
- def screenshot(self, fullscreen=False, save_copy=True):
- time = datetime.datetime.today().strftime("%Y-%m-%d_%H-%M-%S")
- file_path = self.screenshot_path + str(time) + ".png"
- command = (
- ["grimblast", "copysave", "screen", file_path]
- if save_copy
- else ["grimblast", "copyscreen"]
- )
- if not fullscreen:
- command[2] = "area"
- try:
- subprocess.run(command, check=True)
- self.send_screenshot_notification(
- file_path=file_path if file_path else None,
- )
- except Exception as e:
- logger.error(f"[SCREENSHOT] Failed to run command: {command}")
-
- def screencast_start(self, fullscreen=False):
- if self.is_recording:
- logger.error(
- "[SCREENRECORD] Another instance of wf-recorder is already running"
- )
- return
- time = datetime.datetime.today().strftime("%Y-%m-%d_%H-%M-%S")
- file_path = self.screenrecord_path + str(time) + ".mp4"
- self._current_screencast_path = file_path
- area = "" if fullscreen else f"-g '{exec_shell_command('slurp')}'"
- command = f"wf-recorder --file={file_path} --pixel-format yuv420p {area}"
- exec_shell_command_async(command)
- self.emit("recording", True)
-
- def screencast_stop(self):
- exec_shell_command_async("killall -INT wf-recorder")
- self.emit("recording", False)
- self.send_screencast_notification(self._current_screencast_path)
-
- def send_screencast_notification(self, file_path):
- # TODO: Generate a thumbnail
- # exec_shell_command_async(f"totem-video-thumbnailer {file_path} {FABRIC_CACHE}")
-
- cmd = ["notify-send"]
- cmd.extend(
- [
- "-A",
- "files=Show in Files",
- "-A",
- "view=View",
- "-i",
- "camera-video-symbolic",
- "-a",
- "Fabric Screenshot Utility",
- "Screencast Saved",
- f"Saved Screencast at {file_path}",
- ]
- )
-
- proc: Gio.Subprocess = Gio.Subprocess.new(cmd, Gio.SubprocessFlags.STDOUT_PIPE)
-
- def do_callback(process: Gio.Subprocess, task: Gio.Task):
- try:
- _, stdout, stderr = process.communicate_utf8_finish(task)
- except Exception:
- logger.error(
- f"[SCREENCAST] Failed read notification action with error {stderr}"
- )
- return
-
- match stdout.strip("\n"):
- case "files":
- exec_shell_command_async(f"xdg-open {self.screenrecord_path}")
- case "view":
- exec_shell_command_async(f"xdg-open {file_path}")
-
- proc.communicate_utf8_async(None, None, do_callback)
-
- def send_screenshot_notification(self, file_path=None):
- cmd = ["notify-send"]
- cmd.extend(
- [
- "-A",
- "files=Show in Files",
- "-A",
- "view=View",
- "-A",
- "edit=Edit",
- "-i",
- "camera-photo-symbolic",
- "-a",
- "Fabric Screenshot Utility",
- "-h",
- f"STRING:image-path:{file_path}",
- "Screenshot Saved",
- f"Saved Screenshot at {file_path}",
- ]
- if file_path
- else ["Screenshot Sent to Clipboard"]
- )
-
- proc: Gio.Subprocess = Gio.Subprocess.new(cmd, Gio.SubprocessFlags.STDOUT_PIPE)
-
- def do_callback(process: Gio.Subprocess, task: Gio.Task):
- try:
- _, stdout, stderr = process.communicate_utf8_finish(task)
- except Exception:
- logger.error(
- f"[SCREENSHOT] Failed read notification action with error {stderr}"
- )
- return
-
- match stdout.strip("\n"):
- case "files":
- exec_shell_command_async(f"xdg-open {self.screenshot_path}")
- case "view":
- exec_shell_command_async(f"xdg-open {file_path}")
- case "edit":
- exec_shell_command_async(f"swappy -f {file_path}")
-
- proc.communicate_utf8_async(None, None, do_callback)
-
- @Property(bool, "readable", default_value=False)
- def is_recording(self):
- return False if len(exec_shell_command("pidof wf-recorder")) == 0 else True
diff --git a/styles/bar.css b/styles/bar.css
index 6f4993c..03a3a6f 100644
--- a/styles/bar.css
+++ b/styles/bar.css
@@ -1,196 +1,107 @@
#bar {
- padding: 0px 5px 0px 5px;
- margin: 1px;
- transition: all 0.5s cubic-bezier(0.5, 0.25, 0, 1);
+ padding: 8px;
background-color: var(--surface);
- /* Bar background */
+ border-radius: 0 0 16px 16px;
}
-#start-container,
-#center-container,
-#end-container {
- padding-top: 2px;
- padding-bottom: 2px;
+
+#bar-container {
+ transition: all 0.25s cubic-bezier(0.5, 0.25, 0, 1);
+ opacity: 1;
+ }
+
+ #bar-container.hide-bar {
+ margin-top: -45px;
+ }
+
+ #bar-container.show-bar {
+ margin-top: 0;
+ }
+
+ #bar-container.occluded {
+ margin-top: -45px;
+ }
+
+
+ #bar-corner-left {
+ margin-left: -16px;
+ }
+
+ #bar-corner-right {
+ margin-right: -16px;
+ }
+
+ #active-window {
+ padding: 0px 8px;
+ background-color: var(--surface-container);
+ border-radius: 8px;
+ }
+
+
+ #workspaces {
+ padding: 4px;
+ min-width: 20px;
+ border-radius: 8px;
+ background-color: var(--surface-container);
+ }
+
+ #date-time {
+ background-color: var(--surface-container);
+ border-radius: 8px;
+ padding: 0px 8px;
+ }
+
+ #tray {
+ background-color: var(--surface-container);
+ padding: 4px;
+ border-radius: 0 8px 8px 0px;
+ }
+
+#tray-button{
+ background-color: var(--surface-container);
+ padding: 4px;
+ border-radius: 8px 0 0 8px;
}
-#bar-corner * {
- background-color: var(--surface);
-}
+ #tray > button {
+ margin: 0px;
+ padding: 0px;
+ }
-/**
- * ── Workspace ─────────────────────────────────────────────────────────
- */
-#workspaces {
- font-style: normal;
- background-color: var(--surface-container);
+ menu {
+ border: solid 1px var(--outline-variant);
+ border-radius: 16px;
+ background-color: var(--surface);
+ padding: 6px;
+ }
+
+ menu > menuitem {
+ border-radius: 10px;
+ padding: 6px 10px;
+ }
+
+ menu > menuitem:hover {
+ background-color: var(--surface-bright);
+ }
+
+ menu > menuitem:hover > label {
+ color: var(--on-surface);
+ }
+
+
+#metrics{
padding: 4px;
- margin-top: 1px;
- margin-bottom: 1px;
- border-radius: 8px;
-}
-
-/**
- * ── Update ────────────────────────────────────────────────────────────
- */
-#update {
- background-color: var(--surface-container);
- margin-top: 1px;
- margin-bottom: 1px;
- border-radius: 8px;
- padding: 1px;
- min-width: 24px;
- min-height: 24px;
- transition: background-color 0.1s ease;
-}
-
-#update:hover {
- background-color: var(--surface-bright);
-}
-
-#update:active {
- color: var(--shadow);
- background-color: var(--primary);
-}
-
-#update-icon {
- color: var(--primary);
- font-size: 20px;
- padding: 1px;
- border-radius: 11px;
- transition: all 0.1s ease;
-}
-
-#updated-icon {
- color: var(--primary);
- font-size: 20px;
- padding: 4px;
- border-radius: 8px;
- transition: all 0.1s ease;
-}
-
-#update-label {
- color: var(--primary);
- font-size: 10px;
- font-weight: bold;
- padding: 0px;
- margin-right: 4px;
- border-radius: 8px;
- transition: all 0.1s ease;
-}
-
-/**
- * ── Clock ─────────────────────────────────────────────────────────────
- */
-#datetime {
- background-color: var(--surface-container);
- margin-top: 1px;
- margin-bottom: 1px;
- padding: 4px;
- border-radius: 8px;
-}
-
-#datetime:hover {
- background-color: var(--surface-bright);
-}
-
-/**
- * ── Tray ──────────────────────────────────────────────────────────────
- */
-#tray {
- background-color: var(--surface-container);
- margin-top: 1px;
- margin-bottom: 1px;
- padding: 4px;
- border-radius: 8px;
-}
-
-#tray > button {
- margin: 0px;
- padding: 0px;
-}
-
-menu {
- border: solid 1px var(--outline-variant);
- border-radius: 16px;
- background-color: var(--surface);
- padding: 6px;
-}
-
-menu > menuitem {
- border-radius: 10px;
- padding: 6px 10px;
-}
-
-menu > menuitem:hover {
- background-color: var(--surface-bright);
-}
-
-menu > menuitem:hover > label {
- color: var(--on-surface);
-}
-
-/**
- * ── Logo ──────────────────────────────────────────────────────────────
- */
-#button-bar {
- background-color: var(--surface-container);
- margin-top: 1px;
- padding: 4px;
- border-radius: 8px;
- margin-bottom: 1px;
- transition: all 0.1s ease;
-}
-
-#button-bar-label {
- color: var(--primary);
- font-size: 24px;
- border-radius: 8px;
- transition: all 0.1s ease;
-}
-
-#button-bar:hover #button-bar-label {
- background-color: var(--surface_bright);
-}
-
-#button-bar:active #button-bar-label {
- color: var(--shadow);
- background-color: var(--primary);
-}
-
-/**
- * ── System Indicator ─────────────────────────────────────────────────
- */
-#system-indicator {
- background-color: var(--surface-container);
- margin-top: 1px;
- padding: 4px;
- margin-bottom: 1px;
- border-radius: 8px;
-}
-
-#system-indicator-icon {
- font-size: 20px;
-}
-
-#system-notif-label {
- font-size: 18px;
-}
-
-/**
- * ── Metric ────────────────────────────────────────────────────────────
- */
-#metrics {
background-color: var(--surface-container);
border-radius: 8px;
}
#metrics-circle {
color: var(--surface-bright);
- border: 3px solid var(--primary);
+ border: 2px solid var(--primary);
}
#metrics-circle.bat {
- border: 3px solid var(--green);
+ border: 2px solid var(--green);
}
#metrics-circle.alert {
@@ -207,8 +118,63 @@ menu > menuitem:hover > label {
#metrics-level {
font-weight: bold;
+ padding: 2px;
}
#metrics-sep {
min-width: 6px;
}
+
+#system-indicator {
+ padding: 4px;
+ background-color: var(--surface-container);
+ border-radius: 8px;
+}
+
+#system-indicator-icon {
+ font-size: 20px;
+}
+
+
+#power-menu {
+ padding: 4px;
+ border-radius: 8px;
+ background-color: var(--surface-container);
+ transition: all 0.2s ease;
+}
+
+#power-menu .power-icon{
+ font-size: 20px;
+}
+
+#power-menu.locked {
+ /* opacity: 0.5; */
+ background-color: var(--surface-container);
+}
+
+#power-menu.locked:hover {
+ opacity: 0.7;
+ background-color: var(--surface-container);
+}
+
+#power-menu .power-label {
+ transition: all 0.2s ease;
+}
+
+#power-menu.locked .power-label {
+ color: var(--on-surface-variant);
+}
+
+#power-menu:not(.locked) .power-label {
+ color: var(--on-surface);
+}
+
+
+.powerlabel{
+ margin-left: 4px;
+}
+
+.powericon{
+ font-size: 20px;
+}
+
diff --git a/styles/colors.css b/styles/colors.css
index 11d719c..1b0a1d8 100644
--- a/styles/colors.css
+++ b/styles/colors.css
@@ -1,31 +1,31 @@
:vars {
- --foreground: #dde4e2;
- --background: #0e1514;
- --cursor: #dde4e2;
- --primary: #81d5ca;
- --primary-container: #00504a;
- --on-primary: #003732;
- --on-primary-container: #9df2e6;
- --secondary: #b1ccc7;
- --on-secondary: #1c3531;
- --secondary-container: #324b48;
- --tertiary: #aec9e6;
- --tertiary-container: #2e4961;
- --on-tertiary: #163349;
- --on-tertiary-container: #cde5ff;
- --surface: #0e1514;
- --surface-container: #1a2120;
- --surface-bright: #343a39;
- --surface-variant: #3f4947;
- --surface-container-low: #161d1c;
- --on-surface: #dde4e2;
+ --foreground: #dde4e3;
+ --background: #0e1415;
+ --cursor: #dde4e3;
+ --primary: #80d4d7;
+ --primary-container: #004f51;
+ --on-primary: #003738;
+ --on-primary-container: #9cf1f3;
+ --secondary: #b0cccc;
+ --on-secondary: #1b3435;
+ --secondary-container: #324b4c;
+ --tertiary: #b4c7e9;
+ --tertiary-container: #354863;
+ --on-tertiary: #1e314c;
+ --on-tertiary-container: #d4e3ff;
+ --surface: #0e1415;
+ --surface-container: #1a2121;
+ --surface-bright: #343a3a;
+ --surface-variant: #3f4949;
+ --surface-container-low: #161d1d;
+ --on-surface: #dde4e3;
--error: #ffb4ab;
--error-dim: #ff8678;
--on-error: #690005;
--error-container: #93000a;
--on-error-container: #ffdad6;
- --outline: #899390;
- --outline-variant: #3f4947;
+ --outline: #899393;
+ --outline-variant: #3f4949;
--shadow: #000000;
--red: #ffb595;
--red-dim: #ff9162;
@@ -37,8 +37,8 @@
--blue-dim: #7ca2ff;
--magenta: #e4b7f3;
--magenta-dim: #d48bec;
- --cyan: #80d5cf;
- --cyan-dim: #5ac8c1;
+ --cyan: #80d4d5;
+ --cyan-dim: #5ac7c8;
--white: #82d3e0;
}
diff --git a/styles/dashboard.css b/styles/dashboard.css
deleted file mode 100644
index 6d99fc2..0000000
--- a/styles/dashboard.css
+++ /dev/null
@@ -1,163 +0,0 @@
-#buttons-grid {
- min-height: 52px;
-}
-
-#network-button,
-#bluetooth-button,
-#night-mode-button,
-#power-profile-button,
-#dark-mode-button,
-#caffeine-button {
- border-radius: 20px;
-}
-
-#network-button,
-#bluetooth-button {
- background-color: unset;
- padding: 0;
-}
-
-#night-mode-button,
-#dark-mode-button,
-#power-profile-button,
-#caffeine-button {
- background-color: var(--primary);
- padding: 0 10px;
-}
-
-#night-mode-button.disabled,
-#dark-mode-button.disabled,
-#power-profile-button.disabled,
-#caffeine-button.disabled {
- background-color: var(--surface);
-}
-
-#network-status-button,
-#network-menu-button,
-#bluetooth-status-button,
-#bluetooth-menu-button {
- background-color: var(--primary);
- padding: 0 8px;
-}
-
-#network-menu-button,
-#bluetooth-menu-button {
- border-left: 1px solid var(--outline);
- min-width: 24px;
-}
-
-#network-status-button:hover,
-#network-menu-button:hover,
-#bluetooth-status-button:hover,
-#bluetooth-menu-button:hover,
-#bluetooth-button:hover,
-#night-mode-button:hover,
-#power-profile-button:hover,
-#dark-mode-button:hover,
-#caffeine-button:hover {
- background-color: var(--foreground);
-}
-
-/* Estados Disabled */
-#network-status-button.disabled,
-#network-menu-button.disabled,
-#bluetooth-status-button.disabled,
-#bluetooth-menu-button.disabled {
- background-color: var(--surface);
-}
-
-#network-menu-button.disabled,
-#bluetooth-menu-button.disabled {
- border-left: 1px solid var(--surface-bright);
-}
-
-#network-status-button.disabled:hover,
-#network-menu-button.disabled:hover,
-#bluetooth-status-button.disabled:hover,
-#bluetooth-menu-button.disabled:hover,
-#night-mode-button.disabled:hover,
-#power-profile-button.disabled:hover,
-#dark-mode-button.disabled:hover,
-#caffeine-button.disabled:hover {
- background-color: var(--surface-bright);
-}
-
-#network-status-button,
-#bluetooth-status-button {
- border-radius: 20px 0 0 20px;
-}
-
-#network-menu-button,
-#bluetooth-menu-button {
- border-radius: 0 20px 20px 0;
-}
-
-#network-icon,
-#bluetooth-icon,
-#night-mode-icon,
-#dark-mode-icon,
-#power-profile-icon,
-#caffeine-icon {
- color: var(--shadow);
- font-size: 24px;
-}
-
-#network-icon.disabled,
-#bluetooth-icon.disabled,
-#night-mode-icon.disabled,
-#power-profile-icon.disabled,
-#dark-mode-icon.disabled,
-#caffeine-icon.disabled {
- color: var(--outline);
-}
-
-#network-menu-label,
-#bluetooth-menu-label {
- color: var(--shadow);
- font-size: 16px;
-}
-
-#network-menu-label.disabled,
-#bluetooth-menu-label.disabled {
- color: var(--outline);
-}
-
-#network-label,
-#bluetooth-label,
-#night-mode-label,
-#power-profile-label,
-#caffeine-label,
-#dark-mode-label {
- color: var(--shadow);
- font-size: 14px;
- font-weight: bold;
- margin-bottom: -4px;
-}
-
-#network-label.disabled,
-#bluetooth-label.disabled,
-#night-mode-label.disabled,
-#power-profile-label.disabled,
-#dark-mode-label.disabled,
-#caffeine-label.disabled {
- color: var(--outline);
-}
-
-#network-ssid,
-#bluetooth-status,
-#night-mode-status,
-#power-profile-status,
-#dark-mode-status,
-#caffeine-status {
- color: var(--surface-bright);
- font-size: 12px;
-}
-
-#network-ssid.disabled,
-#bluetooth-status.disabled,
-#night-mode-status.disabled,
-#power-profile-status.disabled,
-#dark-mode-status.disabled,
-#caffeine-status.disabled {
- color: var(--outline);
-}
diff --git a/styles/dock.css b/styles/dock.css
index c0a9860..1d1de04 100644
--- a/styles/dock.css
+++ b/styles/dock.css
@@ -6,21 +6,6 @@
transition: all 0.25s cubic-bezier(0.5, 0.25, 0, 1);
}
-#dock.vertical {
- margin: 8px 79px 8px 8px;
- border-radius: 20px 0 0 20px;
-}
-
-#dock.vertical-left {
- margin: 8px 39px 8px 0px;
- border-radius: 0 20px 20px 0;
-}
-
-#dock.vertical-right{
- margin: 8px 79px 8px 8px;
- border-radius: 20px 0 0 20px;
-}
-
#dock-full {
transition: all 0.25s cubic-bezier(0.5, 0.25, 0, 1);
opacity: 1;
@@ -28,6 +13,7 @@
#dock-full.occluded {
@apply dock_nmargin();
+ /* opacity: 0; */
}
#dock-separator {
@@ -56,7 +42,7 @@
#dock-app-button.instance {
background: linear-gradient(to top, transparent, var(--surface-bright));
- border-bottom: 1px solid var(--surface-container);
+ border-bottom: 1px solid var(--surface);
border-top: 1px solid var(--outline);
padding: 3px 4px;
}
diff --git a/styles/launcher.css b/styles/launcher.css
deleted file mode 100644
index 9728485..0000000
--- a/styles/launcher.css
+++ /dev/null
@@ -1,827 +0,0 @@
-/*
- * ── launcher ──────────────────────────────────────────────────────────
-*/
-#dashboard {
- min-height: 100px;
-}
-
-#launcher {
- margin: 18px;
- border: 1px solid var(--outline-variant);
- background-color: var(--surface);
- border-radius: 24px;
- padding: 18px;
- transition: all 0.5s cubic-bezier(0.5, 0.25, 0, 1);
-}
-
-#launcher entry {
- transition: all 0.1s ease;
- border-radius: 11px;
- padding: 10px;
- margin: 12px;
- box-shadow: inset 0 0 0 1px var(--outline-variant);
-}
-
-#launcher-content.wallpapers {
- padding: 16px;
- min-height: 420px;
- min-width: 620px;
-}
-
-.applauncher-scrolled-window {
- transition: min-height 0.2s ease;
-}
-
-#launcher-content.bluetooth {
- min-height: 464px;
-}
-
-#launcher-content.wifi {
- min-height: 464px;
-}
-
-#launcher-content.tools {
- min-width: 308px;
-}
-
-#launcher-content.calendar {
- min-height: 420px;
- min-width: 420px;
-}
-
-#launcher-content {
- min-width: 485px;
-}
-
-#launcher-content.power {
- min-width: 200px;
- min-height: 52px;
-}
-
-#launcher-content.kanban {
- min-width: 1000px;
- min-height: 478px;
-}
-
-#launcher-content.tmux {
- min-height: 220px;
- min-width: 480px;
-}
-
-#scrolled-window scrollbar {
- border-radius: 10px;
- background-color: var(--surface);
- padding: 4px;
- margin-left: 6px;
-}
-
-#scrolled-window slider {
- border-radius: 8px;
- min-width: 10px;
- min-height: 48px;
- background-color: var(--primary);
-}
-
-#config-button:active,
-#new-session-button:active {
- background-color: var(--primary);
-}
-
-#config-label,
-#clear-label,
-#new-session-label {
- color: var(--primary);
- font-size: 24px;
-}
-
-#config-button:active #config-label,
-#clear-button:active
- #clear-label
- #new-session-button:active
- #new-session-label {
- color: var(--shadow);
-}
-
-#config-button,
-#clear-button,
-#new-session-button {
- background-color: var(--surface-variant);
- border-radius: 16px;
- padding: 8px;
- margin-bottom: 12px;
- margin-top: 12px;
-}
-
-#config-button:hover,
-#config-button:focus,
-#clear-button:hover,
-#clear-button:focus,
-#new-session-button:hover,
-#new-session-button:focus {
- background-color: var(--surface-bright);
-}
-
-#clear-button:active {
- background-color: var(--red-dim);
-}
-
-/*
- * ── applauncher ───────────────────────────────────────────────────────
-*/
-#app-desc {
- color: var(--outline);
- font-size: 12px;
- font-style: italic;
-}
-
-#slot-button {
- border-radius: 16px;
- padding: 16px;
- min-height: 0;
- transition: padding 0.25s cubic-bezier(0.5, 0.25, 0, 1);
- animation: loadSlot 0.5s ease;
-}
-
-@keyframes loadSlot {
- 0% {
- opacity: 0;
- }
-
- 100% {
- opacity: 1;
- }
-}
-
-#slot-button:hover,
-#slot-button:focus,
-#slot-button:selected,
-#slot-button.selected {
- background-color: var(--surface-bright);
- padding-left: 24px;
-}
-
-#slot-button:active {
- background-color: var(--primary);
-}
-
-#app-icon {
- margin-right: 8px;
-}
-
-#app-label {
- color: var(--foreground);
- font-weight: bold;
-}
-
-#slot-button:hover #app-label,
-#slot-button:focus #app-label,
-#slot-button:selected #app-label,
-#slot-button.selected #app-label {
- color: var(--primary);
-}
-
-#slot-button:active #app-label {
- color: var(--shadow);
-}
-
-/*
- * ── wallpaper ─────────────────────────────────────────────────────────
-*/
-#search-entry-walls {
- min-width: 512px;
-}
-
-#wallpaper-icons:selected {
- background-color: var(--surface-bright);
- border-radius: 0px;
-}
-
-#wallpaper-icons:hover {
- background-color: var(--surface-bright);
- border-radius: 0px;
-}
-
-#scheme-dropdown {
- background-color: var(--surface-container);
- padding: 8px;
- border-radius: 11px;
- margin-top: 12px;
- margin-bottom: 12px;
-}
-
-#scheme-dropdown > * > * {
- font-weight: bold;
- color: var(--shadow);
-}
-
-#scheme-dropdown.box button.box.cellview {
- background-color: var(--primary);
-}
-
-#scheme-dropdown.box button.box.cellview:selected {
- background-color: var(--surface-bright);
-}
-
-/* Customize the overall switch appearance */
-#matugen-switcher {
- min-width: 40px;
- min-height: 20px;
- background-color: var(--surface-variant);
- border-radius: 15px;
- padding: 2px;
- transition: background-color 0.3s ease;
-}
-
-/* Style the switch's slider */
-#matugen-switcher slider {
- background-color: var(--primary);
- border-radius: 16px;
- min-width: 20px;
- min-height: 20px;
- transition:
- background-color 0.1s cubic-bezier(0.5, 0.25, 0, 1.25),
- transform 0.25s cubic-bezier(0.5, 0.25, 0, 1.25);
-}
-
-/* When the switch is active (checked) */
-#matugen-switcher:checked {
- background-color: var(--primary);
-}
-
-/* Optional: additional styling for the slider when active */
-#matugen-switcher:checked slider {
- background-color: var(--surface);
-}
-
-#matugen-switcher:checked image {
- color: var(--surface);
-}
-
-#mat-label {
- font-size: 24px;
- margin-left: 4px;
-}
-
-/*
- * ── emoji ─────────────────────────────────────────────────────────────
-*/
-
-#emoji #search-entry {
- font-weight: bold;
- background-color: var(--surface);
- color: var(--foreground);
- border-radius: 16px;
- padding: 10px;
-}
-
-#emoji #search-entry selection {
- color: var(--background);
- background-color: var(--primary);
-}
-
-#emoji #close-button {
- background-color: var(--surface);
- border-radius: 16px;
- padding: 8px;
-}
-
-#emoji #close-button:hover,
-#emoji #close-button:focus {
- background-color: var(--surface-bright);
-}
-
-#emoji #close-button:active {
- background-color: var(--red-dim);
-}
-
-#emoji #close-label {
- color: var(--red-dim);
- font-size: 24px;
-}
-
-#emoji #close-button:active #close-label {
- color: var(--shadow);
-}
-
-#emoji #emoji-slot-button {
- border-radius: 16px;
- padding: 16px;
- transition: padding 0.1s ease;
-}
-
-@keyframes loadEmojiSlot {
- 0% {
- opacity: 0;
- }
-
- 100% {
- opacity: 1;
- }
-}
-
-#emoji #emoji-slot-button:hover,
-#emoji #emoji-slot-button:focus,
-#emoji #emoji-slot-button:selected,
-#emoji #emoji-slot-button.selected {
- background-color: var(--surface-bright);
- text-shadow: 0 0 4px var(--shadow);
- animation: highlightEmojiSlot 0.25s cubic-bezier(0, 0, 0.5, 1);
-}
-
-@keyframes highlightEmojiSlot {
- 0% {
- padding: 16px;
- text-shadow: 0 4px 4px var(--shadow);
- }
-
- 50% {
- padding: 8px 16px 24px 16px;
- text-shadow: 0 12px 4px var(--shadow);
- }
-
- 100% {
- padding: 16px;
- text-shadow: 0 4px 4px var(--shadow);
- }
-}
-
-#emoji #emoji-slot-button:active {
- background-color: var(--primary);
-}
-
-#emoji #emoji-name-label {
- color: var(--foreground);
- font-weight: bold;
-}
-
-#emoji #emoji-slot-button:hover #emoji-name-label,
-#emoji #emoji-slot-button:focus #emoji-name-label,
-#emoji #emoji-slot-button:selected #emoji-name-label,
-#emoji #emoji-slot-button.selected #emoji-name-label {
- color: var(--primary);
-}
-
-#emoji #emoji-slot-button:active #emoji-name-label {
- color: var(--shadow);
-}
-
-#emoji #emoji-char-label {
- font-size: 24px;
-}
-
-/*
- * ── powermenu ─────────────────────────────────────────────────────────
-*/
-
-#power-menu-button {
- border-radius: 20px;
- min-width: 52px;
- min-height: 52px;
- padding: 5px;
-}
-
-#power-menu-button label {
- font-size: 35px;
- color: var(--foreground);
-}
-
-#power-menu-button:hover,
-#power-menu-button:focus {
- background-color: var(--surface-bright);
-}
-
-#power-menu-button:hover label,
-#power-menu-button:focus label {
- color: var(--primary);
-}
-
-#power-menu-button:active {
- background-color: var(--primary);
-}
-
-#power-menu-button:active label {
- color: var(--shadow);
-}
-
-/*
- * ── sh runner ─────────────────────────────────────────────────────────
-*/
-
-#sh-item {
- border-radius: 11px;
- background-color: transparent;
- background-image: none;
- box-shadow: none;
- padding: 0px;
-}
-
-#sh-item {
- font-size: 24px;
-}
-
-#sh-item:hover,
-#sh-item:focus {
- text-shadow: 2px 2px 2px var(--primary);
-}
-
-/*
- * ── bluetooth ─────────────────────────────────────────────────────────
-*/
-
-#bluetooth-header {
- border: 2px solid var(--shadow);
- padding: 4px;
- background-color: var(--surface-container);
- border-radius: 12px;
-}
-
-#bluetooth-device {
- border: 2px solid var(--shadow);
- background-color: var(--surface-container);
- border-radius: 12px;
- padding: 4px;
-}
-
-#bluetooth-scan,
-#bluetooth-back {
- background-color: var(--surface);
- border-radius: 8px;
- padding: 4px;
-}
-
-#bluetooth-back-label {
- font-size: 20px;
-}
-
-#bluetooth-scan:hover,
-#bluetooth-back:hover {
- background-color: var(--surface-bright);
-}
-
-#bluetooth-scan label,
-#bluetooth-toggle label {
- font-weight: bold;
-}
-
-#bluetooth-text,
-#bluetooth-section {
- font-weight: bold;
-}
-
-#bluetooth-section {
- background-color: var(--surface-container);
- border-radius: 12px;
- padding: 8px;
-}
-
-#bluetooth-connection {
- font-size: 20px;
-}
-
-#bluetooth-connect {
- background-color: var(--surface);
- border-radius: 8px;
- padding: 8px;
-}
-
-#bluetooth-connect:hover {
- background-color: var(--surface-bright);
-}
-
-#bluetooth-paired,
-#bluetooth-available {
- background-color: var(--surface-container);
- border-radius: 20px;
- padding: 4px;
-}
-
-#bt-sep {
- border-radius: 16px;
-}
-
-#bluetooth-scan-label {
- font-size: 20px;
- color: var(--primary);
-}
-
-#bluetooth-scan.scanning {
- animation: blink 0.5s ease infinite;
-}
-
-#bluetooth-scan-label.scanning {
- animation: blink 0.5s ease infinite;
-}
-
-@keyframes blink {
- 0% {
- background-color: var(--blue);
- color: var(--shadow);
- }
-
- 50% {
- background-color: var(--shadow);
- color: var(--blue);
- }
-
- 100% {
- background-color: var(--blue);
- color: var(--shadow);
- }
-}
-
-/*
- * ── wifi ──────────────────────────────────────────────────────────────
-*/
-
-#wifi-header {
- background-color: var(--surface-container);
- padding: 4px;
- border-radius: 20px;
-}
-
-#wifi-device {
- background-color: var(--secondary-container);
- border-radius: 16px;
- padding: 4px;
- margin-bottom: 4px;
-}
-
-#wifi-toggle-icon {
- font-size: 24px;
-}
-
-#wifi-scan,
-#wifi-toggle {
- background-color: var(--surface-container);
- margin-left: 0px;
- min-width: 44px;
- min-height: 44px;
- border-radius: 100%;
-}
-
-#wifi-scan:hover,
-#wifi-toggle:hover {
- background-color: var(--surface-bright);
-}
-
-#wifi-scan label,
-#wifi-toggle label {
- font-weight: bold;
-}
-
-#wifi-text,
-#wifi-section {
- font-weight: bold;
-}
-
-#wifi-text {
- color: var(--primary);
-}
-
-#wifi-connection {
- font-size: 20px;
-}
-
-#wifi-connect {
- background-color: var(--surface);
- border-radius: 16px;
- padding: 8px 16px;
-}
-
-#wifi-connect:hover {
- background-color: var(--surface-bright);
-}
-
-#wifi-available {
- /*background-color: var(--surface-container);*/
- padding: 4px;
- border-radius: 20px;
-}
-
-/*
- * ── calendar ──────────────────────────────────────────────────────────
-*/
-
-#header {
- border-radius: 12px;
- padding: 4px;
-}
-
-#weekday-row {
- background-color: var(--surface);
- border-radius: 8px;
- padding: 4px;
-}
-
-#month-label,
-#weekday-label,
-#day-label {
- font-weight: bold;
-}
-
-#day-label,
-#day-empty {
- min-width: 38px;
- min-height: 22px;
-}
-
-#day-empty {
- color: var(--surface-bright);
-}
-
-#weekday-label {
- color: var(--primary);
-}
-
-#calendar-grid {
- border-radius: 16px;
- padding: 4px;
-}
-
-#day-label.current-day {
- background-color: var(--primary);
- color: var(--shadow);
- border-radius: 20px;
-}
-
-#prev-month-button,
-#next-month-button {
- background-color: var(--surface);
- border-radius: 8px;
- padding: 4px;
-}
-
-#prev-month-button:hover,
-#next-month-button:hover {
- background-color: var(--surface-bright);
-}
-
-#prev-month-button:active,
-#next-month-button:active {
- background-color: var(--primary);
-}
-
-#month-button-label {
- color: var(--primary);
- font-size: 20px;
-}
-
-#prev-month-button:active #month-button-label,
-#next-month-button:active #month-button-label {
- color: var(--shadow);
-}
-
-#prev-month-day,
-#next-month-day {
- color: mix(var(--on-surface), transparent, 0.6);
-}
-
-/*
- * ── tools ─────────────────────────────────────────────────────────────
-*/
-
-#toolbox-button {
- border-radius: 20px;
- min-width: 52px;
- min-height: 52px;
- padding: 5px;
-}
-
-#toolbox-button label {
- font-size: 35px;
- color: var(--foreground);
-}
-
-#toolbox-button.recording label {
- color: var(--red-dim);
-}
-
-#toolbox-button:hover,
-#toolbox-button:focus {
- background-color: var(--surface-bright);
-}
-
-#toolbox-button:hover label,
-#toolbox-button:focus label {
- color: var(--primary);
-}
-
-#toolbox-button.recording:hover label,
-#toolbox-button.recording:focus label {
- color: var(--red-dim);
-}
-
-#toolbox-button:active {
- background-color: var(--primary);
-}
-
-#toolbox-button:active label {
- color: var(--shadow);
-}
-
-#toolbox-button.recording:active {
- background-color: var(--red-dim);
-}
-
-#toolbox-button.recording:active label {
- color: var(--shadow);
-}
-
-#tool-sep {
- min-width: 4px;
- min-height: 4px;
- margin: 0px 4px;
- border-radius: 16px;
- background-color: var(--surface-bright);
-}
-
-/*
- * ── kanban ────────────────────────────────────────────────────────────
-*/
-
-#kanban-header {
- border: 2px solid var(--outline);
- border-radius: 12px;
- padding: 4px;
-}
-
-#column-header {
- padding: 4px;
- font-weight: bold;
- color: var(--primary);
-}
-
-#kanban-note {
- background-color: var(--surface-container);
- border-radius: 4px;
- padding: 8px;
- margin-bottom: 4px;
-}
-
-#kanban-row:first-child > * > * {
- border-radius: 16px 16px 4px 4px;
-}
-
-#kanban-row:last-child > * > * {
- border-radius: 4px 4px 16px 16px;
-}
-
-#inline-editor {
- border: 2px solid var(--surface-bright);
- border-radius: 12px;
- padding: 8px;
-}
-
-#kanban-btn,
-#kanban-btn-add {
- background-color: var(--surface-container);
- border-radius: 8px;
- padding: 4px;
-}
-
-#kanban-btn {
- background-color: var(--surface);
-}
-
-#kanban-btn:hover,
-#kanban-btn-add:hover {
- background-color: var(--surface-bright);
-}
-
-#kanban-btn:active,
-#kanban-btn-add:active {
- background-color: var(--primary);
-}
-
-#kanban-btn-label,
-#kanban-btn-neg {
- font-size: 20px;
- color: var(--primary);
-}
-
-#kanban-btn-neg {
- color: var(--red-dim);
-}
-
-#kanban-btn:active #kanban-btn-label,
-#kanban-btn-add:active #kanban-btn-label {
- color: var(--surface);
-}
-
-/* tmux */
-#no-notif,
-#no-tmux,
-#no-clip {
- font-size: 96px;
- color: var(--surface-container);
-}
-
-#clip-label {
- font-weight: bold;
-}
-
-#tmux-icon,
-#clip-icon {
- font-size: 20px;
- color: var(--primary);
-}
diff --git a/styles/main.css b/styles/main.css
index 79e35a8..7493591 100644
--- a/styles/main.css
+++ b/styles/main.css
@@ -1,26 +1,29 @@
-@import "./colors.css";
-@import "./bar.css";
-@import "./notification.css";
-@import "./osd.css";
-@import "./launcher.css";
-@import "./dashboard.css";
-@import "./dock.css";
-@import "./notif_center.css";
+@import url("./colors.css");
+@import url("./bar.css");
+@import url("./dock.css");
* {
all: unset;
color: var(--foreground);
font-size: 15px;
- font-family: "Monaspace Radon";
+ font-family: "Zed Sans","Jost*", sans-serif;
+}
+
+#corner {
+ background-color: var(--surface);
+ border-radius: 0;
+}
+
+#corner-container {
+ min-width: 20px;
+ min-height: 20px;
}
/*
* ── tooltip ───────────────────────────────────────────────────────────
*/
tooltip {
- border: solid 1px;
border-radius: 16px;
- border-color: var(--outline);
background-color: var(--surface);
animation: tooltipShow 0.25s cubic-bezier(0.5, 0.25, 0, 1);
}
@@ -40,12 +43,4 @@ tooltip > * {
border-radius: 10px;
}
-#corner {
- background-color: var(--surface);
- border-radius: 0;
-}
-#corner-container {
- min-width: 20px;
- min-height: 20px;
-}
diff --git a/styles/notif_center.css b/styles/notif_center.css
deleted file mode 100644
index 85f0f1d..0000000
--- a/styles/notif_center.css
+++ /dev/null
@@ -1,28 +0,0 @@
-#nhh {
- font-weight: bold;
-}
-
-#nhh-button {
- border-radius: 8px;
- background-color: var(--surface);
- padding: 4px;
-}
-
-#nhh-button:hover {
- background-color: var(--surface-bright);
-}
-
-#nhh-button-label {
- color: var(--red-dim);
- font-size: 20px;
-}
-
-#dnd-label {
- font-size: 20px;
- margin-left: 8px;
-}
-
-#no-notifications-label {
- font-size: 96px;
- color: var(--surface-container);
-}
diff --git a/styles/notification.css b/styles/notification.css
deleted file mode 100644
index dddf9bf..0000000
--- a/styles/notification.css
+++ /dev/null
@@ -1,83 +0,0 @@
-#notification-action-buttons button.start-action {
- border-radius: 20px;
-}
-
-#notification-action-buttons button {
- background-color: var(--surface);
- padding: 8px;
- border-radius: 20px;
- transition: background-color 0.1s ease;
-}
-
-#notification-action-buttons button:hover {
- background-color: var(--surface-bright);
-}
-
-#notification-action-buttons button:active {
- background-color: var(--primary);
-}
-
-#notification-action-buttons button:active #button-label {
- color: var(--shadow);
-}
-
-#notification-image image {
- border-radius: 16px;
- color: var(--on-surface);
-}
-
-#notification-summary,
-#button-label {
- font-weight: bold;
-}
-
-#notification-summary {
- font-weight: bold;
- color: var(--primary);
-}
-
-#notification-app-name {
- color: var(--outline);
-}
-
-#action-button {
- margin-top: 8px;
-}
-
-#notification-box {
- border-radius: 20px;
- padding: 16px;
- background-color: var(--surface-container-low);
- margin: 4px;
- min-width: 336px;
- transition: background-color 0.1s ease;
-}
-
-#boxed-notification-revealer {
- margin-top: -14px;
-}
-
-#notif-close-button {
- background-color: var(--surface);
- border-radius: 16px;
- padding: 4px;
- transition: background-color 0.1s ease;
-}
-
-#notif-close-button:hover,
-#notif-close-button:focus {
- background-color: var(--surface-bright);
-}
-
-#notif-close-button:active {
- background-color: var(--red-dim);
-}
-
-#notif-close-label {
- color: var(--red-dim);
- font-size: 20px;
-}
-
-#notif-close-button:active #notif-close-label {
- color: var(--shadow);
-}
diff --git a/styles/osd.css b/styles/osd.css
deleted file mode 100644
index 017865d..0000000
--- a/styles/osd.css
+++ /dev/null
@@ -1,47 +0,0 @@
-#osd {
- background-color: var(--surface);
- padding: 10px;
- border-radius: 6px;
- margin-top: 6px;
-}
-
-#osd scale {
- min-width: 150px;
-}
-
-#osd trough {
- background: var(--surface-container);
- min-height: 10px;
- margin-right: 4px;
- border-radius: 99px;
-}
-
-#osd highlight {
- border-radius: 99px 0px 0px 99px;
- background: var(--primary);
- margin-right: 6px;
-}
-
-#osd slider {
- color: var(--on-primary);
- background-color: var(--primary);
- background-size: 20px;
- min-width: 5px;
- min-height: 10px;
- border-radius: 12px;
- padding: 0px;
- background-repeat: no-repeat;
- background-position: center;
- margin-left: 0px;
- margin-right: 0px;
- margin-top: -10px;
- margin-bottom: -10px;
- box-shadow:
- -1px 0px 0px 1px var(--surface-container),
- 1px 0px 0px 1px var(--surface-container);
-}
-
-#osd-label {
- font-weight: bold;
- font-size: 17px;
-}
diff --git a/utils/__init__.py b/utils/__init__.py
index 43abad3..e69de29 100644
--- a/utils/__init__.py
+++ b/utils/__init__.py
@@ -1,4 +0,0 @@
-from .custom_image import *
-from .icon_resolver import *
-from .occlusion import *
-from .animator import *
diff --git a/utils/animator.py b/utils/animator.py
deleted file mode 100644
index 1799ae6..0000000
--- a/utils/animator.py
+++ /dev/null
@@ -1,179 +0,0 @@
-from typing import cast
-
-from fabric import Property, Service, Signal
-from gi.repository import GLib, Gtk
-
-
-class Animator(Service):
- @Signal
- def finished(self) -> None: ...
-
- @Property(tuple[float, float, float, float], "read-write")
- def bezier_curve(self) -> tuple[float, float, float, float]:
- return self._bezier_curve
-
- @bezier_curve.setter
- def bezier_curve(self, value: tuple[float, float, float, float]):
- self._bezier_curve = value
- return
-
- @Property(float, "read-write")
- def value(self):
- return self._value
-
- @value.setter
- def value(self, value: float):
- self._value = value
- return
-
- @Property(float, "read-write")
- def max_value(self):
- return self._max_value
-
- @max_value.setter
- def max_value(self, value: float):
- self._max_value = value
- return
-
- @Property(float, "read-write")
- def min_value(self):
- return self._min_value
-
- @min_value.setter
- def min_value(self, value: float):
- self._min_value = value
- return
-
- @Property(bool, "read-write", default_value=False)
- def playing(self):
- return self._playing
-
- @playing.setter
- def playing(self, value: bool):
- self._playing = value
- return
-
- @Property(bool, "read-write", default_value=False)
- def repeat(self):
- return self._repeat
-
- @repeat.setter
- def repeat(self, value: bool):
- self._repeat = value
- return
-
- def __init__(
- self,
- bezier_curve: tuple[float, float, float, float],
- duration: float,
- min_value: float = 0.0,
- max_value: float = 1.0,
- repeat: bool = False,
- tick_widget: Gtk.Widget | None = None,
- **kwargs,
- ):
- super().__init__(**kwargs)
- self._bezier_curve = (1, 0, 1, 1)
- self._duration = 5
- self._value = 0.0
- self._min_value = 0.0
- self._max_value = 1.0
- self._repeat = False
-
- self.bezier_curve = bezier_curve
- self.duration = duration
- self.value = min_value
- self.min_value = min_value
- self.max_value = max_value
- self.repeat = repeat
-
- self.playing = False
- self._start_time = None
- self._tick_handler = None
- self._timeline_pos = 0
- self._tick_widget = tick_widget
-
- def do_get_time_now(self):
- return GLib.get_monotonic_time() / 1_000_000
-
- def do_lerp(self, start: float, end: float, time: float) -> float:
- return start + (end - start) * time
-
- def do_interpolate_cubic_bezier(self, time: float) -> float:
- y_points = (0, self.bezier_curve[1], self.bezier_curve[3], 1)
- return (
- (1 - time) ** 3 * y_points[0]
- + 3 * (1 - time) ** 2 * time * y_points[1]
- + 3 * (1 - time) * time**2 * y_points[2]
- + time**3 * y_points[3]
- )
-
- def do_ease(self, time: float) -> float:
- return self.do_lerp(
- self.min_value, self.max_value, self.do_interpolate_cubic_bezier(time)
- )
-
- def do_update_value(self, delta_time: float):
- if not self.playing:
- return
-
- elapsed_time = delta_time - cast(float, self._start_time)
-
- self._timeline_pos = min(1, elapsed_time / self.duration)
-
- self.value = self.do_ease(self._timeline_pos)
-
- if not self._timeline_pos >= 1:
- return
-
- if not self.repeat:
- self.value = self.max_value
- self.finished()
- self.pause()
- return
-
- self._start_time = delta_time
- self._timeline_pos = 0
- return
-
- def do_handle_tick(self, *_):
- current_time = self.do_get_time_now()
- self.do_update_value(current_time)
- return True
-
- def do_remove_tick_handlers(self):
- if self._tick_handler:
- if self._tick_widget:
- self._tick_widget.remove_tick_callback(self._tick_handler)
- else:
- GLib.source_remove(self._tick_handler)
- self._tick_handler = None
- return
-
- def play(self):
- if self.playing:
- return
-
- self._start_time = self.do_get_time_now()
-
- if not self._tick_handler:
- if self._tick_widget:
- self._tick_handler = self._tick_widget.add_tick_callback(
- self.do_handle_tick
- )
- else:
- self._tick_handler = GLib.timeout_add(16, self.do_handle_tick)
-
- self.playing = True
- return
-
- def pause(self):
- self.playing = False
- return self.do_remove_tick_handlers()
-
- def stop(self):
- if not self._tick_handler:
- self._timeline_pos = 0
- self.playing = False
- return
- return self.do_remove_tick_handlers()
diff --git a/utils/custom_image.py b/utils/custom_image.py
deleted file mode 100644
index 1aae7f6..0000000
--- a/utils/custom_image.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import math
-from typing import cast
-
-import cairo
-from gi.repository import Gtk
-
-from fabric.widgets.image import Image
-
-
-class CustomImage(Image):
- def do_render_rectangle(
- self, cr: cairo.Context, width: int, height: int, radius: int = 0
- ):
- cr.move_to(radius, 0)
- cr.line_to(width - radius, 0)
- cr.arc(width - radius, radius, radius, -(math.pi / 2), 0)
- cr.line_to(width, height - radius)
- cr.arc(width - radius, height - radius, radius, 0, (math.pi / 2))
- cr.line_to(radius, height)
- cr.arc(radius, height - radius, radius, (math.pi / 2), math.pi)
- cr.line_to(0, radius)
- cr.arc(radius, radius, radius, math.pi, (3 * (math.pi / 2)))
- cr.close_path()
-
- def do_draw(self, cr: cairo.Context):
- context = self.get_style_context()
- width, height = self.get_allocated_width(), self.get_allocated_height()
- cr.save()
-
- self.do_render_rectangle(
- cr,
- width,
- height,
- cast(int, context.get_property("border-radius", Gtk.StateFlags.NORMAL)),
- )
- cr.clip()
- Image.do_draw(self, cr)
-
- cr.restore()
diff --git a/utils/icon_resolver.py b/utils/icon_resolver.py
index b8bedeb..a80c8f6 100644
--- a/utils/icon_resolver.py
+++ b/utils/icon_resolver.py
@@ -99,4 +99,4 @@ class IconResolver:
self._get_icon_from_desktop_file(desktop_file)
if desktop_file
else self.default_applicaiton_icon
- )
\ No newline at end of file
+ )
diff --git a/utils/popup.py b/utils/popup.py
new file mode 100644
index 0000000..f1c9942
--- /dev/null
+++ b/utils/popup.py
@@ -0,0 +1,279 @@
+from typing import ClassVar
+
+import gi
+from fabric.hyprland.service import HyprlandEvent
+from fabric.hyprland.widgets import get_hyprland_connection
+from fabric.widgets.box import Box
+from fabric.widgets.wayland import WaylandWindow
+from fabric.widgets.widget import Widget
+from gi.repository import Gdk, GLib, GObject, GtkLayerShell
+from loguru import logger
+
+gi.require_versions(
+ {"Gtk": "3.0", "Gdk": "3.0", "GtkLayerShell": "0.1", "GObject": "2.0"}
+)
+
+
+class PopoverManager:
+ """Singleton manager to handle shared resources for popovers."""
+
+ _instance = None
+
+ @classmethod
+ def get_instance(cls):
+ if cls._instance is None:
+ cls._instance = cls()
+ return cls._instance
+
+ def __init__(self):
+ # Shared overlay window for all popovers
+ self.overlay = WaylandWindow(
+ name="popover-overlay",
+ style_classes="popover-overlay",
+ title="fabric-shell-popover-overlay",
+ anchor="left top right bottom",
+ margin="-50px 0px 0px 0px",
+ exclusivity="auto",
+ layer="overlay",
+ type="top-level",
+ visible=False,
+ all_visible=False,
+ style="background-color: rgba(0,0,0,0.0);",
+ )
+
+ # Add empty box so GTK doesn't complain
+ self.overlay.add(Box())
+
+ # Keep track of active popovers
+ self.active_popover = None
+ self.available_windows = []
+
+ # Close popover when clicking overlay
+ self.overlay.connect("button-press-event", self._on_overlay_clicked)
+ self._hyprland_connection = get_hyprland_connection()
+ self._hyprland_connection.connect(
+ "event::focusedmonv2", self._on_monitor_change
+ )
+
+ def _on_monitor_change(self, _, event: HyprlandEvent):
+ if self.active_popover:
+ self.active_popover.hide_popover()
+ return True
+
+ def _on_overlay_clicked(self, widget, event):
+ if self.active_popover:
+ self.active_popover.hide_popover()
+ return True
+
+ def get_popover_window(self):
+ """Get an available popover window or create a new one."""
+ if self.available_windows:
+ return self.available_windows.pop()
+
+ window = WaylandWindow(
+ type="popup",
+ layer="overlay",
+ name="popover-window",
+ anchor="left top",
+ visible=False,
+ all_visible=False,
+ )
+ GtkLayerShell.set_keyboard_interactivity(window, True)
+ window.set_keep_above(True)
+ return window
+
+ def return_popover_window(self, window):
+ """Return a popover window to the pool."""
+ # Remove any children
+ for child in window.get_children():
+ window.remove(child)
+
+ window.hide()
+ # Only keep a reasonable number of windows in the pool
+ if len(self.available_windows) < 5:
+ self.available_windows.append(window)
+ else:
+ # Let the window be garbage collected
+ window.destroy()
+
+ def activate_popover(self, popover):
+ """Set the active popover and show overlay."""
+ if self.active_popover and self.active_popover != popover:
+ self.active_popover.hide_popover()
+
+ self.active_popover = popover
+ self.overlay.show()
+
+
+@GObject.Signal(
+ flags=GObject.SignalFlags.RUN_LAST, return_type=GObject.TYPE_NONE, arg_types=()
+)
+def popover_opened(widget: Widget): ...
+
+
+@GObject.Signal(
+ flags=GObject.SignalFlags.RUN_LAST, return_type=GObject.TYPE_NONE, arg_types=()
+)
+def popover_closed(widget: Widget): ...
+
+
+@GObject.type_register
+class Popover(Widget):
+ """Memory-efficient popover implementation."""
+
+ __gsignals__: ClassVar = {
+ "popover-opened": (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()),
+ "popover-closed": (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()),
+ }
+
+ def __init__(
+ self,
+ point_to,
+ content_factory=None,
+ content=None,
+ ):
+ super().__init__()
+ """
+ Initialize a popover.
+
+ Args:
+ content_factory: Function that returns content widget when called
+ point_to: Widget to position the popover next to
+ """
+ self._content_factory = content_factory
+ self._point_to = point_to
+ self._content_window = None
+ self._content = content
+ self._visible = False
+ self._destroy_timeout = None
+ # Use weak reference to avoid circular reference issues
+ self._manager = PopoverManager.get_instance()
+
+ def set_content_factory(self, content_factory):
+ """Set the content factory for the popover."""
+ self._content_factory = content_factory
+
+ def set_content(self, content):
+ """Set the content for the popover."""
+ self._content = content
+
+ def _on_key_press(self, widget, event):
+ if event.keyval == Gdk.KEY_Escape and self._manager.active_popover:
+ self._manager.active_popover.hide_popover()
+
+ def open(self):
+ if self._destroy_timeout is not None:
+ GLib.source_remove(self._destroy_timeout)
+ self._destroy_timeout = None
+
+ if not self._content_window:
+ try:
+ self._create_popover()
+ except Exception as e:
+ logger.error(f"Could not create popover! Error: {e}")
+ else:
+ self._manager.activate_popover(self)
+ self._content_window.show()
+ self._content_window.steal_input()
+ self._visible = True
+
+ self.emit("popover-opened")
+
+ def _calculate_margins(self):
+ widget_allocation = self._point_to.get_allocation()
+ popover_size = self._content_window.get_size()
+
+ display = Gdk.Display.get_default()
+ screen = display.get_default()
+ monitor_at_window = screen.get_monitor_at_window(self._point_to.get_window())
+ monitor_geometry = monitor_at_window.get_geometry()
+
+ x = (
+ widget_allocation.x
+ + (widget_allocation.width / 2)
+ - (popover_size.width / 2)
+ )
+ # Position below the button instead of above
+ y = widget_allocation.y + widget_allocation.height + 5 # 5px gap below button
+
+ # Ensure popover stays within monitor bounds
+ if x <= 0:
+ x = widget_allocation.x
+ elif x + popover_size.width >= monitor_geometry.width:
+ x = widget_allocation.x - popover_size.width + widget_allocation.width
+
+ # If popover would go below monitor, position it above the button
+ if y + popover_size.height > monitor_geometry.y + monitor_geometry.height:
+ y = widget_allocation.y - popover_size.height - 5 # 5px gap above button
+
+ return [y, 0, 0, x]
+
+ def set_position(self, position: tuple[int, int, int, int] | None = None):
+ if position is None:
+ self._content_window.set_margin(self._calculate_margins())
+ return False
+
+ self._content_window.set_margin(position)
+ return False
+
+ def _on_content_ready(self, widget, event):
+ self.set_position()
+
+ def _create_popover(self):
+ if self._content is None and self._content_factory is not None:
+ self._content = self._content_factory()
+
+ # Get a window from the pool
+ self._content_window = self._manager.get_popover_window()
+
+ # This is a hack to fix wrong positioning for widgets that are not rendered
+ # immediately (e.g., Gtk.Calendar())
+ self._content.connect("draw", self._on_content_ready)
+
+ # Add content to window
+ self._content_window.add(
+ Box(style_classes="popover-content", children=self._content)
+ )
+
+ self._content_window.connect("focus-out-event", self._on_popover_focus_out)
+
+ self._content_window.connect("key-press-event", self._on_key_press)
+ self._manager.activate_popover(self)
+ self._content_window.show()
+ self._content_window.steal_input()
+ self._visible = True
+
+ def _on_popover_focus_out(self, widget, event):
+ # This helps with keyboard focus issues
+ GLib.timeout_add(100, self.hide_popover)
+ return False
+
+ def hide_popover(self):
+ if not self._visible or not self._content_window:
+ return False
+
+ self._content_window.hide()
+ self._manager.overlay.hide()
+ self._visible = False
+
+ if not self._destroy_timeout:
+ self._destroy_timeout = GLib.timeout_add(1000 * 5, self._destroy_popover)
+
+ self.emit("popover-closed")
+
+ return False
+
+ def _destroy_popover(self):
+ """Return resources to the pool and clear references."""
+ self._destroy_timeout = None
+ self._visible = False
+
+ if self._content_window:
+ # Return window to the pool
+ self._manager.return_popover_window(self._content_window)
+ self._content_window = None
+
+ # Allow content to be garbage collected if no longer needed
+ self._content = None
+
+ return False
\ No newline at end of file
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..d3dd3fc
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,154 @@
+version = 1
+revision = 2
+requires-python = ">=3.13"
+
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "fabric"
+version = "0.0.2"
+source = { git = "https://github.com/Fabric-Development/fabric.git#1831ced4d9bb9f4be3893be55a8d502b47bff29e" }
+dependencies = [
+ { name = "click" },
+ { name = "loguru" },
+ { name = "pycairo" },
+ { name = "pygobject" },
+]
+
+[[package]]
+name = "loguru"
+version = "0.7.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "win32-setctime", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
+]
+
+[[package]]
+name = "modusv2"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "fabric" },
+ { name = "loguru" },
+ { name = "psutil" },
+ { name = "pydbus" },
+ { name = "setproctitle" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "fabric", git = "https://github.com/Fabric-Development/fabric.git" },
+ { name = "loguru", specifier = ">=0.7.3" },
+ { name = "psutil", specifier = ">=7.0.0" },
+ { name = "pydbus", specifier = ">=0.6.0" },
+ { name = "setproctitle", specifier = ">=1.3.6" },
+]
+
+[[package]]
+name = "psutil"
+version = "7.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" },
+ { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" },
+ { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
+]
+
+[[package]]
+name = "pycairo"
+version = "1.28.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/40/d9/412da520de9052b7e80bfc810ec10f5cb3dbfa4aa3e23c2820dc61cdb3d0/pycairo-1.28.0.tar.gz", hash = "sha256:26ec5c6126781eb167089a123919f87baa2740da2cca9098be8b3a6b91cc5fbc", size = 662477, upload-time = "2025-04-14T20:11:08.218Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/59/a7/c3e5ed55781dfe1b31eb4a2482aeae707671f3d36b0ea53a1722f4a3dfe9/pycairo-1.28.0-cp313-cp313-win32.whl", hash = "sha256:d13352429d8a08a1cb3607767d23d2fb32e4c4f9faa642155383980ec1478c24", size = 750594, upload-time = "2025-04-14T20:10:59.284Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/1c/ebadd290748aff3b6bc35431114d41e7a42f40a4b988c2aaf2dfed5d8156/pycairo-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:082aef6b3a9dcc328fa648d38ed6b0a31c863e903ead57dd184b2e5f86790140", size = 841774, upload-time = "2025-04-14T20:11:01.79Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ce/a3f5f1946613cd8a4654322b878c59f273c6e9b01dfadadd3f609070e0b9/pycairo-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:026afd53b75291917a7412d9fe46dcfbaa0c028febd46ff1132d44a53ac2c8b6", size = 691675, upload-time = "2025-04-16T19:30:26.565Z" },
+]
+
+[[package]]
+name = "pydbus"
+version = "0.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/56/3e84f2c1f2e39b9ea132460183f123af41e3b9c8befe222a35636baa6a5a/pydbus-0.6.0.tar.gz", hash = "sha256:4207162eff54223822c185da06c1ba8a34137a9602f3da5a528eedf3f78d0f2c", size = 22079, upload-time = "2016-12-18T16:44:31.904Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/56/27148014c2f85ce70332f18612f921f682395c7d4e91ec103783be4fce00/pydbus-0.6.0-py2.py3-none-any.whl", hash = "sha256:66b80106352a718d80d6c681dc2a82588048e30b75aab933e4020eb0660bf85e", size = 19580, upload-time = "2016-12-18T16:44:19.565Z" },
+]
+
+[[package]]
+name = "pygobject"
+version = "3.50.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycairo" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2b/58/d34e67a79631177e3c08e7d02b5165147f590171f2cae6769502af5f7f7e/pygobject-3.50.0.tar.gz", hash = "sha256:4500ad3dbf331773d8dedf7212544c999a76fc96b63a91b3dcac1e5925a1d103", size = 1080367, upload-time = "2024-09-12T12:05:22.579Z" }
+
+[[package]]
+name = "setproctitle"
+version = "1.3.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9e/af/56efe21c53ac81ac87e000b15e60b3d8104224b4313b6eacac3597bd183d/setproctitle-1.3.6.tar.gz", hash = "sha256:c9f32b96c700bb384f33f7cf07954bb609d35dd82752cef57fb2ee0968409169", size = 26889, upload-time = "2025-04-29T13:35:00.184Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/76/f1a2fdbf9b9602945a7489ba5c52e9863de37381ef1a85a2b9ed0ff8bc79/setproctitle-1.3.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e2a9e62647dc040a76d55563580bf3bb8fe1f5b6ead08447c2ed0d7786e5e794", size = 17392, upload-time = "2025-04-29T13:33:30.925Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/5b/4e0db8b10b4543afcb3dbc0827793d46e43ec1de6b377e313af3703d08e0/setproctitle-1.3.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:751ba352ed922e0af60458e961167fa7b732ac31c0ddd1476a2dfd30ab5958c5", size = 11951, upload-time = "2025-04-29T13:33:32.296Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/fe/d5d00aaa700fe1f6160b6e95c225b29c01f4d9292176d48fd968815163ea/setproctitle-1.3.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7890e291bf4708e3b61db9069ea39b3ab0651e42923a5e1f4d78a7b9e4b18301", size = 32087, upload-time = "2025-04-29T13:33:33.469Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/b3/894b827b93ef813c082479bebf88185860f01ac243df737823dd705e7fff/setproctitle-1.3.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2b17855ed7f994f3f259cf2dfbfad78814538536fa1a91b50253d84d87fd88d", size = 33502, upload-time = "2025-04-29T13:33:34.831Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/cd/5330734cca1a4cfcb721432c22cb7899ff15a4101ba868b2ef452ffafea1/setproctitle-1.3.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e51ec673513465663008ce402171192a053564865c2fc6dc840620871a9bd7c", size = 30713, upload-time = "2025-04-29T13:33:36.739Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/d3/c2590c5daa2e9a008d3f2b16c0f4a351826193be55f147cb32af49c6d814/setproctitle-1.3.6-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63cc10352dc6cf35a33951656aa660d99f25f574eb78132ce41a85001a638aa7", size = 31792, upload-time = "2025-04-29T13:33:37.974Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b1/c553ed5af8cfcecd5ae7737e63af58a17a03d26f3d61868c7eb20bf7e3cf/setproctitle-1.3.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0dba8faee2e4a96e934797c9f0f2d093f8239bf210406a99060b3eabe549628e", size = 31927, upload-time = "2025-04-29T13:33:39.203Z" },
+ { url = "https://files.pythonhosted.org/packages/70/78/2d5385206540127a3dca0ff83225b1ac66873f5cc89d4a6d3806c92f5ae2/setproctitle-1.3.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e3e44d08b61de0dd6f205528498f834a51a5c06689f8fb182fe26f3a3ce7dca9", size = 30981, upload-time = "2025-04-29T13:33:40.431Z" },
+ { url = "https://files.pythonhosted.org/packages/31/62/e3e4a4e006d0e549748e53cded4ff3b667be0602860fc61b7de8b412b667/setproctitle-1.3.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:de004939fc3fd0c1200d26ea9264350bfe501ffbf46c8cf5dc7f345f2d87a7f1", size = 33244, upload-time = "2025-04-29T13:33:41.817Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/05/4b223fd4ef94e105dc7aff27fa502fb7200cf52be2bb0c064bd2406b5611/setproctitle-1.3.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3f8194b4d631b003a1176a75d1acd545e04b1f54b821638e098a93e6e62830ef", size = 31630, upload-time = "2025-04-29T13:33:43.093Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/ba/5f68eb969f7336f54b54a599fd3ffbd7662f9733b080bc8598705971b3dd/setproctitle-1.3.6-cp313-cp313-win32.whl", hash = "sha256:d714e002dd3638170fe7376dc1b686dbac9cb712cde3f7224440af722cc9866a", size = 11480, upload-time = "2025-04-29T13:34:01.257Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/f5/7f47f0ca35c9c357f16187cee9229f3eda0237bc6fdd3061441336f361c0/setproctitle-1.3.6-cp313-cp313-win_amd64.whl", hash = "sha256:b70c07409d465f3a8b34d52f863871fb8a00755370791d2bd1d4f82b3cdaf3d5", size = 12198, upload-time = "2025-04-29T13:34:02.293Z" },
+ { url = "https://files.pythonhosted.org/packages/39/ad/c3941b8fc6b32a976c9e2d9615a90ae793b69cd010ca8c3575dbc822104f/setproctitle-1.3.6-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:23a57d3b8f1549515c2dbe4a2880ebc1f27780dc126c5e064167563e015817f5", size = 17401, upload-time = "2025-04-29T13:33:44.186Z" },
+ { url = "https://files.pythonhosted.org/packages/04/38/a184f857b988d3a9c401e470a4e38182a5c99ee77bf90432d7665e9d35a3/setproctitle-1.3.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81c443310831e29fabbd07b75ebbfa29d0740b56f5907c6af218482d51260431", size = 11959, upload-time = "2025-04-29T13:33:45.71Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/b9/4878ef9d8483adfd1edf6bf95151362aaec0d05aac306a97ff0383f491b5/setproctitle-1.3.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d88c63bd395c787b0aa81d8bbc22c1809f311032ce3e823a6517b711129818e4", size = 33463, upload-time = "2025-04-29T13:33:46.913Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/60/3ef49d1931aff2a36a7324a49cca10d77ef03e0278452fd468c33a52d7e3/setproctitle-1.3.6-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73f14b86d0e2858ece6bf5807c9889670e392c001d414b4293d0d9b291942c3", size = 34959, upload-time = "2025-04-29T13:33:48.216Z" },
+ { url = "https://files.pythonhosted.org/packages/81/c6/dee0a973acecefb0db6c9c2e0ea7f18b7e4db773a72e534741ebdee8bbb8/setproctitle-1.3.6-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3393859eb8f19f5804049a685bf286cb08d447e28ba5c6d8543c7bf5500d5970", size = 32055, upload-time = "2025-04-29T13:33:49.443Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/a5/5dd5c4192cf18d16349a32a07f728a9a48a2a05178e16966cabd6645903e/setproctitle-1.3.6-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:785cd210c0311d9be28a70e281a914486d62bfd44ac926fcd70cf0b4d65dff1c", size = 32986, upload-time = "2025-04-29T13:33:51.519Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a6/1508d37eb8008670d33f13fcdb91cbd8ef54697276469abbfdd3d4428c59/setproctitle-1.3.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c051f46ed1e13ba8214b334cbf21902102807582fbfaf0fef341b9e52f0fafbf", size = 32736, upload-time = "2025-04-29T13:33:52.852Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/73/c84ec8880d543766a12fcd6b65dbd013770974a40577889f357409b0441e/setproctitle-1.3.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:49498ebf68ca3e75321ffe634fcea5cc720502bfaa79bd6b03ded92ce0dc3c24", size = 31945, upload-time = "2025-04-29T13:33:54.665Z" },
+ { url = "https://files.pythonhosted.org/packages/95/0a/126b9ff7a406a69a62825fe5bd6d1ba8671919a7018c4f9e2c63f49bfcb6/setproctitle-1.3.6-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4431629c178193f23c538cb1de3da285a99ccc86b20ee91d81eb5f1a80e0d2ba", size = 34333, upload-time = "2025-04-29T13:33:56.101Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/fd/5474b04f1c013ff460129d2bc774557dd6e186da4667865efef9a83bf378/setproctitle-1.3.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d136fbf8ad4321716e44d6d6b3d8dffb4872626010884e07a1db54b7450836cf", size = 32508, upload-time = "2025-04-29T13:33:57.43Z" },
+ { url = "https://files.pythonhosted.org/packages/32/21/2503e38520cb076a7ecaef6a35d6a6fa89cf02af3541c84c811fd7500d20/setproctitle-1.3.6-cp313-cp313t-win32.whl", hash = "sha256:d483cc23cc56ab32911ea0baa0d2d9ea7aa065987f47de847a0a93a58bf57905", size = 11482, upload-time = "2025-04-29T13:33:58.602Z" },
+ { url = "https://files.pythonhosted.org/packages/65/23/7833d75a27fba25ddc5cd3b54cd03c4bf8e18b8e2dbec622eb6326278ce8/setproctitle-1.3.6-cp313-cp313t-win_amd64.whl", hash = "sha256:74973aebea3543ad033b9103db30579ec2b950a466e09f9c2180089e8346e0ec", size = 12209, upload-time = "2025-04-29T13:33:59.727Z" },
+]
+
+[[package]]
+name = "win32-setctime"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
+]