From e0f7e936e805eb75019b91a56a749714049d94ba Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 21 Feb 2025 21:03:31 +0545 Subject: [PATCH 01/22] huge updates --- README.md | 4 +- install.sh | 3 +- main.py | 10 +- modules/bar/bar.py | 6 - modules/bar/components/idle.py | 27 -- modules/bar/components/powerprofile.py | 44 ---- modules/launcher/components/__init__.py | 1 + modules/launcher/components/calendar.py | 199 +++++++++++++++ modules/launcher/components/dashboard.py | 242 ------------------ modules/launcher/components/wallpapers.py | 172 +++++++++---- modules/launcher/launcher.py | 21 +- modules/osd.py | 285 +++++++--------------- styles/launcher.css | 87 ++++++- styles/osd.css | 64 ++--- 14 files changed, 553 insertions(+), 612 deletions(-) delete mode 100644 modules/bar/components/idle.py delete mode 100644 modules/bar/components/powerprofile.py create mode 100644 modules/launcher/components/calendar.py delete mode 100644 modules/launcher/components/dashboard.py diff --git a/README.md b/README.md index ba723d3..92c8644 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ 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 +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 @@ -92,7 +92,7 @@ cd Tela-icon-theme - [x] Update Screenshot - [x] Installation script - [x] Network Manager -- [ ] Calendar with events +- [x] Calendar - [ ] Power Manager - [ ] Notification Panel - [ ] Improve CSS Styling diff --git a/install.sh b/install.sh index 40b0d1d..59be581 100644 --- a/install.sh +++ b/install.sh @@ -33,6 +33,7 @@ PACKAGES=( libadwaita adw-gtk-theme brightnessctl + wlinhibit power-profile-daemon ttf-font-awesome otf-font-awesome @@ -75,7 +76,7 @@ fi # Install required packages using paru echo ":: Installing required packages..." -paru -S --needed --noconfirm "${PACKAGES[@]}" || true +paru -Syy --needed --noconfirm "${PACKAGES[@]}" || true # Backup and replace GTK configs echo ":: Updating GTK configurations..." diff --git a/main.py b/main.py index 56baf28..dba0ee4 100644 --- a/main.py +++ b/main.py @@ -8,9 +8,11 @@ from loguru import logger from modules.bar.bar import Bar from modules.launcher.launcher import Launcher from modules.notification_popup import NotificationPopup -from modules.osd import OSDContainer + +from modules.osd import OSD from modules.corners import Corners from services import sc +from config.config import open_config for log in [ "fabric.hyprland.widgets", @@ -19,6 +21,8 @@ for log in [ ]: logger.disable(log) +config_path = os.path.expanduser("~/Modus/config/assets/config.json") + def update_main_css(): colors_css_path = f"/home/{os.getlogin()}/.cache/material/colors.css" @@ -42,10 +46,12 @@ def apply_style(app: Application): if __name__ == "__main__": update_main_css() + if not os.path.isfile(config_path): + open_config() sc = sc bar = Bar() corners = Corners() - osd = OSDContainer() + osd = OSD() notif = NotificationPopup() launcher = Launcher() diff --git a/modules/bar/bar.py b/modules/bar/bar.py index 74ee2d4..62b6132 100644 --- a/modules/bar/bar.py +++ b/modules/bar/bar.py @@ -11,10 +11,8 @@ from gi.repository import GLib from modules.bar.components import ( BatteryLabel, BluetoothIndicator, - IdleIndicator, MicrophoneIndicator, NetworkIndicator, - PowerProfileIndicator, SystemInfo, TaskBar, VolumeIndicator, @@ -57,10 +55,8 @@ class Bar(Window): self.bluetooth = BluetoothIndicator() self.network = NetworkIndicator() self.microphone = MicrophoneIndicator() - self.idle = IdleIndicator() self.taskbar = TaskBar() self.info = SystemInfo() - self.powerindicator = PowerProfileIndicator() self.tray = SystemTray(name="tray", icon_size=16, spacing=4) self.launcher = Button( name="logo", @@ -76,12 +72,10 @@ class Bar(Window): orientation="h", children=[ self.language, - self.powerindicator, self.bluetooth, self.network, self.volume, self.microphone, - self.idle, ], ) diff --git a/modules/bar/components/idle.py b/modules/bar/components/idle.py deleted file mode 100644 index 6ad444a..0000000 --- a/modules/bar/components/idle.py +++ /dev/null @@ -1,27 +0,0 @@ -from fabric import Fabricator -from fabric.utils import exec_shell_command -from fabric.widgets.box import Box -from snippets import MaterialIcon - - -class IdleIndicator(Box): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.idle_icon = self.create_idle_icon() - self.fabricator = Fabricator(interval=1000, poll_from=self.check_idle_status) - - self.children = [self.idle_icon] - - def create_idle_icon(self): - return MaterialIcon("coffee", 16) - - def check_idle_status(self, *_): - output = exec_shell_command("pidof wayland-idle-inhibitor.py") - is_idle = bool(str(output).strip()) - self.update_idle_status(is_idle) - - def update_idle_status(self, is_idle): - if is_idle: - self.idle_icon.set_visible(True) - else: - self.idle_icon.set_visible(False) diff --git a/modules/bar/components/powerprofile.py b/modules/bar/components/powerprofile.py deleted file mode 100644 index a1817cc..0000000 --- a/modules/bar/components/powerprofile.py +++ /dev/null @@ -1,44 +0,0 @@ -from fabric import Fabricator -from fabric.utils import exec_shell_command -from fabric.widgets.box import Box -from snippets import MaterialIcon - -PROFILE_ICONS = { - "power-saver": "data_saver_on", - "balanced": None, - "performance": "mode_heat", -} - - -class PowerProfileIndicator(Box): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.profile_icon = None - - self.fabricator = Fabricator(interval=1000, poll_from=self.update_power_profile) - - def update_power_profile(self, *_): - profile = self.get_power_profile() - self.update_icon(profile) - return True - - def get_power_profile(self): - result = exec_shell_command("powerprofilesctl get") - return result.strip() or "power-saver" - - def update_icon(self, profile): - icon_name = PROFILE_ICONS.get(profile) - - new_icon = MaterialIcon(icon_name, 16) if icon_name else None - - if self.profile_icon: - self.remove(self.profile_icon) - - if new_icon: - self.profile_icon = new_icon - self.add(self.profile_icon) - else: - if self.profile_icon: - self.profile_icon.hide() - self.profile_icon = None diff --git a/modules/launcher/components/__init__.py b/modules/launcher/components/__init__.py index e5f7be3..e542a03 100644 --- a/modules/launcher/components/__init__.py +++ b/modules/launcher/components/__init__.py @@ -8,3 +8,4 @@ from .sh import * from .todo import * from .wallpapers import * from .wifi import * +from .calendar import * diff --git a/modules/launcher/components/calendar.py b/modules/launcher/components/calendar.py new file mode 100644 index 0000000..fdf712e --- /dev/null +++ b/modules/launcher/components/calendar.py @@ -0,0 +1,199 @@ +import calendar +from datetime import datetime +import gi +from snippets import MaterialIcon +from fabric.widgets.label import Label + +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 + self.previous_key = (self.current_year, self.current_month) + + self.cache_threshold = 1 # Allow current month +/- 1 month + self.month_views = {} + + self.header = Gtk.Box(spacing=4, name="header") + self.pack_start(self.header, False, False, 0) + + self.prev_month_button = Gtk.Button( + name="prev-month-button", + child=MaterialIcon("chevron_left"), + ) + self.prev_month_button.connect("clicked", self.on_prev_month_clicked) + self.header.pack_start(self.prev_month_button, False, False, 0) + + self.month_label = Gtk.Label(name="month-label") + self.header.pack_start(self.month_label, True, True, 0) + + self.next_month_button = Gtk.Button( + name="next-month-button", + child=MaterialIcon("chevron_right"), + ) + self.next_month_button.connect("clicked", self.on_next_month_clicked) + self.header.pack_start(self.next_month_button, False, False, 0) + + self.weekday_row = Gtk.Box(spacing=4, name="weekday-row") + self.pack_start(self.weekday_row, False, False, 0) + + self.stack = Gtk.Stack(name="calendar-stack") + self.stack.set_transition_duration(500) + 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): + self.month_label.set_text( + datetime(self.current_year, self.current_month, 1) + .strftime("%B %Y") + .capitalize() + ) + 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) + + 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) + + self.previous_key = new_key + + 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}", + ) + + self.stack.set_visible_child_name(f"{self.current_year}_{self.current_month}") + self.update_header() + self.stack.show_all() + self.prune_cache() + + def prune_cache(self): + 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 = [ + key + for key in self.month_views + if abs(month_index(key) - current_index) > self.cache_threshold + ] + + 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): + 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/dashboard.py b/modules/launcher/components/dashboard.py deleted file mode 100644 index d266e0e..0000000 --- a/modules/launcher/components/dashboard.py +++ /dev/null @@ -1,242 +0,0 @@ -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.label import Label -from snippets import MaterialIcon - - -class Buttons(Box): - def __init__(self, **kwargs): - super().__init__( - name="buttons", - spacing=4, - h_align="center", - v_align="start", - h_expand=True, - v_expand=True, - visible=True, - all_visible=True, - ) - - self.network_status_button = Button( - name="network-status-button", - h_expand=True, - child=Box( - h_align="start", - v_align="center", - spacing=10, - children=[ - MaterialIcon("wifi"), - Box( - name="network-text", - orientation="v", - h_align="start", - v_align="center", - children=[ - Box( - children=[ - Label( - name="network-label", - label="Wi-Fi", - justification="left", - ), - Box(h_expand=True), - ] - ), - Box( - children=[ - Label( - name="network-ssid", - label="ARGOS_5GHz", - justification="left", - ), - Box(h_expand=True), - ] - ), - ], - ), - ], - ), - ) - - self.network_menu_button = Button( - name="network-menu-button", - child=MaterialIcon("chevron_right"), - ) - - self.network_button = Box( - name="network-button", - orientation="h", - h_align="fill", - v_align="center", - h_expand=True, - v_expand=True, - children=[ - self.network_status_button, - self.network_menu_button, - ], - ) - - self.bluetooth_status_button = Button( - name="bluetooth-status-button", - h_expand=True, - child=Box( - h_align="start", - v_align="center", - spacing=10, - children=[ - MaterialIcon("bluetooth"), - Box( - name="bluetooth-text", - orientation="v", - h_align="start", - v_align="center", - children=[ - Box( - children=[ - Label( - name="bluetooth-label", - label="Bluetooth", - justification="left", - ), - Box(h_expand=True), - ] - ), - Box( - children=[ - Label( - name="bluetooth-status", - label="Connected", - justification="left", - ), - Box(h_expand=True), - ] - ), - ], - ), - ], - ), - ) - - self.bluetooth_menu_button = Button( - name="bluetooth-menu-button", - child=MaterialIcon("chevron_right"), - ) - - self.bluetooth_button = Box( - name="bluetooth-button", - orientation="h", - h_align="fill", - v_align="center", - h_expand=True, - v_expand=True, - children=[ - self.bluetooth_status_button, - self.bluetooth_menu_button, - ], - ) - - self.night_mode_button = Button( - name="night-mode-button", - child=Box( - h_align="start", - v_align="center", - spacing=10, - children=[ - MaterialIcon("nightlight"), - Box( - name="night-mode-text", - orientation="v", - h_align="start", - v_align="center", - children=[ - Box( - children=[ - Label( - name="night-mode-label", - label="Night Mode", - justification="left", - ), - Box(h_expand=True), - ] - ), - Box( - children=[ - Label( - name="night-mode-status", - label="Enabled", - justification="left", - ), - Box(h_expand=True), - ] - ), - ], - ), - ], - ), - ) - - self.caffeine_button = Button( - name="caffeine-button", - child=Box( - h_align="start", - v_align="center", - spacing=10, - children=[ - MaterialIcon("coffee"), - Box( - name="caffeine-text", - orientation="v", - h_align="start", - v_align="center", - children=[ - Box( - children=[ - Label( - name="caffeine-label", - label="Caffeine", - justification="left", - ), - Box(h_expand=True), - ] - ), - Box( - children=[ - Label( - name="caffeine-status", - label="Enabled", - justification="left", - ), - Box(h_expand=True), - ] - ), - ], - ), - ], - ), - ) - - self.add(self.network_button) - self.add(self.bluetooth_button) - self.add(self.night_mode_button) - self.add(self.caffeine_button) - - self.show_all() - - -class Dashboard(Box): - def __init__(self, **kwargs): - super().__init__( - name="dashboard", - h_align="center", - v_align="start", - h_expand=True, - v_expand=True, - visible=True, - all_visible=True, - ) - - self.buttons = Buttons() - - self.add(self.buttons) - - self.show_all() diff --git a/modules/launcher/components/wallpapers.py b/modules/launcher/components/wallpapers.py index 2a3aa9c..0085c97 100644 --- a/modules/launcher/components/wallpapers.py +++ b/modules/launcher/components/wallpapers.py @@ -3,10 +3,11 @@ import json import os from PIL import Image from fabric.widgets.box import Box +from fabric.widgets.centerbox import CenterBox from fabric.widgets.button import Button from fabric.widgets.entry import Entry from fabric.widgets.scrolledwindow import ScrolledWindow -from gi.repository import GdkPixbuf, GLib, Gtk, Gio +from gi.repository import GdkPixbuf, GLib, Gtk, Gio, Gdk from snippets import MaterialIcon from fabric.utils import exec_shell_command, get_relative_path import concurrent.futures @@ -30,10 +31,16 @@ class WallpaperSelector(Box): } def __init__(self, **kwargs): - super().__init__(name="wallpapers", spacing=4, orientation="v", **kwargs) + super().__init__( + name="wallpapers", + spacing=4, + orientation="v", + h_expand=False, + v_expand=False, + **kwargs, + ) self.launcher = kwargs["launcher"] - # self.wallpapers_dir = os.path.expanduser("~/Pictures/wallpapers") os.makedirs(self.CACHE_DIR, exist_ok=True) self.files = [f for f in os.listdir(WALLPAPERS_DIR) if self._is_image(f)] @@ -41,10 +48,13 @@ class WallpaperSelector(Box): self.thumbnail_queue = [] self.executor = ThreadPoolExecutor(max_workers=4) # Shared executor - self.viewport = Gtk.IconView() + self.selected_index = -1 + + # IconView for displaying thumbnails. + self.viewport = Gtk.IconView(name="wallpaper-icons") self.viewport.set_model(Gtk.ListStore(GdkPixbuf.Pixbuf, str)) self.viewport.set_pixbuf_column(0) - self.viewport.set_text_column(1) + self.viewport.set_text_column(-1) self.viewport.set_item_width(0) self.viewport.connect("item-activated", self.on_wallpaper_selected) @@ -57,10 +67,12 @@ class WallpaperSelector(Box): ) self.search_entry = Entry( - name="search-entry", + name="search-entry-walls", 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.connect("focus-out-event", self.on_search_entry_focus_out) self.scheme_dropdown = Gtk.ComboBoxText() self.scheme_dropdown.set_name("scheme-dropdown") @@ -72,7 +84,6 @@ class WallpaperSelector(Box): self.scheme_dropdown.set_active_id("tonalSpot") self.scheme_dropdown.connect("changed", self.on_scheme_changed) - # Set the initial icon based on dark mode state initial_icon = "light_mode" if not self.check_dark_mode_state() else "dark_mode" self.toggle_button = Button( child=MaterialIcon(initial_icon), @@ -82,18 +93,17 @@ class WallpaperSelector(Box): self.dropdown_box = Box( name="dropdown-box", - orientation="h", children=[ self.scheme_dropdown, - MaterialIcon("keyboard_arrow_down"), ], ) - self.header_box = Box( + self.header_box = CenterBox( name="header-box", spacing=10, orientation="h", - children=[self.search_entry, self.dropdown_box, self.toggle_button], + start_children=[self.search_entry], + end_children=[self.dropdown_box, self.toggle_button], ) self.add(self.header_box) @@ -101,6 +111,7 @@ class WallpaperSelector(Box): self._start_thumbnail_thread() self.setup_file_monitor() self.show_all() + self.search_entry.grab_focus() def setup_file_monitor(self): gfile = Gio.File.new_for_path(WALLPAPERS_DIR) @@ -135,11 +146,9 @@ class WallpaperSelector(Box): print(f"Error deleting cache for changed file {file_name}: {e}") self.executor.submit(self._process_file, file_name) - def close_selector(self): - self.launcher.close() - def arrange_viewport(self, query: str = ""): - self.viewport.get_model().clear() + model = self.viewport.get_model() + model.clear() filtered_thumbnails = [ (thumb, name) for thumb, name in self.thumbnails @@ -147,32 +156,14 @@ class WallpaperSelector(Box): ] filtered_thumbnails.sort(key=lambda x: x[1].lower()) - for pixbuf, file_name in filtered_thumbnails: - self.viewport.get_model().append([pixbuf, file_name]) + model.append([pixbuf, file_name]) - def toggle_dark_mode(self, *_): - current_state = self.check_dark_mode_state() - new_mode = "dark" if not current_state else "light" - - # Call the shell script with the --set argument - command = f"bash {get_relative_path('../../../config/scripts/dark-theme.sh')} --set {new_mode}" - GLib.spawn_command_line_async(command) - - # Set the appropriate icon based on the mode - icon_name = "dark_mode" if new_mode == "dark" else "light_mode" - - # Update button icon - if self.toggle_button.get_child(): - self.toggle_button.remove(self.toggle_button.get_child()) - self.toggle_button.add(MaterialIcon(icon_name)) - self.toggle_button.show_all() - - def check_dark_mode_state(self): - result = exec_shell_command( - "gsettings get org.gnome.desktop.interface color-scheme" - ) - return result.strip().replace("'", "") == "prefer-dark" + 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() @@ -214,6 +205,76 @@ class WallpaperSelector(Box): except Exception as error: print(f"Failed to update generation-scheme in settings.json: {error}") + 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 + ) + if event.keyval == Gdk.KEY_Up: + new_index = (current_index - 1) % len(schemes_list) + else: + new_index = (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 + # Calculate columns based on the allocation width. + allocation = self.viewport.get_allocation() + item_width = 108 + 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 + + # Ensure new_index remains within limits. + 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) + self.selected_index = new_index + def _start_thumbnail_thread(self): thread = GLib.Thread.new("thumbnail-loader", self._preload_thumbnails, None) @@ -231,8 +292,15 @@ class WallpaperSelector(Box): if not os.path.exists(cache_path): try: with Image.open(full_path) as img: - img.thumbnail((96, 96), Image.Resampling.BILINEAR) - img.save(cache_path, "PNG") + 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 @@ -262,3 +330,25 @@ class WallpaperSelector(Box): 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 + + def toggle_dark_mode(self, *_): + current_state = self.check_dark_mode_state() + new_mode = "dark" if not current_state else "light" + command = f"bash {get_relative_path('../../../config/scripts/dark-theme.sh')} --set {new_mode}" + GLib.spawn_command_line_async(command) + icon_name = "dark_mode" if new_mode == "dark" else "light_mode" + if self.toggle_button.get_child(): + self.toggle_button.remove(self.toggle_button.get_child()) + self.toggle_button.add(MaterialIcon(icon_name)) + self.toggle_button.show_all() + + def check_dark_mode_state(self): + result = exec_shell_command( + "gsettings get org.gnome.desktop.interface color-scheme" + ) + return result.strip().replace("'", "") == "prefer-dark" diff --git a/modules/launcher/launcher.py b/modules/launcher/launcher.py index 0287a61..d672865 100644 --- a/modules/launcher/launcher.py +++ b/modules/launcher/launcher.py @@ -1,8 +1,6 @@ from fabric.widgets.centerbox import CenterBox from fabric.widgets.stack import Stack -from fabric.widgets.box import Box from fabric.widgets.wayland import WaylandWindow as Window -from gi.repository import Gtk from modules.launcher.components import ( AppLauncher, BluetoothConnections, @@ -13,6 +11,7 @@ from modules.launcher.components import ( TodoManager, WallpaperSelector, WifiManager, + Calendar, ) @@ -35,6 +34,7 @@ class Launcher(Window): self.bluetooth = BluetoothConnections() self.sh = Sh(launcher=self) self.wifi = WifiManager() + self.calendar = Calendar() self.stack = Stack( name="launcher-content", @@ -52,6 +52,7 @@ class Launcher(Window): self.bluetooth, self.sh, self.wifi, + self.calendar, ], ) @@ -64,7 +65,6 @@ class Launcher(Window): self.add(self.launcher_box) self.show_all() self.hide() - self.wallpapers.viewport.hide() self.add_keybinding("Escape", lambda *_: self.close()) def close(self): @@ -81,11 +81,8 @@ class Launcher(Window): self.bluetooth, self.sh, self.wifi, + self.calendar, ]: - if widget == self.wallpapers: - self.wallpapers.viewport.hide() - self.wallpapers.viewport.set_property("name", None) - if hasattr(widget, "viewport") and widget.viewport: widget.viewport.hide() @@ -99,6 +96,7 @@ class Launcher(Window): "bluetooth", "sh", "wifi", + "calendar", ]: self.stack.remove_style_class(style) @@ -118,6 +116,7 @@ class Launcher(Window): "bluetooth": self.bluetooth, "sh": self.sh, "wifi": self.wifi, + "calendar": self.calendar, } for w in widgets.values(): @@ -130,13 +129,6 @@ class Launcher(Window): self.stack.set_visible_child(widgets[widget]) widgets[widget].show() - # if widget != "launcher": - # self.launcher.hide() - - if widget != "wallpapers": - self.wallpapers.viewport.hide() - self.wallpapers.viewport.set_property("name", None) - if widget == "launcher": self.launcher.open_launcher() self.launcher.search_entry.set_text("") @@ -146,7 +138,6 @@ class Launcher(Window): self.wallpapers.search_entry.set_text("") self.wallpapers.search_entry.grab_focus() self.wallpapers.viewport.show() - self.wallpapers.viewport.set_property("name", "wallpaper-icons") elif widget == "emoji": self.emoji.open_launcher() diff --git a/modules/osd.py b/modules/osd.py index c2764cb..8092959 100644 --- a/modules/osd.py +++ b/modules/osd.py @@ -1,228 +1,113 @@ import time -from typing import ClassVar, Literal -from fabric.utils import invoke_repeater -from fabric.widgets.box import Box -from fabric.widgets.revealer import Revealer -from fabric.widgets.scale import Scale, ScaleMark from fabric.widgets.wayland import WaylandWindow as Window -from gi.repository import GObject -from snippets import Animator, MaterialIcon +from fabric.widgets.label import Label +from fabric.widgets.box import Box +from gi.repository import GLib, GObject +from fabric.widgets.centerbox import CenterBox from services import brightness, audio +from snippets import Animator, MaterialIcon -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.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 = value - self.animator.play() +def create_progress_bar(percentage, width=150): + container = Box(orientation="h", name="progress-container") + progress = Box( + name="progress-fill", style=f"min-width: {percentage * width / 100}px;" + ) + background = Box(name="progress-background", style=f"min-width: {width}px;") + background.children = [progress] + container.children = [background] + return container -class BrightnessOSDContainer(Box): - def __init__(self, **kwargs): - super().__init__(**kwargs, orientation="h", spacing=12, name="osd") - self.brightness_service = brightness - self.icon = MaterialIcon(icon_name="brightness_7", size=20) - self.scale = AnimatedScale( - marks=(ScaleMark(value=i) for i in range(0, 101, 10)), - value=70, - min_value=0, - max_value=100, - increments=(1, 1), - orientation="h", - ) - - self.add(self.icon) - self.add(self.scale) - 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.animate_value(normalized_brightness) - self.update_icon(normalized_brightness) - - def update_icon(self, brightness_percentage: float) -> None: - icon_name = self.get_brightness_icon(brightness_percentage) - self.icon.set_label(icon_name) - - 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_icon(normalized_brightness) - - def _normalize_brightness(self, brightness: float) -> float: - return (brightness / self.brightness_service.max_screen) * 100 - - def get_brightness_icon(self, brightness_percentage: float) -> str: - brightness_icons = [ - "brightness_1", - "brightness_2", - "brightness_3", - "brightness_4", - "brightness_5", - "brightness_6", - "brightness_7", - ] - thresholds = [ - 100 / (1.6 ** (len(brightness_icons) - i - 1)) - for i in range(len(brightness_icons)) - ] - - for i, threshold in enumerate(thresholds): - if brightness_percentage < threshold: - return brightness_icons[i] - - return brightness_icons[-1] +def update_progress_bar(progress_bar, percentage, width=150): + progress_fill = progress_bar.children[0].children[0] + progress_fill.set_style(f"min-width: {percentage * width / 100}px;") -class AudioOSDContainer(Box): - __gsignals__: ClassVar[dict] = { - "volume-changed": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), - } - - def __init__(self, **kwargs): - super().__init__(**kwargs, orientation="h", spacing=13, name="osd") - self.audio = audio - self.icon = MaterialIcon(icon_name="volume_up", size=20) - self.scale = AnimatedScale( - marks=(ScaleMark(value=i) for i in range(1, 100, 10)), - value=70, - min_value=0, - max_value=100, - increments=(1, 1), - orientation="h", - ) - - self.add(self.icon) - self.add(self.scale) - self.sync_with_audio() - - self.scale.connect("value-changed", self.on_volume_changed) - self.audio.connect("notify::speaker", self.on_audio_speaker_changed) - - def sync_with_audio(self): - if self.audio.speaker: - volume = round(self.audio.speaker.volume) - self.scale.set_value(volume) - self.update_icon(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_icon(volume) - self.emit("volume-changed") - - def on_audio_speaker_changed(self, *_): - if self.audio.speaker: - self.audio.speaker.connect("notify::volume", self.update_volume) - 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(volume) - self.update_icon(volume) - - def update_icon(self, volume): - icon_name = self._get_audio_icon_name(volume) - self.icon.set_label(icon_name) - - def _get_audio_icon_name(self, volume: int) -> str: - if volume == 101: - return "sound_detection_loud_sound" - elif volume >= 67: - return "volume_up" - elif volume >= 34: - return "volume_down" - elif volume >= 1: - return "volume_mute" - else: - return "volume_off" +def create_labeled_progress(label_text, value_text, percentage): + label = Label(name="osd-label", markup=label_text) + value = Label(name="osd-value", markup=value_text) + header = CenterBox(orientation="h", start_children=[label], end_children=[value]) + progress = create_progress_bar(percentage) + return Box( + name="control-section", orientation="v", spacing=5, children=[header, progress] + ) -class OSDContainer(Window): - def __init__(self, **kwargs): - self.audio_container = AudioOSDContainer() - self.brightness_container = BrightnessOSDContainer() +def get_brightness(): + return ( + round((brightness.screen_brightness / brightness.max_screen) * 100) + if brightness.screen_brightness + else 0 + ) - self.timeout = 1000 - self.revealer = Revealer( - transition_type="slide-right", - transition_duration=200, - child_revealed=False, - ) +def get_volume(): + return round(audio.speaker.volume) if audio.speaker else 0 - self.main_box = Box( - orientation="v", - spacing=13, - children=[self.revealer], - ) +class OSD(Window): + def __init__(self): super().__init__( + name="osd-menu", layer="overlay", anchor="bottom", - child=self.main_box, - visible=False, - pass_through=True, + margin="0 0 10px 0", keyboard_mode="on-demand", - **kwargs, + visible=False, + style_classes="osd-panel", ) - - self.last_activity_time = time.time() - - self.audio_container.audio.connect("notify::speaker", self.show_audio) - self.brightness_container.brightness_service.connect( - "screen", self.show_brightness + self.last_volume = get_volume() + self.last_brightness = get_brightness() + self.volume_container = create_labeled_progress( + "Speaker", str(self.last_volume), self.last_volume ) - self.audio_container.connect("volume-changed", self.show_audio) + self.brightness_container = create_labeled_progress( + "Brightness", str(self.last_brightness), self.last_brightness + ) + self.children = Box( + orientation="h", + spacing=10, + children=[self.brightness_container, self.volume_container], + ) + self.hide_timeout_id = None + self._connect_signals() + GLib.timeout_add(100, self._check_changes) - invoke_repeater(100, self.check_inactivity, initial_call=True) + def _connect_signals(self): + audio.connect("notify::speaker", self._update_volume) + brightness.connect("screen", self._update_brightness) - def show_audio(self, *_): - self.show_box(box_to_show="audio") - self.reset_inactivity_timer() + def _update_volume(self, *_): + volume = get_volume() + self.volume_container.children[0].end_children[0].set_markup(str(volume)) + update_progress_bar(self.volume_container.children[1], volume) + self._reset_timeout() - def show_brightness(self, *_): - self.show_box(box_to_show="brightness") - self.reset_inactivity_timer() + def _update_brightness(self, *_): + brightness_level = get_brightness() + self.brightness_container.children[0].end_children[0].set_markup( + str(brightness_level) + ) + update_progress_bar(self.brightness_container.children[1], brightness_level) + self._reset_timeout() - def show_box(self, box_to_show: Literal["audio", "brightness"]): - self.set_visible(True) - if box_to_show == "audio": - self.revealer.children = self.audio_container - elif box_to_show == "brightness": - self.revealer.children = self.brightness_container - self.revealer.set_reveal_child(True) - self.reset_inactivity_timer() + def _reset_timeout(self): + if self.hide_timeout_id: + GLib.source_remove(self.hide_timeout_id) + self.hide_timeout_id = GLib.timeout_add(1500, self._hide_osd) - def start_hide_timer(self): - self.set_visible(False) + def _hide_osd(self): + self.hide() + self.hide_timeout_id = None + return 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() + def _check_changes(self): + volume = get_volume() + brightness_level = get_brightness() + if volume != self.last_volume or brightness_level != self.last_brightness: + self.last_volume, self.last_brightness = volume, brightness_level + self.show_all() + self._update_volume() + self._update_brightness() return True diff --git a/styles/launcher.css b/styles/launcher.css index 7f0ebdc..db2e522 100644 --- a/styles/launcher.css +++ b/styles/launcher.css @@ -43,6 +43,11 @@ min-height: 52px; } +#launcher-content.calendar { + min-height: 420px; + min-width: 420px; +} + #launcher-content { min-width: 420px; } @@ -63,7 +68,7 @@ color: @onSecondaryContainer; margin: 11px; margin-left: 0px; - min-width: 54px; + min-width: 48px; min-height: 44px; border-radius: 100%; } @@ -125,6 +130,9 @@ /* * ── wallpaper ───────────────────────────────────────────────────────── */ +#search-entry-walls { + min-width: 300px; +} #wallpaper-icons:selected { background-color: @surfaceBright; @@ -410,3 +418,80 @@ border-radius: 20px; min-height: 200px; } + +/* + * ── calendar ────────────────────────────────────────────────────────── +*/ + +#header { + border-radius: 16px; + padding: 4px; +} + +#weekday-row { + border-radius: 8px; + padding: 4px; +} + +#month-label, +#weekday-label, +#day-label { + font-weight: bold; +} + +#day-label, +#day-empty { + min-width: 41px; + min-height: 30px; +} + +#day-empty { + color: @surfaceBright; +} + +#weekday-label { + color: @primary; +} + +#calendar-grid { + border-radius: 16px; + padding: 4px; +} + +#day-label.current-day { + background-color: @primary; + color: @onPrimary; + border-radius: 20px; +} + +#prev-month-button, +#next-month-button { + border-radius: 8px; + padding: 4px; +} + +#prev-month-button:hover, +#next-month-button:hover { + background-color: @surfaceBright; +} + +#prev-month-button:active, +#next-month-button:active { + background-color: @primary; +} + +#month-button-label { + color: @primary; + font-size: 20px; +} + +#prev-month-button:active #month-button-label, +#next-month-button:active #month-button-label { + color: @surface; +} + +#prev-month-day, +#next-month-day { + /*color: lighter(@surfaceBright);*/ + color: mix(@onSurfaceVariant, transparent, 0.6); +} diff --git a/styles/osd.css b/styles/osd.css index f86f4e2..8664d65 100644 --- a/styles/osd.css +++ b/styles/osd.css @@ -1,42 +1,44 @@ -#osd { - background: @surfaceContainer; - padding: 10px 4px 10px 4px; - border-radius: 14px; - margin-bottom: 16px; - min-width: 190px; +.osd-panel { + background-color: transparent; } -#osd scale { +#osd-label { + font-size: 17px; + font-weight: bold; +} + +#osd-value { + font-weight: bold; + font-size: 25px; +} + +#progress-container { + border-radius: 6px; + min-height: 12px; min-width: 150px; } -#osd slider { - color: @onPrimary; +#progress-fill { background-color: @primary; - background-size: 20px 20px; - min-width: 5px; - min-height: 14px; - border-radius: 100px; - padding: 0px; - background-repeat: no-repeat; - background-position: center; - margin-left: 0px; - margin-right: 0px; - margin-top: -16px; - margin-bottom: -16px; - box-shadow: - -4px 0px 0px 1px @surfaceContainer, - 4px 0px 0px 1px @surfaceContainer; + border-radius: 6px; + min-height: 8px; + transition: min-width 0.3s ease-in-out; } -#osd trough { - background: @secondaryContainer; - min-height: 10px; - margin-right: 4px; - border-radius: 100px; +#progress-background { + background-color: @surfaceContainer; + border-radius: 6px; + min-height: 8px; + min-width: 150px; } -#osd highlight { - border-radius: 100px 0px 0px 100px; - background: @primary; +#control-section { + padding: 10px; + border-radius: 10px; + background-color: @surface; +} + +.osd-hidden { + opacity: 0; + transition: opacity 0.3s ease-in-out; } From 00111ebe9df3d1dc1cd3285227115cd4564c08a8 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 21 Feb 2025 21:04:42 +0545 Subject: [PATCH 02/22] huge updates --- modules/launcher/components/__init__.py | 1 + modules/launcher/components/applauncher.py | 5 + modules/launcher/components/bluetooth.py | 51 ++- modules/launcher/components/buttons.py | 396 ++++++++++++++++ modules/launcher/components/dashboard.py | 504 +++++++++++++++++++++ modules/launcher/components/wallpapers.py | 30 +- modules/launcher/launcher.py | 20 +- styles/bar.css | 81 +++- styles/dashboard.css | 143 ++++++ styles/launcher.css | 19 +- styles/main.css | 6 +- styles/notification.css | 3 + 12 files changed, 1199 insertions(+), 60 deletions(-) create mode 100644 modules/launcher/components/buttons.py create mode 100644 modules/launcher/components/dashboard.py create mode 100644 styles/dashboard.css diff --git a/modules/launcher/components/__init__.py b/modules/launcher/components/__init__.py index e542a03..8ea1e96 100644 --- a/modules/launcher/components/__init__.py +++ b/modules/launcher/components/__init__.py @@ -9,3 +9,4 @@ from .todo import * from .wallpapers import * from .wifi import * from .calendar import * +from .dashboard import * diff --git a/modules/launcher/components/applauncher.py b/modules/launcher/components/applauncher.py index 4e755ae..54dd1c1 100644 --- a/modules/launcher/components/applauncher.py +++ b/modules/launcher/components/applauncher.py @@ -81,6 +81,11 @@ class AppLauncher(Box): self.viewport.children = [] self.selected_index = -1 + if query.strip(): + self.launcher.dashboard.hide() + else: + self.launcher.dashboard.show() + if not query.strip(): return False diff --git a/modules/launcher/components/bluetooth.py b/modules/launcher/components/bluetooth.py index 68fb90a..0cf1704 100644 --- a/modules/launcher/components/bluetooth.py +++ b/modules/launcher/components/bluetooth.py @@ -6,6 +6,7 @@ from fabric.widgets.centerbox import CenterBox from fabric.widgets.scrolledwindow import ScrolledWindow from fabric.bluetooth import BluetoothClient, BluetoothDevice from snippets import MaterialIcon +# from .buttons import class BluetoothDeviceSlot(CenterBox): @@ -62,6 +63,15 @@ class BluetoothConnections(Box): orientation="vertical", **kwargs, ) + self.launcher = kwargs["launcher"] + + self.bluetooth_button = self.launcher.dashboard.bluetooth_button + self.bt_status_text = self.bluetooth_button.bluetooth_status_text + self.bt_status_button = self.bluetooth_button.bluetooth_status_button + self.bt_icon = self.bluetooth_button.bluetooth_icon + self.bt_label = self.bluetooth_button.bluetooth_label + self.bt_menu_button = self.bluetooth_button.bluetooth_menu_button + self.bt_menu_label = self.bluetooth_button.bluetooth_menu_label self.client = BluetoothClient(on_device_added=self.on_device_added) self.toggle_icon = MaterialIcon("bluetooth_disabled") @@ -79,14 +89,8 @@ class BluetoothConnections(Box): child=self.toggle_icon, on_clicked=lambda *_: self.client.toggle_power(), ) - self.client.notify("scanning") - self.client.notify("enabled") - self.client.connect( - "notify::enabled", - lambda *_: self.toggle_icon.set_label( - "bluetooth" if self.client.enabled else "bluetooth_disabled" - ), - ) + + self.client.connect("notify::enabled", lambda *_: self.status_label()) self.client.connect( "notify::scanning", lambda *_: self.scan_icon.set_label( @@ -120,6 +124,37 @@ class BluetoothConnections(Box): # to run notify closures thus display the status # without having to wait until an actual change + self.client.notify("scanning") + self.client.notify("enabled") + + def status_label(self): + print(self.client.enabled) + if self.client.enabled: + self.toggle_icon.set_label("bluetooth") + 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_label("bluetooth") + else: + self.toggle_icon.set_label("bluetooth_disabled") + 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_label("bluetooth_disabled") def on_device_added(self, client: BluetoothClient, address: str): if not (device := client.get_device(address)): diff --git a/modules/launcher/components/buttons.py b/modules/launcher/components/buttons.py new file mode 100644 index 0000000..d19fb89 --- /dev/null +++ b/modules/launcher/components/buttons.py @@ -0,0 +1,396 @@ +import subprocess +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.utils.helpers import exec_shell_command_async, exec_shell_command +import gi +from gi.repository import Gtk, Gdk, GLib # Added Gdk import +from fabric.utils import get_relative_path + +gi.require_version("Gtk", "3.0") +from snippets import MaterialIcon + + +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_icon = MaterialIcon( + "wifi", + ) + self.network_icon.add_style_class("network-icon") + 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", + label="Hello_World!!", + 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, + ) + add_hover_cursor(self.network_status_button) # <-- Added hover + + self.network_menu_label = MaterialIcon("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], + ) + + +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 = MaterialIcon("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 = MaterialIcon("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 = MaterialIcon("mode_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() + + 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 check_hyprsunset(self, *args): + """ + Update the button state based on whether hyprsunset is running. + """ + try: + subprocess.check_output(["pgrep", "hyprsunset"]) + self.night_mode_status.set_label("Enabled") + for widget in self.widgets: + widget.remove_style_class("disabled") + except subprocess.CalledProcessError: + 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 = MaterialIcon("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, + ] + self.check_wlinhibit() + + def toggle_wlinhibit(self, *args): + """ + Toggle the 'wlinhibit' process: + - If running, kill it and mark as 'Disabled' (add 'disabled' class). + - If not running, start it and mark as 'Enabled' (remove 'disabled' class). + """ + try: + subprocess.check_output(["pgrep", "wlinhibit"]) + exec_shell_command_async("pkill wlinhibit") + self.caffeine_status.set_label("Disabled") + for i in self.widgets: + i.add_style_class("disabled") + except subprocess.CalledProcessError: + exec_shell_command_async("wlinhibit") + self.caffeine_status.set_label("Enabled") + for i in self.widgets: + i.remove_style_class("disabled") + + def check_wlinhibit(self, *args): + try: + subprocess.check_output(["pgrep", "wlinhibit"]) + self.caffeine_status.set_label("Enabled") + for i in self.widgets: + i.remove_style_class("disabled") + except subprocess.CalledProcessError: + self.caffeine_status.set_label("Disabled") + for i in self.widgets: + i.add_style_class("disabled") + + +class DarkModeButton(Button): + def __init__(self): + self.dark_mode_icon = MaterialIcon("contrast") + self.dark_mode_icon.add_style_class("iconss") + self.dark_mode_label = Label( + name="dark-mode-label", label="Dark Mode", justification="left" + ) + + self.dark_mode_label_box = Box( + children=[self.dark_mode_label, Box(h_expand=True)] + ) + self.dark_mode_status = Label( + name="dark-mode-status", label="Enabled", justification="left" + ) + self.dark_mode_status_box = Box( + children=[self.dark_mode_status, Box(h_expand=True)] + ) + self.dark_mode_text = Box( + name="dark-mode-text", + orientation="v", + h_align="start", + v_align="center", + children=[self.dark_mode_label_box, self.dark_mode_status_box], + ) + self.dark_mode_box = Box( + h_align="start", + v_align="center", + spacing=10, + children=[self.dark_mode_icon, self.dark_mode_text], + ) + + super().__init__( + name="dark-mode-button", + h_expand=True, + child=self.dark_mode_box, + on_clicked=self.toggle_darkmode, + ) + add_hover_cursor(self) + + self.widgets = [ + self, + self.dark_mode_label, + self.dark_mode_status, + self.dark_mode_icon, + ] + self.check_darkmode() + + def toggle_darkmode(self, *_): + current_state = self.check_darkmode() + new_mode = "dark" if not current_state else "light" + command = f"bash {get_relative_path('../../../config/scripts/dark-theme.sh')} --set {new_mode}" + GLib.spawn_command_line_async(command) + self.dark_mode_status.set_label("Enabled" if new_mode == "dark" else "Disabled") + + def check_darkmode(self): + result = exec_shell_command( + "gsettings get org.gnome.desktop.interface color-scheme" + ) + return result.strip().replace("'", "") == "prefer-dark" + + +class Buttons(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.dark_mode_button = DarkModeButton() + + # Attach buttons into the grid (one row, four columns) + self.attach(self.network_button, 0, 0, 1, 1) + self.attach(self.bluetooth_button, 1, 0, 1, 1) + self.attach(self.dark_mode_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/dashboard.py b/modules/launcher/components/dashboard.py new file mode 100644 index 0000000..246bfde --- /dev/null +++ b/modules/launcher/components/dashboard.py @@ -0,0 +1,504 @@ +import subprocess +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.utils.helpers import exec_shell_command_async, exec_shell_command +import gi +from gi.repository import Gtk, Gdk, GLib # Added Gdk import +from fabric.utils import get_relative_path + +gi.require_version("Gtk", "3.0") +from snippets import MaterialIcon + + +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_icon = MaterialIcon( + "wifi", + ) + 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", + label="Hello_World!!", + 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, + ) + add_hover_cursor(self.network_status_button) # <-- Added hover + + self.network_menu_label = MaterialIcon("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], + ) + + +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 = MaterialIcon("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 = MaterialIcon("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 = MaterialIcon("mode_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() + + 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 check_hyprsunset(self, *args): + """ + Update the button state based on whether hyprsunset is running. + """ + try: + subprocess.check_output(["pgrep", "hyprsunset"]) + self.night_mode_status.set_label("Enabled") + for widget in self.widgets: + widget.remove_style_class("disabled") + except subprocess.CalledProcessError: + 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 = MaterialIcon("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, + ] + self.check_wlinhibit() + + def toggle_wlinhibit(self, *args): + """ + Toggle the 'wlinhibit' process: + - If running, kill it and mark as 'Disabled' (add 'disabled' class). + - If not running, start it and mark as 'Enabled' (remove 'disabled' class). + """ + try: + subprocess.check_output(["pgrep", "wlinhibit"]) + exec_shell_command_async("pkill wlinhibit") + self.caffeine_status.set_label("Disabled") + for i in self.widgets: + i.add_style_class("disabled") + except subprocess.CalledProcessError: + exec_shell_command_async("wlinhibit") + self.caffeine_status.set_label("Enabled") + for i in self.widgets: + i.remove_style_class("disabled") + + def check_wlinhibit(self, *args): + try: + subprocess.check_output(["pgrep", "wlinhibit"]) + self.caffeine_status.set_label("Enabled") + for i in self.widgets: + i.remove_style_class("disabled") + except subprocess.CalledProcessError: + self.caffeine_status.set_label("Disabled") + for i in self.widgets: + i.add_style_class("disabled") + + +class DarkModeButton(Button): + def __init__(self): + self.dark_mode_icon = MaterialIcon("contrast") + self.dark_mode_label = Label( + name="dark-mode-label", label="Dark Mode", justification="left" + ) + + self.dark_mode_label_box = Box( + children=[self.dark_mode_label, Box(h_expand=True)] + ) + self.dark_mode_status = Label( + name="dark-mode-status", label="Enabled", justification="left" + ) + self.dark_mode_status_box = Box( + children=[self.dark_mode_status, Box(h_expand=True)] + ) + self.dark_mode_text = Box( + name="dark-mode-text", + orientation="v", + h_align="start", + v_align="center", + children=[self.dark_mode_label_box, self.dark_mode_status_box], + ) + self.dark_mode_box = Box( + h_align="start", + v_align="center", + spacing=10, + children=[self.dark_mode_icon, self.dark_mode_text], + ) + + super().__init__( + name="dark-mode-button", + h_expand=True, + child=self.dark_mode_box, + on_clicked=self.toggle_darkmode, + ) + add_hover_cursor(self) + + self.widgets = [ + self, + self.dark_mode_label, + self.dark_mode_status, + self.dark_mode_icon, + ] + self.check_darkmode() + + def toggle_darkmode(self, *_): + current_state = self.check_darkmode() + new_mode = "dark" if not current_state else "light" + command = f"bash {get_relative_path('../../../config/scripts/dark-theme.sh')} --set {new_mode}" + GLib.spawn_command_line_async(command) + + self.dark_mode_status.set_label("Enabled" if new_mode == "dark" else "Disabled") + + # Apply or remove 'disabled' class + if new_mode == "light": + for widget in self.widgets: + widget.add_style_class("disabled") + else: + for widget in self.widgets: + widget.remove_style_class("disabled") + + def check_darkmode(self): + result = exec_shell_command( + "gsettings get org.gnome.desktop.interface color-scheme" + ) + is_dark_mode = result.strip().replace("'", "") == "prefer-dark" + + if not is_dark_mode: + for widget in self.widgets: + widget.add_style_class("disabled") + else: + for widget in self.widgets: + widget.remove_style_class("disabled") + + return is_dark_mode + + +# +# class PowerProfileButton(Button): +# def __init__(self): +# self.power_profile_icon = MaterialIcon("local_fire_department") +# 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="Loading...", 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, +# ] +# +# # **Delay the profile check to allow system services to start** +# GLib.timeout_add(1000, self.check_power_profile) +# +# def toggle_power_profile(self, *_): +# current_profile = self.check_power_profile() +# if current_profile == "balanced": +# self.set_power_profile("performance") +# elif current_profile == "performance": +# self.set_power_profile("power-saver") +# else: +# self.set_power_profile("balanced") +# +# def check_power_profile(self): +# try: +# result = exec_shell_command("powerprofilesctl get") +# if isinstance(result, str): +# current_profile = result.strip().lower() +# self.power_profile_status.set_label(current_profile.capitalize()) +# self.update_button_style(current_profile) +# return current_profile +# except Exception as e: +# print(f"Error checking power profile: {e}") +# self.power_profile_status.set_label("Balanced") # Fallback +# return "balanced" +# +# def set_power_profile(self, profile): +# exec_shell_command_async(f"powerprofilesctl set {profile}") +# self.power_profile_status.set_label(profile.capitalize()) +# self.update_button_style(profile) +# +# def update_button_style(self, profile): +# if profile == "balanced": +# self.power_profile_icon.set_label("balance") +# self.add_style_class("balanced-profile") +# self.power_profile_label.add_style_class("disabled") +# else: +# self.remove_style_class("balanced-profile") +# +# if profile == "performance": +# self.power_profile_icon.set_label("local_fire_department") +# elif profile == "power-saver": +# self.power_profile_icon.set_label("data_saver_on") +# + + +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.dark_mode_button = DarkModeButton() + # self.power_profile_button = PowerProfileButton() + + # Attach buttons into the grid (one row, four columns) + self.attach(self.network_button, 0, 0, 1, 1) + self.attach(self.bluetooth_button, 1, 0, 1, 1) + self.attach(self.dark_mode_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.attach(self.power_profile_button, 2, 1, 1, 1) + + self.show_all() diff --git a/modules/launcher/components/wallpapers.py b/modules/launcher/components/wallpapers.py index 0085c97..b777155 100644 --- a/modules/launcher/components/wallpapers.py +++ b/modules/launcher/components/wallpapers.py @@ -4,12 +4,10 @@ import os from PIL import Image from fabric.widgets.box import Box from fabric.widgets.centerbox import CenterBox -from fabric.widgets.button import Button from fabric.widgets.entry import Entry from fabric.widgets.scrolledwindow import ScrolledWindow from gi.repository import GdkPixbuf, GLib, Gtk, Gio, Gdk -from snippets import MaterialIcon -from fabric.utils import exec_shell_command, get_relative_path +from fabric.utils import get_relative_path import concurrent.futures from concurrent.futures import ThreadPoolExecutor from snippets import WALLPAPERS_DIR @@ -84,13 +82,6 @@ class WallpaperSelector(Box): self.scheme_dropdown.set_active_id("tonalSpot") self.scheme_dropdown.connect("changed", self.on_scheme_changed) - initial_icon = "light_mode" if not self.check_dark_mode_state() else "dark_mode" - self.toggle_button = Button( - child=MaterialIcon(initial_icon), - name="toggle-launcher-button", - on_clicked=self.toggle_dark_mode, - ) - self.dropdown_box = Box( name="dropdown-box", children=[ @@ -103,7 +94,7 @@ class WallpaperSelector(Box): spacing=10, orientation="h", start_children=[self.search_entry], - end_children=[self.dropdown_box, self.toggle_button], + end_children=[self.dropdown_box], ) self.add(self.header_box) @@ -335,20 +326,3 @@ class WallpaperSelector(Box): if self.get_mapped(): widget.grab_focus() return False - - def toggle_dark_mode(self, *_): - current_state = self.check_dark_mode_state() - new_mode = "dark" if not current_state else "light" - command = f"bash {get_relative_path('../../../config/scripts/dark-theme.sh')} --set {new_mode}" - GLib.spawn_command_line_async(command) - icon_name = "dark_mode" if new_mode == "dark" else "light_mode" - if self.toggle_button.get_child(): - self.toggle_button.remove(self.toggle_button.get_child()) - self.toggle_button.add(MaterialIcon(icon_name)) - self.toggle_button.show_all() - - def check_dark_mode_state(self): - result = exec_shell_command( - "gsettings get org.gnome.desktop.interface color-scheme" - ) - return result.strip().replace("'", "") == "prefer-dark" diff --git a/modules/launcher/launcher.py b/modules/launcher/launcher.py index d672865..6e484f6 100644 --- a/modules/launcher/launcher.py +++ b/modules/launcher/launcher.py @@ -1,6 +1,8 @@ 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 fabric.widgets.label import Label from modules.launcher.components import ( AppLauncher, BluetoothConnections, @@ -12,6 +14,7 @@ from modules.launcher.components import ( WallpaperSelector, WifiManager, Calendar, + Dashboard, ) @@ -25,17 +28,27 @@ class Launcher(Window): **kwargs, ) - self.launcher = AppLauncher(launcher=self) + self.dashboard = Dashboard(launcher=self) self.wallpapers = WallpaperSelector(launcher=self) self.power = PowerMenu(launcher=self) self.emoji = Emoji(launcher=self) self.cliphist = Cliphist(launcher=self) self.todo = TodoManager() - self.bluetooth = BluetoothConnections() + self.bluetooth = BluetoothConnections(launcher=self) self.sh = Sh(launcher=self) self.wifi = WifiManager() self.calendar = Calendar() + 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, @@ -60,6 +73,7 @@ class Launcher(Window): name="launcher", orientation="v", start_children=self.stack, + end_children=self.dashboard, ) self.add(self.launcher_box) @@ -121,6 +135,7 @@ class Launcher(Window): for w in widgets.values(): w.hide() + self.dashboard.hide() for style in widgets.keys(): self.stack.remove_style_class(style) @@ -133,6 +148,7 @@ class Launcher(Window): 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("") diff --git a/styles/bar.css b/styles/bar.css index 95edb20..f4d1ca6 100644 --- a/styles/bar.css +++ b/styles/bar.css @@ -51,18 +51,87 @@ /* * ── battery ─────────────────────────────────────────────────────────── */ +/*#battery {*/ +/* background-color: @surfaceContainer;*/ +/* border-radius: 100px;*/ +/* padding: 2px 10px 2px 10px;*/ +/*}*/ +/**/ +/*#battery label {*/ +/* font-weight: 600;*/ +/*}*/ +/**/ +/*#battery-label {*/ +/* margin-right: 5px;*/ +/*}*/ + #battery { background-color: @surfaceContainer; - border-radius: 100px; - padding: 2px 10px 2px 10px; + padding: 4px; + border-radius: 16px; + margin-top: 4px; } -#battery label { - font-weight: 600; +#battery-circle { + color: @surfaceBright; + border: 3px solid @primary; } -#battery-label { - margin-right: 5px; +#battery-circle.alert { + border: 2px solid @error-dim; +} + +#battery-icon { + font-size: 20px; +} + +#battery-icon.alert { + color: @error-dim; +} + +#battery-save, +#battery-balanced, +#battery-performance { + border-radius: 12px; + min-width: 28px; + min-height: 28px; +} + +#battery-save-label, +#battery-balanced-label, +#battery-performance-label { + color: @outline; + font-size: 20px; +} + +#battery-save:hover, +#battery-balanced:hover, +#battery-performance:hover { + background-color: @surfaceBright; +} + +#battery-save:hover #battery-save-label, +#battery-balanced:hover #battery-balanced-label, +#battery-performance:hover #battery-performance-label { + color: @primary; +} + +#battery-save.active, +#battery-balanced.active, +#battery-performance.active { + background-color: @primary; +} + +#battery-save.active #battery-save-label, +#battery-balanced.active #battery-balanced-label, +#battery-performance.active #battery-performance-label { + color: @shadow; +} + +#battery-level { + color: @primary; + font-weight: bold; + margin: 0 4px; } /* diff --git a/styles/dashboard.css b/styles/dashboard.css new file mode 100644 index 0000000..7857729 --- /dev/null +++ b/styles/dashboard.css @@ -0,0 +1,143 @@ +#buttons-grid { + min-height: 52px; +} + +#network-button, +#bluetooth-button, +#night-mode-button, +#caffeine-button, +#dark-mode-button, +#power-profile-button { + border-radius: 20px; +} + +#network-button, +#bluetooth-button { + background-color: unset; + padding: 0; +} + +#night-mode-button, +#caffeine-button, +#dark-mode-button, +#power-profile-button { + background-color: @primary; + padding: 0 10px; +} + +#night-mode-button.disabled, +#caffeine-button.disabled, +#dark-mode-button.disabled, +#power-profile.disabled { + background-color: @surface; +} + +#network-status-button, +#network-menu-button, +#bluetooth-status-button, +#bluetooth-menu-button { + background-color: @primary; + padding: 0 8px; +} + +#network-status-button:hover, +#network-menu-button:hover, +#bluetooth-status-button:hover, +#bluetooth-menu-button:hover, +#bluetooth-button:hover, +#night-mode-button:hover, +#caffeine-button:hover, +#dark-mode-button:hover, +#power-profile-button:hover { + background-color: mix(@onPrimary, @primary, 0.7); +} + +#network-status-button.disabled, +#network-menu-button.disabled, +#bluetooth-status-button.disabled, +#bluetooth-menu-button.disabled, +#dark-mode-button.disabled, +#power-profile-button.disabled { + background-color: @surface; +} + +#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, +#caffeine-button.disabled:hover, +#dark-mode-button.disabled:hover, +#power-profile-button.disabled:hover { + background-color: @surfaceBright; +} + +#network-status-button, +#bluetooth-status-button, +#dark-mode-status-button, +#power-profile-status-button { + border-radius: 20px 0 0 20px; +} + +#network-menu-button, +#bluetooth-menu-button { + border-radius: 0 20px 20px 0; +} + +#network-menu-label, +#bluetooth-menu-label, +#dark-mode-label, +#power-profile-label { + font-size: 16px; +} + +#network-menu-label.disabled, +#bluetooth-menu-label.disabled, +#dark-mode-label.disabled, +#power-profile-label.disabled { + color: @outline; +} + +#network-label, +#bluetooth-label, +#night-mode-label, +#caffeine-label, +#dark-mode-label, +#power-profile-label { + color: @shadow; + font-size: 14px; + font-weight: bold; + margin-bottom: -4px; +} + +#network-label.disabled, +#bluetooth-label.disabled, +#night-mode-label.disabled, +#caffeine-label.disabled, +#dark-mode-label.disabled, +#power-profile-label.disabled { + color: @outline; +} + +#network-ssid, +#bluetooth-status, +#night-mode-status, +#caffeine-status, +#dark-mode-status, +#power-profile-status { + color: @surfaceBright; + font-size: 12px; +} + +#network-ssid.disabled, +#bluetooth-status.disabled, +#night-mode-status.disabled, +#caffeine-status.disabled, +#dark-mode-status.disabled, +#power-profile-status.disabled { + color: @outline; +} + +#power-profile-button.balanced-profile { + background-color: transparent; +} diff --git a/styles/launcher.css b/styles/launcher.css index db2e522..9057c92 100644 --- a/styles/launcher.css +++ b/styles/launcher.css @@ -1,6 +1,9 @@ /* * ── launcher ────────────────────────────────────────────────────────── */ +#dashboard { + min-height: 100px; +} #launcher { margin: 18px; @@ -31,12 +34,12 @@ #launcher-content.bluetooth { min-height: 464px; - min-width: 464px; + min-width: 485px; } #launcher-content.wifi { min-height: 464px; - min-width: 464px; + min-width: 485px; } #launcher-content.power { @@ -49,7 +52,7 @@ } #launcher-content { - min-width: 420px; + min-width: 485px; } #app-launcher, @@ -63,16 +66,6 @@ transition: all 0.25s cubic-bezier(0.5, 0.25, 0, 1); } -#toggle-launcher-button { - background-color: @secondaryContainer; - color: @onSecondaryContainer; - margin: 11px; - margin-left: 0px; - min-width: 48px; - min-height: 44px; - border-radius: 100%; -} - #clear-launcher-button { background-color: @secondaryContainer; color: @onSecondaryContainer; diff --git a/styles/main.css b/styles/main.css index a971d02..7703a10 100644 --- a/styles/main.css +++ b/styles/main.css @@ -3,6 +3,7 @@ @import "./notification.css"; @import "./osd.css"; @import "./launcher.css"; +@import "./dashboard.css"; * { all: unset; @@ -61,9 +62,8 @@ tooltip label { /* * ── icon ────────────────────────────────────────────────────────────── */ - -#material_icon, -#material_icon * { +#material-icon, +#material-icon * { all: unset; font-family: "Material Symbols Outlined", "Material Symbols Sharp"; } diff --git a/styles/notification.css b/styles/notification.css index b1ba83c..16f1817 100644 --- a/styles/notification.css +++ b/styles/notification.css @@ -20,6 +20,8 @@ #notification .close-button { border-radius: 100px; + min-width: 40px; + min-height: 40px; } #notification .close-button:hover { @@ -42,6 +44,7 @@ border-radius: 100px; background-color: transparent; } + #notification-action-buttons button:hover { background-color: lighter(@outlineVariant); } From 28f47aed3c5b45a7541b0c382233f5eedd9c5dbd Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 25 Feb 2025 05:19:35 +0545 Subject: [PATCH 03/22] Avoid to download paru if yay is already present --- install.sh | 53 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/install.sh b/install.sh index 59be581..aba00ab 100644 --- a/install.sh +++ b/install.sh @@ -44,19 +44,25 @@ PACKAGES=( ) if ! grep -q "arch" /etc/os-release; then - echo ":: This script is designed to run on Arch Linux." + echo "This script is designed to run on Arch Linux." exit 1 fi -# Optional: Prevent running as root +# Prevent running as root if [ "$(id -u)" -eq 0 ]; then - echo ":: Please do not run this script as root." + echo "Please do not run this script as root." exit 1 fi -# Install paru if not installed -if ! command -v yay &>/dev/null; then - echo ":: Installing paru..." +aur_helper="" +if command -v yay &>/dev/null; then + aur_helper="yay" + echo $aur_helper +elif command -v paru &>/dev/null; then + aur_helper="paru" + echo $aur_helper +else + echo "Installing paru..." tmpdir=$(mktemp -d) git clone https://aur.archlinux.org/paru.git "$tmpdir/paru" cd "$tmpdir/paru" @@ -67,19 +73,36 @@ fi # Clone or update the repository if [ -d "$INSTALL_DIR" ]; then - echo ":: Updating Modus..." + echo "Updating Modus..." git -C "$INSTALL_DIR" pull else - echo ":: Cloning Modus..." + echo "Cloning Modus..." git clone "$REPO_URL" "$INSTALL_DIR" fi -# Install required packages using paru -echo ":: Installing required packages..." -paru -Syy --needed --noconfirm "${PACKAGES[@]}" || true +# Install required packages using $aur_helper (only if missing +echo "Installing required packages..." +$aur_helper -Syy --needed --noconfirm "${PACKAGES[@]}" || true + +# Update outdated packages from the list +echo "Updating outdated required packages..." +# Get a list of outdated packages +outdated=$($aur_helper -Qu | awk '{print $1}') +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 + $aur_helper -S --noconfirm "${to_update[@]}" || true +else + echo "All required packages are up-to-date." +fi # Backup and replace GTK configs -echo ":: Updating 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" [[ -d "$HOME/.config/gtk-4.0" ]] && mv "$HOME/.config/gtk-4.0" "$HOME/.config/gtk-4.0-bk" @@ -88,7 +111,7 @@ cp -r "$INSTALL_DIR/assets/gtk-3.0" "$HOME/.config/" cp -r "$INSTALL_DIR/assets/gtk-4.0" "$HOME/.config/" # Install Icon -echo ":: Installing icon theme..." +echo "Installing icon theme..." tmpdir=$(mktemp -d) cd /tmp/install git clone https://github.com/vinceliuice/Tela-icon-theme @@ -96,9 +119,9 @@ cd "$tmpdir/Tela-icon-theme" ./install.sh nord # Launch Modus without terminal output -echo ":: Starting Modus..." +echo "Starting Modus..." killall modus 2>/dev/null || true python "$INSTALL_DIR/main.py" >/dev/null 2>&1 & disown -echo ":: Installation complete." +echo "Installation complete." From 1885a2dc30d050367084ef9ff065202252f79920 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 25 Feb 2025 05:21:53 +0545 Subject: [PATCH 04/22] Huge Updates --- config/scripts/systemupdates.sh | 164 +++++++++ config/scripts/wayland-idle-inhibitor.py | 83 ----- modules/bar/bar.py | 48 +-- modules/bar/components/__init__.py | 8 +- modules/bar/components/battery.py | 285 ++++++++++++--- modules/bar/components/bluetooth.py | 34 -- modules/bar/components/indicators.py | 134 +++++++ modules/bar/components/microphone.py | 33 -- modules/bar/components/network.py | 84 ----- modules/bar/components/stats.py | 131 ++++--- modules/bar/components/taskbar.py | 2 +- modules/bar/components/updates.py | 103 ++++++ modules/bar/components/volume.py | 81 ----- modules/launcher/components/applauncher.py | 191 +++++++--- modules/launcher/components/bluetooth.py | 72 ++-- modules/launcher/components/buttons.py | 396 --------------------- modules/launcher/components/calendar.py | 6 +- modules/launcher/components/cliphist.py | 76 ++-- modules/launcher/components/dashboard.py | 130 ++----- modules/launcher/components/power.py | 49 +-- modules/launcher/components/sh.py | 6 +- modules/launcher/components/todo.py | 18 +- modules/launcher/components/wifi.py | 22 +- modules/launcher/launcher.py | 1 - modules/osd.py | 4 +- snippets/iconss.py | 142 ++++++++ styles/bar.css | 143 ++++++-- styles/dashboard.css | 79 ++-- styles/launcher.css | 151 +++++--- styles/main.css | 5 +- 30 files changed, 1421 insertions(+), 1260 deletions(-) create mode 100755 config/scripts/systemupdates.sh delete mode 100755 config/scripts/wayland-idle-inhibitor.py delete mode 100644 modules/bar/components/bluetooth.py create mode 100644 modules/bar/components/indicators.py delete mode 100644 modules/bar/components/microphone.py delete mode 100644 modules/bar/components/network.py create mode 100644 modules/bar/components/updates.py delete mode 100644 modules/bar/components/volume.py delete mode 100644 modules/launcher/components/buttons.py create mode 100644 snippets/iconss.py diff --git a/config/scripts/systemupdates.sh b/config/scripts/systemupdates.sh new file mode 100755 index 0000000..e8044eb --- /dev/null +++ b/config/scripts/systemupdates.sh @@ -0,0 +1,164 @@ +#!/bin/bash + +# Word of caution: changing this can break update module, be vigilant + +check_arch_updates() { + official_updates=0 + aur_updates=0 + flatpak_updates=0 + tooltip="" # Initialize tooltip variable + + # Get the number of official updates using 'checkupdates' + official_updates=$(checkupdates 2>/dev/null | wc -l) + + if command -v paru &>/dev/null; then + aur_helper="paru" + else + aur_helper="yay" + fi + + # Get the number of AUR updates using 'yay' + aur_updates=$($aur_helper -Qum 2>/dev/null | wc -l) + + # Calculate total updates + total_updates=$((official_updates + aur_updates + flatpak_updates)) + + # Build the tooltip for official updates + tooltip="󰣇 Official $official_updates\n󰮯 AUR $aur_updates" + + # Check if flatpak is installed + if command -v flatpak &>/dev/null; then + # Get the number of Flatpak updates if flatpak is installed + flatpak_updates=$(flatpak remote-ls --updates | wc -l) + + # Append Flatpak updates to the tooltip + tooltip="$tooltip\n Flatpak $flatpak_updates" + + # Recalculate total updates + total_updates=$((official_updates + aur_updates + flatpak_updates)) + fi + + # Output the result as a JSON object + echo "{\"total\":\"$total_updates\", \"tooltip\":\"$tooltip\"}" +} + +check_ubuntu_updates() { + official_updates=0 + flatpak_updates=0 + + # Check if flatpak is installed and get Flatpak updates if it is + if command -v flatpak &>/dev/null; then + flatpak_updates=$(flatpak remote-ls --updates | wc -l) + # Always show Flatpak updates in the tooltip, even if there are none + tooltip="󰕈 Official $official_updates\n Flatpak $flatpak_updates" + else + # If flatpak is not installed, only show official updates + tooltip="󰕈 Official $official_updates" + fi + + # Get the number of official updates using 'apt-get' + official_updates=$(apt-get -s -o Debug::NoLocking=true upgrade | grep -c ^Inst) + + # Calculate total updates + total_updates=$((official_updates + flatpak_updates)) + + # Output the result as a JSON object + echo "{\"total\":\"$total_updates\", \"tooltip\":\"$tooltip\"}" +} + +check_fedora_updates() { + official_updates=0 + flatpak_updates=0 + + # Get the number of official updates using 'dnf' + official_updates=$(dnf check-update -q | grep -v '^Loaded plugins' | grep -v '^No match for' | wc -l) + + # Calculate total updates + total_updates=$((official_updates + flatpak_updates)) + + # Check if flatpak is installed and get Flatpak updates if it is + if command -v flatpak &>/dev/null; then + flatpak_updates=$(flatpak remote-ls --updates | wc -l) + # Always show Flatpak updates in the tooltip, even if there are none + tooltip="󰣛 Official $official_updates\n Flatpak $flatpak_updates" + else + # If flatpak is not installed, only show official updates + tooltip="󰣛 Official $official_updates" + fi + + # Output the result as a JSON object + echo "{\"total\":\"$total_updates\", \"tooltip\":\"$tooltip\"}" +} + +check_opensuse_updates() { + official_updates=0 + flatpak_updates=0 + + # Check if flatpak is installed and get Flatpak updates if it is + if command -v flatpak &>/dev/null; then + flatpak_updates=$(flatpak remote-ls --updates | wc -l) + fi + + # Get the number of official updates using 'dnf' + official_updates=$(zypper lu | wc -l) + + # Calculate total updates + total_updates=$((official_updates + flatpak_updates)) + + # Check if flatpak is installed and get Flatpak updates if it is + if command -v flatpak &>/dev/null; then + flatpak_updates=$(flatpak remote-ls --updates | wc -l) + # Always show Flatpak updates in the tooltip, even if there are none + tooltip=" Official $official_updates\n Flatpak $flatpak_updates" + else + # If flatpak is not installed, only show official updates + tooltip=" Official $official_updates" + fi + + # Output the result as a JSON object + echo "{\"total\":\"$total_updates\", \"tooltip\":\"$tooltip\"}" +} + +update_arch() { + command=" + fastfetch + yay -Syu + read -n 1 -p 'Press any key to continue...' + " + kitty --title systemupdate sh -c "${command}" +} + +case "$1" in +-arch) + if [ -z "$2" ]; then # If second argument is null (not provided) + check_arch_updates + else + update_arch + fi + ;; +-ubuntu) + if [ -z "$2" ]; then # If second argument is null (not provided) + check_ubuntu_updates + else + update_ubuntu + fi + ;; +-fedora) + if [ -z "$2" ]; then # If second argument is null (not provided) + check_fedora_updates + else + update_fedora + fi + ;; +-suse) + if [ -z "$2" ]; then # If second argument is null (not provided) + check_opensuse_updates + else + update_arch + fi + ;; +*) + echo "Usage: $0 [-arch|-ubuntu|-fedora|-suse] [up (optional)]" + exit 1 + ;; +esac diff --git a/config/scripts/wayland-idle-inhibitor.py b/config/scripts/wayland-idle-inhibitor.py deleted file mode 100755 index c731f67..0000000 --- a/config/scripts/wayland-idle-inhibitor.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python - -import sys -from dataclasses import dataclass -from signal import SIGINT, SIGTERM, signal -from threading import Event -import setproctitle - -from pywayland.client.display import Display # type: ignore -from pywayland.protocol.idle_inhibit_unstable_v1.zwp_idle_inhibit_manager_v1 import ( # type: ignore - ZwpIdleInhibitManagerV1, -) -from pywayland.protocol.wayland.wl_compositor import WlCompositor # type: ignore -from pywayland.protocol.wayland.wl_registry import WlRegistryProxy # type: ignore -from pywayland.protocol.wayland.wl_surface import WlSurface # type: ignore - - -@dataclass -class GlobalRegistry: - surface: WlSurface | None = None - inhibit_manager: ZwpIdleInhibitManagerV1 | None = None - - -def handle_registry_global( - wl_registry: WlRegistryProxy, id_num: int, iface_name: str, version: int -) -> None: - global_registry: GlobalRegistry = wl_registry.user_data or GlobalRegistry() - - if iface_name == "wl_compositor": - compositor = wl_registry.bind(id_num, WlCompositor, version) - global_registry.surface = compositor.create_surface() # type: ignore - elif iface_name == "zwp_idle_inhibit_manager_v1": - global_registry.inhibit_manager = wl_registry.bind( - id_num, ZwpIdleInhibitManagerV1, version - ) - - -def main() -> None: - done = Event() - signal(SIGINT, lambda _, __: done.set()) - signal(SIGTERM, lambda _, __: done.set()) - - global_registry = GlobalRegistry() - - display = Display() - display.connect() - - registry = display.get_registry() # type: ignore - registry.user_data = global_registry - registry.dispatcher["global"] = handle_registry_global - - def shutdown() -> None: - display.dispatch() - display.roundtrip() - display.disconnect() - - display.dispatch() - display.roundtrip() - - if global_registry.surface is None or global_registry.inhibit_manager is None: - print("Wayland seems not to support idle_inhibit_unstable_v1 protocol.") - shutdown() - sys.exit(1) - - inhibitor = global_registry.inhibit_manager.create_inhibitor( # type: ignore - global_registry.surface - ) - - display.dispatch() - display.roundtrip() - - print("Inhibiting idle...") - done.wait() - print("Shutting down...") - - inhibitor.destroy() - - shutdown() - - -if __name__ == "__main__": - setproctitle.setproctitle("wayland-idle-inhibitor.py") - main() diff --git a/modules/bar/bar.py b/modules/bar/bar.py index 62b6132..dc91a6e 100644 --- a/modules/bar/bar.py +++ b/modules/bar/bar.py @@ -1,6 +1,11 @@ from fabric.hyprland.widgets import Language from fabric.system_tray.widgets import SystemTray -from fabric.utils import FormattedString, bulk_replace +from fabric.utils import ( + FormattedString, + bulk_replace, + exec_shell_command_async, + get_relative_path, +) from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox @@ -10,16 +15,14 @@ from fabric.widgets.wayland import WaylandWindow as Window from gi.repository import GLib from modules.bar.components import ( BatteryLabel, - BluetoothIndicator, - MicrophoneIndicator, - NetworkIndicator, SystemInfo, TaskBar, - VolumeIndicator, workspace, + Indicators, + UpdatesWidget, ) -from snippets import MaterialIcon from services import sc +import snippets.iconss as icons class Bar(Window): @@ -39,7 +42,7 @@ class Bar(Window): self.recording_indicator = Button( name="recording-indicator", - child=MaterialIcon("stop_circle", 16), + child=Label(name="recorder", markup=icons.record), visible=False, on_clicked=lambda *_: sc.screencast_stop(), ) @@ -50,13 +53,9 @@ class Bar(Window): self.date_time = DateTime(formatters=["%-I:%M 󰧞 %a %d %b"], name="datetime") - self.battery = BatteryLabel(name="battery") - self.volume = VolumeIndicator() - self.bluetooth = BluetoothIndicator() - self.network = NetworkIndicator() - self.microphone = MicrophoneIndicator() + self.battery = BatteryLabel() self.taskbar = TaskBar() - self.info = SystemInfo() + self.stats = SystemInfo() self.tray = SystemTray(name="tray", icon_size=16, spacing=4) self.launcher = Button( name="logo", @@ -65,6 +64,15 @@ class Bar(Window): "fabric-cli exec modus 'launcher.open(\"launcher\")'" ), ) + self.updates = UpdatesWidget() + self.indicators = Indicators() + self.button_config = Button( + name="button-bar", + on_clicked=lambda *_: exec_shell_command_async( + f"python {get_relative_path('../../config/config.py')}" + ), + child=Label(name="button-bar-label", markup=icons.config), + ) self.applets = Box( name="applets", @@ -72,10 +80,7 @@ class Bar(Window): orientation="h", children=[ self.language, - self.bluetooth, - self.network, - self.volume, - self.microphone, + self.indicators, ], ) @@ -85,11 +90,7 @@ class Bar(Window): name="start-container", spacing=8, orientation="h", - children=[ - self.launcher, - self.workspaces, - self.info, - ], + children=[self.launcher, self.workspaces, self.stats, self.updates], ), center_children=Box( name="center-container", @@ -107,6 +108,7 @@ class Bar(Window): self.battery, self.applets, self.date_time, + self.button_config, ], ), ) @@ -127,6 +129,8 @@ class Bar(Window): def toggle_hidden(self): self.hidden = not self.hidden if self.hidden: + self.bar.remove_style_class("visible") self.bar.add_style_class("hidden") else: self.bar.remove_style_class("hidden") + self.bar.add_style_class("visible") diff --git a/modules/bar/components/__init__.py b/modules/bar/components/__init__.py index 278c35f..116ed6d 100644 --- a/modules/bar/components/__init__.py +++ b/modules/bar/components/__init__.py @@ -1,10 +1,6 @@ from .battery import * -from .bluetooth import * -from .idle import * from .stats import * -from .microphone import * -from .network import * -from .powerprofile import * from .taskbar import * -from .volume import * from .workspace import * +from .indicators import * +from .updates import * diff --git a/modules/bar/components/battery.py b/modules/bar/components/battery.py index f6ddcac..60440b6 100644 --- a/modules/bar/components/battery.py +++ b/modules/bar/components/battery.py @@ -1,70 +1,237 @@ -import psutil -from fabric import Fabricator +import subprocess +import re from fabric.widgets.box import Box -from fabric.widgets.overlay import Overlay +from fabric.widgets.eventbox import EventBox +from fabric.widgets.button import Button from fabric.widgets.label import Label from fabric.widgets.circularprogressbar import CircularProgressBar -from snippets import MaterialIcon +from fabric.widgets.overlay import Overlay +from fabric.widgets.revealer import Revealer +from fabric.core.fabricator import Fabricator +from fabric.utils.helpers import exec_shell_command_async +from gi.repository import GLib +import snippets.iconss as icons class BatteryLabel(Box): - ICONS_CHARGING = [ - "battery_charging_20", - "battery_charging_20", - "battery_charging_20", - "battery_charging_30", - "battery_charging_30", - "battery_charging_50", - "battery_charging_60", - "battery_charging_80", - "battery_charging_80", - "battery_charging_90", - "battery_charging_full", - ] - ICONS_NOT_CHARGING = [ - "battery_alert", - "battery_1_bar", - "battery_1_bar", - "battery_2_bar", - "battery_2_bar", - "battery_3_bar", - "battery_4_bar", - "battery_4_bar", - "battery_5_bar", - "battery_6_bar", - "battery_full", - ] - def __init__(self, **kwargs): - super().__init__(**kwargs) - Fabricator(interval=1000, poll_from=self.update_battery_status) - - def update_battery_status(self, *_): - battery = psutil.sensors_battery() - if battery is None: - self.hide() - return - is_charging = battery.power_plugged if battery else False - icons = self.ICONS_CHARGING if is_charging else self.ICONS_NOT_CHARGING - battery_percent = round(battery.percent) - index = min(max(battery_percent // 10, 0), 10) - battery_icon = MaterialIcon(icons[index], size=16) - - self.progress_bar = CircularProgressBar( - name="progress", line_style="round", line_width=2, size=24 + super().__init__( + name="battery", + orientation="h", + spacing=0, ) - self.battery_overlay = Overlay(child=self.progress_bar, overlays=battery_icon) - self.battery_label = Label(label=f"{battery_percent}%", name="battery-label") - - self.children = self.battery_label, self.battery_overlay - - battery_percent = round(battery.percent) - self.progress_bar.value = battery_percent / 100 - self.show() if battery_percent < 100 else self.hide() - - self.set_tooltip_text( - f"Battery: {battery_percent}%\nStatus: {'Charging' if is_charging else 'Discharging'}" + self.bat_save = Button( + name="battery-save", + child=Label(name="battery-save-label", markup=icons.power_saving), + on_clicked=lambda *_: self.set_power_mode("powersave"), + ) + self.bat_balanced = Button( + name="battery-balanced", + child=Label(name="battery-balanced-label", markup=icons.power_balanced), + on_clicked=lambda *_: self.set_power_mode("balanced"), + ) + self.bat_perf = Button( + name="battery-performance", + child=Label( + name="battery-performance-label", markup=icons.power_performance + ), + on_clicked=lambda *_: self.set_power_mode("performance"), ) - return True + # Attach mouse enter/leave events to buttons as well + for btn in [self.bat_save, self.bat_balanced, self.bat_perf]: + btn.connect("enter-notify-event", self.on_mouse_enter) + btn.connect("leave-notify-event", self.on_mouse_leave) + + # Group the mode buttons into a container. + self.mode_switcher = Box( + name="power-mode-switcher", + orientation="h", + spacing=4, + children=[self.bat_save, self.bat_balanced, self.bat_perf], + ) + + self.bat_icon = Label(name="battery-icon", markup=icons.battery) + + self.bat_circle = CircularProgressBar( + name="battery-circle", + value=0, + size=28, + line_width=2, + start_angle=180, + end_angle=360, + ) + + self.bat_overlay = Overlay( + name="battery-overlay", + visible=False, + child=self.bat_circle, + overlays=[self.bat_icon], + ) + + self.bat_level = Label( + name="battery-level", + label="100%", + ) + + self.bat_revealer = Revealer( + name="battery-revealer", + transition_duration=250, + transition_type="slide-left", + child=self.bat_level, + child_revealed=False, + ) + + # Create an inner container to hold the battery elements. + inner_container = Box(orientation="h", spacing=3) + inner_container.add(self.bat_overlay) + inner_container.add(self.bat_revealer) + inner_container.add(self.mode_switcher) + + # Wrap the inner container in an EventBox. + self.event_box = EventBox( + events=["enter-notify-event", "leave-notify-event"], + name="battery-eventbox", + ) + self.event_box.connect("enter-notify-event", self.on_mouse_enter) + self.event_box.connect("leave-notify-event", self.on_mouse_leave) + self.event_box.add(inner_container) + + # Add the EventBox to the Battery widget. + self.add(self.event_box) + + self.current_mode = None + # Initialize hide_timer for delayed hiding. + self.hide_timer = None + # Initialize counter to track hover status across all related widgets. + self.hover_counter = 0 + + # Initialize Fabricator for battery polling every 5 seconds. + self.batt_fabricator = Fabricator( + lambda *args, **kwargs: self.poll_battery(), + interval=1000, + stream=False, + default_value=0, + ) + self.batt_fabricator.changed.connect(self.update_battery) + + # Run update_battery immediately at startup. + GLib.idle_add(self.update_battery, None, self.poll_battery()) + self.set_power_mode("balanced") + + def on_mouse_enter(self, widget, event): + """Reveal battery level on hover.""" + self.hover_counter += 1 + if self.hide_timer is not None: + GLib.source_remove(self.hide_timer) + self.hide_timer = None + self.bat_revealer.set_reveal_child(True) + return False + + def on_mouse_leave(self, widget, event): + """Schedule hiding the battery level after a 0.5s delay only if not hovering any element.""" + 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): + self.bat_revealer.set_reveal_child(False) + self.hide_timer = None + return False + + def poll_battery(self): + """ + Polls the battery status by running the "acpi -b" command. + If no battery information is found, returns (0, None). + Otherwise, returns a tuple: (battery percentage as a float between 0 and 1, battery status string) + """ + try: + output = subprocess.check_output(["acpi", "-b"]).decode("utf-8").strip() + if "Battery" not in output: + return (0, None) + match_percent = re.search(r"(\d+)%", output) + match_status = re.search(r"Battery \d+: (\w+)", output) + if match_percent: + percent = int(match_percent.group(1)) + status = match_status.group(1) if match_status else None + return (percent / 100.0, status) + except Exception: + pass + return (0, None) + + def update_battery(self, sender, battery_data): + """ + Updates the battery widget... + """ + value, status = battery_data + if value == 0: + self.bat_overlay.set_visible(False) + self.bat_revealer.set_visible(False) + else: + self.bat_overlay.set_visible(True) + self.bat_revealer.set_visible(True) + self.bat_circle.set_value(value) + + percentage = int(value * 100) + self.bat_level.set_label(f"{percentage}%") + + # Actualizar el icono de la batería basado en el nivel y el estado de carga. + if percentage <= 15: + self.bat_icon.set_markup(icons.alert) + self.bat_icon.add_style_class("alert") + self.bat_circle.add_style_class("alert") + else: + self.bat_icon.remove_style_class("alert") + self.bat_circle.remove_style_class("alert") + if ( + status == "Discharging" + ): # Muestra discharging en cualquier nivel, incluso al 100%. + self.bat_icon.set_markup(icons.discharging) + elif percentage == 100: + self.bat_icon.set_markup(icons.battery) + elif status == "Charging": + self.bat_icon.set_markup(icons.charging) + else: + self.bat_icon.set_markup(icons.battery) + + def set_power_mode(self, mode): + """ + Switches power mode by running the corresponding auto-cpufreq command. + mode: one of 'powersave', 'balanced', or 'performance' + """ + commands = { + "powersave": "sudo auto-cpufreq --force powersave", + "balanced": "sudo auto-cpufreq --force reset", + "performance": "sudo auto-cpufreq --force performance", + } + if mode in commands: + try: + exec_shell_command_async(commands[mode]) + self.current_mode = mode + self.update_button_styles() + except Exception as err: + # Optionally, handle errors or display a notification. + print(f"Error setting power mode: {err}") + + def update_button_styles(self): + """ + Optionally updates button styles to reflect the current mode. + Adjust the styling method based on your toolkit's capabilities. + """ + if self.current_mode == "powersave": + self.bat_save.add_style_class("active") + self.bat_balanced.remove_style_class("active") + self.bat_perf.remove_style_class("active") + elif self.current_mode == "balanced": + self.bat_save.remove_style_class("active") + self.bat_balanced.add_style_class("active") + self.bat_perf.remove_style_class("active") + elif self.current_mode == "performance": + self.bat_save.remove_style_class("active") + self.bat_balanced.remove_style_class("active") + self.bat_perf.add_style_class("active") diff --git a/modules/bar/components/bluetooth.py b/modules/bar/components/bluetooth.py deleted file mode 100644 index ffc4fec..0000000 --- a/modules/bar/components/bluetooth.py +++ /dev/null @@ -1,34 +0,0 @@ -from fabric.bluetooth import BluetoothClient -from fabric.widgets.box import Box -from snippets import MaterialIcon - - -class BluetoothIndicator(Box): - ICON_ENABLED = "bluetooth" - ICON_DISABLED = "bluetooth_disabled" - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.bluetooth_client = BluetoothClient() - - self.bluetooth_icon = None - - self.bluetooth_client.connect("changed", self.update_bluetooth_status) - - self.update_bluetooth_status() - - def update_bluetooth_status(self, *_): - icon_label = ( - self.ICON_ENABLED if self.bluetooth_client.enabled else self.ICON_DISABLED - ) - - new_icon = MaterialIcon(icon_label, size=16) - - if self.bluetooth_icon: - self.remove(self.bluetooth_icon) - - self.bluetooth_icon = new_icon - self.add(self.bluetooth_icon) - - def on_destroy(self): - self.bluetooth_client.disconnect("changed", self.update_bluetooth_status) diff --git a/modules/bar/components/indicators.py b/modules/bar/components/indicators.py new file mode 100644 index 0000000..5bf8fe8 --- /dev/null +++ b/modules/bar/components/indicators.py @@ -0,0 +1,134 @@ +import subprocess +from fabric import Fabricator +from fabric.widgets.label import Label +from fabric.widgets.box import Box +from fabric.bluetooth import BluetoothClient +from services import NetworkClient, audio +import snippets.iconss as icons + + +class Indicators(Box): + def __init__(self, **kwargs): + super().__init__(orientation="h", spacing=2, **kwargs) + + self.bluetooth_icon = Label(name="indicator-icon") + self.wifi_icon = Label(name="indicator-icon") + self.volume_icon_button = Label(name="indicator-icon") + self.microphone_icon = Label(name="indicator-icon") + self.idle_label = Label(name="indicator-icon") + self.night_label = Label(name="indicator-icon") + + for widget in [ + self.bluetooth_icon, + self.wifi_icon, + self.volume_icon_button, + self.microphone_icon, + self.idle_label, + self.night_label, + ]: + self.add(widget) + + self.audio_service = audio + self.bluetooth_client = BluetoothClient() + self.network_client = NetworkClient() + + # 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) + + if self.network_client.wifi_device: + self.network_client.wifi_device.connect( + "changed", self.update_network_status + ) + + if self.network_client.ethernet_device: + self.network_client.ethernet_device.connect( + "changed", self.update_network_status + ) + + Fabricator(interval=1000, poll_from=self.update_all_statuses) + + self.update_bluetooth_status() + + def update_all_statuses(self, *_args): + self.update_idle_night_status() + self.update_network_status() + + def update_idle_night_status(self, *_): + """Updates the idle and night mode status icons.""" + self.idle_label.set_markup( + icons.coffee + if subprocess.run( + ["pgrep", "-x", "wlinhibit"], capture_output=True + ).returncode + == 0 + else "" + ) + + self.night_label.set_markup( + icons.night + if subprocess.run( + ["pgrep", "-x", "hyprsunset"], capture_output=True + ).returncode + == 0 + else "" + ) + + def update_volume_status(self, *_): + """Updates the volume icon based on speaker status.""" + stream = self.audio_service.speaker + if stream: + volume_level = stream.volume + self.volume_icon_button.set_markup( + icons.vol_off + if stream.muted + else ( + icons.vol_mute + if volume_level == 0 + else (icons.vol_medium if volume_level <= 50 else icons.vol_high) + ) + ) + + def update_mic_status(self, *_): + """Updates the microphone icon based on mute status.""" + mic = self.audio_service.microphone + self.microphone_icon.set_markup(icons.mic_off if mic and mic.muted else "") + self.microphone_icon.set_visible(bool(mic)) + + def update_bluetooth_status(self, *_): + """Updates the Bluetooth icon based on enabled status.""" + self.bluetooth_icon.set_markup( + icons.bluetooth if self.bluetooth_client.enabled else icons.bluetooth_off + ) + + def update_network_status(self, *_): + primary_device = self.network_client.primary_device + + if primary_device == "wifi" and self.network_client.wifi_device: + wifi_device = self.network_client.wifi_device + self.set_tooltip_text(self.network_client.wifi_device.ssid) + + if wifi_device.enabled: + wifi_strength = wifi_device.strength + + if wifi_strength == -1: # No active connection + icon_label = icons.wifi_off + else: + icon_label = { + 80: icons.wifi, + 60: icons.good_signal, + 40: icons.moderate_signal, + 20: icons.weak_signal, + 0: icons.no_signal, + }.get(min(80, 20 * round(wifi_strength / 20)), icons.wifi_off) + else: + icon_label = icons.wifi_off + + elif primary_device == "wired": + icon_label = icons.lan + else: + icon_label = icons.wifi_off + + self.wifi_icon.set_markup(icon_label) diff --git a/modules/bar/components/microphone.py b/modules/bar/components/microphone.py deleted file mode 100644 index a89baf7..0000000 --- a/modules/bar/components/microphone.py +++ /dev/null @@ -1,33 +0,0 @@ -from fabric.audio.service import Audio -from fabric.widgets.box import Box -from snippets import MaterialIcon - - -class MicrophoneIndicator(Box): - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.audio_service = Audio() - self.microphone_icon = self.create_microphone_icon() - - self.audio_service.connect("microphone_changed", self.update_microphone_status) - self.update_microphone_status() - - self.children = (self.microphone_icon,) - - def create_microphone_icon(self): - return MaterialIcon("mic_off", size=16) - - def update_microphone_status(self, *args): - current_microphone = self.audio_service.microphone - - if current_microphone: - is_muted = current_microphone.muted - icon_name = "mic_off" if is_muted else "mic" - - self.microphone_icon.set_label(icon_name) - self.microphone_icon.set_visible(is_muted) - else: - self.microphone_icon.set_visible(False) - - return True diff --git a/modules/bar/components/network.py b/modules/bar/components/network.py deleted file mode 100644 index 3407134..0000000 --- a/modules/bar/components/network.py +++ /dev/null @@ -1,84 +0,0 @@ -import subprocess - -import psutil -from fabric import Fabricator -from fabric.widgets.box import Box -from snippets import MaterialIcon - - -class NetworkIndicator(Box): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.update_interval = 5000 # Increased interval to 5 seconds - self.last_status = None - self.wifi_strength_cache = None - self.fabricator = Fabricator( - interval=self.update_interval, - poll_from=self.update_network_status, - ) - - def update_network_status(self, *_): - current_status = self.get_network_status() - if current_status != self.last_status: - self.update_icon(current_status) - self.last_status = current_status - return True - - def update_icon(self, current_status): - self.icon_label = MaterialIcon(current_status, size=16) - self.children = (self.icon_label,) - - def get_network_status(self): - if self.is_ethernet_connected(): - return "lan" - if self.is_wifi_connected(): - return self.get_wifi_strength_icon() - return "signal_wifi_off" - - def get_wifi_strength_icon(self): - wifi_strength = self.check_wifi_strength() - if wifi_strength is not None: - self.wifi_strength_cache = wifi_strength # Cache the result - else: - wifi_strength = self.wifi_strength_cache # Use cached value if available - - return self.wifi_signal_icon(wifi_strength) - - def wifi_signal_icon(self, strength): - if strength is None or strength <= -100: - return "signal_wifi_0_bar" - if strength > -50: - return "signal_wifi_4_bar" - if strength > -60: - return "network_wifi_3_bar" - if strength > -70: - return "network_wifi_2_bar" - return "network_wifi_1_bar" - - def check_wifi_strength(self): - try: - result = subprocess.run(["iwconfig"], capture_output=True, text=True) - for line in result.stdout.splitlines(): - if "Signal level" in line: - signal_level = line.split("Signal level=")[1].split(" ")[0] - return int(signal_level.strip().replace("dBm", "")) - return None - except Exception: - return None - - def is_wifi_connected(self): - return any( - self._is_interface_up(interface, ["wlan", "wifi"]) - for interface in psutil.net_if_addrs() - ) - - def is_ethernet_connected(self): - return any( - self._is_interface_up(interface, ["enp", "eth"]) - for interface in psutil.net_if_addrs() - ) - - def _is_interface_up(self, interface, keywords): - if any(substring in interface.lower() for substring in keywords): - return psutil.net_if_stats().get(interface, {}).isup - return False diff --git a/modules/bar/components/stats.py b/modules/bar/components/stats.py index 2b54fa5..9b5c5ab 100644 --- a/modules/bar/components/stats.py +++ b/modules/bar/components/stats.py @@ -1,88 +1,83 @@ -from time import sleep import psutil -from fabric import Fabricator -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.circularprogressbar import CircularProgressBar +from gi.repository import GLib + from fabric.widgets.label import Label +from fabric.widgets.box import Box from fabric.widgets.overlay import Overlay -from snippets import MaterialIcon +from fabric.widgets.circularprogressbar import CircularProgressBar + +import snippets.iconss as icons -class SystemInfo(Button): - ICONS = [ - ("CPU", "memory"), - ("Swap", "swap_horiz"), - ("RAM", "memory_alt"), - ("Temp", "thermometer"), - ] +class SystemInfo(Box): + ICONS = { + "CPU": icons.cpu, + "RAM": icons.memory, + "Swap": icons.swap, + "Disk": icons.disk, + "Temp": icons.temp, + } - def __init__(self): - super().__init__(child=self._initialize_content()) - - Fabricator( - poll_from=self._refresh_system_info, - stream=True, - on_changed=lambda fab, data: self._update_ui(data), + def __init__(self, **kwargs): + super().__init__( + name="system-info", + spacing=8, + h_align="center", + v_align="fill", + visible=True, + all_visible=True, ) - def _initialize_content(self): - self.box = Box(orientation="h", name="tray") - self.progress_bars = {} - self.icons = {} + self.progress_bars = { + system: CircularProgressBar( + name="stats-progress", + line_style="round", + line_width=3, + size=28, + start_at=0, + end_at=0.5, + start_angle=0, + end_angle=180, + ) + for system in self.ICONS + } - for system, icon_name in self.ICONS: - self._create_system_item(system, icon_name) + for system, icon_name in self.ICONS.items(): + overlay = Overlay( + child=self.progress_bars[system], + overlays=[Label(name="stats-icons", markup=icon_name)], + ) + self.add(overlay) - return self.box + GLib.timeout_add_seconds(1, self._update_system_info) - def _create_system_item(self, system, icon_name): - self.progress_bars[system] = self._create_progress_bar() - self.icons[system] = self._create_icon(icon_name) + def _update_system_info(self): + ram_usage = psutil.virtual_memory().percent + swap_usage = psutil.swap_memory().percent + disk_usage = psutil.disk_usage("/").percent + cpu = psutil.cpu_percent(interval=0) - overlay = Overlay( - child=self.progress_bars[system], overlays=[self.icons[system]] - ) - self.box.pack_start(overlay, False, False, 4) + usages = { + "CPU": cpu, + "RAM": ram_usage, + "Swap": swap_usage, + "Disk": disk_usage, + "Temp": self._get_device_temperature(), + } - def _create_progress_bar(self): - return CircularProgressBar( - name="progress", - line_style="round", - line_width=3, - size=28, - start_at=0, - end_at=0.5, - # start_angle=180, - end_angle=180, - ) + for system, usage in usages.items(): + self.progress_bars[system].value = usage / 100.0 + self.progress_bars[system].set_tooltip_text(f"{system} {usage:.1f}%") - def _create_icon(self, icon_name): - return MaterialIcon(icon_name, 16) - - def _refresh_system_info(self, fab: Fabricator): - while True: - yield { - "CPU": round(psutil.cpu_percent()), - "RAM": round(psutil.virtual_memory().percent), - "Swap": round(psutil.swap_memory().percent), - "Temp": self._get_device_temperature(), - } - sleep(1) + return True @staticmethod def _get_device_temperature(): try: temps = psutil.sensors_temperatures() - if "coretemp" in temps: - return round(temps["coretemp"][0].current, 1) - elif "cpu_thermal" in temps: - return round(temps["cpu_thermal"][0].current, 1) - return None + for key in ("coretemp", "cpu_thermal"): + if key in temps: + return round(temps[key][0].current, 1) except Exception: - return None - - def _update_ui(self, usages): - for system, usage in usages.items(): - self.progress_bars[system].value = usage / 100 - self.progress_bars[system].set_tooltip_text(f"{system} {usage}%") + pass + return None diff --git a/modules/bar/components/taskbar.py b/modules/bar/components/taskbar.py index cc8af45..b54d0bb 100644 --- a/modules/bar/components/taskbar.py +++ b/modules/bar/components/taskbar.py @@ -20,7 +20,7 @@ class PagerClient(TypedDict): class TaskBar(Box): - def __init__(self, icon_size: int = 20, **kwargs): + def __init__(self, icon_size: int = 22, **kwargs): super().__init__( orientation="h", spacing=7, diff --git a/modules/bar/components/updates.py b/modules/bar/components/updates.py new file mode 100644 index 0000000..0674d3e --- /dev/null +++ b/modules/bar/components/updates.py @@ -0,0 +1,103 @@ +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.stack import Stack +import snippets.iconss as icons +from loguru import logger +from gi.repository import Gtk +from fabric.widgets.button import Button + + +class UpdatesWidget(Button): + """A widget to display the number of available updates.""" + + def __init__( + self, + **kwargs, + ): + # Initialize the button with specific name and style + super().__init__(name="button-bar", **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) + + self.update_box = CenterBox( + center_children=[self.update_icon, self.update_level_label], + ) + + self.updated_box = CenterBox( + center_children=[self.updated_icon], + ) + + 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( + children=self.stack, + ) + + # Ensure the stack resizes to the current visible child + self.stack.set_homogeneous(False) + self.stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) + + # Connect the button press event + self.connect("button-press-event", self.on_button_press) + + # Set up a repeater to call the update method at specified intervals + invoke_repeater(60000, self.update, initial_call=True) + + def update_values(self, value: str): + logger.info(f"[Updates] Received update JSON: {value}") + + try: + value = json.loads(value) + except json.JSONDecodeError: + logger.error("[Updates] Failed to parse update JSON") + return False + + update_count = str(value["total"]) + self.update_level_label.set_label(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): + if event.button == 1: + exec_shell_command_async( + f"{self.script_file} -arch -up", + lambda _: None, + ) + self.update() + return True + else: + self.update() + + def update(self): + logger.info("[Updates] Checking for updates...") + + # Execute the update script asynchronously and update values + exec_shell_command_async( + f"{self.script_file} -arch", + lambda output: self.update_values(output), + ) + + return True diff --git a/modules/bar/components/volume.py b/modules/bar/components/volume.py deleted file mode 100644 index 0fed57c..0000000 --- a/modules/bar/components/volume.py +++ /dev/null @@ -1,81 +0,0 @@ -from fabric import Fabricator -from fabric.utils import exec_shell_command -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from snippets import MaterialIcon -from services import audio - -VOLUME_ICONS = { - "muted": "volume_off", - "low": "volume_mute", - "medium-low": "volume_down", - "medium-high": "volume_up", - "high": "volume_up", -} - - -class VolumeIndicator(Box): - def __init__(self, **kwargs): - super().__init__(**kwargs) - - self.audio_service = audio - self.volume_icon_button = self.create_volume_icon() - - self.volume_icon_button.connect("button-press-event", self.on_button_press) - self.audio_service.connect("changed", self.update_volume_status) - - Fabricator(interval=1000, poll_from=self.update_volume_status) - self.children = (self.volume_icon_button,) - - def create_volume_icon(self): - icon = MaterialIcon(VOLUME_ICONS["muted"], size=16) - button = Button(child=icon) - button.icon_widget = icon - return button - - def on_button_press(self, widget, event): - if event.button == 1: - self.toggle_mute() - elif event.button == 3: - self.open_audio_settings() - - def toggle_mute(self): - current_stream = self.audio_service.speaker - if current_stream: - current_stream.muted = not current_stream.muted - # self.update_volume_status() - - def open_audio_settings(self): - exec_shell_command("pavucontrol") - - def update_volume_status(self, *_): - current_stream = self.audio_service.speaker - if current_stream: - self.update_icon(current_stream) - - return True - - def update_icon(self, stream): - if stream.muted: - icon_name = VOLUME_ICONS["muted"] - volume_percentage = 0 - tooltip_text = "Volume Muted" - else: - volume_level = stream.volume - icon_name = self.get_volume_icon(volume_level) - volume_percentage = int(volume_level) - tooltip_text = f"Volume: {volume_percentage}%" - - self.volume_icon_button.icon_widget.set_label(icon_name) - - self.volume_icon_button.set_tooltip_text(tooltip_text) - - def get_volume_icon(self, volume_level): - if volume_level == 0: - return VOLUME_ICONS["low"] - elif volume_level <= 33: - return VOLUME_ICONS["medium-low"] - elif volume_level <= 66: - return VOLUME_ICONS["medium-high"] - else: - return VOLUME_ICONS["high"] diff --git a/modules/launcher/components/applauncher.py b/modules/launcher/components/applauncher.py index 54dd1c1..69669b3 100644 --- a/modules/launcher/components/applauncher.py +++ b/modules/launcher/components/applauncher.py @@ -9,6 +9,12 @@ from fabric.widgets.scrolledwindow import ScrolledWindow from gi.repository import GLib, Gdk from snippets import read_config from fabric.widgets.image import Image +import json +import os +import re +import math +import subprocess +import webbrowser class AppLauncher(Box): @@ -22,11 +28,18 @@ class AppLauncher(Box): self.launcher = kwargs["launcher"] self.selected_index = -1 - self._arranger_handler: int = 0 self._all_apps = get_desktop_applications() self.config = read_config() + # Calculator history initialization + self.calc_history_path = os.path.expanduser("~/.cache/modus/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", @@ -35,18 +48,13 @@ class AppLauncher(Box): 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.scrolled_window = ScrolledWindow( - name="scrolled-window", - spacing=10, - h_scrollbar_policy="never", - v_scrollbar_policy="never", - child=self.viewport, + on_key_press_event=self.on_search_entry_key_press, ) + self.scrolled_window = None + self.header_box = Box( - name="header_box", + name="header-box", orientation="h", children=[self.search_entry], ) @@ -55,41 +63,46 @@ class AppLauncher(Box): name="launcher-box", h_expand=True, orientation="v", - children=[ - self.header_box, - self.scrolled_window, - ], + children=[self.header_box], ) - 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.selected_index = -1 self.launcher.close() def open_launcher(self): self._all_apps = get_desktop_applications() - self.arrange_viewport() - self.show_all() + self.arrange_viewport("") def arrange_viewport(self, query: str = ""): + if query.startswith(":"): # Don't show ScrolledWindow for web search + self.destroy_scrolled_window() + self.launcher.dashboard.hide() + return + if query.startswith("="): + self.show_scrolled_window() + # In calculator mode, update history view once (not per keystroke) + self.update_calculator_viewport() + self.launcher.dashboard.hide() + return + remove_handler(self._arranger_handler) if self._arranger_handler else None self.viewport.children = [] self.selected_index = -1 if query.strip(): self.launcher.dashboard.hide() + self.show_scrolled_window() else: self.launcher.dashboard.show() - - if not query.strip(): + self.destroy_scrolled_window() return False - filtered_apps = iter( + filtered_apps_iter = iter( sorted( [ app @@ -100,19 +113,41 @@ class AppLauncher(Box): + (" " + app.name + " ") + (app.generic_name or "") ).casefold() - ][:6], + ], key=lambda app: (app.display_name or "").casefold(), ) ) - should_resize = operator.length_hint(filtered_apps) == len(self._all_apps) + 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, + filtered_apps_iter, pin=True, ) + def show_scrolled_window(self): + if self.scrolled_window is None: + self.scrolled_window = ScrolledWindow( + name="scrolled-window", + spacing=10, + min_content_size=(-1, -1), + h_scrollbar_policy="never", + child=self.viewport, + v_expand=True, + ) + self.launcher_box.add(self.scrolled_window) + self.scrolled_window.show_all() + self.get_style_context().add_class("applauncher") + + def destroy_scrolled_window(self): + if hasattr(self, "scrolled_window") and self.scrolled_window: + # self.scrolled_window.destroy() + self.launcher_box.remove(self.scrolled_window) + self.scrolled_window = None + + self.get_style_context().remove_class("applauncher") + def handle_arrange_complete(self, should_resize, query): if should_resize: self.resize_viewport() @@ -128,12 +163,6 @@ class AppLauncher(Box): self.viewport.add(self.bake_application_slot(app)) return True - 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="launcher-app", @@ -160,13 +189,12 @@ class AppLauncher(Box): 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") @@ -180,41 +208,49 @@ class AppLauncher(Box): adj = self.scrolled_window.get_vadjustment() alloc = button.get_allocation() if alloc.height == 0: - return False # Retry if allocation isn't ready + return False 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): commands = self.config.get("commands", {}) + if text.startswith(":i "): # Detecting ':i' search + search_query = text[3:].strip() # Extract query after ':i ' + if search_query: + url = f"https://www.google.com/search?q={search_query}" + webbrowser.open(url) + self.close_launcher() + return + + if text.startswith("="): + # If in calculator mode and no history item is selected, evaluate new expression. + if self.selected_index == -1: + self.evaluate_calculator_expression(text) + return if text in commands: command = commands[text] - eval(command, {"launcher": self.launcher}) # Dynamically execute command + exec(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 + return selected_index = self.selected_index if self.selected_index != -1 else 0 if 0 <= selected_index < len(children): children[selected_index].clicked() @@ -236,10 +272,81 @@ class AppLauncher(Box): 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): + # Remove the '=' prefix and extra spaces + expr = text.lstrip("=").strip() + if not expr: + return + # Replace operators: '^' -> '**', and '×' -> '*' + expr = expr.replace("^", "**").replace("×", "*") + # Replace factorial: e.g. 5! -> math.factorial(5) + expr = re.sub(r"(\d+)!", r"math.factorial(\1)", expr) + # Replace brackets: allow [] and {} as () + for old, new in [("[", "("), ("]", ")"), ("{", "("), ("}", ")")]: + expr = expr.replace(old, new) + try: + result = eval(expr, {"__builtins__": None, "math": math}) + except Exception as e: + result = f"Error: {e}" + # Prepend to history (newest first) + self.calc_history.insert(0, f"{text} => {result}") + 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) + # Remove resetting selected_index unconditionally so that a highlighted result isn't lost. + # Optionally, only reset if the input is not more than "=". + # if self.search_entry.get_text().strip() != "=": + # self.selected_index = -1 + + def create_calc_history_button(self, text: str) -> Button: + btn = Button( + name="app-slot-button", # reuse existing CSS styling + 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): + del self.calc_history[self.selected_index] + self.save_calc_history() + self.update_calculator_viewport() diff --git a/modules/launcher/components/bluetooth.py b/modules/launcher/components/bluetooth.py index 0cf1704..a61849a 100644 --- a/modules/launcher/components/bluetooth.py +++ b/modules/launcher/components/bluetooth.py @@ -5,8 +5,7 @@ from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox from fabric.widgets.scrolledwindow import ScrolledWindow from fabric.bluetooth import BluetoothClient, BluetoothDevice -from snippets import MaterialIcon -# from .buttons import +import snippets.iconss as icons class BluetoothDeviceSlot(CenterBox): @@ -18,7 +17,9 @@ class BluetoothDeviceSlot(CenterBox): "notify::closed", lambda *_: self.device.closed and self.destroy() ) - self.connection_icon = MaterialIcon("bluetooth_disabled", 12) + self.connection_label = Label( + name="bluetooth-connection", markup=icons.bluetooth_disconnected + ) self.connect_button = Button( name="bluetooth-connect", label="Connect", @@ -29,9 +30,9 @@ class BluetoothDeviceSlot(CenterBox): Box( spacing=8, children=[ - Image(icon_name=device.icon_name + "-symbolic", size=32), + Image(icon_name=f"{device.icon_name}-symbolic", size=32), Label(label=device.name), - self.connection_icon, + self.connection_label, ], ) ] @@ -40,10 +41,11 @@ class BluetoothDeviceSlot(CenterBox): self.device.emit("changed") # to update display status def on_changed(self, *_): - self.connection_icon.set_label( - "bluetooth" if self.device.connected else "bluetooth_disabled" + 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..." @@ -63,38 +65,37 @@ class BluetoothConnections(Box): orientation="vertical", **kwargs, ) + self.launcher = kwargs["launcher"] - self.bluetooth_button = self.launcher.dashboard.bluetooth_button - self.bt_status_text = self.bluetooth_button.bluetooth_status_text - self.bt_status_button = self.bluetooth_button.bluetooth_status_button - self.bt_icon = self.bluetooth_button.bluetooth_icon - self.bt_label = self.bluetooth_button.bluetooth_label - self.bt_menu_button = self.bluetooth_button.bluetooth_menu_button - self.bt_menu_label = self.bluetooth_button.bluetooth_menu_label + 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.toggle_icon = MaterialIcon("bluetooth_disabled") - self.scan_icon = MaterialIcon("stop") - self.scan_button = Button( name="bluetooth-scan", - child=self.scan_icon, - tooltip_text="scan", + child=Label(name="bluetooth-scan-icon", markup=icons.scan_stop), on_clicked=lambda *_: self.client.toggle_scan(), ) self.toggle_button = Button( name="bluetooth-toggle", - tooltip_text="bluetooth-toggle", - child=self.toggle_icon, + child=Label(name="bluetooth-toggle-icon", markup=icons.bluetooth_off), on_clicked=lambda *_: self.client.toggle_power(), ) - self.client.connect("notify::enabled", lambda *_: self.status_label()) + self.client.connect( + "notify::enabled", + lambda *_: self.status_label(), + ) self.client.connect( "notify::scanning", - lambda *_: self.scan_icon.set_label( - "stop" if self.client.scanning else "bluetooth_searching" + lambda *_: self.scan_button.get_child().set_markup( + icons.scan_stop if self.client.scanning else icons.scan ), ) @@ -111,14 +112,16 @@ class BluetoothConnections(Box): Label(name="bluetooth-section", label="Paired"), ScrolledWindow( name="bluetooth-paired", - v_expand=True, + min_content_size=(-1, -1), child=self.paired_box, + v_expand=True, ), Label(name="bluetooth-section", label="Available"), ScrolledWindow( - name="bluetooth-paired", - v_expand=True, + name="bluetooth-available", + min_content_size=(-1, -1), child=self.available_box, + v_expand=True, ), ] @@ -130,7 +133,7 @@ class BluetoothConnections(Box): def status_label(self): print(self.client.enabled) if self.client.enabled: - self.toggle_icon.set_label("bluetooth") + self.toggle_button.get_child().set_markup(icons.bluetooth) self.bt_status_text.set_label("Enabled") for i in [ self.bt_status_button, @@ -141,9 +144,9 @@ class BluetoothConnections(Box): self.bt_menu_label, ]: i.remove_style_class("disabled") - self.bt_icon.set_label("bluetooth") + self.bt_icon.set_markup(icons.bluetooth) else: - self.toggle_icon.set_label("bluetooth_disabled") + self.toggle_button.get_child().set_markup(icons.bluetooth_off) self.bt_status_text.set_label("Disabled") for i in [ self.bt_status_button, @@ -154,7 +157,7 @@ class BluetoothConnections(Box): self.bt_menu_label, ]: i.add_style_class("disabled") - self.bt_icon.set_label("bluetooth_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)): @@ -162,5 +165,6 @@ class BluetoothConnections(Box): slot = BluetoothDeviceSlot(device) if device.paired: - return self.paired_box.add(slot) - return self.available_box.add(slot) + self.paired_box.add(slot) + else: + self.available_box.add(slot) diff --git a/modules/launcher/components/buttons.py b/modules/launcher/components/buttons.py deleted file mode 100644 index d19fb89..0000000 --- a/modules/launcher/components/buttons.py +++ /dev/null @@ -1,396 +0,0 @@ -import subprocess -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.utils.helpers import exec_shell_command_async, exec_shell_command -import gi -from gi.repository import Gtk, Gdk, GLib # Added Gdk import -from fabric.utils import get_relative_path - -gi.require_version("Gtk", "3.0") -from snippets import MaterialIcon - - -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_icon = MaterialIcon( - "wifi", - ) - self.network_icon.add_style_class("network-icon") - 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", - label="Hello_World!!", - 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, - ) - add_hover_cursor(self.network_status_button) # <-- Added hover - - self.network_menu_label = MaterialIcon("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], - ) - - -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 = MaterialIcon("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 = MaterialIcon("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 = MaterialIcon("mode_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() - - 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 check_hyprsunset(self, *args): - """ - Update the button state based on whether hyprsunset is running. - """ - try: - subprocess.check_output(["pgrep", "hyprsunset"]) - self.night_mode_status.set_label("Enabled") - for widget in self.widgets: - widget.remove_style_class("disabled") - except subprocess.CalledProcessError: - 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 = MaterialIcon("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, - ] - self.check_wlinhibit() - - def toggle_wlinhibit(self, *args): - """ - Toggle the 'wlinhibit' process: - - If running, kill it and mark as 'Disabled' (add 'disabled' class). - - If not running, start it and mark as 'Enabled' (remove 'disabled' class). - """ - try: - subprocess.check_output(["pgrep", "wlinhibit"]) - exec_shell_command_async("pkill wlinhibit") - self.caffeine_status.set_label("Disabled") - for i in self.widgets: - i.add_style_class("disabled") - except subprocess.CalledProcessError: - exec_shell_command_async("wlinhibit") - self.caffeine_status.set_label("Enabled") - for i in self.widgets: - i.remove_style_class("disabled") - - def check_wlinhibit(self, *args): - try: - subprocess.check_output(["pgrep", "wlinhibit"]) - self.caffeine_status.set_label("Enabled") - for i in self.widgets: - i.remove_style_class("disabled") - except subprocess.CalledProcessError: - self.caffeine_status.set_label("Disabled") - for i in self.widgets: - i.add_style_class("disabled") - - -class DarkModeButton(Button): - def __init__(self): - self.dark_mode_icon = MaterialIcon("contrast") - self.dark_mode_icon.add_style_class("iconss") - self.dark_mode_label = Label( - name="dark-mode-label", label="Dark Mode", justification="left" - ) - - self.dark_mode_label_box = Box( - children=[self.dark_mode_label, Box(h_expand=True)] - ) - self.dark_mode_status = Label( - name="dark-mode-status", label="Enabled", justification="left" - ) - self.dark_mode_status_box = Box( - children=[self.dark_mode_status, Box(h_expand=True)] - ) - self.dark_mode_text = Box( - name="dark-mode-text", - orientation="v", - h_align="start", - v_align="center", - children=[self.dark_mode_label_box, self.dark_mode_status_box], - ) - self.dark_mode_box = Box( - h_align="start", - v_align="center", - spacing=10, - children=[self.dark_mode_icon, self.dark_mode_text], - ) - - super().__init__( - name="dark-mode-button", - h_expand=True, - child=self.dark_mode_box, - on_clicked=self.toggle_darkmode, - ) - add_hover_cursor(self) - - self.widgets = [ - self, - self.dark_mode_label, - self.dark_mode_status, - self.dark_mode_icon, - ] - self.check_darkmode() - - def toggle_darkmode(self, *_): - current_state = self.check_darkmode() - new_mode = "dark" if not current_state else "light" - command = f"bash {get_relative_path('../../../config/scripts/dark-theme.sh')} --set {new_mode}" - GLib.spawn_command_line_async(command) - self.dark_mode_status.set_label("Enabled" if new_mode == "dark" else "Disabled") - - def check_darkmode(self): - result = exec_shell_command( - "gsettings get org.gnome.desktop.interface color-scheme" - ) - return result.strip().replace("'", "") == "prefer-dark" - - -class Buttons(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.dark_mode_button = DarkModeButton() - - # Attach buttons into the grid (one row, four columns) - self.attach(self.network_button, 0, 0, 1, 1) - self.attach(self.bluetooth_button, 1, 0, 1, 1) - self.attach(self.dark_mode_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/calendar.py b/modules/launcher/components/calendar.py index fdf712e..cdb2562 100644 --- a/modules/launcher/components/calendar.py +++ b/modules/launcher/components/calendar.py @@ -1,11 +1,11 @@ import calendar from datetime import datetime import gi -from snippets import MaterialIcon from fabric.widgets.label import Label gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib +import snippets.iconss as icons class Calendar(Gtk.Box): @@ -27,7 +27,7 @@ class Calendar(Gtk.Box): self.prev_month_button = Gtk.Button( name="prev-month-button", - child=MaterialIcon("chevron_left"), + child=Label(name="month-button-label", markup=icons.chevron_left) ) self.prev_month_button.connect("clicked", self.on_prev_month_clicked) self.header.pack_start(self.prev_month_button, False, False, 0) @@ -37,7 +37,7 @@ class Calendar(Gtk.Box): self.next_month_button = Gtk.Button( name="next-month-button", - child=MaterialIcon("chevron_right"), + child=Label(name="month-button-label", markup=icons.chevron_right) ) self.next_month_button.connect("clicked", self.on_next_month_clicked) self.header.pack_start(self.next_month_button, False, False, 0) diff --git a/modules/launcher/components/cliphist.py b/modules/launcher/components/cliphist.py index 6153fb1..86fe079 100644 --- a/modules/launcher/components/cliphist.py +++ b/modules/launcher/components/cliphist.py @@ -1,16 +1,14 @@ import os import subprocess from typing import List - from fabric import Fabricator -from fabric.utils import remove_handler 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 from gi.repository import GLib -from snippets import MaterialIcon +import snippets.iconss as icons class Cliphist(Box): @@ -25,30 +23,30 @@ class Cliphist(Box): self._arranger_handler = 0 self.cliphist_manager = CliphistManager(self) self.viewport = None - self.launcher = kwargs["launcher"] self.search_entry = Entry( name="search-entry", 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.toggle_button = Button( - child=MaterialIcon("delete_forever"), - name="clear-launcher-button", - on_clicked=self.on_button_clicked, + on_activate=lambda entry, *_: self.handle_search(entry.get_text()), ) self.header_box = Box( name="header-box", spacing=10, orientation="h", - children=[self.search_entry, self.toggle_button], + children=[ + self.search_entry, + Button( + name="clear-button", + child=Label(name="cliphist-clear-button", markup=icons.clear), + tooltip_text="Clear Clipboard History", + on_clicked=self.handle_clear_history, + ), + ], ) self.launcher_box = Box( - name="cliphist-history", + name="cliphist-launcher-box", spacing=10, orientation="v", h_expand=True, @@ -57,32 +55,31 @@ class Cliphist(Box): self.add(self.launcher_box) - def on_button_clicked(self, *_): - GLib.spawn_command_line_async("cliphist wipe") - self.cliphist_manager.update_cliphist_history() - def open_launcher(self): if not self.viewport: self.viewport = Box(name="viewport", spacing=4, orientation="v") self.scrolled_window = ScrolledWindow( name="scrolled-window", spacing=10, + min_content_size=(-1, -1), h_scrollbar_policy="never", + # v_scrollbar_policy="never", child=self.viewport, ) self.launcher_box.add(self.scrolled_window) self.viewport.children = [] - self.cliphist_manager.arrange_viewport() + self.cliphist_manager.update_cliphist_history() self.viewport.show() self.search_entry.grab_focus() - def close_launcher(self): - self.launcher.close() + def handle_search(self, text: str): + self.cliphist_manager.arrange_viewport(query=text) - def handle_search_input(self, text: str): - self.cliphist_manager.arrange_viewport(text) + def handle_clear_history(self, *_): + GLib.spawn_command_line_async("cliphist wipe") + self.cliphist_manager.update_cliphist_history() class CliphistManager: @@ -121,7 +118,7 @@ class CliphistManager: ["cliphist", "decode", clip_id], capture_output=True, check=True ) subprocess.run(["wl-copy"], input=decode_result.stdout, check=True) - self.launcher.close_launcher() + self.launcher.open_launcher() except subprocess.CalledProcessError as e: print(f"Error copying clip {clip_id}: {e}") @@ -133,22 +130,25 @@ class CliphistManager: decode_result = subprocess.run( ["cliphist", "decode", clip_id], capture_output=True, check=True ) - with open(output_file, "wb") as f: f.write(decode_result.stdout) - return output_file except (subprocess.CalledProcessError, IOError) as e: print(f"Error saving image file for clip {clip_id}: {e}") return "" + def update_cliphist_history(self): + self.cliphist_history = self.get_clip_history() + self.arrange_viewport() + def query_clips(self, query: str = "") -> List[dict]: - history = self.get_clip_history() - if not query.strip(): - return history - - return [item for item in history if query.lower() in item["content"].lower()] + return self.cliphist_history + return [ + clip + for clip in self.cliphist_history + if query.lower() in clip["content"].lower() + ] def bake_clip_slot(self, item: dict) -> Button: if "[[ binary data" in item["content"].lower(): @@ -163,19 +163,17 @@ class CliphistManager: name="clip-img-item", on_clicked=lambda _, clip_id=item["id"]: self.copy_clip(clip_id), ) - button.set_style( f"background-image: url('{output_file}'); background-size: cover; background-position: center;" ) return button - return None def _create_text_button(self, item: dict) -> Button: return Button( child=Label( - label=item["content"][:55] - + ("..." if len(item["content"]) > 55 else ""), + label=item["content"][:35] + + ("..." if len(item["content"]) > 35 else ""), h_expand=True, v_align="center", h_align="start", @@ -184,18 +182,10 @@ class CliphistManager: name="clip-item", ) - def update_cliphist_history(self): - self.cliphist_history = self.get_clip_history() - if self.launcher.viewport: - GLib.idle_add(self.arrange_viewport) - def arrange_viewport(self, query: str = ""): if not self.launcher.viewport: return - if self.launcher._arranger_handler: - remove_handler(self.launcher._arranger_handler) - self.launcher.viewport.children = [] filtered_clips = self.query_clips(query) diff --git a/modules/launcher/components/dashboard.py b/modules/launcher/components/dashboard.py index 246bfde..da835aa 100644 --- a/modules/launcher/components/dashboard.py +++ b/modules/launcher/components/dashboard.py @@ -9,7 +9,7 @@ from gi.repository import Gtk, Gdk, GLib # Added Gdk import from fabric.utils import get_relative_path gi.require_version("Gtk", "3.0") -from snippets import MaterialIcon +import snippets.iconss as icons def add_hover_cursor(widget): @@ -31,8 +31,9 @@ def add_hover_cursor(widget): class NetworkButton(Box): def __init__(self): - self.network_icon = MaterialIcon( - "wifi", + self.network_icon = Label( + name="network-icon", + markup=icons.wifi, ) self.network_label = Label( name="network-label", @@ -66,7 +67,10 @@ class NetworkButton(Box): ) add_hover_cursor(self.network_status_button) # <-- Added hover - self.network_menu_label = MaterialIcon("chevron_right") + 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, @@ -97,7 +101,10 @@ class BluetoothButton(Box): ) self.launcher = launcher - self.bluetooth_icon = MaterialIcon("bluetooth") + self.bluetooth_icon = Label( + name="bluetooth-icon", + markup=icons.bluetooth, + ) self.bluetooth_label = Label( name="bluetooth-label", label="Bluetooth", @@ -133,8 +140,10 @@ class BluetoothButton(Box): on_clicked=lambda *_: self.launcher.bluetooth.client.toggle_power(), ) add_hover_cursor(self.bluetooth_status_button) # <-- Added hover - - self.bluetooth_menu_label = MaterialIcon("chevron_right") + 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"), @@ -148,7 +157,10 @@ class BluetoothButton(Box): class NightModeButton(Button): def __init__(self): - self.night_mode_icon = MaterialIcon("mode_night") + self.night_mode_icon = Label( + name="night-mode-icon", + markup=icons.night, + ) self.night_mode_label = Label( name="night-mode-label", label="Night Mode", @@ -230,7 +242,10 @@ class NightModeButton(Button): class CaffeineButton(Button): def __init__(self): - self.caffeine_icon = MaterialIcon("coffee") + self.caffeine_icon = Label( + name="caffeine-icon", + markup=icons.coffee, + ) self.caffeine_label = Label( name="caffeine-label", label="Caffeine", @@ -308,7 +323,10 @@ class CaffeineButton(Button): class DarkModeButton(Button): def __init__(self): - self.dark_mode_icon = MaterialIcon("contrast") + self.dark_mode_icon = Label( + name="dark-mode-icon", + markup=icons.darkmode, + ) self.dark_mode_label = Label( name="dark-mode-label", label="Dark Mode", justification="left" ) @@ -384,96 +402,6 @@ class DarkModeButton(Button): return is_dark_mode -# -# class PowerProfileButton(Button): -# def __init__(self): -# self.power_profile_icon = MaterialIcon("local_fire_department") -# 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="Loading...", 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, -# ] -# -# # **Delay the profile check to allow system services to start** -# GLib.timeout_add(1000, self.check_power_profile) -# -# def toggle_power_profile(self, *_): -# current_profile = self.check_power_profile() -# if current_profile == "balanced": -# self.set_power_profile("performance") -# elif current_profile == "performance": -# self.set_power_profile("power-saver") -# else: -# self.set_power_profile("balanced") -# -# def check_power_profile(self): -# try: -# result = exec_shell_command("powerprofilesctl get") -# if isinstance(result, str): -# current_profile = result.strip().lower() -# self.power_profile_status.set_label(current_profile.capitalize()) -# self.update_button_style(current_profile) -# return current_profile -# except Exception as e: -# print(f"Error checking power profile: {e}") -# self.power_profile_status.set_label("Balanced") # Fallback -# return "balanced" -# -# def set_power_profile(self, profile): -# exec_shell_command_async(f"powerprofilesctl set {profile}") -# self.power_profile_status.set_label(profile.capitalize()) -# self.update_button_style(profile) -# -# def update_button_style(self, profile): -# if profile == "balanced": -# self.power_profile_icon.set_label("balance") -# self.add_style_class("balanced-profile") -# self.power_profile_label.add_style_class("disabled") -# else: -# self.remove_style_class("balanced-profile") -# -# if profile == "performance": -# self.power_profile_icon.set_label("local_fire_department") -# elif profile == "power-saver": -# self.power_profile_icon.set_label("data_saver_on") -# - - class Dashboard(Gtk.Grid): def __init__(self, **kwargs): super().__init__(name="buttons-grid") @@ -491,7 +419,6 @@ class Dashboard(Gtk.Grid): self.night_mode_button = NightModeButton() self.caffeine_button = CaffeineButton() self.dark_mode_button = DarkModeButton() - # self.power_profile_button = PowerProfileButton() # Attach buttons into the grid (one row, four columns) self.attach(self.network_button, 0, 0, 1, 1) @@ -499,6 +426,5 @@ class Dashboard(Gtk.Grid): self.attach(self.dark_mode_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.attach(self.power_profile_button, 2, 1, 1, 1) self.show_all() diff --git a/modules/launcher/components/power.py b/modules/launcher/components/power.py index e00efa1..6964a12 100644 --- a/modules/launcher/components/power.py +++ b/modules/launcher/components/power.py @@ -1,14 +1,14 @@ from fabric.widgets.box import Box +from fabric.widgets.label import Label from fabric.widgets.button import Button -from gi.repository import GLib - -from snippets import MaterialIcon +from fabric.utils.helpers import exec_shell_command_async +import snippets.iconss as icons class PowerMenu(Box): def __init__(self, **kwargs): super().__init__( - name="powermenu", + name="power-menu", orientation="h", spacing=10, v_align="center", @@ -22,32 +22,32 @@ class PowerMenu(Box): self.launcher = kwargs["launcher"] self.btn_lock = Button( - name="powermenu-button", - child=MaterialIcon("lock", 45), + name="power-menu-button", + child=Label(name="button-label", markup=icons.lock), on_clicked=self.lock, ) self.btn_suspend = Button( - name="powermenu-button", - child=MaterialIcon("sleep", 45), + name="power-menu-button", + child=Label(name="button-label", markup=icons.suspend), on_clicked=self.suspend, ) self.btn_logout = Button( - name="powermenu-button", - child=MaterialIcon("logout", 45), + name="power-menu-button", + child=Label(name="button-label", markup=icons.logout), on_clicked=self.logout, ) self.btn_reboot = Button( - name="powermenu-button", - child=MaterialIcon("restart_alt", 45), + name="power-menu-button", + child=Label(name="button-label", markup=icons.reboot), on_clicked=self.reboot, ) self.btn_shutdown = Button( - name="powermenu-button", - child=MaterialIcon("power_settings_new", 45), + name="power-menu-button", + child=Label(name="button-label", markup=icons.shutdown), on_clicked=self.poweroff, ) @@ -67,22 +67,23 @@ class PowerMenu(Box): def close_menu(self): self.launcher.close() - def lock(self, *_): - GLib.spawn_command_line_async("hyprlock --immediate") + def lock(self, *args): + exec_shell_command_async("hyprlock --immediate") self.close_menu() - def suspend(self, *_): - GLib.spawn_command_line_async("systemctl suspend") + def suspend(self, *args): + exec_shell_command_async("systemctl suspend") self.close_menu() - def logout(self, *_): - GLib.spawn_command_line_async("hyprctl dispatch exit") + def logout(self, *args): + exec_shell_command_async("hyprctl dispatch exit") self.close_menu() - def reboot(self, *_): - GLib.spawn_command_line_async("systemctl reboot") + def reboot(self, *args): + exec_shell_command_async("systemctl reboot") self.close_menu() - def poweroff(self, *_): - GLib.spawn_command_line_async("systemctl poweroff") + 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 index 5992ec8..e9a92de 100644 --- a/modules/launcher/components/sh.py +++ b/modules/launcher/components/sh.py @@ -6,7 +6,7 @@ from fabric.widgets.button import Button from fabric.widgets.entry import Entry from fabric.widgets.label import Label from fabric.widgets.scrolledwindow import ScrolledWindow -from snippets import MaterialIcon + CACHE_DIR = os.getenv( "XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache", "fabric") @@ -37,7 +37,7 @@ class Sh(Box): self.header_box = Box( spacing=10, orientation="h", - children=[self.search_entry, MaterialIcon("terminal")], + children=[self.search_entry], ) self.launcher_box = Box( @@ -53,9 +53,11 @@ class Sh(Box): 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) diff --git a/modules/launcher/components/todo.py b/modules/launcher/components/todo.py index 7cc569b..ef81852 100644 --- a/modules/launcher/components/todo.py +++ b/modules/launcher/components/todo.py @@ -6,7 +6,7 @@ from fabric.widgets.button import Button from fabric.widgets.entry import Entry from fabric.widgets.label import Label from fabric.widgets.scrolledwindow import ScrolledWindow -from snippets import MaterialIcon +import snippets.iconss as icons class TodoManager(Box): @@ -36,7 +36,7 @@ class TodoManager(Box): self.todo_entry, Button( name="add-button", - child=MaterialIcon("add"), + child=Label(name="todo-add-button", markup=icons.add), tooltip_text="Add Todo", on_clicked=lambda *_: self.handle_add_todo( self.todo_entry.get_text() @@ -59,8 +59,9 @@ class TodoManager(Box): if not self.viewport: self.viewport = Box(name="viewport", spacing=4, orientation="v") self.scrolled_window = ScrolledWindow( - name="todo-content", + name="scrolled-window", spacing=10, + min_content_size=(-1, -1), h_scrollbar_policy="never", child=self.viewport, ) @@ -82,7 +83,7 @@ class TodoListManager: def __init__(self, launcher): self.launcher = launcher self.todos = [] - self.todo_file = os.path.expanduser("~/.local/share/fabric/todos.json") + self.todo_file = os.path.expanduser("~/.local/share/modus/todos.json") self.load_todos() def load_todos(self): @@ -130,9 +131,10 @@ class TodoListManager: ) checkbox = Button( - name="todo-checkbox", - child=MaterialIcon( - "check_box" if todo["completed"] else "check_box_outline_blank" + name="checkbox", + child=Label( + name="todo-checkbox", + markup=icons.check_box if todo["completed"] else icons.box, ), on_clicked=lambda _, todo_id=todo["id"]: self.toggle_todo(todo_id), ) @@ -147,7 +149,7 @@ class TodoListManager: delete_button = Button( name="delete-button", - child=MaterialIcon("delete"), + child=Label(name="todo-delete-button", markup=icons.trash), on_clicked=lambda _, todo_id=todo["id"]: self.delete_todo(todo_id), ) diff --git a/modules/launcher/components/wifi.py b/modules/launcher/components/wifi.py index 675704f..b2f656e 100644 --- a/modules/launcher/components/wifi.py +++ b/modules/launcher/components/wifi.py @@ -4,8 +4,8 @@ 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 snippets import MaterialIcon from services import NetworkClient +import snippets.iconss as icons class WifiNetworkSlot(CenterBox): @@ -15,7 +15,7 @@ class WifiNetworkSlot(CenterBox): self.client = client self.connecting = False # Tracks if we are connecting/disconnecting - self.connection_icon = MaterialIcon("signal_wifi_off", 12) + self.connection_icon = Label(name="wifi-connection", markup=icons.wifi_off) self.connect_button = Button( name="wifi-connect", label="Connect", @@ -68,9 +68,7 @@ class WifiNetworkSlot(CenterBox): self.connect_button.sensitive = True if not self.connecting: - self.connection_icon.set_label( - "signal_wifi_4_bar" if connected else "signal_wifi_off" - ) + self.connection_icon.set_markup(icons.wifi if connected else icons.wifi_off) self.connect_button.set_label("Disconnect" if connected else "Connect") @@ -84,8 +82,8 @@ class WifiManager(Box): ) self.client = NetworkClient() - self.scan_icon = MaterialIcon("wifi_find") - self.toggle_icon = MaterialIcon("signal_wifi_off") + 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", @@ -113,8 +111,10 @@ class WifiManager(Box): end_children=self.toggle_button, ), ScrolledWindow( - name="scrolled-window", + name="wifi-available", v_expand=True, + h_scrollbar_policy="never", + min_content_size=(-1, -1), child=self.available_box, ), ] @@ -135,10 +135,8 @@ class WifiManager(Box): self.build_wifi_options() def update_toggle_icon(self): - self.toggle_icon.set_label( - "signal_wifi_4_bar" - if self.client.wifi_device.enabled - else "signal_wifi_off" + self.toggle_icon.set_markup( + icons.wifi if self.client.wifi_device.enabled else icons.wifi_off ) def build_wifi_options(self): diff --git a/modules/launcher/launcher.py b/modules/launcher/launcher.py index 6e484f6..97a950e 100644 --- a/modules/launcher/launcher.py +++ b/modules/launcher/launcher.py @@ -2,7 +2,6 @@ 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 fabric.widgets.label import Label from modules.launcher.components import ( AppLauncher, BluetoothConnections, diff --git a/modules/osd.py b/modules/osd.py index 8092959..37534b7 100644 --- a/modules/osd.py +++ b/modules/osd.py @@ -1,11 +1,9 @@ -import time from fabric.widgets.wayland import WaylandWindow as Window from fabric.widgets.label import Label from fabric.widgets.box import Box -from gi.repository import GLib, GObject +from gi.repository import GLib from fabric.widgets.centerbox import CenterBox from services import brightness, audio -from snippets import Animator, MaterialIcon def create_progress_bar(percentage, width=150): diff --git a/snippets/iconss.py b/snippets/iconss.py new file mode 100644 index 0000000..78eb34d --- /dev/null +++ b/snippets/iconss.py @@ -0,0 +1,142 @@ +# Parameters +font_family: str = "tabler-icons" +font_weight: str = "normal" + +span: str = f"" + +# Panels +apps: str = "" +dashboard: str = "" +chat: str = "" +wallpapers: str = "" +windows: str = "" + +# Bar +colorpicker: str = "" +media: str = "" + +# Circles +temp: str = "" +disk: str = "" +battery: str = "" +memory: str = "流" +cpu: str = "" +swap: str = "" + + +# Wallpapers +add: str = "" +sort: str = "" +circle: str = "" +check_box: str = "" +box: str = "" +scan: str = "" +scan_stop: str = "" +record: str = "" +update: str = "見" +updated: str = "" + +# Chevrons +chevron_up: str = "" +chevron_down: str = "" +chevron_left: str = "" +chevron_right: str = "" + +# Power +lock: str = "" +suspend: str = "" +logout: str = "" +reboot: str = "" +shutdown: str = "" + +# Power Manager +power_saving: str = "" +power_balanced: str = "勺" +power_performance: str = "" +charging: str = "𐁣" +discharging: str = "𐁪" +alert: str = "" + +# Applets +wifi: str = "" +bluetooth: str = "" +night: str = "" +coffee: str = "" +dnd: str = "" +darkmode: str = "﹖" + +wifi_off: str = "" +bluetooth_off: str = "" +night_off: str = "" +dnd_off: str = "" +mic_off: str = "" + +# Bluetooth +bluetooth_connected: str = "" +bluetooth_disconnected: str = "" + + +# Player +pause: str = "" +play: str = "" +stop: str = "" +skip_back: str = "" +skip_forward: str = "" +prev: str = "" +next: str = "" +shuffle: str = "" +repeat: str = "" +music: str = "" +rewind_backward_5: str = "謹" +rewind_forward_5: str = "難" + +# Volume +vol_off: str = "" +vol_mute: str = "" +vol_medium: str = "" +vol_high: str = "" + +# Network +lan: str = "" +moderate_signal: str = "" +good_signal: str = "" +no_signal: str = "" +weak_signal: str = "" + +# Overview +circle_plus: str = "" + +# Pins +copy_plus: str = "ﶮ" +paperclip: str = "" + +# Confirm +accept: str = "" +cancel: str = "" +trash: str = "" +clear: str = "" + +# Config +config: str = "" + +# Icons +firefox: str = "" +chromium: str = "" +spotify: str = "ﺆ" +disc: str = "𐀾" +disc_off: str = "" + +# Misc +dot: str = "" + +exceptions: list[str] = ["font_family", "font_weight", "span"] + + +def apply_span() -> None: + global_dict = globals() + for key in global_dict: + if key not in exceptions and not key.startswith("__"): + global_dict[key] = f"{span}{global_dict[key]}" + + +apply_span() diff --git a/styles/bar.css b/styles/bar.css index f4d1ca6..fe0d1e2 100644 --- a/styles/bar.css +++ b/styles/bar.css @@ -14,6 +14,41 @@ transition: all 0.5s cubic-bezier(0.5, 0.25, 0, 1); } +#bar.visible { + opacity: 1; + padding: 3px; + margin-bottom: 0px; + transition: all 0.5s cubic-bezier(0.5, 0.25, 0, 1); +} + +/* + * ── button bar ──────────────────────────────────────────────────────── +*/ + +#button-bar { + background-color: @surfaceContainer; + min-width: 38px; + border-radius: 100px; + transition: all 0.1s ease; +} + +#button-bar-label { + color: @primary; + font-size: 24px; + border-radius: 100px; + transition: all 0.1s ease; +} + +#button-bar:hover #button-bar-label { + background-color: @surfaceBright; + box-shadow: 0px 3px 3px -2px rgba(0, 0, 0, 0.2); +} + +#button-bar:active #button-bar-label { + color: @shadow; + background-color: @primary; +} + /* * ── bar modules ─────────────────────────────────────────────────────── */ @@ -38,6 +73,36 @@ padding: 0px 10px 0px 10px; } +/* + * ── update ──────────────────────────────────────────────────────────── +*/ + +#update-icon { + color: @primary; + font-size: 22px; + margin-right: 1px; + padding: 2px; + border-radius: 11px; + transition: all 0.1s ease; +} + +#updated-icon { + color: @primary; + font-size: 20px; + padding: 8px; + border-radius: 99px; + transition: all 0.1s ease; +} + +#update-label { + color: @primary; + font-size: 10px; + font-weight: bold; + padding: 0px; + border-radius: 11px; + transition: all 0.1s ease; +} + /* * ── Clock ───────────────────────────────────────────────────────────── */ @@ -51,25 +116,11 @@ /* * ── battery ─────────────────────────────────────────────────────────── */ -/*#battery {*/ -/* background-color: @surfaceContainer;*/ -/* border-radius: 100px;*/ -/* padding: 2px 10px 2px 10px;*/ -/*}*/ -/**/ -/*#battery label {*/ -/* font-weight: 600;*/ -/*}*/ -/**/ -/*#battery-label {*/ -/* margin-right: 5px;*/ -/*}*/ #battery { background-color: @surfaceContainer; - padding: 4px; - border-radius: 16px; - margin-top: 4px; + padding: 2px 10px 2px 10px; + border-radius: 100px; } #battery-circle { @@ -78,7 +129,7 @@ } #battery-circle.alert { - border: 2px solid @error-dim; + border: 2px solid @error; } #battery-icon { @@ -86,22 +137,23 @@ } #battery-icon.alert { - color: @error-dim; + color: @onError; } #battery-save, #battery-balanced, #battery-performance { - border-radius: 12px; - min-width: 28px; + min-width: 30px; min-height: 28px; + margin: 2px 0px; + border-radius: 100px; } #battery-save-label, #battery-balanced-label, #battery-performance-label { color: @outline; - font-size: 20px; + font-size: 24px; } #battery-save:hover, @@ -132,6 +184,17 @@ color: @primary; font-weight: bold; margin: 0 4px; + font-size: 11px; +} + +/* + * ── systeminfo ──────────────────────────────────────────────────────── +*/ + +#system-info { + background-color: @surfaceContainer; + border-radius: 100px; + padding: 2px 10px 0px 10px; } /* @@ -198,15 +261,21 @@ menu > menuitem:hover > label { margin: 0px; } -#applets .icon { - color: @onSurface; - font-weight: 900; +#indicator-icon { + font-size: 24px; } -#applets > button { - color: @onSurface; - margin: 0px; - padding: 0px; +/* + * ── stats i.e cpu,ram,swap,temp ─────────────────────────────────────── +*/ + +#stats-icons { + font-size: 20px; +} + +#stats-progress { + color: @surfaceBright; + border: 3px solid @primary; } /* @@ -241,18 +310,20 @@ menu > menuitem:hover > label { margin-right: 0px; } -#progress { - color: lighter(@outlineVariant); - border: 1px solid @primary; -} +/* + * ── recording indicator ─────────────────────────────────────────────── +*/ #recording-indicator { - padding: 6px 12px 6px 12px; - border-radius: 99px; + min-width: 38px; + border-radius: 100px; } -#recording-indicator { - color: @primary; +#recorder { + color: @error; + border-radius: 100px; + min-width: 25px; + font-size: 20px; animation: error 2s infinite; } diff --git a/styles/dashboard.css b/styles/dashboard.css index 7857729..8792c72 100644 --- a/styles/dashboard.css +++ b/styles/dashboard.css @@ -5,9 +5,8 @@ #network-button, #bluetooth-button, #night-mode-button, -#caffeine-button, #dark-mode-button, -#power-profile-button { +#caffeine-button { border-radius: 20px; } @@ -18,17 +17,15 @@ } #night-mode-button, -#caffeine-button, #dark-mode-button, -#power-profile-button { +#caffeine-button { background-color: @primary; padding: 0 10px; } #night-mode-button.disabled, -#caffeine-button.disabled, #dark-mode-button.disabled, -#power-profile.disabled { +#caffeine-button.disabled { background-color: @surface; } @@ -40,42 +37,48 @@ padding: 0 8px; } +#network-menu-button, +#bluetooth-menu-button { + border-left: 1px solid @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, -#caffeine-button:hover, #dark-mode-button:hover, -#power-profile-button:hover { - background-color: mix(@onPrimary, @primary, 0.7); +#caffeine-button:hover { + background-color: @foreground; } +/* Estados Disabled */ #network-status-button.disabled, #network-menu-button.disabled, #bluetooth-status-button.disabled, -#bluetooth-menu-button.disabled, -#dark-mode-button.disabled, -#power-profile-button.disabled { +#bluetooth-menu-button.disabled { background-color: @surface; } +#network-menu-button.disabled, +#bluetooth-menu-button.disabled { + border-left: 1px solid @surfaceBright; +} + #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, -#caffeine-button.disabled:hover, #dark-mode-button.disabled:hover, -#power-profile-button.disabled:hover { +#caffeine-button.disabled:hover { background-color: @surfaceBright; } #network-status-button, -#bluetooth-status-button, -#dark-mode-status-button, -#power-profile-status-button { +#bluetooth-status-button { border-radius: 20px 0 0 20px; } @@ -84,17 +87,31 @@ border-radius: 0 20px 20px 0; } +#network-icon, +#bluetooth-icon, +#night-mode-icon, +#dark-mode-icon, +#caffeine-icon { + color: @shadow; + font-size: 24px; +} + +#network-icon.disabled, +#bluetooth-icon.disabled, +#night-mode-icon.disabled, +#dark-mode-icon.disabled, +#caffeine-icon.disabled { + color: @outline; +} + #network-menu-label, -#bluetooth-menu-label, -#dark-mode-label, -#power-profile-label { +#bluetooth-menu-label { + color: @shadow; font-size: 16px; } #network-menu-label.disabled, -#bluetooth-menu-label.disabled, -#dark-mode-label.disabled, -#power-profile-label.disabled { +#bluetooth-menu-label.disabled { color: @outline; } @@ -102,8 +119,7 @@ #bluetooth-label, #night-mode-label, #caffeine-label, -#dark-mode-label, -#power-profile-label { +#dark-mode-label { color: @shadow; font-size: 14px; font-weight: bold; @@ -113,18 +129,16 @@ #network-label.disabled, #bluetooth-label.disabled, #night-mode-label.disabled, -#caffeine-label.disabled, #dark-mode-label.disabled, -#power-profile-label.disabled { +#caffeine-label.disabled { color: @outline; } #network-ssid, #bluetooth-status, #night-mode-status, -#caffeine-status, #dark-mode-status, -#power-profile-status { +#caffeine-status { color: @surfaceBright; font-size: 12px; } @@ -132,12 +146,7 @@ #network-ssid.disabled, #bluetooth-status.disabled, #night-mode-status.disabled, -#caffeine-status.disabled, #dark-mode-status.disabled, -#power-profile-status.disabled { +#caffeine-status.disabled { color: @outline; } - -#power-profile-button.balanced-profile { - background-color: transparent; -} diff --git a/styles/launcher.css b/styles/launcher.css index 9057c92..0accbe3 100644 --- a/styles/launcher.css +++ b/styles/launcher.css @@ -28,22 +28,26 @@ min-width: 620px; } -#launcer-content.cliphist { - min-height: 420px; +.cliphist { + min-height: 464px; + min-width: 300px; +} + +.applauncher { + min-height: 464px; } #launcher-content.bluetooth { min-height: 464px; - min-width: 485px; } #launcher-content.wifi { min-height: 464px; - min-width: 485px; } -#launcher-content.power { - min-height: 52px; +#launcher-content.todo { + min-width: 460px; + min-height: 300px; } #launcher-content.calendar { @@ -62,25 +66,15 @@ #sh, #cliphist, #emoji, -#powermenu { +#power { transition: all 0.25s cubic-bezier(0.5, 0.25, 0, 1); } -#clear-launcher-button { - background-color: @secondaryContainer; - color: @onSecondaryContainer; - margin: 11px; - margin-left: 0px; - min-width: 44px; - min-height: 44px; - border-radius: 100%; -} - #scrolled-window scrollbar { border-radius: 10px; background-color: @surface; - /*padding: 4px;*/ - /*margin-left: 6px;*/ + padding: 4px; + margin-left: 6px; } #scrolled-window slider { @@ -174,28 +168,41 @@ * ── powermenu ───────────────────────────────────────────────────────── */ -#powermenu-button { +#power-menu-button { border-radius: 20px; + min-width: 52px; + min-height: 52px; padding: 20px; } -#powermenu-button:hover, -#powermenu-button:focus { +#power-menu-button label { + font-size: 49px; + color: @foreground; +} + +#power-menu-button:hover, +#power-menu-button:focus { background-color: @surfaceBright; } -#powermenu-button:active { +#power-menu-button:hover label, +#power-menu-button:focus label { + color: @primary; +} + +#power-menu-button:active { background-color: @primary; } +#power-menu-button:active label { + color: @shadow; +} + /* * ── cliphist ────────────────────────────────────────────────────────── */ -#cliphist-history { - min-height: 520px; -} -#launcher button#clip-img-item { +button#clip-img-item { background-size: cover; background-position: center; background-repeat: no-repeat; @@ -204,12 +211,12 @@ min-height: 70px; } -#launcher button#clip-img-item:hover, -#launcher button#clip-img-item:focus { +button#clip-img-item:hover, +button#clip-img-item:focus { box-shadow: 2px 2px 2px @primary; } -#launcher button#clip-item { +button#clip-item { border-radius: 11px; background-color: transparent; background-image: none; @@ -217,22 +224,34 @@ padding: 6px; } -#launcher button#clip-item { +button#clip-item { font-size: 30px; } -#launcher button#clip-item:hover, -#launcher button#clip-item:focus { +button#clip-item:hover, +button#clip-item:focus { text-shadow: 2px 2px 2px @primary; } +#clear-button { + margin: 11px; + margin-left: 0px; + min-width: 44px; + min-height: 44px; + border-radius: 100%; +} + +#clear-button:hover { + background-color: mix(@onSurface, @surface, 0.92); +} + +#cliphist-clear-button { + font-size: 24px; +} + /* * ── todo ────────────────────────────────────────────────────────────── */ -#todo-content { - min-height: 250px; -} - #todo-launcher { background-color: @surface; padding: 10px; @@ -246,7 +265,7 @@ } #todo-label { - color: @primary; + font-size: 16px; } #todo-label-completed { @@ -254,8 +273,34 @@ text-decoration: line-through; } -#delete-button { - color: @error; +#add-button { + margin: 11px; + margin-left: 0px; + margin-right: 0px; + min-width: 44px; + min-height: 44px; + border-radius: 100%; +} + +#delete-button, +#checkbox { + margin-left: 0px; + margin-right: 0px; + min-width: 44px; + min-height: 44px; + border-radius: 100%; +} + +#add-button:hover, +#delete-button:hover, +#checkbox:hover { + background-color: mix(@onSurface, @surface, 0.92); +} + +#todo-add-button, +#todo-checkbox, +#todo-delete-button { + font-size: 24px; } /* @@ -296,6 +341,11 @@ margin-bottom: 4px; } +#bluetooth-scan-button, +#bluetooth-toggle-button { + font-size: 24px; +} + #bluetooth-scan, #bluetooth-toggle { background-color: @secondaryContainer; @@ -343,7 +393,6 @@ background-color: @surfaceContainer; padding: 4px; border-radius: 20px; - min-height: 200px; } /* @@ -363,6 +412,10 @@ margin-bottom: 4px; } +#wifi-toggle-icon { + font-size: 24px; +} + #wifi-scan, #wifi-toggle { background-color: @secondaryContainer; @@ -409,7 +462,6 @@ background-color: @surfaceContainer; padding: 4px; border-radius: 20px; - min-height: 200px; } /* @@ -434,8 +486,8 @@ #day-label, #day-empty { - min-width: 41px; - min-height: 30px; + min-width: 42px; + min-height: 36px; } #day-empty { @@ -454,7 +506,9 @@ #day-label.current-day { background-color: @primary; color: @onPrimary; - border-radius: 20px; + min-height: 36px; + min-width: 42px; + border-radius: 99px; } #prev-month-button, @@ -483,6 +537,13 @@ color: @surface; } +#day-box { + padding: 5px; + border-radius: 5px; + min-width: 30px; + min-height: 30px; +} + #prev-month-day, #next-month-day { /*color: lighter(@surfaceBright);*/ diff --git a/styles/main.css b/styles/main.css index 7703a10..a0cd973 100644 --- a/styles/main.css +++ b/styles/main.css @@ -9,8 +9,7 @@ all: unset; color: @foreground; font-size: 16px; - font-family: "Product Sans", Roboto, "Font Awesome 6 Free", Helvetica, Arial, - sans-serif; + font-family: "Product Sans", sans-serif; } /* @@ -19,7 +18,7 @@ #corner { background-color: @surface; - border-radius: 0; + border-radius: 0px; } #corner-container { From 8d41eab8fa5e343c2da3f63beae6f915674757af Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 25 Feb 2025 05:22:48 +0545 Subject: [PATCH 05/22] Huge Updates --- config/assets/launcher.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/assets/launcher.json b/config/assets/launcher.json index a153df6..c2cfc5b 100644 --- a/config/assets/launcher.json +++ b/config/assets/launcher.json @@ -7,6 +7,7 @@ ":bt": "launcher.open(\"bluetooth\")", ":sh": "launcher.open(\"sh\")", ":pw": "launcher.open(\"power\")", - ":nw": "launcher.open(\"wifi\")" + ":nw": "launcher.open(\"wifi\")", + ":cl": "launcher.open(\"calendar\")" } } From 7d6e00cb9ea51c7c4c9f8a33abca557bfc55aa24 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 25 Feb 2025 05:24:18 +0545 Subject: [PATCH 06/22] Update READEME.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 92c8644..25828fa 100644 --- a/README.md +++ b/README.md @@ -93,11 +93,11 @@ cd Tela-icon-theme - [x] Installation script - [x] Network Manager - [x] Calendar -- [ ] Power Manager +- [x] Power Manager - [ ] Notification Panel - [ ] Improve CSS Styling - [ ] Window Switcher -- [ ] Web search in Launcher +- [x] Web search in Launcher ## Special Thanks From d126ec38cc5ccf438f9ba18c7a0fd8125e58d113 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 25 Feb 2025 05:37:35 +0545 Subject: [PATCH 07/22] Update READEME.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 25828fa..3fb3f82 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ cd Tela-icon-theme - [x] Installation script - [x] Network Manager - [x] Calendar -- [x] Power Manager +- [ ] Power Manager - [ ] Notification Panel - [ ] Improve CSS Styling - [ ] Window Switcher From 295f54c8d117813c37d3dcdfcc56b71c84eafea0 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 28 Feb 2025 16:22:38 +0545 Subject: [PATCH 08/22] idk what i update --- config/assets/config.json | 2 +- config/assets/{launcher.json => modus.json} | 13 +- config/assets/settings.json | 2 +- config/config.py | 4 +- config/hypr/modus.conf | 5 +- config/material-colors/generate.py | 261 ++++++++-------- .../templates/colors.tdesktop-theme | 4 +- .../templates/material-discord.scss | 10 +- services/__init__.py | 2 +- services/mpris.py | 279 ++++++++++++++++++ services/{wifi.py => network.py} | 0 11 files changed, 425 insertions(+), 157 deletions(-) rename config/assets/{launcher.json => modus.json} (68%) create mode 100644 services/mpris.py rename services/{wifi.py => network.py} (100%) diff --git a/config/assets/config.json b/config/assets/config.json index 2f0a891..a93e26d 100644 --- a/config/assets/config.json +++ b/config/assets/config.json @@ -1 +1 @@ -{"prefix_restart": "SHIFT ALT", "suffix_restart": "T", "prefix_bluetooth": "ALT", "suffix_bluetooth": "B", "prefix_emoji": "SUPER", "suffix_emoji": "E", "prefix_cliphist": "SUPER", "suffix_cliphist": "V", "prefix_todo": "SUPER", "suffix_todo": "T", "prefix_sh": "SUPER", "suffix_sh": "S", "prefix_walls": "SUPER", "suffix_walls": "W", "prefix_screenshotregion": "SUPER", "suffix_screenshotregion": "Z", "prefix_screenshot": "", "suffix_screenshot": "Print", "prefix_screencast": "SUPER", "suffix_screencast": "R", "prefix_launcher": "SUPER", "suffix_launcher": "D", "prefix_power": "SUPER", "suffix_power": "X", "prefix_toggle": "SUPER", "suffix_toggle": "H", "prefix_randomwallpaper": "ALT SHIFT", "suffix_randomwallpaper": "W", "wallpapers_dir": "/home/sankalp/Pictures/wallpapers"} \ No newline at end of file +{"prefix_restart": "SHIFT ALT", "suffix_restart": "T", "prefix_bluetooth": "ALT", "suffix_bluetooth": "B", "prefix_emoji": "SUPER", "suffix_emoji": "E", "prefix_cliphist": "SUPER", "suffix_cliphist": "V", "prefix_todo": "SUPER", "suffix_todo": "T", "prefix_sh": "SUPER", "suffix_sh": "S", "prefix_walls": "SUPER", "suffix_walls": "W", "prefix_screenshotregion": "SUPER", "suffix_screenshotregion": "Z", "prefix_screenshot": "", "suffix_screenshot": "Print", "prefix_screencast": "SUPER", "suffix_screencast": "R", "prefix_launcher": "SUPER", "suffix_launcher": "D", "prefix_power": "SUPER", "suffix_power": "X", "prefix_wifi": "SUPER", "suffix_wifi": "N", "prefix_toggle": "SUPER", "suffix_toggle": "H", "prefix_randomwallpaper": "ALT SHIFT", "suffix_randomwallpaper": "W", "wallpapers_dir": "/home/sankalp/Pictures/wallpapers"} \ No newline at end of file diff --git a/config/assets/launcher.json b/config/assets/modus.json similarity index 68% rename from config/assets/launcher.json rename to config/assets/modus.json index c2cfc5b..12e8657 100644 --- a/config/assets/launcher.json +++ b/config/assets/modus.json @@ -9,5 +9,16 @@ ":pw": "launcher.open(\"power\")", ":nw": "launcher.open(\"wifi\")", ":cl": "launcher.open(\"calendar\")" - } + }, + "pinned_apps": [ + "kitty", + "code", + "materialgram", + "vesktop", + "spotify", + "libreoffice", + "wasistlos", + "pavucontrol", + "zen-browser" + ] } diff --git a/config/assets/settings.json b/config/assets/settings.json index 65e8d2b..d11f7c8 100644 --- a/config/assets/settings.json +++ b/config/assets/settings.json @@ -2,4 +2,4 @@ "color-scheme": "dark", "custom-color": "none", "generation-scheme": "tonalSpot" -} +} \ No newline at end of file diff --git a/config/config.py b/config/config.py index 46cd4c7..c280ec1 100644 --- a/config/config.py +++ b/config/config.py @@ -117,7 +117,7 @@ def copy_wallpapers(): subprocess.run( [ "python", - os.path.expanduser("~/Modus/config/wallpaper.py"), + os.path.expanduser("~/Modus/config/scripts/wallpaper.py"), "-I", os.path.join(dest_wallpaper_dir, "example-1.jpg"), ] @@ -150,7 +150,7 @@ 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/Modus/config +$scriptsDir = $HOME/Modus/config/scripts/ bind = {bind_vars["prefix_restart"]}, {bind_vars["suffix_restart"]}, exec, killall modus; python {home}/Modus/main.py # Reload Modus | Default: SUPER ALT + T bind = {bind_vars["prefix_bluetooth"]}, {bind_vars["suffix_bluetooth"]}, exec, $fabricSend 'launcher.open("bluetooth")' # Bluetooth | Default: ALT + B diff --git a/config/hypr/modus.conf b/config/hypr/modus.conf index cd7dc0d..2eee02b 100644 --- a/config/hypr/modus.conf +++ b/config/hypr/modus.conf @@ -5,10 +5,11 @@ 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/Modus/config +$scriptsDir = $HOME/Modus/config/scripts/ bind = SHIFT ALT, T, exec, killall modus; python /home/sankalp/Modus/main.py # Reload Modus | Default: SUPER ALT + T bind = ALT, B, exec, $fabricSend 'launcher.open("bluetooth")' # Bluetooth | Default: ALT + B +bind = SUPER, N, exec, $fabricSend 'launcher.open("wifi")' # Wifi | Default: SUPER + N bind = SUPER, E, exec, $fabricSend 'launcher.open("emoji")' # Emoji | Default: SUPER + E bind = SUPER, V, exec, $fabricSend 'launcher.open("cliphist")' # Cliphist | Default: SUPER + V bind = SUPER, T, exec, $fabricSend 'launcher.open("todo")' # Todo | Default: SUPER + T @@ -20,7 +21,7 @@ bind = SUPER, X, exec, $fabricSend 'launcher.open("power")' # Power Menu | Defau bind = SUPER, H, exec, $fabricSend 'bar.toggle_hidden()' # Toggle Bar | Default: SUPER + H bind = SUPER, Z, exec, $fabricSend 'sc.screenshot()' # Screenshot Region | Default: SUPER + Z bind = , Print, exec, $fabricSend 'sc.screenshot(True)' # ScreenshotFullScreen | Default: Print -bind = SUPER, R, exec, $fabricSend 'sc.screencast()' # Screencast | Default: SUPER + R +bind = SUPER, R, exec, $fabricSend 'sc.screencast_start()' # Screencast | Default: SUPER + R # Wallpapers directory: /home/sankalp/Pictures/wallpapers diff --git a/config/material-colors/generate.py b/config/material-colors/generate.py index 32504e0..6fa3446 100644 --- a/config/material-colors/generate.py +++ b/config/material-colors/generate.py @@ -7,23 +7,22 @@ import argparse from materialyoucolor.quantize import QuantizeCelebi # type: ignore from materialyoucolor.score.score import Score # type: ignore from materialyoucolor.hct import Hct # type: ignore -from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors # type: ignore # noqa -from materialyoucolor.scheme.dynamic_scheme import DynamicScheme # type: ignore # noqa -from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot # type: ignore # noqa -from materialyoucolor.scheme.scheme_expressive import SchemeExpressive # type: ignore # noqa -from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad # type: ignore # noqa -from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome # type: ignore # noqa -from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow # type: ignore # noqa -from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant # type: ignore # noqa -from materialyoucolor.scheme.scheme_neutral import SchemeNeutral # type: ignore # noqa -from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity # type: ignore # noqa -from materialyoucolor.scheme.scheme_content import SchemeContent # type: ignore # noqa +from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors # type: ignore # noqa +from materialyoucolor.scheme.dynamic_scheme import DynamicScheme # type: ignore # noqa +from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot # type: ignore # noqa +from materialyoucolor.scheme.scheme_expressive import SchemeExpressive # type: ignore # noqa +from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad # type: ignore # noqa +from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome # type: ignore # noqa +from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow # type: ignore # noqa +from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant # type: ignore # noqa +from materialyoucolor.scheme.scheme_neutral import SchemeNeutral # type: ignore # noqa +from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity # type: ignore # noqa +from materialyoucolor.scheme.scheme_content import SchemeContent # type: ignore # noqa import hashlib import pickle import numpy as np import re - schemes = { "tonalSpot": SchemeTonalSpot, "expressive": SchemeExpressive, @@ -41,22 +40,21 @@ def join(*args): return os.path.join(*args) -home = os.path.expanduser('~') - +home = os.path.expanduser("~") script_dir = os.path.dirname(os.path.abspath(__file__)) cache_path = f"{home}/.cache/material/" os.makedirs(cache_path, exist_ok=True) def rgb_to_hex(rgb): - return '#{:02x}{:02x}{:02x}'.format(*rgb[:3]) + return "#{:02x}{:02x}{:02x}".format(*rgb[:3]) def rgba_to_rgb(rgba): - return f'{rgba[0]}, {rgba[1]}, {rgba[2]}' + return f"{rgba[0]}, {rgba[1]}, {rgba[2]}" -class Color(): +class Color: def __init__(self, scheme) -> None: self.scheme = scheme @@ -70,8 +68,12 @@ class Color(): class ColorsCache: def __init__( - self, colors: DynamicScheme | dict, wallpaper: str, - original_color: int, contrast_level: int, scheme_name: str + self, + colors: DynamicScheme | dict, + wallpaper: str, + original_color: int, + contrast_level: int, + scheme_name: str, ) -> None: self.colors: dict[str, str] = {} self.wallpaper = wallpaper @@ -90,14 +92,13 @@ class ColorsCache: def colors_dict(cache: ColorsCache): - dict = { + return { "wallpaper": cache.wallpaper, "colors": cache.colors, "original_color": cache.original_color, "contrast_level": cache.contrast_level, - "scheme_name": cache.scheme_name + "scheme_name": cache.scheme_name, } - return dict def get_cache_object(object: dict | str): @@ -108,10 +109,7 @@ def get_cache_object(object: dict | str): original_color = object["original_color"] contrast_level = object.get("contrast_level", 0) scheme_name = object.get("scheme_name", "tonalSpot") - return ColorsCache( - colors, wallpaper, original_color, - contrast_level, scheme_name - ) + return ColorsCache(colors, wallpaper, original_color, contrast_level, scheme_name) def get_file_list(folder_path): @@ -124,21 +122,18 @@ def get_file_list(folder_path): ready_templates = { "colors.css": "@define-color {name} {hex};\n", - "colors.scss": "${name}: {hex};\n" -} -additional = { - "onBackground": "foreground" + "colors.scss": "${name}: {hex};\n", } +additional = {"onBackground": "foreground"} -def generate_color_map(scheme: DynamicScheme): - color_map: dict[str, str] = {} - for color in vars(MaterialDynamicColors).keys(): - color_name = getattr(MaterialDynamicColors, color) - if hasattr(color_name, "get_hct"): - rgba = color_name.get_hct(scheme).to_rgba() - color_map[color.strip()] = rgb_to_hex(rgba) - return color_map +def generate_color_map(scheme): + colors = {} + for color_name in vars(MaterialDynamicColors).keys(): + color_obj = getattr(MaterialDynamicColors, color_name) + if hasattr(color_obj, "get_hct"): + colors[color_name] = rgb_to_hex(color_obj.get_hct(scheme).to_rgba()) + return colors class ColorFormatter: @@ -147,25 +142,23 @@ class ColorFormatter: def apply_transformations(self, value, transformations): intermediate_transforms = [ - t for t in transformations - if not (t.startswith("strip") or t == "rgb") + t for t in transformations if not (t.startswith("strip") or t == "rgb") ] final_transforms = [ - t for t in transformations - if t.startswith("strip") or t == "rgb" + t for t in transformations if t.startswith("strip") or t == "rgb" ] for transform in intermediate_transforms: if transform.startswith("lighten"): - percent = int(re.search(r'\d+', transform).group()) + percent = int(re.search(r"\d+", transform).group()) value = self.adjust_brightness(value, percent) elif transform.startswith("darken"): - percent = int(re.search(r'\d+', transform).group()) + percent = int(re.search(r"\d+", transform).group()) value = self.adjust_brightness(value, -percent) for transform in final_transforms: if transform.startswith("strip"): - value = value.lstrip('#') + value = value.lstrip("#") elif transform == "rgb": value = self.hex_to_rgb(value) @@ -175,24 +168,20 @@ class ColorFormatter: def min_max(v): return min(255, max(0, v)) - hex_color = hex_color.lstrip('#') - r, g, b = (int(hex_color[i:i+2], 16) for i in (0, 2, 4)) - + hex_color = hex_color.lstrip("#") + r, g, b = (int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) r = min_max(int(r * (1 + factor / 100))) g = min_max(int(g * (1 + factor / 100))) b = min_max(int(b * (1 + factor / 100))) - - return f'#{r:02X}{g:02X}{b:02X}'.lower() + return f"#{r:02X}{g:02X}{b:02X}".lower() def hex_to_rgb(self, hex_color): - hex_color = hex_color.lstrip('#') - r, g, b = (int(hex_color[i:i+2], 16) for i in (0, 2, 4)) - return f'{r},{g},{b}' + hex_color = hex_color.lstrip("#") + r, g, b = (int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) + return f"{r},{g},{b}" def parse_transformations(self, transformations_str): - transformations = re.findall( - r'(\w+)(?:\((\d*)\))?', transformations_str - ) + transformations = re.findall(r"(\w+)(?:\((\d*)\))?", transformations_str) result = [] for command, arg in transformations: if command: @@ -203,7 +192,7 @@ class ColorFormatter: return result def format(self, text): - pattern = r'(<(\w+)(?:\.(.+))?>)' + pattern = r"(<(\w+)(?:\.(.+))?>)" matches = re.findall(pattern, text) result = [] last_end = 0 @@ -211,27 +200,28 @@ class ColorFormatter: for match in matches: full_match, key, transformations_str = match if key in self.color_map: - result.append(text[last_end:text.find(full_match, last_end)]) + result.append(text[last_end : text.find(full_match, last_end)]) value = self.color_map[key] if transformations_str: - transformations = self.parse_transformations( - transformations_str - ) + transformations = self.parse_transformations(transformations_str) value = self.apply_transformations(value, transformations) result.append(value) last_end = text.find(full_match, last_end) + len(full_match) result.append(text[last_end:]) - result = ''.join(result) - pattern = r'<\\\\([^>]+)>' - - result = re.sub(pattern, r'<\1>', result) - + result = "".join(result) + pattern = r"<\\\\([^>]+)>" + result = re.sub(pattern, r"<\1>", result) return result -def generate_templates(folder: str, output_folder: str, scheme: DynamicScheme, - color_scheme: str, wallpaper: str = ''): +def generate_templates( + folder: str, + output_folder: str, + scheme: DynamicScheme, + color_scheme: str, + wallpaper: str = "", +): if not os.path.exists(output_folder): os.makedirs(output_folder) if not os.path.exists(folder): @@ -244,13 +234,12 @@ def generate_templates(folder: str, output_folder: str, scheme: DynamicScheme, formatter = ColorFormatter(scheme) template = formatter.format(template) template = ( - template - .replace("", color_scheme) + template.replace("", color_scheme) .replace("", output_folder) .replace("", wallpaper) ) new_path = join(output_folder, os.path.basename(file_path)) - with open(new_path, 'w') as f: + with open(new_path, "w") as f: f.write(template) for file in ready_templates: @@ -262,21 +251,17 @@ def generate_templates(folder: str, output_folder: str, scheme: DynamicScheme, hex_color = rgb_to_hex(rgba) rgb_color = rgba_to_rgb(rgba) new_line = ready_templates[file].format( - name=color, - hex=hex_color, - rgb=rgb_color + name=color, hex=hex_color, rgb=rgb_color ) _template += new_line if color in additional: new_line = ready_templates[file].format( - name=additional[color], - hex=hex_color, - rgb=rgb_color + name=additional[color], hex=hex_color, rgb=rgb_color ) _template += new_line new_path = join(output_folder, os.path.basename(file)) - with open(new_path, 'w') as f: + with open(new_path, "w") as f: f.write(_template) @@ -286,10 +271,10 @@ def run_hooks(folder: str): file_list = get_file_list(folder) for file_path in file_list: file_name = os.path.basename(file_path) - if str(file_name).endswith(".py"): - subprocess.run(['python', '-O', file_path]) - elif str(file_name).endswith(".sh"): - subprocess.run(['sh', file_path]) + if file_name.endswith(".py"): + subprocess.run(["python", "-O", file_path]) + elif file_name.endswith(".sh"): + subprocess.run(["sh", file_path]) def process_image(image_path, quality=2, num_colors=128): @@ -297,81 +282,69 @@ def process_image(image_path, quality=2, num_colors=128): global cache_path _cache_path = join(cache_path, "cached_colors") hash_object = hashlib.md5(image_path.encode()) - cache_filename = hash_object.hexdigest() + '.pkl' + cache_filename = hash_object.hexdigest() + ".pkl" os.makedirs(_cache_path, exist_ok=True) return join(_cache_path, cache_filename) def load_from_cache(cache_path): if os.path.exists(cache_path): - with open(cache_path, 'rb') as f: + with open(cache_path, "rb") as f: return pickle.load(f) return None def save_to_cache(cache_path, data): - with open(cache_path, 'wb') as f: + with open(cache_path, "wb") as f: pickle.dump(data, f) - cache_path = get_cache_path(image_path) - - cached_result = load_from_cache(cache_path) + cache_file = get_cache_path(image_path) + cached_result = load_from_cache(cache_file) if cached_result is not None: return cached_result - image = Image.open(image_path).convert('RGB') - + image = Image.open(image_path).convert("RGB") image_data = np.array(image) pixel_array = image_data[::quality, ::quality].reshape(-1, 3) - result = QuantizeCelebi(pixel_array, num_colors) - color = Score.score(result)[0] - - save_to_cache(cache_path, color) - + save_to_cache(cache_file, color) return color def main( - color_scheme: str, image_path: str, use_color: int | None = None, - scheme_name: str | None = None, contrast_level: int = 0 + color_scheme: str, + image_path: str, + use_color: int | None = None, + scheme_name: str | None = None, + contrast_level: int = 0, ): scheme_name = scheme_name or "tonalSpot" scheme = schemes[scheme_name] is_dark = True if color_scheme == "dark" else False if use_color is None: color = process_image(image_path, 4, 1024) - else: color = use_color - scheme = scheme( - Hct.from_int(color), - is_dark, - contrast_level - ) + scheme = scheme(Hct.from_int(color), is_dark, contrast_level) # for color in vars(MaterialDynamicColors).keys(): # color_name = getattr(MaterialDynamicColors, color) # if hasattr(color_name, "get_hct"): # print(color, rgb_to_hex(color_name.get_hct(scheme).to_rgba())) - with open(join(cache_path, "colors.json"), 'w') as f: - object = ColorsCache( - scheme, image_path, color, contrast_level, scheme_name + with open(join(cache_path, "colors.json"), "w") as f: + cache_obj = ColorsCache( + generate_color_map(scheme), image_path, color, contrast_level, scheme_name ) - json.dump(colors_dict(object), f, indent=2) + json.dump(colors_dict(cache_obj), f, indent=2) generate_templates( f"{script_dir}/templates", cache_path, scheme, color_scheme, image_path ) - generate_templates( - f"{script_dir}/templates/svg", join(cache_path, "svg/"), scheme, - color_scheme, image_path - ) if is_dark: - subprocess.run(['sh', join(script_dir, 'gtk-material.sh')]) + subprocess.run(["sh", join(script_dir, "gtk-material.sh")]) else: - subprocess.run(['sh', join(script_dir, 'gtk-material.sh'), '--light']) + subprocess.run(["sh", join(script_dir, "gtk-material.sh"), "--light"]) run_hooks(join(script_dir, "hooks")) @@ -379,30 +352,29 @@ def main( def _argparse() -> tuple[bool, bool, str, str, str, str]: parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("-R", help="Restore previous colorscheme.", action="store_true") group.add_argument( - "-R", help="Restore previous colorscheme.", action='store_true' - ) - group.add_argument( - "-w", help="Use last used wallpaper for color generation.", - action='store_true' - ) - group.add_argument( - "--image", type=str, help="Image for color scheme generation" + "-w", help="Use last used wallpaper for color generation.", action="store_true" ) + group.add_argument("--image", type=str, help="Image for color scheme generation") group.add_argument( "--color", type=str, help="Main color for color scheme generation" ) parser.add_argument( - '--color-scheme', type=str, default='dark', choices=['dark', 'light'], - help="Color scheme variant" + "--color-scheme", + type=str, + default="dark", + choices=["dark", "light"], + help="Color scheme variant", ) parser.add_argument( - '--scheme', type=str, default="", - choices=list(schemes.keys()), help="Scheme for color generation" + "--scheme", + type=str, + default="", + choices=list(schemes.keys()), + help="Scheme for color generation", ) - args = parser.parse_args() - restore_palette = args.R use_last_used_wallpaper = args.w image = args.image @@ -410,16 +382,19 @@ def _argparse() -> tuple[bool, bool, str, str, str, str]: use_color = args.color scheme = args.scheme return ( - restore_palette, use_last_used_wallpaper, image, - color_scheme, use_color, scheme + restore_palette, + use_last_used_wallpaper, + image, + color_scheme, + use_color, + scheme, ) def _main(): - ( - restore_palette, use_last_used_wallpaper, image, - color_scheme, use_color, scheme - ) = _argparse() + restore_palette, use_last_used_wallpaper, image, color_scheme, use_color, scheme = ( + _argparse() + ) if not scheme: scheme = None if use_color is not None: @@ -427,15 +402,18 @@ def _main(): with open(join(cache_path, "colors.json")) as f: content = get_cache_object(f.read()) main( - color_scheme, image_path=content.wallpaper, - use_color=int(use_color.lstrip('#'), 16), scheme_name=scheme + color_scheme, + image_path=content.wallpaper, + use_color=int(use_color.lstrip("#"), 16), + scheme_name=scheme, ) except Exception: main( - color_scheme, image_path="/dev/null", - use_color=int(use_color.lstrip('#'), 16), scheme_name=scheme + color_scheme, + image_path="/dev/null", + use_color=int(use_color.lstrip("#"), 16), + scheme_name=scheme, ) - elif restore_palette: with open(join(cache_path, "colors.json")) as f: content = get_cache_object(f.read()) @@ -444,7 +422,7 @@ def _main(): image_path=content.wallpaper, use_color=content.original_color, scheme_name=scheme or content.scheme_name, - contrast_level=content.contrast_level + contrast_level=content.contrast_level, ) elif use_last_used_wallpaper: with open(join(cache_path, "colors.json")) as f: @@ -453,13 +431,12 @@ def _main(): color_scheme, image_path=content.wallpaper, scheme_name=scheme or content.scheme_name, - contrast_level=content.contrast_level + contrast_level=content.contrast_level, ) else: if os.path.isdir(image): files = [ - f for f in os.listdir(image) - if f.endswith(('.png', '.jpg', '.jpeg')) + f for f in os.listdir(image) if f.endswith((".png", ".jpg", ".jpeg")) ] if files: image = join(image, random.choice(files)) diff --git a/config/material-colors/templates/colors.tdesktop-theme b/config/material-colors/templates/colors.tdesktop-theme index 1dc6a05..7b4db51 100644 --- a/config/material-colors/templates/colors.tdesktop-theme +++ b/config/material-colors/templates/colors.tdesktop-theme @@ -392,7 +392,7 @@ msgFileOutBgSelected: ACCENT; // Outgoing selected audio file msgFile1Bg: #72b1df; // Blue shared links / files without image square thumbnail msgFile1BgDark: #5c9ece; // Blue shared files without image download circle background msgFile1BgOver: #5294c4; // Blue shared files without image download circle background with mouse over -msgFile1BgSelected: #5099d0; // Blue shared files without image download circle background if file is selected +msgFile1BgSelected: #50100d0; // Blue shared files without image download circle background if file is selected msgFile2Bg: #61b96e; // Green shared links / shared files without image square thumbnail msgFile2BgDark: #4da859; // Green shared files without image download circle background msgFile2BgOver: #44a050; // Green shared files without image download circle background with mouse over @@ -514,7 +514,7 @@ mediaviewFileSizeFg: ON_BACKGROUND_DARKER; // File size text in file rectan mediaviewFileRedCornerFg: #d55959; // Red file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .pdf) mediaviewFileYellowCornerFg: #e8a659; // Yellow file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .zip) mediaviewFileGreenCornerFg: #49a957; // Green file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .exe) -mediaviewFileBlueCornerFg: #599dcf; // Blue file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .dmg) +mediaviewFileBlueCornerFg: #5100dcf; // Blue file thumbnail placeholder corner in file rectangle (for a file without thumbnail, like .dmg) mediaviewFileExtFg: activeButtonFg; // File extension text in file thumbnail placeholder in file rectangle mediaviewMenuBg: BACKGROUND; // Context menu in Media Viewer background diff --git a/config/material-colors/templates/material-discord.scss b/config/material-colors/templates/material-discord.scss index 607ee99..f302ab7 100644 --- a/config/material-colors/templates/material-discord.scss +++ b/config/material-colors/templates/material-discord.scss @@ -1,14 +1,14 @@ @use "sass:color"; +@use "sass:math"; +@use "colors.scss" as *; @import url(https://capnkitten.github.io/BetterDiscord/Themes/Material-Discord/css/source.css); -@import "/colors.scss"; - $color-scheme: ; -$hue: round(color.channel($primary, "hue", $space: hsl)); -$saturation: round(color.channel($primary, "saturation", $space: hsl)); -$lightness: round(color.channel($primary, "lightness", $space: hsl)); +$hue: math.round(color.channel($primary, "hue", $space: hsl)); +$saturation: math.round(color.channel($primary, "saturation", $space: hsl)); +$lightness: math.round(color.channel($primary, "lightness", $space: hsl)); * { transition: color 0.2s ease-in-out; diff --git a/services/__init__.py b/services/__init__.py index b464ce6..3d7d12d 100644 --- a/services/__init__.py +++ b/services/__init__.py @@ -1,7 +1,7 @@ from fabric.audio import Audio from .brightness import Brightness from .screen_record import ScreenRecorder -from .wifi import * +from .network import * brightness = Brightness() diff --git a/services/mpris.py b/services/mpris.py new file mode 100644 index 0000000..9c5afb5 --- /dev/null +++ b/services/mpris.py @@ -0,0 +1,279 @@ +import contextlib + +import gi +from fabric.core.service import Property, Service, Signal +from fabric.utils import bulk_connect +from gi.repository import GLib # type: ignore +from loguru import logger + + +class PlayerctlImportError(ImportError): + """An error to raise when playerctl is not installed.""" + + def __init__(self, *args): + super().__init__( + "Playerctl is not installed, please install it first", + *args, + ) + + +try: + gi.require_version("Playerctl", "2.0") + from gi.repository import Playerctl + + # from gi.repository import Playerctl # type: ignore +except ValueError: + raise PlayerctlImportError + + +class MprisPlayer(Service): + """A service to manage a mpris player.""" + + @Signal + def exit(self, value: bool) -> bool: ... + + @Signal + def changed(self) -> None: ... + + def __init__( + self, + player: Playerctl.Player, + **kwargs, + ): + self._signal_connectors: dict = {} + self._player: Playerctl.Player = player + super().__init__(**kwargs) + for sn in ["playback-status", "loop-status", "shuffle", "volume", "seeked"]: + self._signal_connectors[sn] = self._player.connect( + sn, + lambda *args, sn=sn: self.notifier(sn, args), + ) + + self._signal_connectors["exit"] = self._player.connect( + "exit", + self.on_player_exit, + ) + self._signal_connectors["metadata"] = self._player.connect( + "metadata", + lambda *args: self.update_status(), + ) + GLib.idle_add(lambda *args: self.update_status_once()) + + def update_status(self): + # schedule each notifier asynchronously. + def notify_property(prop): + if self.get_property(prop) is not None: + self.notifier(prop) + + for prop in [ + "metadata", + "title", + "artist", + "arturl", + "length", + ]: + GLib.idle_add(lambda p=prop: (notify_property(p), False)) + for prop in [ + "can-seek", + "can-pause", + "can-shuffle", + "can-go-next", + "can-go-previous", + ]: + GLib.idle_add(lambda p=prop: (self.notifier(p), False)) + + def update_status_once(self): + # schedule notifier calls for each property + def notify_all(): + for prop in self.list_properties(): # type: ignore + self.notifier(prop.name) + return False + + GLib.idle_add(notify_all, priority=GLib.PRIORITY_DEFAULT_IDLE) + + def notifier(self, name: str, args=None): + def notify_and_emit(): + self.notify(name) + self.emit("changed") + return False + + GLib.idle_add(notify_and_emit, priority=GLib.PRIORITY_DEFAULT_IDLE) + + def on_player_exit(self, player): + for id in list(self._signal_connectors.values()): + with contextlib.suppress(Exception): + self._player.disconnect(id) + del self._signal_connectors + GLib.idle_add(lambda: (self.emit("exit", True), False)) + del self._player + + def toggle_shuffle(self): + if self.can_shuffle: + # schedule the shuffle toggle in the GLib idle loop + GLib.idle_add(lambda: (setattr(self, "shuffle", not self.shuffle), False)) + # else do nothing + + def play_pause(self): + if self.can_pause: + GLib.idle_add(lambda: (self._player.play_pause(), False)) + + def next(self): + if self.can_go_next: + GLib.idle_add(lambda: (self._player.next(), False)) + + def previous(self): + if self.can_go_previous: + GLib.idle_add(lambda: (self._player.previous(), False)) + + # Properties + @Property(str, "readable") + def player_name(self) -> int: + return self._player.get_property("player-name") # type: ignore + + @Property(int, "read-write", default_value=0) + def position(self) -> int: + return self._player.get_property("position") # type: ignore + + @position.setter + def position(self, new_pos: int): + self._player.set_position(new_pos) + + @Property(object, "readable") + def metadata(self) -> dict: + return self._player.get_property("metadata") # type: ignore + + @Property(str or None, "readable") + def arturl(self) -> str | None: + if "mpris:artUrl" in self.metadata.keys(): # type: ignore # noqa: SIM118 + return self.metadata["mpris:artUrl"] # type: ignore + return None + + @Property(str or None, "readable") + def length(self) -> str | None: + if "mpris:length" in self.metadata.keys(): # type: ignore # noqa: SIM118 + return self.metadata["mpris:length"] # type: ignore + return None + + @Property(str, "readable") + def artist(self) -> str: + artist = self._player.get_artist() # type: ignore + if isinstance(artist, (list, tuple)): + return ", ".join(artist) + return artist + + @Property(str, "readable") + def album(self) -> str: + return self._player.get_album() # type: ignore + + @Property(str, "readable") + def title(self): + return self._player.get_title() + + @Property(bool, "read-write", default_value=False) + def shuffle(self) -> bool: + return self._player.get_property("shuffle") # type: ignore + + @shuffle.setter + def shuffle(self, do_shuffle: bool): + self.notifier("shuffle") + return self._player.set_shuffle(do_shuffle) + + @Property(str, "readable") + def playback_status(self) -> str: + return { + Playerctl.PlaybackStatus.PAUSED: "paused", + Playerctl.PlaybackStatus.PLAYING: "playing", + Playerctl.PlaybackStatus.STOPPED: "stopped", + }.get(self._player.get_property("playback_status"), "unknown") # type: ignore + + @Property(str, "read-write") + def loop_status(self) -> str: + return { + Playerctl.LoopStatus.NONE: "none", + Playerctl.LoopStatus.TRACK: "track", + Playerctl.LoopStatus.PLAYLIST: "playlist", + }.get(self._player.get_property("loop_status"), "unknown") # type: ignore + + @loop_status.setter + def loop_status(self, status: str): + loop_status = { + "none": Playerctl.LoopStatus.NONE, + "track": Playerctl.LoopStatus.TRACK, + "playlist": Playerctl.LoopStatus.PLAYLIST, + }.get(status) + self._player.set_loop_status(loop_status) if loop_status else None + + @Property(bool, "readable", default_value=False) + def can_go_next(self) -> bool: + return self._player.get_property("can_go_next") # type: ignore + + @Property(bool, "readable", default_value=False) + def can_go_previous(self) -> bool: + return self._player.get_property("can_go_previous") # type: ignore + + @Property(bool, "readable", default_value=False) + def can_seek(self) -> bool: + return self._player.get_property("can_seek") # type: ignore + + @Property(bool, "readable", default_value=False) + def can_pause(self) -> bool: + return self._player.get_property("can_pause") # type: ignore + + @Property(bool, "readable", default_value=False) + def can_shuffle(self) -> bool: + try: + self._player.set_shuffle(self._player.get_property("shuffle")) + return True + except Exception: + return False + + @Property(bool, "readable", default_value=False) + def can_loop(self) -> bool: + try: + self._player.set_shuffle(self._player.get_property("shuffle")) + return True + except Exception: + return False + + +class MprisPlayerManager(Service): + """A service to manage mpris players.""" + + @Signal + def player_appeared(self, player: Playerctl.Player) -> Playerctl.Player: ... + + @Signal + def player_vanished(self, player_name: str) -> str: ... + + def __init__( + self, + **kwargs, + ): + self._manager = Playerctl.PlayerManager.new() + bulk_connect( + self._manager, + { + "name-appeared": self.on_name_appeard, + "name-vanished": self.on_name_vanished, + }, + ) + self.add_players() + super().__init__(**kwargs) + + def on_name_appeard(self, manager, player_name: Playerctl.PlayerName): + logger.info(f"[MprisPlayer] {player_name.name} appeared") + new_player = Playerctl.Player.new_from_name(player_name) + manager.manage_player(new_player) + self.emit("player-appeared", new_player) # type: ignore + + def on_name_vanished(self, manager, player_name: Playerctl.PlayerName): + logger.info(f"[MprisPlayer] {player_name.name} vanished") + self.emit("player-vanished", player_name.name) # type: ignore + + def add_players(self): + for player in self._manager.get_property("player-names"): # type: ignore + self._manager.manage_player(Playerctl.Player.new_from_name(player)) # type: ignore + + @Property(object, "readable") + def players(self): + return self._manager.get_property("players") # type: ignore diff --git a/services/wifi.py b/services/network.py similarity index 100% rename from services/wifi.py rename to services/network.py From b89d4f514e85b23b27b503173e49549731070a19 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 28 Feb 2025 16:22:59 +0545 Subject: [PATCH 09/22] remove unused service --- services/mpris.py | 279 ---------------------------------------------- 1 file changed, 279 deletions(-) delete mode 100644 services/mpris.py diff --git a/services/mpris.py b/services/mpris.py deleted file mode 100644 index 9c5afb5..0000000 --- a/services/mpris.py +++ /dev/null @@ -1,279 +0,0 @@ -import contextlib - -import gi -from fabric.core.service import Property, Service, Signal -from fabric.utils import bulk_connect -from gi.repository import GLib # type: ignore -from loguru import logger - - -class PlayerctlImportError(ImportError): - """An error to raise when playerctl is not installed.""" - - def __init__(self, *args): - super().__init__( - "Playerctl is not installed, please install it first", - *args, - ) - - -try: - gi.require_version("Playerctl", "2.0") - from gi.repository import Playerctl - - # from gi.repository import Playerctl # type: ignore -except ValueError: - raise PlayerctlImportError - - -class MprisPlayer(Service): - """A service to manage a mpris player.""" - - @Signal - def exit(self, value: bool) -> bool: ... - - @Signal - def changed(self) -> None: ... - - def __init__( - self, - player: Playerctl.Player, - **kwargs, - ): - self._signal_connectors: dict = {} - self._player: Playerctl.Player = player - super().__init__(**kwargs) - for sn in ["playback-status", "loop-status", "shuffle", "volume", "seeked"]: - self._signal_connectors[sn] = self._player.connect( - sn, - lambda *args, sn=sn: self.notifier(sn, args), - ) - - self._signal_connectors["exit"] = self._player.connect( - "exit", - self.on_player_exit, - ) - self._signal_connectors["metadata"] = self._player.connect( - "metadata", - lambda *args: self.update_status(), - ) - GLib.idle_add(lambda *args: self.update_status_once()) - - def update_status(self): - # schedule each notifier asynchronously. - def notify_property(prop): - if self.get_property(prop) is not None: - self.notifier(prop) - - for prop in [ - "metadata", - "title", - "artist", - "arturl", - "length", - ]: - GLib.idle_add(lambda p=prop: (notify_property(p), False)) - for prop in [ - "can-seek", - "can-pause", - "can-shuffle", - "can-go-next", - "can-go-previous", - ]: - GLib.idle_add(lambda p=prop: (self.notifier(p), False)) - - def update_status_once(self): - # schedule notifier calls for each property - def notify_all(): - for prop in self.list_properties(): # type: ignore - self.notifier(prop.name) - return False - - GLib.idle_add(notify_all, priority=GLib.PRIORITY_DEFAULT_IDLE) - - def notifier(self, name: str, args=None): - def notify_and_emit(): - self.notify(name) - self.emit("changed") - return False - - GLib.idle_add(notify_and_emit, priority=GLib.PRIORITY_DEFAULT_IDLE) - - def on_player_exit(self, player): - for id in list(self._signal_connectors.values()): - with contextlib.suppress(Exception): - self._player.disconnect(id) - del self._signal_connectors - GLib.idle_add(lambda: (self.emit("exit", True), False)) - del self._player - - def toggle_shuffle(self): - if self.can_shuffle: - # schedule the shuffle toggle in the GLib idle loop - GLib.idle_add(lambda: (setattr(self, "shuffle", not self.shuffle), False)) - # else do nothing - - def play_pause(self): - if self.can_pause: - GLib.idle_add(lambda: (self._player.play_pause(), False)) - - def next(self): - if self.can_go_next: - GLib.idle_add(lambda: (self._player.next(), False)) - - def previous(self): - if self.can_go_previous: - GLib.idle_add(lambda: (self._player.previous(), False)) - - # Properties - @Property(str, "readable") - def player_name(self) -> int: - return self._player.get_property("player-name") # type: ignore - - @Property(int, "read-write", default_value=0) - def position(self) -> int: - return self._player.get_property("position") # type: ignore - - @position.setter - def position(self, new_pos: int): - self._player.set_position(new_pos) - - @Property(object, "readable") - def metadata(self) -> dict: - return self._player.get_property("metadata") # type: ignore - - @Property(str or None, "readable") - def arturl(self) -> str | None: - if "mpris:artUrl" in self.metadata.keys(): # type: ignore # noqa: SIM118 - return self.metadata["mpris:artUrl"] # type: ignore - return None - - @Property(str or None, "readable") - def length(self) -> str | None: - if "mpris:length" in self.metadata.keys(): # type: ignore # noqa: SIM118 - return self.metadata["mpris:length"] # type: ignore - return None - - @Property(str, "readable") - def artist(self) -> str: - artist = self._player.get_artist() # type: ignore - if isinstance(artist, (list, tuple)): - return ", ".join(artist) - return artist - - @Property(str, "readable") - def album(self) -> str: - return self._player.get_album() # type: ignore - - @Property(str, "readable") - def title(self): - return self._player.get_title() - - @Property(bool, "read-write", default_value=False) - def shuffle(self) -> bool: - return self._player.get_property("shuffle") # type: ignore - - @shuffle.setter - def shuffle(self, do_shuffle: bool): - self.notifier("shuffle") - return self._player.set_shuffle(do_shuffle) - - @Property(str, "readable") - def playback_status(self) -> str: - return { - Playerctl.PlaybackStatus.PAUSED: "paused", - Playerctl.PlaybackStatus.PLAYING: "playing", - Playerctl.PlaybackStatus.STOPPED: "stopped", - }.get(self._player.get_property("playback_status"), "unknown") # type: ignore - - @Property(str, "read-write") - def loop_status(self) -> str: - return { - Playerctl.LoopStatus.NONE: "none", - Playerctl.LoopStatus.TRACK: "track", - Playerctl.LoopStatus.PLAYLIST: "playlist", - }.get(self._player.get_property("loop_status"), "unknown") # type: ignore - - @loop_status.setter - def loop_status(self, status: str): - loop_status = { - "none": Playerctl.LoopStatus.NONE, - "track": Playerctl.LoopStatus.TRACK, - "playlist": Playerctl.LoopStatus.PLAYLIST, - }.get(status) - self._player.set_loop_status(loop_status) if loop_status else None - - @Property(bool, "readable", default_value=False) - def can_go_next(self) -> bool: - return self._player.get_property("can_go_next") # type: ignore - - @Property(bool, "readable", default_value=False) - def can_go_previous(self) -> bool: - return self._player.get_property("can_go_previous") # type: ignore - - @Property(bool, "readable", default_value=False) - def can_seek(self) -> bool: - return self._player.get_property("can_seek") # type: ignore - - @Property(bool, "readable", default_value=False) - def can_pause(self) -> bool: - return self._player.get_property("can_pause") # type: ignore - - @Property(bool, "readable", default_value=False) - def can_shuffle(self) -> bool: - try: - self._player.set_shuffle(self._player.get_property("shuffle")) - return True - except Exception: - return False - - @Property(bool, "readable", default_value=False) - def can_loop(self) -> bool: - try: - self._player.set_shuffle(self._player.get_property("shuffle")) - return True - except Exception: - return False - - -class MprisPlayerManager(Service): - """A service to manage mpris players.""" - - @Signal - def player_appeared(self, player: Playerctl.Player) -> Playerctl.Player: ... - - @Signal - def player_vanished(self, player_name: str) -> str: ... - - def __init__( - self, - **kwargs, - ): - self._manager = Playerctl.PlayerManager.new() - bulk_connect( - self._manager, - { - "name-appeared": self.on_name_appeard, - "name-vanished": self.on_name_vanished, - }, - ) - self.add_players() - super().__init__(**kwargs) - - def on_name_appeard(self, manager, player_name: Playerctl.PlayerName): - logger.info(f"[MprisPlayer] {player_name.name} appeared") - new_player = Playerctl.Player.new_from_name(player_name) - manager.manage_player(new_player) - self.emit("player-appeared", new_player) # type: ignore - - def on_name_vanished(self, manager, player_name: Playerctl.PlayerName): - logger.info(f"[MprisPlayer] {player_name.name} vanished") - self.emit("player-vanished", player_name.name) # type: ignore - - def add_players(self): - for player in self._manager.get_property("player-names"): # type: ignore - self._manager.manage_player(Playerctl.Player.new_from_name(player)) # type: ignore - - @Property(object, "readable") - def players(self): - return self._manager.get_property("players") # type: ignore From 54afacbcbf75dbeb8d75a540c2b0d3f1ba10f5cb Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 28 Feb 2025 16:28:06 +0545 Subject: [PATCH 10/22] rename snippets to utils --- snippets/animator.py | 179 --------------------------- snippets/icon.py | 16 --- {snippets => utils}/__init__.py | 5 +- {snippets => utils}/custom_image.py | 0 utils/icon_resolver.py | 103 +++++++++++++++ snippets/iconss.py => utils/icons.py | 6 +- 6 files changed, 109 insertions(+), 200 deletions(-) delete mode 100644 snippets/animator.py delete mode 100644 snippets/icon.py rename {snippets => utils}/__init__.py (84%) rename {snippets => utils}/custom_image.py (100%) create mode 100644 utils/icon_resolver.py rename snippets/iconss.py => utils/icons.py (96%) diff --git a/snippets/animator.py b/snippets/animator.py deleted file mode 100644 index 1799ae6..0000000 --- a/snippets/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/snippets/icon.py b/snippets/icon.py deleted file mode 100644 index 86e77c0..0000000 --- a/snippets/icon.py +++ /dev/null @@ -1,16 +0,0 @@ -from fabric.widgets.label import Label - - -def MaterialIcon(icon_name: str, size: int = 24, props=None): - label_props = { - "label": str(icon_name), # Directly use the provided icon name - "name": "material_icon", - "style": f"font-size: {size}px;", # Set font family for Material Icons - "h_align": "center", # Align horizontally - "v_align": "center", # Align vertically - } - - if props: - label_props.update(props) - - return Label(**label_props) diff --git a/snippets/__init__.py b/utils/__init__.py similarity index 84% rename from snippets/__init__.py rename to utils/__init__.py index d4d3f96..587fe40 100644 --- a/snippets/__init__.py +++ b/utils/__init__.py @@ -1,13 +1,12 @@ -from .animator import * from .custom_image import * -from .icon import * +from .icon_resolver import * import json import os from fabric.utils import get_relative_path def read_config(): - config_path = get_relative_path("../config/assets/launcher.json") + config_path = get_relative_path("../config/assets/modus.json") with open(config_path, "r") as file: # Load JSON data into a Python dictionary diff --git a/snippets/custom_image.py b/utils/custom_image.py similarity index 100% rename from snippets/custom_image.py rename to utils/custom_image.py diff --git a/utils/icon_resolver.py b/utils/icon_resolver.py new file mode 100644 index 0000000..cd814c7 --- /dev/null +++ b/utils/icon_resolver.py @@ -0,0 +1,103 @@ +import json +import os +import re + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import GLib, Gtk +from loguru import logger + + +# TODO WIP +# Idea: nearest string matching algorithm +# if already exists: retrieve from json +# if found: stor in json: app_id -> icon_name +# if not found: store in json -> misisng-icon + + +CACHE_DIR = str(GLib.get_user_cache_dir()) + "/modus" +ICON_CACHE_FILE = CACHE_DIR + "/icons.json" +if not os.path.exists(CACHE_DIR): + os.makedirs(CACHE_DIR) + + +class IconResolver: + def __init__( + self, default_applicaiton_icon: str = "application-x-executable-symbolic" + ): + if os.path.exists(ICON_CACHE_FILE): + f = open(ICON_CACHE_FILE) + try: + self._icon_dict = json.load(f) + except json.JSONDecodeError: + logger.info("[ICONS] Cache file does not exist or is corrupted") + f.close() + else: + self._icon_dict = {} + + self.default_applicaiton_icon = default_applicaiton_icon + + def get_icon_name(self, app_id: str): + if app_id in self._icon_dict: + return self._icon_dict[app_id] + new_icon = self._compositor_find_icon(app_id) + logger.info( + f"[ICONS] found new icon: '{new_icon}' for app id: '{app_id}', storing..." + ) + self._store_new_icon(app_id, new_icon) + return new_icon + + def get_icon_pixbuf(self, app_id: str, size: int = 16): + return Gtk.IconTheme.get_default().load_icon( + self.get_icon_name(app_id), + size, + Gtk.IconLookupFlags.FORCE_SIZE, + ) + + def _store_new_icon(self, app_id: str, icon: str): + self._icon_dict[app_id] = icon + with open(ICON_CACHE_FILE, "w") as f: + json.dump(self._icon_dict, f) + f.close() + + def _get_icon_from_desktop_file(self, desktop_file_path: str): + # TODO: get icon in the [Desktop Entry] section only + with open(desktop_file_path) as f: + for line in f.readlines(): + if "Icon=" in line: + return "".join(line[5:].split()) + return self.default_applicaiton_icon + + def _get_desktop_file(self, app_id: str) -> str | None: + data_dirs = GLib.get_system_data_dirs() + for data_dir in data_dirs: + data_dir = data_dir + "/applications/" + if os.path.exists(data_dir): + # Do name resolving here + + files = os.listdir(data_dir) + matching = [ + s for s in files if "".join(app_id.lower().split()) in s.lower() + ] + if matching: + return data_dir + matching[0] + + for word in list(filter(None, re.split(r"-|\.|_|\s", app_id))): + matching = [s for s in files if word.lower() in s.lower()] + if matching: + return data_dir + matching[0] + + return None + + def _compositor_find_icon(self, app_id: str): + if Gtk.IconTheme.get_default().has_icon(app_id): + return app_id + if Gtk.IconTheme.get_default().has_icon(app_id + "-desktop"): + return app_id + "-desktop" + desktop_file = self._get_desktop_file(app_id) + return ( + self._get_icon_from_desktop_file(desktop_file) + if desktop_file + else self.default_applicaiton_icon + ) diff --git a/snippets/iconss.py b/utils/icons.py similarity index 96% rename from snippets/iconss.py rename to utils/icons.py index 78eb34d..79c2407 100644 --- a/snippets/iconss.py +++ b/utils/icons.py @@ -53,8 +53,8 @@ shutdown: str = "" power_saving: str = "" power_balanced: str = "勺" power_performance: str = "" -charging: str = "𐁣" -discharging: str = "𐁪" +charging: str = "" +discharging: str = "" alert: str = "" # Applets @@ -64,6 +64,7 @@ night: str = "" coffee: str = "" dnd: str = "" darkmode: str = "﹖" +world: str = "" wifi_off: str = "" bluetooth_off: str = "" @@ -128,6 +129,7 @@ disc_off: str = "" # Misc dot: str = "" +palette: str = "" exceptions: list[str] = ["font_family", "font_weight", "span"] From 7d7407df3ee873e2ada94f553b6cb01f0a176768 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 28 Feb 2025 16:28:32 +0545 Subject: [PATCH 11/22] added dock --- modules/dock.py | 196 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 modules/dock.py diff --git a/modules/dock.py b/modules/dock.py new file mode 100644 index 0000000..593b2c6 --- /dev/null +++ b/modules/dock.py @@ -0,0 +1,196 @@ +import json +import threading +from gi.repository import GLib +from utils import IconResolver, read_config +from fabric.widgets.box import Box +from fabric.widgets.button import Button +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, idle_add, remove_handler + + +class Dock(Window): + def __init__(self, **kwargs): + super().__init__( + layer="top", + anchor="bottom center", + exclusivity="none", + **kwargs, + ) + self.config = read_config() + self.conn = get_hyprland_connection() + self.icon = IconResolver() + self.pinned = self.config.get("pinned_apps", []) + self.is_hidden = False + self.hide_id = None + self._arranger_handler = None + + # Set up UI containers + self.view = Box(name="viewport", orientation="h") + self.wrapper = Box(name="dock", orientation="v", children=[self.view]) + self.hover = EventBox() + self.hover.set_size_request(-1, 1) + self.hover.connect("enter-notify-event", self._on_hover_enter) + self.hover.connect("leave-notify-event", self._on_hover_leave) + self.view.connect("enter-notify-event", self._on_hover_enter) + self.view.connect("leave-notify-event", self._on_hover_leave) + self.main_box = Box(orientation="v", children=[self.wrapper, self.hover]) + self.add(self.main_box) + + if self.conn.ready: + self.update_dock() + GLib.timeout_add( + 500, self.check_hide + ) # Delay to ensure Hyprland loads clients + else: + self.conn.connect("event::ready", self.update_dock) + self.conn.connect("event::ready", self.check_hide) + + for ev in ("activewindow", "openwindow", "closewindow", "changefloatingmode"): + self.conn.connect(f"event::{ev}", self.update_dock) + self.conn.connect("event::workspace", self.check_hide) + + def _on_hover_enter(self, *args): + self.toggle_dock(show=True) + + def _on_hover_leave(self, *args): + self.delay_hide() + + def toggle_dock(self, show): + """Show or hide the dock immediately.""" + if show: + if self.is_hidden: + self.is_hidden = False + self.wrapper.add_style_class("show-dock") + self.wrapper.remove_style_class("hide-dock") + if self.hide_id: + GLib.source_remove(self.hide_id) + self.hide_id = None + else: + if not self.is_hidden: + self.is_hidden = True + self.wrapper.add_style_class("hide-dock") + self.wrapper.remove_style_class("show-dock") + + def delay_hide(self): + """Schedule hiding the dock after a short delay.""" + if self.hide_id: + GLib.source_remove(self.hide_id) + self.hide_id = GLib.timeout_add(1000, self.hide_dock) + + def hide_dock(self): + self.toggle_dock(show=False) + self.hide_id = None + return False + + def check_hide(self, *args): + """Show the dock immediately in an empty workspace; otherwise, hide if needed.""" + clients = self.get_clients() + current_ws = self.get_workspace() + ws_clients = [w for w in clients if w["workspace"]["id"] == current_ws] + + if not ws_clients: + self.toggle_dock(show=True) + elif any(not w.get("floating") and not w.get("fullscreen") for w in ws_clients): + self.delay_hide() + else: + self.toggle_dock(show=True) + + def update_dock(self, *args): + """Refresh the dock icons based on running and pinned apps.""" + arranger_handler = getattr(self, "_arranger_handler", None) + + if arranger_handler: + remove_handler(arranger_handler) + clients = self.get_clients() + running = {} + for c in clients: + key = c["initialClass"].lower() + running.setdefault(key, []).append(c) + pinned_buttons = [ + self.create_button(app, running.get(app.lower(), [])) for app in self.pinned + ] + open_buttons = [ + self.create_button( + c["initialClass"], running.get(c["initialClass"].lower(), []) + ) + for group in running.values() + for c in group + if c["initialClass"].lower() not in self.pinned + ] + children = pinned_buttons + if pinned_buttons and open_buttons: + children += [Box(orientation="v", v_expand=True, name="dock-separator")] + children += open_buttons + self.view.children = children + idle_add(self._update_size) + + def _update_size(self): + width, _ = self.view.get_preferred_width() + self.set_size_request(width, -1) + return False + + def create_button(self, app, instances): + """Create a button for an app with an icon and an optional indicator.""" + icon_img = self.icon.get_icon_pixbuf( + app.lower(), 30 + ) or self.icon.get_icon_pixbuf("image-missing", 30) + items = [Image(pixbuf=icon_img)] + if instances: + items.append( + Image( + pixbuf=self.icon.get_icon_pixbuf("filename-dot-amarok", 11), + name="dock-app-indicator", + ) + ) + return Button( + child=Box( + name="dock-icon", + orientation="v", + h_align="center", + children=items, + ), + on_clicked=lambda *a: self.handle_app(app, instances), + tooltip_text=instances[0]["title"] if instances else app, + name="dock-app-button", + ) + + def handle_app(self, app, instances): + if not instances: + threading.Thread( + target=exec_shell_command, args=(f"nohup {app}",), daemon=True + ).start() + else: + focused = self.get_focused() + idx = next( + (i for i, inst in enumerate(instances) if inst["address"] == focused), + -1, + ) + next_inst = instances[(idx + 1) % len(instances)] + exec_shell_command( + f"hyprctl dispatch focuswindow address:{next_inst['address']}" + ) + + def get_clients(self): + try: + return json.loads(self.conn.send_command("j/clients").reply.decode()) + except json.JSONDecodeError: + return [] + + def get_focused(self): + try: + return json.loads( + self.conn.send_command("j/activewindow").reply.decode() + ).get("address", "") + except json.JSONDecodeError: + return "" + + def get_workspace(self): + try: + return json.loads( + self.conn.send_command("j/activeworkspace").reply.decode() + ).get("id", 0) + except json.JSONDecodeError: + return 0 From f5153d8f291653cd81521cef97db98df66e06909 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 28 Feb 2025 16:28:46 +0545 Subject: [PATCH 12/22] removed corner.py --- modules/corners.py | 64 ---------------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 modules/corners.py diff --git a/modules/corners.py b/modules/corners.py deleted file mode 100644 index 10ce26d..0000000 --- a/modules/corners.py +++ /dev/null @@ -1,64 +0,0 @@ -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() From 5d88e059d99cd5a5c3a26145291ab95be12e3736 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 28 Feb 2025 16:28:58 +0545 Subject: [PATCH 13/22] update --- main.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index dba0ee4..ad71f77 100644 --- a/main.py +++ b/main.py @@ -5,12 +5,13 @@ from fabric import Application from fabric.utils import get_relative_path, monitor_file from loguru import logger -from modules.bar.bar import Bar +from modules.bar.bar import Bar, ScreenCorners from modules.launcher.launcher import Launcher +from modules.dock import Dock from modules.notification_popup import NotificationPopup from modules.osd import OSD -from modules.corners import Corners + from services import sc from config.config import open_config @@ -50,7 +51,8 @@ if __name__ == "__main__": open_config() sc = sc bar = Bar() - corners = Corners() + dock = Dock() + corners = ScreenCorners() osd = OSD() notif = NotificationPopup() launcher = Launcher() @@ -60,6 +62,7 @@ if __name__ == "__main__": bar, launcher, osd, + dock, ) setproctitle.setproctitle("modus") From 4350f7459e305a022c4283779b733cd0dfe8fcfc Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 28 Feb 2025 16:44:38 +0545 Subject: [PATCH 14/22] huge updates but idk what i update --- modules/bar/bar.py | 184 ++++++--- modules/bar/components/__init__.py | 5 +- modules/bar/components/battery.py | 8 +- .../bar/components/{stats.py => metric.py} | 30 +- .../{indicators.py => system_indicators.py} | 20 +- modules/bar/components/taskbar.py | 154 -------- modules/bar/components/updates.py | 4 +- modules/launcher/components/applauncher.py | 320 ++++++--------- modules/launcher/components/bluetooth.py | 2 +- modules/launcher/components/calendar.py | 6 +- modules/launcher/components/cliphist.py | 30 +- modules/launcher/components/dashboard.py | 2 +- modules/launcher/components/emoji.py | 26 +- modules/launcher/components/power.py | 3 +- modules/launcher/components/sh.py | 2 +- modules/launcher/components/todo.py | 4 +- modules/launcher/components/wallpapers.py | 368 +++++++++++------- modules/launcher/components/wifi.py | 2 +- modules/launcher/launcher.py | 10 +- 19 files changed, 563 insertions(+), 617 deletions(-) rename modules/bar/components/{stats.py => metric.py} (77%) rename modules/bar/components/{indicators.py => system_indicators.py} (88%) delete mode 100644 modules/bar/components/taskbar.py diff --git a/modules/bar/bar.py b/modules/bar/bar.py index dc91a6e..278e228 100644 --- a/modules/bar/bar.py +++ b/modules/bar/bar.py @@ -1,3 +1,4 @@ +from typing import Literal from fabric.hyprland.widgets import Language from fabric.system_tray.widgets import SystemTray from fabric.utils import ( @@ -5,24 +6,76 @@ from fabric.utils import ( bulk_replace, exec_shell_command_async, get_relative_path, + bulk_connect, ) +from fabric.widgets.shapes import Corner from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox from fabric.widgets.datetime import DateTime from fabric.widgets.label import Label +from fabric.widgets.revealer import Revealer from fabric.widgets.wayland import WaylandWindow as Window from gi.repository import GLib from modules.bar.components import ( - BatteryLabel, - SystemInfo, - TaskBar, + Battery, + Metrics, workspace, - Indicators, + SystemIndicators, UpdatesWidget, ) from services import sc -import snippets.iconss as icons +import utils.icons as icons + + +class StatusBarCorner(Box): + def __init__(self, corner: Literal["top-right", "top-left"]): + super().__init__( + style="margin-bottom: 15px;", + name="bar-corner", + children=Corner( + orientation=corner, + size=15, + ), + ) + + +class Tray(Box): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.tray = Box( + name="left-tray", + children=[SystemTray(name="tray", icon_size=16, spacing=4)], + ) + self.profile = Button( + child=Label(name="tray-revealer", markup=icons.chevron_left) + ) + self.revealer = Revealer(transition_type="slide-left", transition_duration=1000) + self.revealer.add(self.tray) + + bulk_connect( + self.profile, + { + "enter-notify-event": self._on_enter, + "leave-notify-event": self._on_leave, + "button-press-event": self.toggle_revealer, + }, + ) + + self.add(self.profile) + self.add(self.revealer) + + def _on_enter(self, *args): + self.profile.set_cursor("pointer") + + def _on_leave(self, *args): + self.profile.set_cursor("default") + + def toggle_revealer(self, *args): + new_state = not self.revealer.get_reveal_child() + self.revealer.set_reveal_child(new_state) + new_icon = icons.chevron_right if new_state else icons.chevron_left + self.profile.get_child().set_markup(new_icon) class Bar(Window): @@ -39,7 +92,7 @@ class Bar(Window): ), ), ) - + self.bar_content = CenterBox(name="center-bar") self.recording_indicator = Button( name="recording-indicator", child=Label(name="recorder", markup=icons.record), @@ -47,16 +100,10 @@ class Bar(Window): on_clicked=lambda *_: sc.screencast_stop(), ) - sc.connect( - "recording", lambda _, status: self.on_recording_status_change(status) - ) + sc.connect("recording", self.on_recording_status_change) self.date_time = DateTime(formatters=["%-I:%M 󰧞 %a %d %b"], name="datetime") - - self.battery = BatteryLabel() - self.taskbar = TaskBar() - self.stats = SystemInfo() - self.tray = SystemTray(name="tray", icon_size=16, spacing=4) + self.battery = Battery() self.launcher = Button( name="logo", child=Label(name="logo-name", label="󰣇 󰫿󰫰󰫵"), @@ -64,8 +111,6 @@ class Bar(Window): "fabric-cli exec modus 'launcher.open(\"launcher\")'" ), ) - self.updates = UpdatesWidget() - self.indicators = Indicators() self.button_config = Button( name="button-bar", on_clicked=lambda *_: exec_shell_command_async( @@ -73,35 +118,22 @@ class Bar(Window): ), child=Label(name="button-bar-label", markup=icons.config), ) - + self.stats = Metrics() + self.updates = UpdatesWidget() + self.tray = Tray() + self.indicators = SystemIndicators() self.applets = Box( - name="applets", + name="system-indicators", spacing=4, orientation="h", - children=[ - self.language, - self.indicators, - ], + children=[self.language, self.indicators], ) - self.bar = CenterBox( - name="bar", - start_children=Box( - name="start-container", - spacing=8, - orientation="h", - children=[self.launcher, self.workspaces, self.stats, self.updates], - ), - center_children=Box( - name="center-container", - spacing=8, - orientation="h", - children=self.taskbar, - ), - end_children=Box( - name="end-container", - spacing=8, - orientation="h", + self.bar_content.end_children = [ + StatusBarCorner("top-right"), + Box( + name="bar", + spacing=4, children=[ self.recording_indicator, self.tray, @@ -110,27 +142,79 @@ class Bar(Window): self.date_time, self.button_config, ], + style_classes="end-container", ), - ) + ] + + self.bar_content.start_children = [ + Box( + name="bar", + spacing=4, + children=[self.launcher, self.workspaces, self.stats, self.updates], + style_classes="start-container", + ), + StatusBarCorner("top-left"), + ] + super().__init__( layer="top", - anchor="left bottom right", + anchor="left top right", exclusivity="auto", visible=True, - child=self.bar, + child=self.bar_content, ) - self.hidden = False - def on_recording_status_change(self, status): - print(f"Recording status changed: {status}") + def on_recording_status_change(self, _, status): self.recording_indicator.set_visible(status) def toggle_hidden(self): self.hidden = not self.hidden if self.hidden: - self.bar.remove_style_class("visible") - self.bar.add_style_class("hidden") + self.bar_content.add_style_class("hidden") + self.bar_content.remove_style_class("visible") + self.set_property("visible", False) else: - self.bar.remove_style_class("hidden") - self.bar.add_style_class("visible") + self.bar_content.add_style_class("visible") + self.bar_content.remove_style_class("hidden") + self.set_property("visible", True) + + +class ScreenCorners(Window): + def __init__(self): + super().__init__( + layer="top", + anchor="top left bottom right", + pass_through=True, + child=Box( + orientation="vertical", + children=[ + Box( + children=[ + self.make_corner("top-left"), + Box(h_expand=True), + self.make_corner("top-right"), + ] + ), + Box(v_expand=True), + Box( + children=[ + self.make_corner("bottom-left"), + Box(h_expand=True), + self.make_corner("bottom-right"), + ] + ), + ], + ), + ) + + def make_corner(self, orientation) -> Box: + return Box( + h_expand=False, + v_expand=False, + name="bar-corner", + children=Corner( + orientation=orientation, + size=15, + ), + ) diff --git a/modules/bar/components/__init__.py b/modules/bar/components/__init__.py index 116ed6d..4cba446 100644 --- a/modules/bar/components/__init__.py +++ b/modules/bar/components/__init__.py @@ -1,6 +1,5 @@ from .battery import * -from .stats import * -from .taskbar import * +from .metric import * from .workspace import * -from .indicators import * +from .system_indicators import * from .updates import * diff --git a/modules/bar/components/battery.py b/modules/bar/components/battery.py index 60440b6..5707911 100644 --- a/modules/bar/components/battery.py +++ b/modules/bar/components/battery.py @@ -10,10 +10,10 @@ from fabric.widgets.revealer import Revealer from fabric.core.fabricator import Fabricator from fabric.utils.helpers import exec_shell_command_async from gi.repository import GLib -import snippets.iconss as icons +import utils.icons as icons -class BatteryLabel(Box): +class Battery(Box): def __init__(self, **kwargs): super().__init__( name="battery", @@ -59,8 +59,8 @@ class BatteryLabel(Box): value=0, size=28, line_width=2, - start_angle=180, - end_angle=360, + start_angle=150, + end_angle=390, ) self.bat_overlay = Overlay( diff --git a/modules/bar/components/stats.py b/modules/bar/components/metric.py similarity index 77% rename from modules/bar/components/stats.py rename to modules/bar/components/metric.py index 9b5c5ab..d0e026a 100644 --- a/modules/bar/components/stats.py +++ b/modules/bar/components/metric.py @@ -5,11 +5,10 @@ from fabric.widgets.label import Label from fabric.widgets.box import Box from fabric.widgets.overlay import Overlay from fabric.widgets.circularprogressbar import CircularProgressBar - -import snippets.iconss as icons +import utils.icons as icons -class SystemInfo(Box): +class Metrics(Box): ICONS = { "CPU": icons.cpu, "RAM": icons.memory, @@ -20,7 +19,7 @@ class SystemInfo(Box): def __init__(self, **kwargs): super().__init__( - name="system-info", + name="metrics", spacing=8, h_align="center", v_align="fill", @@ -30,14 +29,12 @@ class SystemInfo(Box): self.progress_bars = { system: CircularProgressBar( - name="stats-progress", + name="metric-circle", line_style="round", - line_width=3, + line_width=2, size=28, - start_at=0, - end_at=0.5, - start_angle=0, - end_angle=180, + start_angle=150, + end_angle=390, ) for system in self.ICONS } @@ -45,7 +42,7 @@ class SystemInfo(Box): for system, icon_name in self.ICONS.items(): overlay = Overlay( child=self.progress_bars[system], - overlays=[Label(name="stats-icons", markup=icon_name)], + overlays=[Label(name="metric-icon", markup=icon_name)], ) self.add(overlay) @@ -55,14 +52,17 @@ class SystemInfo(Box): ram_usage = psutil.virtual_memory().percent swap_usage = psutil.swap_memory().percent disk_usage = psutil.disk_usage("/").percent - cpu = psutil.cpu_percent(interval=0) + cpu_usage = psutil.cpu_percent(interval=0) + + temp = self._get_device_temperature() + temp_usage = temp if temp is not None else 0 usages = { - "CPU": cpu, + "CPU": cpu_usage, "RAM": ram_usage, "Swap": swap_usage, "Disk": disk_usage, - "Temp": self._get_device_temperature(), + "Temp": temp_usage, } for system, usage in usages.items(): @@ -76,7 +76,7 @@ class SystemInfo(Box): try: temps = psutil.sensors_temperatures() for key in ("coretemp", "cpu_thermal"): - if key in temps: + if key in temps and temps[key]: return round(temps[key][0].current, 1) except Exception: pass diff --git a/modules/bar/components/indicators.py b/modules/bar/components/system_indicators.py similarity index 88% rename from modules/bar/components/indicators.py rename to modules/bar/components/system_indicators.py index 5bf8fe8..0f49299 100644 --- a/modules/bar/components/indicators.py +++ b/modules/bar/components/system_indicators.py @@ -4,19 +4,19 @@ from fabric.widgets.label import Label from fabric.widgets.box import Box from fabric.bluetooth import BluetoothClient from services import NetworkClient, audio -import snippets.iconss as icons +import utils.icons as icons -class Indicators(Box): +class SystemIndicators(Box): def __init__(self, **kwargs): super().__init__(orientation="h", spacing=2, **kwargs) - self.bluetooth_icon = Label(name="indicator-icon") - self.wifi_icon = Label(name="indicator-icon") - self.volume_icon_button = Label(name="indicator-icon") - self.microphone_icon = Label(name="indicator-icon") - self.idle_label = Label(name="indicator-icon") - self.night_label = Label(name="indicator-icon") + 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.idle_label = Label(name="system-indicator-icon") + self.night_label = Label(name="system-indicator-icon") for widget in [ self.bluetooth_icon, @@ -86,8 +86,8 @@ class Indicators(Box): if stream.muted else ( icons.vol_mute - if volume_level == 0 - else (icons.vol_medium if volume_level <= 50 else icons.vol_high) + if volume_level < 1 + else (icons.vol_medium if volume_level >= 1 else icons.vol_high) ) ) diff --git a/modules/bar/components/taskbar.py b/modules/bar/components/taskbar.py deleted file mode 100644 index b54d0bb..0000000 --- a/modules/bar/components/taskbar.py +++ /dev/null @@ -1,154 +0,0 @@ -import json -import os -from typing import TypedDict - -from fabric.hyprland.widgets import get_hyprland_connection -from fabric.utils import exec_shell_command_async -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.image import Image -from gi.repository import GdkPixbuf, GLib, Gtk -from loguru import logger - - -class PagerClient(TypedDict): - title: str - initialClass: str - mapped: bool - hidden: bool - address: str - - -class TaskBar(Box): - def __init__(self, icon_size: int = 22, **kwargs): - super().__init__( - orientation="h", - spacing=7, - name="taskbar", - **kwargs, - ) - self.connection = get_hyprland_connection() - self.icon_size = icon_size - self.icon_theme = Gtk.IconTheme.get_default() - - self.set_visible(False) - - if self.connection.ready: - self.render_with_delay() - else: - self.connection.connect("event::ready", lambda *_: self.render_with_delay()) - - for event in ( - "activewindow", - "openwindow", - "closewindow", - "changefloatingmode", - ): - self.connection.connect("event::" + event, lambda *_: self.render(None)) - - def render_with_delay(self, *_): - GLib.timeout_add(101, self.render) - - def render(self, *_): - self.children = [] - - clients = self.fetch_clients() - active_window_address = self.get_active_window_address() - - visible_clients = [ - client for client in clients if client["mapped"] and not client["hidden"] - ] - - if visible_clients: - for client in visible_clients: - window_class = client["initialClass"].lower() - icon = self.bake_window_icon(window_class) - - button = Button(image=icon, tooltip_text=client["title"]) - if client["address"] == active_window_address: - button.add_style_class("activated") - button.connect( - "button-press-event", - self.on_icon_click, - client["address"], - ) - - self.add(button) - self.set_visible(True) - self.show_all() - else: - self.set_visible(False) - - self.set_visible(len(self.children) > 0) - if self.children: - self.show_all() - - def get_active_window_address(self) -> str: - active_window_info = json.loads( - self.connection.send_command("j/activewindow").reply.decode() - ) - return active_window_info.get("address", "") - - def on_icon_click(self, widget, event, address): - exec_shell_command_async( - f"hyprctl dispatch focuswindow address:{address}", lambda *_: None - ) - - def fetch_clients(self) -> list[PagerClient]: - return json.loads(self.connection.send_command("j/clients").reply.decode()) - - def bake_window_icon( - self, - window_class: str, - fallback_icon: str = "image-missing", - ) -> Image: - icon_name = self.get_icon_from_desktop_entry(window_class) - - if icon_name: - pixbuf = self.load_icon(icon_name) - else: - pixbuf = self.load_icon(window_class, fallback_icon) - - return Image(pixbuf=pixbuf, size=self.icon_size) - - def get_icon_from_desktop_entry(self, window_class: str) -> str: - for data_dir in GLib.get_system_data_dirs(): - applications_dir = os.path.join(data_dir, "applications") - - if os.path.isdir(applications_dir): - for desktop_file in os.listdir(applications_dir): - if desktop_file.endswith(".desktop"): - file_path = os.path.join(applications_dir, desktop_file) - try: - with open(file_path) as f: - app_name = None - icon_name = None - for line in f: - if line.startswith("Name="): - app_name = line.split("=", 2)[1].strip().lower() - elif line.startswith("Icon=") and app_name: - icon_name = line.split("=", 2)[1].strip() - if window_class in app_name: - return icon_name - except Exception as e: - logger.error(f"Error reading {file_path}: {e}") - return None - - def load_icon( - self, - icon_name: str, - fallback_icon: str = "image-missing", - ) -> GdkPixbuf.Pixbuf: - try: - pixbuf = self.icon_theme.load_icon( - icon_name, - self.icon_size, - Gtk.IconLookupFlags.FORCE_SIZE, - ) - except Exception: - pixbuf = self.icon_theme.load_icon( - fallback_icon, - self.icon_size, - Gtk.IconLookupFlags.FORCE_SIZE, - ) - return pixbuf diff --git a/modules/bar/components/updates.py b/modules/bar/components/updates.py index 0674d3e..a285479 100644 --- a/modules/bar/components/updates.py +++ b/modules/bar/components/updates.py @@ -3,11 +3,11 @@ from fabric.utils import exec_shell_command_async, get_relative_path, invoke_rep 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 -import snippets.iconss as icons from loguru import logger from gi.repository import Gtk -from fabric.widgets.button import Button +import utils.icons as icons class UpdatesWidget(Button): diff --git a/modules/launcher/components/applauncher.py b/modules/launcher/components/applauncher.py index 69669b3..fb9ce05 100644 --- a/modules/launcher/components/applauncher.py +++ b/modules/launcher/components/applauncher.py @@ -1,62 +1,49 @@ from collections.abc import Iterator -import operator from fabric.utils import DesktopApp, get_desktop_applications, idle_add, remove_handler 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 -from gi.repository import GLib, Gdk -from snippets import read_config from fabric.widgets.image import Image +from gi.repository import Gdk, GLib import json import os -import re import math +import re import subprocess import webbrowser +from functools import partial +from utils import read_config class AppLauncher(Box): def __init__(self, **kwargs): super().__init__( - name="app-launcher", - visible=False, - all_visible=False, - **kwargs, + name="app-launcher", visible=False, all_visible=False, **kwargs ) + self.scrolled_window = None self.launcher = kwargs["launcher"] self.selected_index = -1 - self._arranger_handler: int = 0 + self._arranger_handler = 0 self._all_apps = get_desktop_applications() self.config = read_config() - # Calculator history initialization self.calc_history_path = os.path.expanduser("~/.cache/modus/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.calc_history = self._load_calc_history() self.viewport = Box(name="viewport", spacing=4, orientation="v") self.search_entry = Entry( name="search-entry", 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() - ), + notify_text=self._on_notify_text, + on_activate=self._on_activate, on_key_press_event=self.on_search_entry_key_press, ) - self.scrolled_window = None - self.header_box = Box( - name="header-box", - orientation="h", - children=[self.search_entry], + name="header-box", orientation="h", children=[self.search_entry] ) self.launcher_box = Box( @@ -69,28 +56,43 @@ class AppLauncher(Box): self.add(self.launcher_box) self.show_all() + def _load_calc_history(self): + if os.path.exists(self.calc_history_path): + with open(self.calc_history_path, "r") as f: + return json.load(f) + return [] + + def _on_notify_text(self, entry, *args): + self.arrange_viewport(entry.get_text()) + + def _on_activate(self, entry, *args): + self.on_search_entry_activate(entry.get_text()) + def close_launcher(self, *_): self.viewport.children = [] self.selected_index = -1 self.launcher.close() + self.hide_scrolled_window() def open_launcher(self): self._all_apps = get_desktop_applications() self.arrange_viewport("") def arrange_viewport(self, query: str = ""): - if query.startswith(":"): # Don't show ScrolledWindow for web search - self.destroy_scrolled_window() + if query.startswith(":"): + self.hide_scrolled_window() self.launcher.dashboard.hide() return + if query.startswith("="): self.show_scrolled_window() - # In calculator mode, update history view once (not per keystroke) self.update_calculator_viewport() self.launcher.dashboard.hide() return - remove_handler(self._arranger_handler) if self._arranger_handler else None + if self._arranger_handler: + remove_handler(self._arranger_handler) + self._arranger_handler = 0 self.viewport.children = [] self.selected_index = -1 @@ -99,171 +101,140 @@ class AppLauncher(Box): self.show_scrolled_window() else: self.launcher.dashboard.show() - self.destroy_scrolled_window() + self.hide_scrolled_window() return False + filtered_apps = [ + app + for app in self._all_apps + if query.casefold() + in ( + (app.display_name or "") + + " " + + app.name + + " " + + (app.generic_name or "") + ).casefold() + ] 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(), - ) + sorted(filtered_apps, 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, + partial(self._arranger_callback, query=query), filtered_apps_iter, pin=True ) + def _arranger_callback(self, apps_iter: Iterator[DesktopApp], query: str): + if self.add_next_application(apps_iter): + return True + return self.handle_arrange_complete(query) + def show_scrolled_window(self): if self.scrolled_window is None: self.scrolled_window = ScrolledWindow( name="scrolled-window", - spacing=10, - min_content_size=(-1, -1), + style_classes="applauncher-scrolled-window", h_scrollbar_policy="never", child=self.viewport, - v_expand=True, ) self.launcher_box.add(self.scrolled_window) - self.scrolled_window.show_all() - self.get_style_context().add_class("applauncher") - def destroy_scrolled_window(self): - if hasattr(self, "scrolled_window") and self.scrolled_window: - # self.scrolled_window.destroy() - self.launcher_box.remove(self.scrolled_window) - self.scrolled_window = None + self.scrolled_window.show_all() + GLib.idle_add(self.update_scrolled_window_size) - self.get_style_context().remove_class("applauncher") + def hide_scrolled_window(self): + if self.scrolled_window: + self.scrolled_window.hide() - 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(): + def handle_arrange_complete(self, query): + self.update_scrolled_window_size() + 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)): + app = next(apps_iter, None) + if app is None: return False - self.viewport.add(self.bake_application_slot(app)) return True - def bake_application_slot(self, app: DesktopApp, **kwargs) -> Button: - button = Button( + def bake_application_slot(self, app: DesktopApp) -> Button: + return Button( name="launcher-app", child=Box( orientation="h", children=[ - Image( - pixbuf=app.get_icon_pixbuf(size=32), - h_align="start", - name="launcher-app-icon", - ), + Image(pixbuf=app.get_icon_pixbuf(size=32), h_align="start"), Label( label=app.display_name or "Unknown", ellipsization="end", v_align="center", - h_align="center", ), ], ), tooltip_text=app.description, - on_clicked=lambda *_: (app.launch(), self.close_launcher()), - **kwargs, + on_clicked=partial(self._on_app_clicked, app), ) - return button + + def _on_app_clicked(self, app, *args): + app.launch() + self.close_launcher() def update_selection(self, new_index: int): - 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") + children = self.viewport.get_children() + if self.selected_index >= 0 < len(children): + children[self.selected_index].get_style_context().remove_class("selected") - 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") + if 0 <= new_index < len(children): + children[new_index].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 + def update_scrolled_window_size(self): + if self.scrolled_window is None: + return - y = alloc.y - height = alloc.height - page_size = adj.get_page_size() - current_value = adj.get_value() + children = self.viewport.get_children() + item_count = len(children) - visible_top = current_value - visible_bottom = current_value + page_size + if item_count == 0: + self.scrolled_window.set_size_request(-1, 0) + return - if y < visible_top: - adj.set_value(y) - elif y + height > visible_bottom: - new_value = y + height - page_size - adj.set_value(new_value) - return False + first_child = children[0] + first_child_height = first_child.get_allocation().height - GLib.idle_add(scroll) + if first_child_height == 0: + GLib.idle_add(self.update_scrolled_window_size) + return + + spacing = self.viewport.props.spacing + total_height = (item_count * first_child_height) + ((item_count - 1) * spacing) + + max_height = 400 + self.scrolled_window.set_size_request(-1, min(total_height, max_height)) def on_search_entry_activate(self, text): - commands = self.config.get("commands", {}) - if text.startswith(":i "): # Detecting ':i' search - search_query = text[3:].strip() # Extract query after ':i ' - if search_query: - url = f"https://www.google.com/search?q={search_query}" - webbrowser.open(url) - self.close_launcher() + if text.startswith(":i "): + webbrowser.open(f"https://www.google.com/search?q={text[3:].strip()}") + self.close_launcher() return if text.startswith("="): - # If in calculator mode and no history item is selected, evaluate new expression. - if self.selected_index == -1: - self.evaluate_calculator_expression(text) + self.evaluate_calculator_expression(text) return - if text in commands: - command = commands[text] - exec(command, {"launcher": self.launcher}) - else: - children = self.viewport.get_children() - if children: - if text.strip() == "" and self.selected_index == -1: - return - selected_index = self.selected_index if self.selected_index != -1 else 0 - if 0 <= selected_index < len(children): - children[selected_index].clicked() + children = self.viewport.get_children() + if children: + (children[self.selected_index or 0]).clicked() def on_search_entry_key_press(self, widget, event): - keyval = event.keyval - if keyval == Gdk.KEY_Down: - self.move_selection(1) + if event.keyval in (Gdk.KEY_Down, Gdk.KEY_Up): + self.move_selection(1 if event.keyval == Gdk.KEY_Down else -1) return True - elif keyval == Gdk.KEY_Up: - self.move_selection(-1) - return True - elif keyval == Gdk.KEY_Escape: + if event.keyval == Gdk.KEY_Escape: self.close_launcher() return True return False @@ -272,81 +243,32 @@ class AppLauncher(Box): children = self.viewport.get_children() if not children: return - 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)) + new_index = max(0, min(self.selected_index + delta, 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): - # Remove the '=' prefix and extra spaces - expr = text.lstrip("=").strip() - if not expr: - return - # Replace operators: '^' -> '**', and '×' -> '*' - expr = expr.replace("^", "**").replace("×", "*") - # Replace factorial: e.g. 5! -> math.factorial(5) + expr = text.lstrip("=").replace("^", "**").replace("×", "*") expr = re.sub(r"(\d+)!", r"math.factorial(\1)", expr) - # Replace brackets: allow [] and {} as () - for old, new in [("[", "("), ("]", ")"), ("{", "("), ("}", ")")]: - expr = expr.replace(old, new) - try: - result = eval(expr, {"__builtins__": None, "math": math}) - except Exception as e: - result = f"Error: {e}" - # Prepend to history (newest first) + result = eval(expr, {"__builtins__": None, "math": math}) self.calc_history.insert(0, f"{text} => {result}") 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) - # Remove resetting selected_index unconditionally so that a highlighted result isn't lost. - # Optionally, only reset if the input is not more than "=". - # if self.search_entry.get_text().strip() != "=": - # self.selected_index = -1 + self.viewport.children = [ + Button( + child=Label(label=item), + on_clicked=partial(self.copy_text_to_clipboard, item), + ) + for item in self.calc_history + ] + self.update_scrolled_window_size() - def create_calc_history_button(self, text: str) -> Button: - btn = Button( - name="app-slot-button", # reuse existing CSS styling - 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), + def save_calc_history(self): + with open(self.calc_history_path, "w") as f: + json.dump(self.calc_history, f) + + def copy_text_to_clipboard(self, text: str, *args): + subprocess.run( + ["wl-copy"], input=text.split("=>", 1)[-1].strip().encode(), check=True ) - 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): - del self.calc_history[self.selected_index] - self.save_calc_history() - self.update_calculator_viewport() diff --git a/modules/launcher/components/bluetooth.py b/modules/launcher/components/bluetooth.py index a61849a..69bf2a8 100644 --- a/modules/launcher/components/bluetooth.py +++ b/modules/launcher/components/bluetooth.py @@ -5,7 +5,7 @@ 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 snippets.iconss as icons +import utils.icons as icons class BluetoothDeviceSlot(CenterBox): diff --git a/modules/launcher/components/calendar.py b/modules/launcher/components/calendar.py index cdb2562..76bd139 100644 --- a/modules/launcher/components/calendar.py +++ b/modules/launcher/components/calendar.py @@ -5,7 +5,7 @@ from fabric.widgets.label import Label gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GLib -import snippets.iconss as icons +import utils.icons as icons class Calendar(Gtk.Box): @@ -27,7 +27,7 @@ class Calendar(Gtk.Box): self.prev_month_button = Gtk.Button( name="prev-month-button", - child=Label(name="month-button-label", markup=icons.chevron_left) + child=Label(name="month-button-label", markup=icons.chevron_left), ) self.prev_month_button.connect("clicked", self.on_prev_month_clicked) self.header.pack_start(self.prev_month_button, False, False, 0) @@ -37,7 +37,7 @@ class Calendar(Gtk.Box): self.next_month_button = Gtk.Button( name="next-month-button", - child=Label(name="month-button-label", markup=icons.chevron_right) + child=Label(name="month-button-label", markup=icons.chevron_right), ) self.next_month_button.connect("clicked", self.on_next_month_clicked) self.header.pack_start(self.next_month_button, False, False, 0) diff --git a/modules/launcher/components/cliphist.py b/modules/launcher/components/cliphist.py index 86fe079..f2e66fd 100644 --- a/modules/launcher/components/cliphist.py +++ b/modules/launcher/components/cliphist.py @@ -1,6 +1,7 @@ import os import subprocess from typing import List +from functools import partial from fabric import Fabricator from fabric.widgets.box import Box from fabric.widgets.button import Button @@ -8,7 +9,7 @@ from fabric.widgets.entry import Entry from fabric.widgets.label import Label from fabric.widgets.scrolledwindow import ScrolledWindow from gi.repository import GLib -import snippets.iconss as icons +import utils.icons as icons class Cliphist(Box): @@ -19,15 +20,15 @@ class Cliphist(Box): visible=False, **kwargs, ) - self._arranger_handler = 0 self.cliphist_manager = CliphistManager(self) self.viewport = None + self.launcher = kwargs["launcher"] self.search_entry = Entry( name="search-entry", h_expand=True, - on_activate=lambda entry, *_: self.handle_search(entry.get_text()), + on_activate=self._on_search_activate, ) self.header_box = Box( @@ -55,6 +56,9 @@ class Cliphist(Box): self.add(self.launcher_box) + def _on_search_activate(self, entry, *args): + self.handle_search(entry.get_text()) + def open_launcher(self): if not self.viewport: self.viewport = Box(name="viewport", spacing=4, orientation="v") @@ -63,7 +67,6 @@ class Cliphist(Box): spacing=10, min_content_size=(-1, -1), h_scrollbar_policy="never", - # v_scrollbar_policy="never", child=self.viewport, ) self.launcher_box.add(self.scrolled_window) @@ -89,9 +92,10 @@ class CliphistManager: self.wl_paste_watcher = Fabricator( poll_from="wl-paste --watch echo", stream=True, interval=-1 ) - self.wl_paste_watcher.connect( - "changed", lambda *_: self.update_cliphist_history() - ) + self.wl_paste_watcher.connect("changed", self._on_wl_paste_changed) + + def _on_wl_paste_changed(self, *args): + self.update_cliphist_history() def get_clip_history(self) -> List[dict]: try: @@ -118,14 +122,13 @@ class CliphistManager: ["cliphist", "decode", clip_id], capture_output=True, check=True ) subprocess.run(["wl-copy"], input=decode_result.stdout, check=True) - self.launcher.open_launcher() + self.launcher.close() except subprocess.CalledProcessError as e: print(f"Error copying clip {clip_id}: {e}") def save_image_file(self, clip_id: str) -> str: output_file = f"/tmp/cliphist-{clip_id}.png" os.makedirs("/tmp", exist_ok=True) - try: decode_result = subprocess.run( ["cliphist", "decode", clip_id], capture_output=True, check=True @@ -150,6 +153,9 @@ class CliphistManager: if query.lower() in clip["content"].lower() ] + def _on_clip_clicked(self, clip_id: str, *args): + self.copy_clip(clip_id) + def bake_clip_slot(self, item: dict) -> Button: if "[[ binary data" in item["content"].lower(): return self._create_image_button(item) @@ -161,7 +167,7 @@ class CliphistManager: button = Button( h_align="start", name="clip-img-item", - on_clicked=lambda _, clip_id=item["id"]: self.copy_clip(clip_id), + on_clicked=partial(self._on_clip_clicked, item["id"]), ) button.set_style( f"background-image: url('{output_file}'); background-size: cover; background-position: center;" @@ -178,17 +184,15 @@ class CliphistManager: v_align="center", h_align="start", ), - on_clicked=lambda _, clip_id=item["id"]: self.copy_clip(clip_id), + on_clicked=partial(self._on_clip_clicked, item["id"]), name="clip-item", ) def arrange_viewport(self, query: str = ""): if not self.launcher.viewport: return - self.launcher.viewport.children = [] filtered_clips = self.query_clips(query) - for clip in filtered_clips: button = self.bake_clip_slot(clip) if button: diff --git a/modules/launcher/components/dashboard.py b/modules/launcher/components/dashboard.py index da835aa..2dbe112 100644 --- a/modules/launcher/components/dashboard.py +++ b/modules/launcher/components/dashboard.py @@ -9,7 +9,7 @@ from gi.repository import Gtk, Gdk, GLib # Added Gdk import from fabric.utils import get_relative_path gi.require_version("Gtk", "3.0") -import snippets.iconss as icons +import utils.icons as icons def add_hover_cursor(widget): diff --git a/modules/launcher/components/emoji.py b/modules/launcher/components/emoji.py index 731c108..b3d688b 100644 --- a/modules/launcher/components/emoji.py +++ b/modules/launcher/components/emoji.py @@ -1,7 +1,7 @@ import subprocess from typing import Generator, List - import ijson +from functools import partial from fabric.utils import remove_handler, get_relative_path from fabric.widgets.box import Box from fabric.widgets.button import Button @@ -18,26 +18,23 @@ class Emoji(Box): visible=False, **kwargs, ) - self.launcher = kwargs["launcher"] - self._arranger_handler: int = 0 + self._arranger_handler = 0 self.emoji_manager = EmojiManager(self) self.viewport = None self.search_entry = Entry( name="search-entry", 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()), + notify_text=self._on_search_input, + on_activate=self._on_search_input, ) self.header_box = Box( name="header-box", spacing=10, orientation="h", - children=[ - self.search_entry, - ], + children=[self.search_entry], ) self.launcher_box = Box( @@ -49,6 +46,9 @@ class Emoji(Box): self.add(self.launcher_box) + def _on_search_input(self, entry, *args): + self.handle_search_input(entry.get_text()) + def open_launcher(self): if not self.viewport: self.viewport = Box(name="viewport", spacing=4, orientation="v") @@ -63,12 +63,11 @@ class Emoji(Box): self.viewport.children = [] self.emoji_manager.arrange_viewport() - self.viewport.show() self.search_entry.grab_focus() def close_launcher(self): - self.launcher.close() + self.launcher.close_launcher() def handle_search_input(self, text: str): self.emoji_manager.arrange_viewport(text) @@ -105,7 +104,7 @@ class EmojiManager: name="emoji-item", child=Label(label=emoji[0], h_align="center"), tooltip_text=emoji[1], - on_clicked=lambda *_: self.copy_emoji(emoji), + on_clicked=partial(self.copy_emoji, emoji), **kwargs, ) @@ -113,11 +112,8 @@ class EmojiManager: if not self.launcher.viewport: return - ( + if self.launcher._arranger_handler: remove_handler(self.launcher._arranger_handler) - if self.launcher._arranger_handler - else None - ) self.launcher.viewport.children = [] filtered_emojis = self.query_emojis(query) diff --git a/modules/launcher/components/power.py b/modules/launcher/components/power.py index 6964a12..b441e25 100644 --- a/modules/launcher/components/power.py +++ b/modules/launcher/components/power.py @@ -2,7 +2,7 @@ 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 snippets.iconss as icons +import utils.icons as icons class PowerMenu(Box): @@ -86,4 +86,3 @@ class PowerMenu(Box): 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 index e9a92de..194b75b 100644 --- a/modules/launcher/components/sh.py +++ b/modules/launcher/components/sh.py @@ -9,7 +9,7 @@ from fabric.widgets.scrolledwindow import ScrolledWindow CACHE_DIR = os.getenv( - "XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache", "fabric") + "XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache", "modus") ) BINS = os.path.join(CACHE_DIR, "binaries") diff --git a/modules/launcher/components/todo.py b/modules/launcher/components/todo.py index ef81852..b34bc94 100644 --- a/modules/launcher/components/todo.py +++ b/modules/launcher/components/todo.py @@ -6,7 +6,7 @@ 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 snippets.iconss as icons +import utils.icons as icons class TodoManager(Box): @@ -83,7 +83,7 @@ class TodoListManager: def __init__(self, launcher): self.launcher = launcher self.todos = [] - self.todo_file = os.path.expanduser("~/.local/share/modus/todos.json") + self.todo_file = os.path.expanduser("~/.local/state/modus/todos.json") self.load_todos() def load_todos(self): diff --git a/modules/launcher/components/wallpapers.py b/modules/launcher/components/wallpapers.py index b777155..b3eb956 100644 --- a/modules/launcher/components/wallpapers.py +++ b/modules/launcher/components/wallpapers.py @@ -1,21 +1,21 @@ -import hashlib -import json -import os +import hashlib, json, os, colorsys from PIL import Image from fabric.widgets.box import Box from fabric.widgets.centerbox import CenterBox from fabric.widgets.entry import Entry +from fabric.widgets.label import Label from fabric.widgets.scrolledwindow import ScrolledWindow from gi.repository import GdkPixbuf, GLib, Gtk, Gio, Gdk -from fabric.utils import get_relative_path -import concurrent.futures -from concurrent.futures import ThreadPoolExecutor -from snippets import WALLPAPERS_DIR +from fabric.utils import get_relative_path, exec_shell_command_async +from concurrent.futures import ThreadPoolExecutor, wait +from utils import WALLPAPERS_DIR +import utils.icons as icons class WallpaperSelector(Box): CACHE_DIR = os.path.expanduser("~/.cache/modus/wallpapers") SETTINGS_FILE = get_relative_path("../../../config/assets/settings.json") + CURRENT_WALLPAPER_FILE = os.path.expanduser("~/.cache/current_wallpaper") SCHEMES = { "TonalSpot": "tonalSpot", "Expressive": "expressive", @@ -27,6 +27,7 @@ class WallpaperSelector(Box): "Fidelity": "fidelity", "Content": "content", } + ITEM_WIDTH = 108 def __init__(self, **kwargs): super().__init__( @@ -38,24 +39,32 @@ class WallpaperSelector(Box): **kwargs, ) self.launcher = kwargs["launcher"] - os.makedirs(self.CACHE_DIR, exist_ok=True) - self.files = [f for f in os.listdir(WALLPAPERS_DIR) if self._is_image(f)] - self.thumbnails = [] - self.thumbnail_queue = [] - self.executor = ThreadPoolExecutor(max_workers=4) # Shared executor + self.thumbnails, self.thumbnail_queue = [], [] + self.executor = ThreadPoolExecutor(max_workers=4) + self.selected_index, self.current_wallpaper = -1, None + self._init_widgets() + self._start_thumbnail_thread() + self.setup_file_monitor() + if os.path.exists(self.CURRENT_WALLPAPER_FILE): + gfile = Gio.File.new_for_path(self.CURRENT_WALLPAPER_FILE) + self.current_wp_monitor = gfile.monitor_file( + Gio.FileMonitorFlags.NONE, None + ) + self.current_wp_monitor.connect( + "changed", self.on_current_wallpaper_changed + ) + self.show_all() + self.search_entry.grab_focus() - self.selected_index = -1 - - # IconView for displaying thumbnails. + def _init_widgets(self): self.viewport = Gtk.IconView(name="wallpaper-icons") self.viewport.set_model(Gtk.ListStore(GdkPixbuf.Pixbuf, str)) self.viewport.set_pixbuf_column(0) self.viewport.set_text_column(-1) - self.viewport.set_item_width(0) + self.viewport.set_item_width(self.ITEM_WIDTH) self.viewport.connect("item-activated", self.on_wallpaper_selected) - self.scrolled_window = ScrolledWindow( name="scrolled-window", spacing=10, @@ -63,94 +72,117 @@ class WallpaperSelector(Box): v_expand=True, child=self.viewport, ) - self.search_entry = Entry( name="search-entry-walls", 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.connect("focus-out-event", self.on_search_entry_focus_out) - self.scheme_dropdown = Gtk.ComboBoxText() self.scheme_dropdown.set_name("scheme-dropdown") self.scheme_dropdown.set_tooltip_text("Select color scheme") - for display_name, scheme_id in sorted(self.SCHEMES.items()): self.scheme_dropdown.append(scheme_id, display_name) - self.scheme_dropdown.set_active_id("tonalSpot") self.scheme_dropdown.connect("changed", self.on_scheme_changed) - - self.dropdown_box = Box( - name="dropdown-box", - children=[ - self.scheme_dropdown, - ], + self.materialyoucolor_switcher = Gtk.Switch(name="materialyoucolor-switcher") + self.materialyoucolor_switcher.set_vexpand(False) + self.materialyoucolor_switcher.set_hexpand(False) + self.materialyoucolor_switcher.set_valign(Gtk.Align.CENTER) + self.materialyoucolor_switcher.set_halign(Gtk.Align.CENTER) + self.materialyoucolor_switcher.set_active(True) + self.mat_icon = Label(name="mat-label", markup=icons.palette) + self.dropdown_box = Box(name="dropdown-box", children=[self.scheme_dropdown]) + self.custom_color_entry = Entry( + name="entry-custom-color", + h_expand=True, + notify_text=lambda entry, *_: self.on_custom_color_submitted(entry), + on_key_press_event=self.on_custom_color_key_press, ) - self.header_box = CenterBox( name="header-box", spacing=10, orientation="h", - start_children=[self.search_entry], + start_children=[self.materialyoucolor_switcher, self.mat_icon], + center_children=[ + self.search_entry, + Label(label="Custom Color:"), + self.custom_color_entry, + ], end_children=[self.dropdown_box], ) - self.add(self.header_box) self.add(self.scrolled_window) - self._start_thumbnail_thread() - self.setup_file_monitor() - self.show_all() - self.search_entry.grab_focus() def setup_file_monitor(self): gfile = Gio.File.new_for_path(WALLPAPERS_DIR) self.file_monitor = gfile.monitor_directory(Gio.FileMonitorFlags.NONE, None) self.file_monitor.connect("changed", self.on_directory_changed) + def on_current_wallpaper_changed(self, monitor, file, other_file, event_type): + if event_type in (Gio.FileMonitorEvent.CHANGED, Gio.FileMonitorEvent.CREATED): + try: + with open(self.CURRENT_WALLPAPER_FILE, "r") as f: + new_wp = f.read().strip() + if new_wp and new_wp != self.current_wallpaper: + self.current_wallpaper = new_wp + if self.custom_color_entry.get_text().strip().lower() in [ + "", + "none", + ]: + scheme = self.scheme_dropdown.get_active_id() + color_generator = get_relative_path( + "../../../config/material-colors/generate.py" + ) + command = f'python -O {color_generator} --scheme "{scheme}" --image "{self.current_wallpaper}"' + self._run_command(command) + except Exception as e: + print(f"Error updating current wallpaper: {e}") + 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._delete_cache(file_name) 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) and file_name not in self.files: + return + if event_type in ( + Gio.FileMonitorEvent.CREATED, + Gio.FileMonitorEvent.CHANGED, + ) and self._is_image(file_name): + if ( + event_type == Gio.FileMonitorEvent.CREATED + and 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) + elif event_type == Gio.FileMonitorEvent.CHANGED and file_name in self.files: + self._delete_cache(file_name) + self.executor.submit(self._process_file, file_name) + + def _delete_cache(self, file_name: str): + cache_path = self._get_cache_path(file_name) + if os.path.exists(cache_path): + try: + os.remove(cache_path) + except Exception: + pass 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: + for pixbuf, file_name in sorted( + [ + (thumb, name) + for thumb, name in self.thumbnails + if query.casefold() in name.casefold() + ], + key=lambda x: x[1].lower(), + ): model.append([pixbuf, file_name]) - - if query.strip() == "": + if not query.strip(): self.viewport.unselect_all() self.selected_index = -1 elif len(model) > 0: @@ -161,59 +193,37 @@ class WallpaperSelector(Box): file_name = model[path][1] full_path = os.path.join(WALLPAPERS_DIR, file_name) selected_scheme = self.scheme_dropdown.get_active_id() - wallpaper = get_relative_path("../../../config/scripts/wallpaper.py") - command = f"python -O {wallpaper} -I {full_path}" - GLib.spawn_command_line_async(command) - self.update_scheme(selected_scheme) - - def on_scheme_changed(self, combo): - scheme_id = combo.get_active_id() - display_name = next( - name for name, id in self.SCHEMES.items() if id == scheme_id - ) - print(f"Color scheme selected: {display_name} ({scheme_id})") - self.update_scheme(scheme_id) - - color_generator = get_relative_path( - "../../../config/material-colors/generate.py" - ) - try: - command = f'python -O {color_generator} -R --scheme "{scheme_id}"' + wallpaper_script = get_relative_path("../../../config/scripts/wallpaper.py") + if self.materialyoucolor_switcher.get_active(): + command = f"python -O {wallpaper_script} -I {full_path}" GLib.spawn_command_line_async(command) - print(f"Applied color scheme: {display_name}") - except Exception as e: - print(f"Failed to apply color scheme: {e}") - - def update_scheme(self, scheme: str): - try: - with open(self.SETTINGS_FILE, "r") as f: - settings = json.loads(f.read()) - - settings["generation-scheme"] = scheme - - with open(self.SETTINGS_FILE, "w") as f: - json.dump(settings, f, indent=2) - except Exception as error: - print(f"Failed to update generation-scheme in settings.json: {error}") + self.update_scheme(selected_scheme) + else: + cmd = f"swww img {full_path} -t outer --transition-duration 1.5 --transition-step 255 --transition-fps 60 -f Nearest" + exec_shell_command_async(cmd) 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 + scheme_list = [ + scheme_id for _, scheme_id in sorted(self.SCHEMES.items()) + ] + try: + current_index = scheme_list.index( + self.scheme_dropdown.get_active_id() + ) + except ValueError: + current_index = 0 + new_index = ( + (current_index - 1) % len(scheme_list) + if event.keyval == Gdk.KEY_Up + else (current_index + 1) % len(scheme_list) ) - if event.keyval == Gdk.KEY_Up: - new_index = (current_index - 1) % len(schemes_list) - else: - new_index = (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 @@ -222,7 +232,6 @@ class WallpaperSelector(Box): 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): @@ -230,33 +239,24 @@ class WallpaperSelector(Box): 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 - # Calculate columns based on the allocation width. allocation = self.viewport.get_allocation() - item_width = 108 - 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 - - # Ensure new_index remains within limits. - if new_index < 0: - new_index = 0 - if new_index >= total_items: - new_index = total_items - 1 - + columns = max(1, allocation.width // self.ITEM_WIDTH) + new_index = current_index + ( + 1 + if keyval == Gdk.KEY_Right + else -1 + if keyval == Gdk.KEY_Left + else columns + if keyval == Gdk.KEY_Down + else -columns + ) + new_index = max(0, min(new_index, total_items - 1)) self.update_selection(new_index) def update_selection(self, new_index: int): @@ -267,14 +267,14 @@ class WallpaperSelector(Box): self.selected_index = new_index def _start_thumbnail_thread(self): - thread = GLib.Thread.new("thumbnail-loader", self._preload_thumbnails, None) + 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) + wait(futures) GLib.idle_add(self._process_batch) def _process_file(self, file_name): @@ -287,13 +287,10 @@ class WallpaperSelector(Box): 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 = img.crop((left, top, left + side, top + side)) 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}") + except Exception: return self.thumbnail_queue.append((cache_path, file_name)) GLib.idle_add(self._process_batch) @@ -316,13 +313,110 @@ class WallpaperSelector(Box): file_hash = hashlib.md5(file_name.encode("utf-8")).hexdigest() return os.path.join(self.CACHE_DIR, f"{file_hash}.png") + def on_scheme_changed(self, combo): + scheme_id = combo.get_active_id() + display_name = next( + name for name, id in self.SCHEMES.items() if id == scheme_id + ) + self.update_scheme(scheme_id) + color_generator = get_relative_path( + "../../../config/material-colors/generate.py" + ) + command = f'python -O {color_generator} -R --scheme "{scheme_id}"' + self._run_command( + command, + success_message=f"Applied color scheme: {display_name}", + failure_message="Failed to apply color scheme:", + ) + + def update_scheme(self, scheme: str): + self._update_settings_field("generation-scheme", scheme) + + def on_custom_color_submitted(self, entry: Entry): + color = entry.get_text().strip() + if not color: + self.update_custom_color("") + return + if self.is_valid_hex_color(color): + color_value = color + elif self.is_valid_hue(color): + color_value = self.hue_to_hex(float(color)) + else: + return + self.update_custom_color(color_value) + + def on_custom_color_key_press(self, widget, event): + if event.keyval == Gdk.KEY_Return: + self.on_custom_color_submitted(widget) + return True + return False + + def _fetch_current_wallpaper(self): + if not self.current_wallpaper or not os.path.exists(self.current_wallpaper): + if os.path.exists(self.CURRENT_WALLPAPER_FILE): + try: + with open(self.CURRENT_WALLPAPER_FILE, "r") as f: + self.current_wallpaper = f.read().strip() + except Exception as e: + print(f"Error reading current wallpaper: {e}") + return self.current_wallpaper + + def update_custom_color(self, color: str): + color = color if color and color.lower() != "none" else "none" + self._update_settings_field("custom-color", color) + color_generator = get_relative_path( + "../../../config/material-colors/generate.py" + ) + if color == "none": + scheme = self.scheme_dropdown.get_active_id() + current_wp = self._fetch_current_wallpaper() + command = f'python -O {color_generator} --scheme "{scheme}" --image "{current_wp}"' + else: + command = f'python -O {color_generator} --color "{color}"' + self._run_command(command) + @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 + @staticmethod + def is_valid_hex_color(value: str) -> bool: + return ( + value.startswith("#") + and len(value) == 7 + and all(c in "0123456789ABCDEFabcdef" for c in value[1:]) + ) + + @staticmethod + def is_valid_hue(value: str) -> bool: + try: + return 0 <= float(value) <= 360 + except ValueError: + return False + + def hue_to_hex(self, hue: float) -> str: + r, g, b = colorsys.hls_to_rgb(hue / 360.0, 0.5, 1.0) + return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}" + + def _update_settings_field(self, field: str, value): + try: + with open(self.SETTINGS_FILE, "r") as f: + settings = json.load(f) + settings[field] = value + with open(self.SETTINGS_FILE, "w") as f: + json.dump(settings, f, indent=2) + except Exception: + pass + + def _run_command( + self, command: str, success_message: str = None, failure_message: str = None + ): + try: + GLib.spawn_command_line_async(command) + if success_message: + print(success_message) + except Exception: + if failure_message: + print(failure_message) diff --git a/modules/launcher/components/wifi.py b/modules/launcher/components/wifi.py index b2f656e..657d56c 100644 --- a/modules/launcher/components/wifi.py +++ b/modules/launcher/components/wifi.py @@ -5,7 +5,7 @@ from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox from fabric.widgets.scrolledwindow import ScrolledWindow from services import NetworkClient -import snippets.iconss as icons +import utils.icons as icons class WifiNetworkSlot(CenterBox): diff --git a/modules/launcher/launcher.py b/modules/launcher/launcher.py index 97a950e..ca99e66 100644 --- a/modules/launcher/launcher.py +++ b/modules/launcher/launcher.py @@ -38,13 +38,12 @@ class Launcher(Window): self.wifi = WifiManager() self.calendar = Calendar() + # Wrap the dashboard in a Box container self.dashboard = Box( name="dashboard", orientation="h", spacing=10, - children=[ - self.dashboard, - ], + children=[self.dashboard], ) self.launcher = AppLauncher(launcher=self) @@ -78,7 +77,10 @@ class Launcher(Window): self.add(self.launcher_box) self.show_all() self.hide() - self.add_keybinding("Escape", lambda *_: self.close()) + self.add_keybinding("Escape", self._on_escape) + + def _on_escape(self, *args): + return self.close() def close(self): self.set_keyboard_mode("none") From 056989fd4b4f60f73033e04e7e016e9920bfd06c Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 28 Feb 2025 16:45:17 +0545 Subject: [PATCH 15/22] update styles --- styles/bar.css | 163 +++++++++++++++++++++++++++----------------- styles/launcher.css | 61 ++++++++++++++--- styles/main.css | 96 +++++++++++++++----------- styles/osd.css | 2 +- 4 files changed, 207 insertions(+), 115 deletions(-) diff --git a/styles/bar.css b/styles/bar.css index fe0d1e2..49803cb 100644 --- a/styles/bar.css +++ b/styles/bar.css @@ -1,10 +1,26 @@ #bar { - background-color: @surface; - padding: 3px; + padding: 0px 7px 0px 7px; } -#bar label { - font-weight: bold; +#bar.start-container { + padding-top: 4px; + padding-bottom: 4px; + border-bottom-right-radius: 15px; +} + +#bar.end-container { + padding-top: 4px; + padding-bottom: 4px; + border-bottom-left-radius: 15px; +} + +#center-bar { + background-color: transparent; +} + +#bar, +#bar-corner * { + background-color: lighter(@background); } #bar.hidden { @@ -27,15 +43,19 @@ #button-bar { background-color: @surfaceContainer; - min-width: 38px; - border-radius: 100px; + margin-top: 5px; + margin-bottom: 5px; + padding: 0px 1px 0px 1px; + border-radius: 8px; + min-width: 24px; + min-height: 24px; transition: all 0.1s ease; } #button-bar-label { color: @primary; font-size: 24px; - border-radius: 100px; + border-radius: 8px; transition: all 0.1s ease; } @@ -49,28 +69,19 @@ background-color: @primary; } -/* - * ── bar modules ─────────────────────────────────────────────────────── -*/ - -#start-container, -#center-container, -#end-container { - padding-top: 6px; - padding-bottom: 6px; - margin-left: 12px; - margin-right: 12px; -} - /* * ── workspace ───────────────────────────────────────────────────────── */ #workspaces { - border-radius: 100px; font-style: normal; background: @surfaceContainer; - padding: 0px 10px 0px 10px; + margin-top: 5px; + margin-bottom: 5px; + padding: 0px 8px 0px; + border-radius: 8px; + min-width: 24px; + min-height: 24px; } /* @@ -80,8 +91,7 @@ #update-icon { color: @primary; font-size: 22px; - margin-right: 1px; - padding: 2px; + padding: 1px; border-radius: 11px; transition: all 0.1s ease; } @@ -99,6 +109,7 @@ font-size: 10px; font-weight: bold; padding: 0px; + margin-right: 4px; border-radius: 11px; transition: all 0.1s ease; } @@ -109,8 +120,12 @@ #datetime { background-color: @surfaceContainer; - border-radius: 100px; - padding: 8px; + margin-top: 5px; + margin-bottom: 5px; + padding: 0px 8px 0px; + border-radius: 8px; + min-width: 24px; + min-height: 24px; } /* @@ -119,8 +134,12 @@ #battery { background-color: @surfaceContainer; - padding: 2px 10px 2px 10px; - border-radius: 100px; + margin-top: 5px; + margin-bottom: 5px; + padding: 0px 8px 0px; + border-radius: 8px; + min-width: 24px; + min-height: 24px; } #battery-circle { @@ -188,23 +207,52 @@ } /* - * ── systeminfo ──────────────────────────────────────────────────────── + * ── metric ──────────────────────────────────────────────────────── */ -#system-info { +#metrics { background-color: @surfaceContainer; - border-radius: 100px; - padding: 2px 10px 0px 10px; + margin-top: 5px; + margin-bottom: 5px; + padding: 0px 8px 0px; + border-radius: 8px; + min-width: 24px; + min-height: 24px; +} + +#metric-icon { + font-size: 20px; +} + +#metric-circle { + color: @surfaceBright; + border: 3px solid @primary; } /* * ── tray ────────────────────────────────────────────────────────────── */ +#tray-revealer { + background-color: @surfaceContainer; + margin-top: 5px; + margin-bottom: 5px; + padding: 0px 2px 0px; + border-radius: 8px; + min-width: 24px; + min-height: 24px; + font-size: 20px; +} + #tray { background-color: @surfaceContainer; - border-radius: 100px; - padding: 2px 10px 0px 10px; + margin-top: 5px; + margin-left: 4px; + margin-bottom: 5px; + padding: 0px 8px 0px; + border-radius: 8px; + min-width: 24px; + min-height: 24px; } #tray > button { @@ -237,8 +285,12 @@ menu > menuitem:hover > label { */ #logo { background-color: @surfaceContainer; - border-radius: 100px; - padding: 0px 10px 0px 10px; + margin-top: 5px; + margin-bottom: 5px; + padding: 0px 8px 0px; + border-radius: 8px; + min-width: 24px; + min-height: 24px; } #logo-name { @@ -251,33 +303,23 @@ menu > menuitem:hover > label { } /* - * ── applets ─────────────────────────────────────────────────────────── + * ── status indicator ─────────────────────────────────────────────────────────── */ -#applets { +#system-indicators { background-color: @surfaceContainer; - border-radius: 100px; - padding: 2px 10px 2px 10px; - margin: 0px; + margin-top: 5px; + margin-bottom: 5px; + padding: 0px 8px 0px; + border-radius: 8px; + min-width: 24px; + min-height: 24px; } -#indicator-icon { +#system-indicator-icon { font-size: 24px; } -/* - * ── stats i.e cpu,ram,swap,temp ─────────────────────────────────────── -*/ - -#stats-icons { - font-size: 20px; -} - -#stats-progress { - color: @surfaceBright; - border: 3px solid @primary; -} - /* * ── taskbar ─────────────────────────────────────────────────────────── */ @@ -314,15 +356,12 @@ menu > menuitem:hover > label { * ── recording indicator ─────────────────────────────────────────────── */ -#recording-indicator { - min-width: 38px; - border-radius: 100px; -} - #recorder { color: @error; - border-radius: 100px; - min-width: 25px; + margin-top: 5px; + margin-bottom: 5px; + padding: 0px 6px 0px; + border-radius: 8px; font-size: 20px; animation: error 2s infinite; } diff --git a/styles/launcher.css b/styles/launcher.css index 0accbe3..7c30a14 100644 --- a/styles/launcher.css +++ b/styles/launcher.css @@ -8,7 +8,7 @@ #launcher { margin: 18px; border: 1px solid @outlineVariant; - background-color: @surface; + background-color: lighter(@background); border-radius: 24px; padding: 18px; transition: all 0.5s cubic-bezier(0.5, 0.25, 0, 1); @@ -33,8 +33,8 @@ min-width: 300px; } -.applauncher { - min-height: 464px; +.applauncher-scrolled-window { + transition: min-height 0.2s ease; } #launcher-content.bluetooth { @@ -72,7 +72,7 @@ #scrolled-window scrollbar { border-radius: 10px; - background-color: @surface; + background-color: lighter(@background); padding: 4px; margin-left: 6px; } @@ -91,6 +91,7 @@ #launcher-app { border-radius: 16px; padding: 16px; + min-height: 0; transition: padding 0.25s cubic-bezier(0.5, 0.25, 0, 1); } @@ -121,6 +122,10 @@ min-width: 300px; } +#entry-custom-color { + min-width: 10px; +} + #wallpaper-icons:selected { background-color: @surfaceBright; border-radius: 8px; @@ -132,7 +137,7 @@ } #dropdown-box { - background-color: @surface; + background-color: lighter(@background); box-shadow: inset 0 0 0 1px @outlineVariant; border-radius: 11px; padding: 10px; @@ -143,6 +148,42 @@ font-weight: bold; } +/* Customize the overall switch appearance */ +#materialyoucolor-switcher { + min-width: 40px; + min-height: 20px; + background-color: @surface; + border-radius: 15px; + padding: 2px; + transition: background-color 0.3s ease; +} + +/* Style the switch's slider */ +#materialyoucolor-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) */ +#materialyoucolor-switcher:checked { + background-color: @primary; +} + +/* Optional: additional styling for the slider when active */ +#materialyoucolor-switcher:checked slider { + background-color: @shadow; +} + +#mat-label { + font-size: 24px; + margin-left: 4px; +} + /* * ── emoji ───────────────────────────────────────────────────────────── */ @@ -253,7 +294,6 @@ button#clip-item:focus { * ── todo ────────────────────────────────────────────────────────────── */ #todo-launcher { - background-color: @surface; padding: 10px; border-radius: 8px; } @@ -261,7 +301,7 @@ button#clip-item:focus { #todo-item { padding: 8px; border-radius: 4px; - background-color: alpha(@surface, 0.8); + background-color: alpha(@background, 0.3); } #todo-label { @@ -269,7 +309,7 @@ button#clip-item:focus { } #todo-label-completed { - color: alpha(@outlineVariant, 0.5); + color: alpha(@outlineVariant, 1); text-decoration: line-through; } @@ -379,7 +419,7 @@ button#clip-item:focus { } #bluetooth-connect { - background-color: @surface; + background-color: lighter(@background); border-radius: 16px; padding: 8px 16px; } @@ -449,7 +489,7 @@ button#clip-item:focus { } #wifi-connect { - background-color: @surface; + background-color: lighter(@background); border-radius: 16px; padding: 8px 16px; } @@ -546,6 +586,5 @@ button#clip-item:focus { #prev-month-day, #next-month-day { - /*color: lighter(@surfaceBright);*/ color: mix(@onSurfaceVariant, transparent, 0.6); } diff --git a/styles/main.css b/styles/main.css index a0cd973..5d9aa20 100644 --- a/styles/main.css +++ b/styles/main.css @@ -6,24 +6,10 @@ @import "./dashboard.css"; * { - all: unset; - color: @foreground; - font-size: 16px; - font-family: "Product Sans", sans-serif; -} - -/* - * ── corner ──────────────────────────────────────────────────────────── -*/ - -#corner { - background-color: @surface; - border-radius: 0px; -} - -#corner-container { - min-width: 20px; - min-height: 20px; + all: unset; + color: @foreground; + font-size: 16px; + font-family: "Product Sans", sans-serif; } /* @@ -31,38 +17,66 @@ */ tooltip { - background-color: @surfaceContainer; - border-radius: 10px; - padding: 6px 10px; - outline: none; - border: none; - box-shadow: none; - animation: tooltipShow 0.25s cubic-bezier(0.5, 0.25, 0, 1); + background-color: @surfaceContainer; + border-radius: 10px; + padding: 6px 10px; + outline: none; + border: none; + box-shadow: none; + animation: tooltipShow 0.25s cubic-bezier(0.5, 0.25, 0, 1); } tooltip label { - color: @onSurface; - outline: none; - border: none; - box-shadow: none; - font-weight: normal; + color: @onSurface; + outline: none; + border: none; + box-shadow: none; + font-weight: normal; } @keyframes tooltipShow { - from { - opacity: 0; - } + from { + opacity: 0; + } - to { - opacity: 1; - } + to { + opacity: 1; + } } /* - * ── icon ────────────────────────────────────────────────────────────── + * ── dock ────────────────────────────────────────────────────────────── */ -#material-icon, -#material-icon * { - all: unset; - font-family: "Material Symbols Outlined", "Material Symbols Sharp"; +#dock-icon { + padding: 0px 2px 0px 2px; +} + +#dock { + padding: 10px; + background-color: alpha(@background, 0.8); + box-shadow: inset 0 -0.5px 0 0.5px alpha(@outlineVariant, 0.4); + border: 0.5px solid alpha(@outlineVariant, 0.4); + border-radius: 12px; + margin-bottom: 2px; +} + +#dock-separator { + border-right: 1px solid alpha(@foreground, 0.3); +} + +#dock-app-button { + padding: 2px; + margin-left: 2px; + margin-right: 2px; + border-radius: 12px; +} + +.show-dock { + opacity: 1; + margin: 0; +} + +.hide-dock { + opacity: 1; + margin: -80px; } diff --git a/styles/osd.css b/styles/osd.css index 8664d65..b1f4818 100644 --- a/styles/osd.css +++ b/styles/osd.css @@ -35,7 +35,7 @@ #control-section { padding: 10px; border-radius: 10px; - background-color: @surface; + background-color: lighter(@background); } .osd-hidden { From 62fc3af262b5a0cc5c4863f3f437bf5af84e96b6 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 28 Feb 2025 16:49:36 +0545 Subject: [PATCH 16/22] update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3fb3f82..25828fa 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ cd Tela-icon-theme - [x] Installation script - [x] Network Manager - [x] Calendar -- [ ] Power Manager +- [x] Power Manager - [ ] Notification Panel - [ ] Improve CSS Styling - [ ] Window Switcher From 1b204d307c43140d6f736349704dcec0ce5613fa Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 28 Feb 2025 16:50:45 +0545 Subject: [PATCH 17/22] update --- README.md | 3 ++- modules/osd.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 25828fa..e7b8ec9 100644 --- a/README.md +++ b/README.md @@ -94,10 +94,11 @@ cd Tela-icon-theme - [x] Network Manager - [x] Calendar - [x] Power Manager +- [x] Dock - [ ] Notification Panel - [ ] Improve CSS Styling -- [ ] Window Switcher - [x] Web search in Launcher +- [ ] Update Screenshot ## Special Thanks diff --git a/modules/osd.py b/modules/osd.py index 37534b7..d941046 100644 --- a/modules/osd.py +++ b/modules/osd.py @@ -49,8 +49,8 @@ class OSD(Window): super().__init__( name="osd-menu", layer="overlay", - anchor="bottom", - margin="0 0 10px 0", + anchor="top", + margin="10px 0 0 0", keyboard_mode="on-demand", visible=False, style_classes="osd-panel", From 5558dbba99b3f84318032070064eb793698f4ed3 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 28 Feb 2025 16:52:39 +0545 Subject: [PATCH 18/22] update --- README.md | 2 +- install.sh | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e7b8ec9..0daea2d 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,9 @@ cd Tela-icon-theme - [x] Calendar - [x] Power Manager - [x] Dock +- [x] Web search in Launcher - [ ] Notification Panel - [ ] Improve CSS Styling -- [x] Web search in Launcher - [ ] Update Screenshot ## Special Thanks diff --git a/install.sh b/install.sh index aba00ab..857ca0f 100644 --- a/install.sh +++ b/install.sh @@ -35,12 +35,9 @@ PACKAGES=( brightnessctl wlinhibit power-profile-daemon - ttf-font-awesome - otf-font-awesome - ttf-material-symbols-variable-git ttf-google-sans ttf-opensans - ttf-robot + ttf-tabler-icons ) if ! grep -q "arch" /etc/os-release; then From a18c3d524942234114444167432cfa8ab7063a9e Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 1 Mar 2025 04:00:40 +0545 Subject: [PATCH 19/22] update install script --- install.sh | 52 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/install.sh b/install.sh index 857ca0f..bbc8f65 100644 --- a/install.sh +++ b/install.sh @@ -35,11 +35,15 @@ PACKAGES=( brightnessctl wlinhibit power-profile-daemon + ttf-font-awesome + otf-font-awesome + ttf-material-symbols-variable-git ttf-google-sans ttf-opensans - ttf-tabler-icons + ttf-robot ) +# 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 @@ -51,13 +55,12 @@ if [ "$(id -u)" -eq 0 ]; then exit 1 fi +# Check for AUR helper (yay or paru) aur_helper="" if command -v yay &>/dev/null; then aur_helper="yay" - echo $aur_helper elif command -v paru &>/dev/null; then aur_helper="paru" - echo $aur_helper else echo "Installing paru..." tmpdir=$(mktemp -d) @@ -66,9 +69,10 @@ else makepkg -si --noconfirm cd - >/dev/null rm -rf "$tmpdir" + aur_helper="paru" fi -# Clone or update the repository +# Clone or update the Modus repository if [ -d "$INSTALL_DIR" ]; then echo "Updating Modus..." git -C "$INSTALL_DIR" pull @@ -77,14 +81,13 @@ else git clone "$REPO_URL" "$INSTALL_DIR" fi -# Install required packages using $aur_helper (only if missing +# Install required packages using the AUR helper echo "Installing required packages..." $aur_helper -Syy --needed --noconfirm "${PACKAGES[@]}" || true -# Update outdated packages from the list -echo "Updating outdated required packages..." -# Get a list of outdated packages -outdated=$($aur_helper -Qu | awk '{print $1}') +# 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 @@ -93,29 +96,40 @@ for pkg in "${PACKAGES[@]}"; do 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 configs +# 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" -[[ -d "$HOME/.config/gtk-4.0" ]] && mv "$HOME/.config/gtk-4.0" "$HOME/.config/gtk-4.0-bk" +[[ -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/" -cp -r "$INSTALL_DIR/assets/gtk-4.0" "$HOME/.config/" +cp -r "$INSTALL_DIR/assets/gtk-3.0" "$HOME/.config/" || true +cp -r "$INSTALL_DIR/assets/gtk-4.0" "$HOME/.config/" || true -# Install Icon -echo "Installing icon theme..." +# Install Icon Theme +echo "Installing Tela icon theme..." tmpdir=$(mktemp -d) -cd /tmp/install -git clone https://github.com/vinceliuice/Tela-icon-theme +git clone https://github.com/vinceliuice/Tela-icon-theme "$tmpdir/Tela-icon-theme" cd "$tmpdir/Tela-icon-theme" ./install.sh nord +rm -rf "$tmpdir" -# Launch Modus without terminal output +# 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 & From 4e0fa33ba5c073d4abc46e2c025da9c9a054ab3b Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 1 Mar 2025 21:09:33 +0545 Subject: [PATCH 20/22] update css --- styles/bar.css | 20 +++---- styles/main.css | 76 ++++++++++++-------------- styles/notification.css | 115 +++++++++++++++++++++++----------------- 3 files changed, 109 insertions(+), 102 deletions(-) diff --git a/styles/bar.css b/styles/bar.css index 49803cb..5cca825 100644 --- a/styles/bar.css +++ b/styles/bar.css @@ -3,14 +3,14 @@ } #bar.start-container { - padding-top: 4px; - padding-bottom: 4px; + padding-top: 2px; + padding-bottom: 2px; border-bottom-right-radius: 15px; } #bar.end-container { - padding-top: 4px; - padding-bottom: 4px; + padding-top: 2px; + padding-bottom: 2px; border-bottom-left-radius: 15px; } @@ -25,15 +25,11 @@ #bar.hidden { opacity: 0; - padding: 3px; - margin-bottom: -52px; transition: all 0.5s cubic-bezier(0.5, 0.25, 0, 1); } #bar.visible { opacity: 1; - padding: 3px; - margin-bottom: 0px; transition: all 0.5s cubic-bezier(0.5, 0.25, 0, 1); } @@ -54,7 +50,7 @@ #button-bar-label { color: @primary; - font-size: 24px; + font-size: 20px; border-radius: 8px; transition: all 0.1s ease; } @@ -99,7 +95,7 @@ #updated-icon { color: @primary; font-size: 20px; - padding: 8px; + padding: 4px; border-radius: 99px; transition: all 0.1s ease; } @@ -172,7 +168,7 @@ #battery-balanced-label, #battery-performance-label { color: @outline; - font-size: 24px; + font-size: 20px; } #battery-save:hover, @@ -317,7 +313,7 @@ menu > menuitem:hover > label { } #system-indicator-icon { - font-size: 24px; + font-size: 20px; } /* diff --git a/styles/main.css b/styles/main.css index 5d9aa20..c97593e 100644 --- a/styles/main.css +++ b/styles/main.css @@ -6,10 +6,10 @@ @import "./dashboard.css"; * { - all: unset; - color: @foreground; - font-size: 16px; - font-family: "Product Sans", sans-serif; + all: unset; + color: @foreground; + font-size: 16px; + font-family: "Product Sans", sans-serif; } /* @@ -17,66 +17,60 @@ */ tooltip { - background-color: @surfaceContainer; - border-radius: 10px; - padding: 6px 10px; - outline: none; - border: none; - box-shadow: none; - animation: tooltipShow 0.25s cubic-bezier(0.5, 0.25, 0, 1); -} - -tooltip label { - color: @onSurface; - outline: none; - border: none; - box-shadow: none; - font-weight: normal; + border: solid 1px; + border-radius: 10px; + border-color: @surface; + background-color: @shadow; + animation: tooltipShow 0.25s cubic-bezier(0.5, 0.25, 0, 1); } @keyframes tooltipShow { - from { - opacity: 0; - } + from { + opacity: 0; + } - to { - opacity: 1; - } + to { + opacity: 1; + } +} + +tooltip > * { + padding: 6px 10px; + border-radius: 10px; } /* * ── dock ────────────────────────────────────────────────────────────── */ #dock-icon { - padding: 0px 2px 0px 2px; + padding: 0px 2px 0px 2px; } #dock { - padding: 10px; - background-color: alpha(@background, 0.8); - box-shadow: inset 0 -0.5px 0 0.5px alpha(@outlineVariant, 0.4); - border: 0.5px solid alpha(@outlineVariant, 0.4); - border-radius: 12px; - margin-bottom: 2px; + padding: 10px; + background-color: alpha(@shadow, 0.5); + border: 0.5px solid alpha(@outlineVariant, 0.4); + border-radius: 12px; + margin-bottom: 2px; } #dock-separator { - border-right: 1px solid alpha(@foreground, 0.3); + border-right: 1px solid alpha(@foreground, 0.5); } #dock-app-button { - padding: 2px; - margin-left: 2px; - margin-right: 2px; - border-radius: 12px; + padding: 2px; + margin-left: 2px; + margin-right: 2px; + border-radius: 12px; } .show-dock { - opacity: 1; - margin: 0; + opacity: 1; + margin: 0; } .hide-dock { - opacity: 1; - margin: -80px; + opacity: 1; + margin: -80px; } diff --git a/styles/notification.css b/styles/notification.css index 16f1817..7b8ea88 100644 --- a/styles/notification.css +++ b/styles/notification.css @@ -1,63 +1,80 @@ -#notification { - padding: 16px; - border: solid 1px @outlineVariant; +#notification-action-buttons button.start-action { border-radius: 20px; - margin-bottom: 12px; - background-color: @surfaceContainerLow; - min-width: 350px; -} - -.notification-header .summary { - font-size: 0.92rem; - font-weight: bold; -} - -.notification-body .body { - color: darker(@foreground); - font-weight: 600; - font-size: 0.92rem; -} - -#notification .close-button { - border-radius: 100px; - min-width: 40px; - min-height: 40px; -} - -#notification .close-button:hover { - background: alpha(@foreground, 0.2); - transition: background 0.15s ease-in-out; - border-radius: 100px; -} - -#notification-action-button button.start-action { - margin-left: 0; -} - -#notification-action-button button.end-action { - margin-right: 0; } #notification-action-buttons button { - padding: 0.6rem; - font-weight: 600; - border-radius: 100px; - background-color: transparent; + background-color: @surfaceContainer; + padding: 8px; + border-radius: 20px; } #notification-action-buttons button:hover { - background-color: lighter(@outlineVariant); + background-color: @surfaceBright; } -#notification-icon { - margin-right: 5px; +#notification-action-buttons button:active { + background-color: @primary; } -.image { - border-radius: 30px; +#notification-action-buttons button:active #button-label { + color: @shadow; } -#notification-circular-progress-bar { - border: solid 0px @outlineVariant; - color: transparent; +#notification-image image { + border-radius: 16px; + color: @onSurface; +} + +#notification-summary, +#button-label { + font-weight: bold; +} + +#notification-summary { + font-weight: bold; + color: @primary; +} + +#notification-app-name { + color: @outline; +} + +#action-button { + margin-top: 8px; +} + +#notification-box { + border-radius: 20px; + padding: 16px; + background-color: @surfaceContainerLow; + margin: 4px; + min-width: 336px; +} + +#boxed-notification-revealer { + margin-top: -14px; +} + +#notif-close-button { + background-color: @surface; + border-radius: 16px; + padding: 4px; +} + +#notif-close-button:hover, +#notif-close-button:focus { + background-color: @surfaceBright; +} + +#notif-close-button:active { + background-color: @error; +} + +#notif-close-label { + color: @error; + font-size: 20px; +} + +#notif-close-button:active #notif-close-label { + color: @shadow; } From d1f8fae11bb4d75618638a6f57034a560f92d77b Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 1 Mar 2025 21:09:53 +0545 Subject: [PATCH 21/22] update notification --- modules/notification_popup.py | 411 +++++++++++++++++----------------- 1 file changed, 200 insertions(+), 211 deletions(-) diff --git a/modules/notification_popup.py b/modules/notification_popup.py index 8835778..35982ff 100644 --- a/modules/notification_popup.py +++ b/modules/notification_popup.py @@ -1,70 +1,49 @@ -import time - -import gi - -gi.require_version("GdkPixbuf", "2.0") - +import os +from gi.repository import GdkPixbuf, GLib, Gdk from fabric.notifications import ( Notification, NotificationAction, NotificationCloseReason, Notifications, ) -from fabric.utils import invoke_repeater +from loguru import logger from fabric.widgets.box import Box from fabric.widgets.button import Button -from fabric.widgets.circularprogressbar import CircularProgressBar from fabric.widgets.image import Image +from fabric.widgets.centerbox import CenterBox from fabric.widgets.label import Label -from fabric.widgets.overlay import Overlay from fabric.widgets.revealer import Revealer from fabric.widgets.wayland import WaylandWindow as Window -from gi.repository import GdkPixbuf, GLib, GObject -from snippets import Animator, CustomImage - - -class AnimatedCircularProgressBar(CircularProgressBar): - def __init__(self, duration=0.6, bezier_curve=(0.34, 1.56, 0.64, 1.0), **kwargs): - super().__init__(**kwargs) - - self.animator = ( - Animator( - bezier_curve=bezier_curve, - duration=duration, - min_value=self.min_value, - max_value=self.value, - tick_widget=self, - notify_value=lambda p, *_: self.set_value(p.value), - ) - .build() - .play() - .unwrap() - ) - - def animate_value(self, value: float): - self.animator.pause() - self.animator.min_value = self.value - self.animator.max_value = value - self.animator.play() - return +from utils import CustomImage +import utils.icons as icons class ActionButton(Button): def __init__( - self, action: NotificationAction, action_number: int, total_actions: int + self, action: NotificationAction, index: int, total: int, notification_box ): - self.action = action super().__init__( - label=action.label, + 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) ) - if action_number == 0: - self.add_style_class("start-action") - elif action_number == total_actions - 1: - self.add_style_class("end-action") - else: - self.add_style_class("middle-action") def on_clicked(self, *_): self.action.invoke() @@ -72,180 +51,191 @@ class ActionButton(Button): class NotificationWidget(Box): - def __init__(self, notification: Notification, **kwargs): + def __init__(self, notification: Notification, timeout_ms=5000, **kwargs): super().__init__( - name="notification", - spacing=8, + name="notification-box", orientation="v", - **kwargs, - ) - - self._notification = notification - self._timer = None - self.anim_parts = 20 - self.anim_interval = self.get_timeout() / self.anim_parts - self.timeout_in_sec = self.get_timeout() / 1000 - self.time = GLib.DateTime.new_from_unix_local(time.time()).format("%m/%d") - self.connect("button-press-event", self.on_button_press) - - header_container = Box(spacing=8, orientation="h") - self.progress_timeout = AnimatedCircularProgressBar( - name="notification-circular-progress-bar", - size=30, - min_value=0, - max_value=1, - radius_color=True, - value=0, - ) - - header_container.children = ( - self.get_icon(notification.app_icon, notification.app_name, 25), - Label( - markup=str( - self._notification.summary - if self._notification.summary - else notification.app_name - ), - h_align="start", - style="font-weight: 900;", - ellipsization="end", - ), - ) - - header_container.pack_end( - Box( - v_align="start", - children=( - Overlay( - child=self.progress_timeout, - overlays=Button( - image=Image( - icon_name="window-close-symbolic", - icon_size=16, - ), - style_classes="close-button", - on_clicked=lambda *_: self._notification.close( - "dismissed-by-user" - ), - ), - ), - ), - ), - False, - False, - 0, - ) - - body_container = Box(spacing=4, orientation="h") - - if image_pixbuf := self._notification.image_pixbuf: - body_container.add( - Box( - v_expand=True, - v_align="center", - children=CustomImage( - pixbuf=image_pixbuf.scale_simple( - 64, 64, GdkPixbuf.InterpType.BILINEAR - ), - style_classes="image", - ), - ) - ) - - body_container.add( - Label( - markup=self._notification.body, - line_wrap="word-char", - v_align="start", - ellipsization="end", - max_chars_width=40, - h_align="start", - ) - ) - - actions_container = Box( - spacing=4, - orientation="h", - name="notification-action-buttons", - children=[ - ActionButton(action, i, len(self._notification.actions)) - for i, action in enumerate(self._notification.actions) - ], + 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, + ) ) - self.add(header_container) - self.add(body_container) - self.add(actions_container) - - # Destroy this widget once the notification is closed - self._notification.connect( - "closed", - lambda *_: ( - parent.remove(self) if (parent := self.get_parent()) else None, # type: ignore - self.destroy(), - ), - invoke_repeater( - 5 * 1000, - lambda: self._notification.close("expired"), - initial_call=False, - ), + 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()], ) - self.start_timer() - - def start_timer(self): - self._timer = GObject.timeout_add(200, self.update_progress) - - def update_progress(self): - self.progress_timeout.animate_value( - self.progress_timeout.value + 1 / self.anim_parts + 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), + ], + ), + ], ) - if self.progress_timeout.value >= self.timeout_in_sec: - self.progress_timeout.value = self.timeout_in_sec - self.stop_timer() - return True # Continue the timer - def stop_timer(self): - if self._timer: - GObject.source_remove(self._timer) - self._timer = None + def get_pixbuf(self, icon_path, width, height): + if icon_path.startswith("file://"): + icon_path = icon_path[7:] - def get_icon(self, app_icon, app_name, size) -> Image: - app_name = ( - app_name.lower().replace(" ", "-") - if isinstance(app_name, str) - else app_name + 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) + ], ) - if isinstance(app_icon, str) and app_icon.strip(): - if app_icon.startswith("file://") or app_icon.startswith("/"): - file_path = app_icon[7:] if app_icon.startswith("file://") else app_icon - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file(file_path) - scaled_pixbuf = pixbuf.scale_simple( - size, size, GdkPixbuf.InterpType.BILINEAR - ) - return Image(name="notification-icon", pixbuf=scaled_pixbuf) - except Exception as e: - print(f"DEBUG: Failed to load image from {file_path}: {e}") - if app_icon and not app_icon.startswith(("file://", "/")): - icon_name = app_icon - elif app_name and app_name.strip() not in ["notify-send", ""]: - icon_name = app_name.strip() - else: - icon_name = "dialog-information-symbolic" - - return Image(name="notification-icon", icon_name=icon_name, icon_size=size) - - def on_button_press(self, _, event): - if event.button != 1: - (self._notification.close("dismissed-by-user"),) - - def get_timeout(self): - return ( - self._notification.timeout if self._notification.timeout != -1 else 5 * 1000 + 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): @@ -254,11 +244,10 @@ class NotificationRevealer(Revealer): self._notification = notification super().__init__( child=Box( - style="margin: 12px;", children=[self.notif_box], ), - transition_duration=500, - transition_type="crossfade", + transition_duration=250, + transition_type="slide-down", ) self.connect( @@ -289,7 +278,7 @@ class NotificationPopup(Window): self._server.connect("notification-added", self.on_new_notification) super().__init__( - anchor="bottom", + anchor="top right", child=self.notifications, layer="overlay", all_visible=True, From a37efd524173abeca0219597309486175104afe9 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 1 Mar 2025 21:11:09 +0545 Subject: [PATCH 22/22] idk --- config/material-colors/hooks/kitty.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100755 config/material-colors/hooks/kitty.sh diff --git a/config/material-colors/hooks/kitty.sh b/config/material-colors/hooks/kitty.sh new file mode 100755 index 0000000..d6c3dfd --- /dev/null +++ b/config/material-colors/hooks/kitty.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Define the cache path where your colors-kitty.conf file is stored. +CACHE_PATH="${HOME}/.cache/material" +COLORS_FILE="${CACHE_PATH}/colors-kitty.conf" + +if [ -f "$COLORS_FILE" ]; then + # Reload kitty colors using kitty's remote control. + kitty @ set-colors --all "$COLORS_FILE" + # Send the USR1 signal to kitty to complete the reload process. + pkill -USR1 kitty + echo "Kitty colors reloaded successfully." +else + echo "Colors file not found at $COLORS_FILE." +fi