diff --git a/main.css b/main.css index 45f5310..f96e252 100644 --- a/main.css +++ b/main.css @@ -2,10 +2,10 @@ @import url("./styles/colors.css"); @import url("./styles/components.css"); @import url("./styles/controls.css"); -@import url("./styles/launcher.css"); @import url("./styles/indicators.css"); @import url("./styles/workspaces.css"); @import url("./styles/switcher.css"); +@import url("./styles/osd.css"); * { all: unset; diff --git a/main.py b/main.py index d2f4af0..116a7c7 100644 --- a/main.py +++ b/main.py @@ -9,8 +9,8 @@ from loguru import logger from config.data import APP_NAME, APP_NAME_CAP, CONFIG_FILE from modules.corners import Corners from modules.dock.main import Dock -from modules.launcher.main import Launcher from modules.switcher import ApplicationSwitcher +from modules.osd import OSD gi.require_version("GLib", "2.0") @@ -44,9 +44,8 @@ if __name__ == "__main__": corners = Corners() dock = Dock() - launcher = Launcher() switcher = ApplicationSwitcher() - + osd = OSD() # Set corners visibility based on config corners_visible = config.get("corners_visible", True) corners.set_visible(corners_visible) @@ -62,8 +61,8 @@ if __name__ == "__main__": f"{APP_NAME}", dock, corners, - launcher, switcher, + osd, ) # Make sure corners is added to the app def set_css(): diff --git a/modules/dock/components/controls.py b/modules/dock/components/controls.py index d49ae3d..37bd484 100644 --- a/modules/dock/components/controls.py +++ b/modules/dock/components/controls.py @@ -10,13 +10,13 @@ from gi.repository import Gdk, GLib import config.data as data import utils.icons as icons -from services.brightness import Brightness +from services.brightness import Brightness as BrightnessService -class BrightnessSmall(Box): +class Brightness(Box): def __init__(self, **kwargs): super().__init__(name="button-bar-brightness", **kwargs) - self.brightness = Brightness.get_initial() + self.brightness = BrightnessService.get_initial() if self.brightness.screen_brightness == -1: self.destroy() return @@ -46,19 +46,24 @@ class BrightnessSmall(Box): self.brightness.connect("screen", self.on_brightness_changed) self.on_brightness_changed() - def on_scroll(self, widget, event): + def on_scroll(self, _, event): if self.brightness.max_screen == -1: return + + current_brightness = self.brightness.screen_brightness + max_brightness = self.brightness.max_screen + step = max(1, int(max_brightness * 0.01)) # 1% of max brightness + + if event.direction == Gdk.ScrollDirection.SMOOTH: + if hasattr(event, 'delta_y') and abs(event.delta_y) > 0: + new_brightness = current_brightness - int(event.delta_y * step) + self.brightness.screen_brightness = new_brightness + # Remove delta_x handling + elif event.direction == Gdk.ScrollDirection.UP: + self.brightness.screen_brightness = current_brightness + step + elif event.direction == Gdk.ScrollDirection.DOWN: + self.brightness.screen_brightness = current_brightness - step - step_size = 5 - current_norm = self.progress_bar.value - if event.delta_y < 0: - new_norm = min(current_norm + (step_size / self.brightness.max_screen), 1) - elif event.delta_y > 0: - new_norm = max(current_norm - (step_size / self.brightness.max_screen), 0) - else: - return - self.progress_bar.value = new_norm def on_progress_value_changed(self, widget, pspec): if self._updating_from_brightness: @@ -100,7 +105,7 @@ class BrightnessSmall(Box): GLib.source_remove(self._update_source_id) super().destroy() -class VolumeSmall(Box): +class Volume(Box): def __init__(self, **kwargs): super().__init__(name="button-bar-vol", **kwargs) self.audio = Audio() @@ -143,11 +148,19 @@ class VolumeSmall(Box): def on_scroll(self, _, event): if not self.audio.speaker: return + + current_volume = self.audio.speaker.volume + step = 1 + if event.direction == Gdk.ScrollDirection.SMOOTH: - if abs(event.delta_y) > 0: - self.audio.speaker.volume -= event.delta_y - if abs(event.delta_x) > 0: - self.audio.speaker.volume += event.delta_x + if hasattr(event, 'delta_y') and abs(event.delta_y) > 0: + new_volume = current_volume - int(event.delta_y * step) + self.audio.speaker.volume = new_volume + # Remove delta_x handling + elif event.direction == Gdk.ScrollDirection.UP: + self.audio.speaker.volume = current_volume + step + elif event.direction == Gdk.ScrollDirection.DOWN: + self.audio.speaker.volume = current_volume - step def on_speaker_changed(self, *_): if not self.audio.speaker: @@ -183,7 +196,7 @@ class VolumeSmall(Box): else: self.vol_label.set_markup(vol_off_icon) -class MicSmall(Box): +class MicroPhone(Box): def __init__(self, **kwargs): super().__init__(name="button-bar-mic", **kwargs) self.audio = Audio() @@ -227,10 +240,13 @@ class MicSmall(Box): if not self.audio.microphone: return if event.direction == Gdk.ScrollDirection.SMOOTH: - if abs(event.delta_y) > 0: + if hasattr(event, 'delta_y') and abs(event.delta_y) > 0: self.audio.microphone.volume -= event.delta_y - if abs(event.delta_x) > 0: - self.audio.microphone.volume += event.delta_x + # Remove delta_x handling + elif event.direction == Gdk.ScrollDirection.UP: + self.audio.microphone.volume += 1 + elif event.direction == Gdk.ScrollDirection.DOWN: + self.audio.microphone.volume -= 1 def on_microphone_changed(self, *_): if not self.audio.microphone: @@ -257,11 +273,11 @@ class MicSmall(Box): class Controls(Box): def __init__(self, **kwargs): - brightness = Brightness.get_initial() + brightness = BrightnessService.get_initial() children = [] if (brightness.screen_brightness != -1): - children.append(BrightnessSmall()) - children.extend([VolumeSmall(), MicSmall()]) + children.append(Brightness()) + children.extend([Volume(), MicroPhone()]) super().__init__( name="control", orientation="h" if not data.VERTICAL else "v", diff --git a/modules/launcher/main.py b/modules/launcher/main.py deleted file mode 100644 index a1d4dd3..0000000 --- a/modules/launcher/main.py +++ /dev/null @@ -1,336 +0,0 @@ -from collections.abc import Iterator -from typing import Dict, Any -from utils.wayland import WaylandWindow as Window -from fabric.widgets.box import Box -from fabric.widgets.entry import Entry -from fabric.widgets.scrolledwindow import ScrolledWindow -from fabric.widgets.button import Button -from fabric.widgets.label import Label -from fabric.widgets.image import Image -from fabric.utils import get_relative_path, remove_handler, idle_add, DesktopApp -from gi.repository import Gdk, GLib, Gtk - -# Import plugins -from .plugins import LauncherPlugin -from .plugins.applications import ApplicationsPlugin -from .plugins.calculator import CalculatorPlugin -from .plugins.web_search import WebSearchPlugin -from .plugins.emoji import EmojiPlugin -from .plugins.cliphist import CliphistPlugin -from .plugins.powermenu import PowerMenuPlugin - -class Launcher(Window): - def __init__(self): - super().__init__( - name="launcher", - layer="top", - anchor="center", - exclusivity="none", - keyboard_mode="on-demand", - visible=False, - all_visible=False, - ) - self._arranger_handler: int = 0 - self._selected_index = 0 - self._results = [] - self._active_category = None # No active category by default - - # Initialize plugins - self._plugins = [ - ApplicationsPlugin(), - CalculatorPlugin(), - WebSearchPlugin(), - EmojiPlugin(), - CliphistPlugin(), - PowerMenuPlugin(), - ] - - self.viewport = Box(orientation="v", spacing=2) - - self.search = Entry( - placeholder="Search for apps, files, and more...", - h_expand=True, - name="launcher-search", - ) - - # Create the apps ScrolledWindow - self.apps = ScrolledWindow( - spacing=10, - min_content_size=(450, 405), - max_content_size=(450, 405), - child=self.viewport, - name="launcher-apps", - ) - - # Add components to the main window - self.add( - Box( - orientation="v", - spacing=8, - children=[ - self.search, - self.apps - ], - ) - ) - - self.show_all() - self.hide() - self.add_keybinding("Escape", lambda *_: self.close()) - - # Set up the search callback after all UI components are created - self.search.connect("notify::text", lambda entry, *_: self.arrange_viewport(entry.get_text())) - - # Set up Enter key handling for the search entry - self.search.connect("key-press-event", self.on_search_key_press) - - # Initially hide the scrolled window until search begins - self.apps.set_visible(False) - - - def close(self): - self.search.set_text("") - self._selected_index = 0 - self.hide() - - def on_search_key_press(self, entry, event): - """Handle key press events in the search entry""" - # Check if Enter or Return key was pressed - if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): - self.execute_first_result() - return True # Event handled - return False # Let other handlers process the event - - def execute_first_result(self): - """Execute the action of the first search result if available""" - if self._results: - first_result = self._results[0] - # Execute the action, clear search, and hide launcher - first_result["action"]() - self.search.set_text("") - self.hide() - - def _get_result_priority(self, result: Dict[str, Any], query: str) -> int: - """Get priority score for result sorting (lower = higher priority)""" - title = result.get("title", "").lower() - query_lower = query.lower() - - # Priority 100: Generic web search (fallback option) - Check this FIRST - if "search the web for" in title.lower(): - return 100 - - # Priority 0: Clipboard results when searching for "clip" or "clipboard" - if query_lower in ["clip", "clipboard"] and "clip_id" in result: - return 0 - - # Priority 1: Exact matches (applications, emojis with exact names) - if title == query_lower: - return 1 - - # Priority 2: Starts with query (applications, emojis starting with query) - if title.startswith(query_lower): - return 2 - - # Priority 3: Contains query (partial matches) - if query_lower in title: - return 3 - - # Priority 4: Calculator results (specific functional results) - if "=" in title and any(char.isdigit() for char in title): - return 4 - - # Priority 5: Emoji results (specific content matches) - if "emoji_icon" in result: - return 5 - - # Priority 6: Application results (specific app matches) - if "icon" in result and hasattr(result["icon"], "get_icon_pixbuf"): - return 6 - - # Priority 7: Clipboard results (when not directly searching for clipboard) - if "clip_id" in result: - return 7 - - # Priority 8: Other results - return 8 - - def select_next_result(self): - if not self._results: - return - - self._selected_index = (self._selected_index + 1) % len(self._results) - self.update_selection() - - def update_selection(self): - # Remove focus from all buttons - for i, child in enumerate(self.viewport.get_children()): - if isinstance(child, Button): - if i == self._selected_index: - child.grab_focus() - else: - child.set_state_flags(0, True) - - def arrange_viewport(self, query: str = ""): - remove_handler(self._arranger_handler) if self._arranger_handler else None - self._selected_index = 0 - self.viewport.children = [] - self._results = [] - - # Show or hide UI elements based on query - has_query = bool(query) - self.apps.set_visible(has_query) - - # Get results from all plugins - for plugin in self._plugins: - plugin_results = plugin.search(query) - self._results.extend(plugin_results) - - # Sort results by relevance (prioritize specific matches over generic ones) - self._results.sort(key=lambda result: self._get_result_priority(result, query)) - - results_iter = iter(self._results) - - self._arranger_handler = idle_add( - lambda *args: self.add_next_result(*args) - or self.resize_viewport(), - results_iter, - pin=True, - ) - - # Make sure search entry maintains focus - self.search.grab_focus() - - return False - - def update_action_area(self, query: str): - self.action_area.children = [] - - if not query: - return - - # Get action items from all plugins - action_items = [] - for plugin in self._plugins: - action_items.extend(plugin.get_action_items(query)) - - # Add action items to UI - for item in action_items: - action_box = Box(orientation="h", name="launcher-action") - action_box.add(Image(icon_name=item["icon_name"], name="launcher-action-icon")) - action_box.add(Label(label=item["title"], name="launcher-action-label")) - - # Wrap in a button for clickability - action_button = Button( - name="launcher-action-button", - child=action_box, - on_clicked=lambda _, item=item: ( - item["action"](), - self.search.set_text(""), # Clear search entry when action is executed - self.hide(), - ), - ) - - self.action_area.add(action_button) - - self.action_area.show_all() - - def add_next_result(self, results_iter: Iterator[Dict[str, Any]]): - if not (result := next(results_iter, None)): - return False - - button = self.bake_result_slot(result) - self.viewport.add(button) - - # Highlight the first result without stealing focus - if self._selected_index == 0 and self.search.get_text(): - button.set_state_flags(Gtk.StateFlags.FOCUSED, True) - - return True - - def resize_viewport(self): - self.apps.set_min_content_width( - self.viewport.get_allocation().width # type: ignore - ) - return False - - def bake_result_slot(self, result: Dict[str, Any], **kwargs) -> Button: - # Create a more spotlight-like result entry - if "emoji_icon" in result: - # Handle emoji icons - display emoji as text - icon = Label( - label=result["emoji_icon"], - h_align="start", - name="launcher-emoji-icon", - ) - # Set font size for emoji display - icon.build().set_markup(f'{result["emoji_icon"]}').unwrap() - elif "icon_markup" in result: - # Handle icon markup (e.g., power menu icons) - icon = Label( - markup=result["icon_markup"], - h_align="start", - name="launcher-power-icon", - ) - # Set font size and family for icon display - icon.build().set_markup(f'{result["icon_markup"]}').unwrap() - elif "image_pixbuf" in result: - # Handle direct pixbuf images (e.g., clipboard images) - icon = Image( - pixbuf=result["image_pixbuf"], - h_align="start", - name="launcher-app-icon", - ) - elif "icon" in result and isinstance(result["icon"], DesktopApp): - # Handle DesktopApp icons - icon = Image( - pixbuf=result["icon"].get_icon_pixbuf(size=50), - h_align="start", - name="launcher-app-icon", - ) - else: - # Handle regular icon names - icon = Image( - icon_name=result.get("icon_name", "application-x-executable-symbolic"), - h_align="start", - name="launcher-app-icon", - ) - icon.build().set_pixel_size(50).unwrap() - - result_box = Box( - orientation="h", - spacing=12, - children=[ - icon, - Box( - orientation="v", - spacing=2, - children=[ - Label( - label=result["title"], - h_align="start", - name="launcher-app-title", - ), - Label( - label=result.get("description", ""), - h_align="start", - name="launcher-app-description", - ), - ] - ), - ], - ) - - button = Button( - name="launcher-app", - child=result_box, - on_enter_notify_event=lambda *_: self.set_cursor("pointer"), - on_clicked=lambda *_: ( - result["action"](), - self.search.set_text(""), # Clear search entry when item is opened - self.hide(), - ), - **kwargs, - ) - - return button - diff --git a/modules/launcher/plugins/__init__.py b/modules/launcher/plugins/__init__.py deleted file mode 100644 index daa8fbd..0000000 --- a/modules/launcher/plugins/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Plugin system for the launcher module. -Allows extending search functionality with different content types. -""" - -from abc import ABC, abstractmethod -from typing import List, Dict, Any, Optional - - -class LauncherPlugin(ABC): - """Base class for launcher plugins""" - - @property - @abstractmethod - def name(self) -> str: - """Name of the plugin""" - pass - - @property - @abstractmethod - def category(self) -> str: - """Category this plugin belongs to""" - pass - - @property - def icon_name(self) -> str: - """Icon name for this plugin's category""" - return "application-x-executable-symbolic" - - @abstractmethod - def search(self, query: str) -> List[Dict[str, Any]]: - """ - Search for items matching the query - - Returns a list of dictionaries with at least: - - title: str - - description: str - - icon_name: str (optional) - - action: callable - """ - pass - - def get_action_items(self, query: str) -> List[Dict[str, Any]]: - """ - Get quick action items for the query - - Returns a list of dictionaries with at least: - - title: str - - icon_name: str - - action: callable - """ - return [] diff --git a/modules/launcher/plugins/applications.py b/modules/launcher/plugins/applications.py deleted file mode 100644 index 1d41a1a..0000000 --- a/modules/launcher/plugins/applications.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import List, Dict, Any -from fabric.utils import get_desktop_applications, DesktopApp, exec_shell_command_async -from . import LauncherPlugin - - -class ApplicationsPlugin(LauncherPlugin): - """Plugin for searching desktop applications""" - - @property - def name(self) -> str: - return "Applications" - - @property - def category(self) -> str: - return "Applications" - - @property - def icon_name(self) -> str: - return "application-x-executable-symbolic" - - def search(self, query: str) -> List[Dict[str, Any]]: - all_apps = get_desktop_applications() - - # Filter apps based on query - filtered_apps = [ - app - for app in all_apps - if query.casefold() - in ( - (app.display_name or "") - + (" " + app.name + " ") - + (app.generic_name or "") - ).casefold() - ] - - # Sort by relevance - exact matches first - if query: - exact_matches = [] - partial_matches = [] - - for app in filtered_apps: - name = (app.display_name or app.name or "").casefold() - if name == query.casefold() or name.startswith(query.casefold()): - exact_matches.append(app) - else: - partial_matches.append(app) - - filtered_apps = exact_matches + partial_matches - - # Convert to result format - results = [] - for app in filtered_apps: - results.append( - { - "title": app.display_name or app.name or "Unknown", - "description": app.description or "Application", - "icon": app, # Pass the whole app object for icon rendering - "action": lambda app=app: self.launch_app(app), - "app": app, # Store the original app object - } - ) - - return results - - def launch_app(self, app: DesktopApp): - command = ( - " ".join([arg for arg in app.command_line.split() if "%" not in arg]) - if app.command_line - else None - ) - ( - exec_shell_command_async( - f"hyprctl dispatch exec -- {command}", - lambda *_: print(f"Launched {app.name}"), - ) - if command - else None - ) diff --git a/modules/launcher/plugins/calculator.py b/modules/launcher/plugins/calculator.py deleted file mode 100644 index c19c33a..0000000 --- a/modules/launcher/plugins/calculator.py +++ /dev/null @@ -1,54 +0,0 @@ -import re -from typing import List, Dict, Any -from . import LauncherPlugin - - -class CalculatorPlugin(LauncherPlugin): - """Plugin for calculator functionality""" - - @property - def name(self) -> str: - return "Calculator" - - @property - def category(self) -> str: - return "Utilities" - - @property - def icon_name(self) -> str: - return "accessories-calculator-symbolic" - - def search(self, query: str) -> List[Dict[str, Any]]: - # Only process if query looks like a math expression - if not (query.isdigit() or re.match(r"^[\d+\-*/().]+$", query)): - return [] - - try: - result = eval(query) - return [ - { - "title": f"{query} = {result}", - "description": "Calculator result", - "icon_name": "accessories-calculator-symbolic", - "action": lambda: None, # No action needed - } - ] - except: - return [] - - def get_action_items(self, query: str) -> List[Dict[str, Any]]: - # Only process if query looks like a math expression - if not (query.isdigit() or re.match(r"^[\d+\-*/().]+$", query)): - return [] - - try: - result = eval(query) - return [ - { - "title": f"Calculate: {query} = {result}", - "icon_name": "accessories-calculator-symbolic", - "action": lambda: None, # No action needed - } - ] - except: - return [] diff --git a/modules/launcher/plugins/cliphist.py b/modules/launcher/plugins/cliphist.py deleted file mode 100644 index 55f76cd..0000000 --- a/modules/launcher/plugins/cliphist.py +++ /dev/null @@ -1,259 +0,0 @@ -import re -import subprocess -from typing import List, Dict, Any - -import gi -gi.require_version('GdkPixbuf', '2.0') -from gi.repository import GdkPixbuf - -from . import LauncherPlugin - - -class CliphistPlugin(LauncherPlugin): - """Plugin for clipboard history functionality using cliphist""" - - @property - def name(self) -> str: - return "Clipboard History" - - @property - def category(self) -> str: - return "Utilities" - - @property - def icon_name(self) -> str: - return "edit-paste-symbolic" - - def search(self, query: str) -> List[Dict[str, Any]]: - if not query: - return [] - - query_lower = query.lower() - - # Show clipboard history when user types "clip" (like Albert launcher) - if query_lower == "clip" or query_lower == "clipboard": - return self._get_clipboard_history() - - # Also show clipboard history if query starts with "clip " for further filtering - if query_lower.startswith("clip ") and len(query) > 5: - search_term = query[5:].strip() # Remove "clip " prefix - return self._get_clipboard_history(search_term) - - return [] - - def _get_clipboard_history(self, search_term: str = "") -> List[Dict[str, Any]]: - """Get clipboard history, optionally filtered by search term""" - try: - # Get clipboard history from cliphist - result = subprocess.run( - ["cliphist", "list"], - capture_output=True, - text=True, - timeout=5 - ) - - if result.returncode != 0: - return [] - - entries = result.stdout.strip().split('\n') - if not entries or entries == ['']: - return [] - - results = [] - search_lower = search_term.lower() if search_term else "" - - for entry in entries[:50]: # Limit to first 50 entries for performance - if not entry.strip(): - continue - - # Parse entry format: "ID\tCONTENT" - parts = entry.split('\t', 1) - if len(parts) != 2: - continue - - entry_id, content = parts - content = content.strip() - - # Check if this is an image - is_image = self._is_image_data(content) - - # Skip binary data entries that aren't images - if content.startswith('[[') and 'binary data' in content and not is_image: - continue - - # If we have a search term, filter by it - if search_term and search_lower not in content.lower(): - continue - - # Prepare display content and icon - if is_image: - display_content = "[Image]" - # Try to load image preview - image_pixbuf = self._load_image_preview(entry_id) - if image_pixbuf: - result_item = { - "title": display_content, - "image_pixbuf": image_pixbuf, # Custom field for image preview - "action": lambda eid=entry_id: self.copy_to_clipboard(eid), - "clip_id": entry_id, - } - else: - result_item = { - "title": display_content, - "icon_name": "image-x-generic-symbolic", - "action": lambda eid=entry_id: self.copy_to_clipboard(eid), - "clip_id": entry_id, - } - else: - # Truncate long content for display - display_content = content - if len(display_content) > 80: - display_content = display_content[:77] + "..." - # Replace newlines with spaces for display - display_content = display_content.replace('\n', ' ').replace('\r', ' ') - - result_item = { - "title": display_content, - "icon_name": "edit-paste-symbolic", - "action": lambda eid=entry_id: self.copy_to_clipboard(eid), - "clip_id": entry_id, - } - - results.append(result_item) - - # # Limit results to prevent overwhelming the UI - # if len(results) >= 15: - # break - - # Sort results by relevance if we have a search term - if search_term: - def sort_key(item): - title = item["title"].lower() - if title.startswith(search_lower): - return 0 # Starts with query - elif search_lower in title: - return 1 # Contains query - else: - return 2 # Other matches - results.sort(key=sort_key) - - return results - - except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): - return [] - except Exception as e: - print(f"Error in cliphist plugin: {e}") - return [] - - def copy_to_clipboard(self, entry_id: str): - """Copy clipboard history entry back to clipboard""" - try: - # Use cliphist decode to get the content and pipe it to wl-copy - decode_process = subprocess.Popen( - ["cliphist", "decode", entry_id], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL - ) - - copy_process = subprocess.Popen( - ["wl-copy"], - stdin=decode_process.stdout, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - - decode_process.stdout.close() # Allow decode_process to receive SIGPIPE - copy_process.wait(timeout=2) - decode_process.wait(timeout=2) - - print(f"Copied clipboard history entry {entry_id}") - - except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): - try: - # Fallback to xclip - decode_process = subprocess.Popen( - ["cliphist", "decode", entry_id], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True - ) - - content, _ = decode_process.communicate(timeout=2) - - if decode_process.returncode == 0: - subprocess.Popen( - ["xclip", "-selection", "clipboard"], - input=content, - text=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL - ) - print(f"Copied clipboard history entry {entry_id} (xclip)") - - except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): - print(f"Could not copy clipboard history entry {entry_id}") - except Exception as e: - print(f"Error copying clipboard entry: {e}") - - def _is_image_data(self, content): - """Determine if clipboard content is likely an image""" - return ( - content.startswith("data:image/") or - content.startswith("\x89PNG") or - content.startswith("GIF8") or - content.startswith("\xff\xd8\xff") or - re.match(r'^\s* height: - new_width = max_size - new_height = int(height * (max_size / width)) - else: - new_height = max_size - new_width = int(width * (max_size / height)) - - scaled_pixbuf = pixbuf.scale_simple( - new_width, new_height, GdkPixbuf.InterpType.BILINEAR - ) - return scaled_pixbuf - - except Exception as e: - pass - - return None - - def get_action_items(self, query: str) -> List[Dict[str, Any]]: - """Get quick action items for clipboard history""" - if not query or len(query) < 2: - return [] - - return [ - { - "title": f'Search clipboard history for "{query}"', - "icon_name": "edit-paste-symbolic", - "action": lambda: None, # No specific action needed - } - ] - - diff --git a/modules/launcher/plugins/emoji.py b/modules/launcher/plugins/emoji.py deleted file mode 100644 index 17b30b2..0000000 --- a/modules/launcher/plugins/emoji.py +++ /dev/null @@ -1,122 +0,0 @@ -import json -import os -import subprocess -from typing import List, Dict, Any -from . import LauncherPlugin - - -class EmojiPlugin(LauncherPlugin): - """Plugin for emoji search and clipboard functionality""" - - def __init__(self): - self._emojis = {} - self._load_emojis() - - def _load_emojis(self): - """Load emoji data from the JSON file""" - try: - emoji_file_path = os.path.expanduser("~/Modus/config/emoji.json") - with open(emoji_file_path, "r", encoding="utf-8") as f: - self._emojis = json.load(f) - except Exception as e: - print(f"Failed to load emoji data: {e}") - self._emojis = {} - - @property - def name(self) -> str: - return "Emoji" - - @property - def category(self) -> str: - return "Utilities" - - @property - def icon_name(self) -> str: - return "face-smile-symbolic" - - def search(self, query: str) -> List[Dict[str, Any]]: - if not query or len(query) < 2: # Require at least 2 characters - return [] - - results = [] - query_lower = query.lower() - - # Search through emojis - for emoji_char, emoji_data in self._emojis.items(): - # Search in name, slug, and group - searchable_text = ( - emoji_data.get("name", "").lower() - + " " - + emoji_data.get("slug", "").lower() - + " " - + emoji_data.get("group", "").lower() - ) - - if query_lower in searchable_text: - results.append( - { - "title": emoji_data.get('name', 'Unknown'), - "description": f"{emoji_data.get('group', 'Unknown')}", - "emoji_icon": emoji_char, # Store the emoji character for display - "action": lambda emoji=emoji_char: self.copy_emoji(emoji), - } - ) - - # Limit results to prevent overwhelming the UI - if len(results) >= 20: - break - - # Sort results by relevance (exact matches first) - def sort_key(item): - name = item["title"].lower() - # Check if query matches the emoji name - if name.startswith(query_lower): - return 0 # Name starts with query - elif query_lower in name: - return 1 # Partial name match - else: - return 2 # Other matches - - results.sort(key=sort_key) - return results - - def copy_emoji(self, emoji: str): - """Copy emoji to clipboard using wl-copy""" - # Use a fire-and-forget approach to avoid blocking - try: - # Start wl-copy in the background and don't wait for it - subprocess.Popen( - ["wl-copy", emoji], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL - ) - print(f"Copied emoji to clipboard: {emoji}") - except FileNotFoundError: - try: - # Fallback to xclip - process = subprocess.Popen( - ["xclip", "-selection", "clipboard"], - stdin=subprocess.PIPE, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - text=True, - ) - process.stdin.write(emoji) - process.stdin.close() - print(f"Copied emoji to clipboard (xclip): {emoji}") - except (FileNotFoundError, OSError): - print(f"Could not copy emoji to clipboard: {emoji}") - except OSError: - print(f"Could not copy emoji to clipboard: {emoji}") - - def get_action_items(self, query: str) -> List[Dict[str, Any]]: - """Get quick action items for emoji search""" - if not query or len(query) < 2: - return [] - - # Return a quick action to search for emojis - return [ - { - "title": f'Search emojis for "{query}"', - "icon_name": "face-smile-symbolic", - "action": lambda: None, # No specific action needed - } - ] diff --git a/modules/launcher/plugins/powermenu.py b/modules/launcher/plugins/powermenu.py deleted file mode 100644 index c9e753d..0000000 --- a/modules/launcher/plugins/powermenu.py +++ /dev/null @@ -1,202 +0,0 @@ -import subprocess -from typing import List, Dict, Any - -import utils.icons as icons -from . import LauncherPlugin - - -class PowerMenuPlugin(LauncherPlugin): - """Plugin for system power management functionality""" - - @property - def name(self) -> str: - return "Power Menu" - - @property - def category(self) -> str: - return "System" - - @property - def icon_name(self) -> str: - return "system-shutdown-symbolic" - - def search(self, query: str) -> List[Dict[str, Any]]: - if not query: - return [] - - query_lower = query.lower() - - # Show power menu when user types "sys", "system", "power", etc. - power_triggers = ["sys", "system", "power", "shutdown", "restart", "reboot", "logout", "suspend", "sleep", "lock"] - - if any(query_lower.startswith(trigger) for trigger in power_triggers): - return self._get_power_options(query_lower) - - return [] - - def _get_power_options(self, query: str = "") -> List[Dict[str, Any]]: - """Get available power management options""" - power_options = [ - { - "title": "Shutdown", - "description": "Turn off the computer completely", - "icon_markup": icons.shutdown, - "action": self.shutdown_system, - "keywords": ["shutdown", "power off", "turn off", "sys", "halt"], - }, - { - "title": "Restart", - "description": "Restart the computer", - "icon_markup": icons.reboot, - "action": self.restart_system, - "keywords": ["restart", "reboot", "sys"], - }, - { - "title": "Logout", - "description": "End current user session", - "icon_markup": icons.logout, - "action": self.logout_session, - "keywords": ["logout", "sign out", "end session", "sys"], - }, - { - "title": "Suspend", - "description": "Suspend to RAM (sleep mode)", - "icon_markup": icons.suspend, - "action": self.suspend_system, - "keywords": ["suspend", "sleep", "sys"], - }, - { - "title": "Lock Screen", - "description": "Lock the current session", - "icon_markup": icons.lock, - "action": self.lock_screen, - "keywords": ["lock", "lock screen", "sys"], - }, - ] - - # Filter options based on query if it's more specific - if len(query) > 3: # More than just "sys" - filtered_options = [] - for option in power_options: - # Check if query matches any keywords - if any(query in keyword for keyword in option["keywords"]): - filtered_options.append(option) - - if filtered_options: - power_options = filtered_options - - # Convert to launcher result format - results = [] - for option in power_options: - results.append({ - "title": option["title"], - "description": option["description"], - "icon_markup": option["icon_markup"], # Use markup for custom icons - "action": option["action"], - "power_option": True, # Mark as power option for special handling - }) - - return results - - def shutdown_system(self): - """Shutdown the system""" - print("Initiating system shutdown...") - try: - # Try systemctl first (systemd) - subprocess.run(["systemctl", "poweroff"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - try: - # Fallback to shutdown command - subprocess.run(["shutdown", "-h", "now"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - print("Could not shutdown system - no suitable command found") - - def restart_system(self): - """Restart the system""" - print("Initiating system restart...") - try: - # Try systemctl first (systemd) - subprocess.run(["systemctl", "reboot"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - try: - # Fallback to shutdown command - subprocess.run(["shutdown", "-r", "now"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - print("Could not restart system - no suitable command found") - - def logout_session(self): - """Logout from current session""" - try: - # Try different logout methods based on desktop environment - # Hyprland - subprocess.run(["hyprctl", "dispatch", "exit"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - try: - # GNOME - subprocess.run(["gnome-session-quit", "--logout", "--no-prompt"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - try: - # KDE - subprocess.run(["qdbus", "org.kde.ksmserver", "/KSMServer", "logout", "0", "0", "0"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - try: - # Generic X11 - subprocess.run(["pkill", "-KILL", "-u", subprocess.getoutput("whoami")], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - print("Could not logout - no suitable method found") - - def suspend_system(self): - """Suspend the system""" - try: - # Try systemctl first (systemd) - subprocess.run(["systemctl", "suspend"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - try: - # Fallback to pm-suspend - subprocess.run(["pm-suspend"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - print("Could not suspend system - no suitable command found") - - def lock_screen(self): - """Lock the screen""" - try: - # Try different lock methods - # Hyprland - subprocess.run(["hyprlock"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - try: - # swaylock (Sway/Wayland) - subprocess.run(["swaylock"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - try: - # i3lock (X11) - subprocess.run(["i3lock"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - try: - # GNOME - subprocess.run(["gnome-screensaver-command", "--lock"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - try: - # KDE - subprocess.run(["qdbus", "org.freedesktop.ScreenSaver", "/ScreenSaver", "Lock"], check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - print("Could not lock screen - no suitable command found") - - def get_action_items(self, query: str) -> List[Dict[str, Any]]: - """Get quick action items for power management""" - if not query or len(query) < 2: - return [] - - query_lower = query.lower() - - # Show quick actions for system-related queries - if any(trigger in query_lower for trigger in ["sys", "power", "shutdown", "restart"]): - return [ - { - "title": f'System power options for "{query}"', - "icon_name": "system-shutdown-symbolic", - "action": lambda: None, # No specific action needed - } - ] - - return [] diff --git a/modules/launcher/plugins/web_search.py b/modules/launcher/plugins/web_search.py deleted file mode 100644 index 26dba34..0000000 --- a/modules/launcher/plugins/web_search.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import List, Dict, Any -from fabric.utils import exec_shell_command_async -from . import LauncherPlugin - - -class WebSearchPlugin(LauncherPlugin): - """Plugin for web search functionality""" - - @property - def name(self) -> str: - return "Web Search" - - @property - def category(self) -> str: - return "Web" - - @property - def icon_name(self) -> str: - return "web-browser-symbolic" - - def search(self, query: str) -> List[Dict[str, Any]]: - if not query: - return [] - - return [ - { - "title": f'Search the web for "{query}"', - "description": "Open in default browser", - "icon_name": "web-browser-symbolic", - "action": lambda q=query: self.search_web(q), - } - ] - - def get_action_items(self, query: str) -> List[Dict[str, Any]]: - if not query: - return [] - - return [ - { - "title": f'Search web for "{query}"', - "icon_name": "web-browser-symbolic", - "action": lambda q=query: self.search_web(q), - } - ] - - def search_web(self, query: str): - # Use xdg-open to open the default browser with a search query - search_url = f"https://www.google.com/search?q={query.replace(' ', '+')}" - exec_shell_command_async(f"xdg-open '{search_url}'", lambda *_: None) diff --git a/modules/osd.py b/modules/osd.py new file mode 100644 index 0000000..5d19ebf --- /dev/null +++ b/modules/osd.py @@ -0,0 +1,426 @@ +import time +from typing import ClassVar, Literal + +import utils.icons as icons +from fabric.audio import Audio +from fabric.widgets.box import Box +from fabric.widgets.label import Label +from fabric.widgets.revealer import Revealer +from fabric.widgets.scale import Scale, ScaleMark +from gi.repository import GLib, GObject +from services.brightness import Brightness +from utils.animator import Animator +from utils.wayland import WaylandWindow as Window + + +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() + + +class BrightnessOSDContainer(Box): + def __init__(self, **kwargs): + super().__init__(**kwargs, orientation="h", spacing=12, name="osd") + self.brightness_service = Brightness.get_initial() + self.icon = Label(markup=icons.brightness, name="brighntess-icons") + 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: + if brightness_percentage >= 67: + return icons.brightness_high + elif brightness_percentage >= 34: + return icons.brightness_medium + else: + return icons.brightness_low + + +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 = Label(markup=icons.vol_high, name="vol-icon") + 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) + + # Connect to speaker-changed signal directly + self.audio.connect("speaker-changed", self.on_speaker_changed) + + # Connect to current speaker if available + if self.audio.speaker: + self.audio.speaker.connect("changed", self.on_speaker_changed) + self.audio.speaker.connect("notify::muted", self.on_mute_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) + + # Apply muted style based on volume and mute state + if volume == 0 or (self.audio.speaker and self.audio.speaker.muted): + self.scale.add_style_class("muted") + self.icon.add_style_class("muted") + else: + self.scale.remove_style_class("muted") + self.icon.remove_style_class("muted") + + self.emit("volume-changed") + + def on_audio_speaker_changed(self, *_): + if self.audio.speaker: + self.audio.speaker.connect("changed", self.on_speaker_changed) + self.update_volume() + + def on_speaker_changed(self, *_): + # This will be called when any property of the speaker changes, including mute + self.update_volume() + self.update_icon(round(self.audio.speaker.volume)) + self.emit("volume-changed") + + 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) + + # Apply muted style based on volume and mute state + if volume == 0 or (self.audio.speaker and self.audio.speaker.muted): + self.scale.add_style_class("muted") + self.icon.add_style_class("muted") + else: + self.scale.remove_style_class("muted") + self.icon.remove_style_class("muted") + + def update_icon(self, volume): + # Check if muted first, regardless of volume level + if not self.audio.speaker: + return + + # Get appropriate icons based on device type + vol_high_icon = icons.vol_high + vol_medium_icon = icons.vol_medium + vol_mute_icon = icons.vol_off + vol_off_icon = icons.vol_mute + + # Check if it's a bluetooth device + if self.audio.speaker and "bluetooth" in self.audio.speaker.icon_name: + vol_high_icon = icons.bluetooth_connected + vol_medium_icon = icons.bluetooth + vol_mute_icon = icons.bluetooth_off + vol_off_icon = icons.bluetooth_disconnected + + # Set the appropriate icon based on mute state and volume + if self.audio.speaker.muted: + self.icon.set_label(vol_mute_icon) + self.scale.add_style_class("muted") + self.icon.add_style_class("muted") + else: + if volume == 0: + self.scale.add_style_class("muted") + self.icon.add_style_class("muted") + else: + self.scale.remove_style_class("muted") + self.icon.remove_style_class("muted") + + if volume >= 67: + self.icon.set_label(vol_high_icon) + elif volume >= 34: + self.icon.set_label(vol_medium_icon) + elif volume >= 1: + self.icon.set_label(vol_medium_icon) # Use medium for low volumes too + else: + self.icon.set_label(vol_off_icon) + + def _get_audio_icon_name(self, volume: int) -> str: + # This method is now deprecated in favor of update_icon + # but kept for compatibility + if volume >= 67: + return icons.vol_high + elif volume >= 34: + return icons.vol_medium + elif volume >= 1: + return icons.vol_mute + else: + return icons.vol_off + + def on_mute_changed(self, *_): + if self.audio.speaker: + self.update_icon(round(self.audio.speaker.volume)) + # Ensure we emit the signal immediately + GLib.idle_add(lambda: self.emit("volume-changed")) + + +class MicrophoneOSDContainer(Box): + __gsignals__: ClassVar[dict] = { + "mic-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 = Label(markup=icons.mic, name="mic-icon") + 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::microphone", self.on_audio_microphone_changed) + + # Connect to microphone-changed signal directly + self.audio.connect("microphone-changed", self.on_microphone_changed) + + # Connect to current microphone if available + if self.audio.microphone: + self.audio.microphone.connect("changed", self.on_microphone_changed) + self.audio.microphone.connect("notify::muted", self.on_mute_changed) + + def sync_with_audio(self): + if self.audio.microphone: + volume = round(self.audio.microphone.volume) + self.scale.set_value(volume) + self.update_icon(volume) + + def on_volume_changed(self, *_): + if self.audio.microphone: + volume = self.scale.value + if 0 <= volume <= 100: + self.audio.microphone.set_volume(volume) + self.update_icon(volume) + + # Apply muted style based on volume and mute state + if volume == 0 or (self.audio.microphone and self.audio.microphone.muted): + self.scale.add_style_class("muted") + self.icon.add_style_class("muted") + else: + self.scale.remove_style_class("muted") + self.icon.remove_style_class("muted") + + self.emit("mic-changed") + + def on_audio_microphone_changed(self, *_): + if self.audio.microphone: + self.audio.microphone.connect("changed", self.on_microphone_changed) + self.update_volume() + + def on_microphone_changed(self, *_): + # This will be called when any property of the microphone changes, including mute + self.update_volume() + self.update_icon(round(self.audio.microphone.volume)) + self.emit("mic-changed") + + def update_volume(self, *_): + if self.audio.microphone and not self.is_hovered(): + volume = round(self.audio.microphone.volume) + self.scale.set_value(volume) + self.update_icon(volume) + + # Apply muted style based on volume and mute state + if volume == 0 or (self.audio.microphone and self.audio.microphone.muted): + self.scale.add_style_class("muted") + self.icon.add_style_class("muted") + else: + self.scale.remove_style_class("muted") + self.icon.remove_style_class("muted") + + def update_icon(self, volume): + # Check if muted first, regardless of volume level + if not self.audio.microphone: + return + + if self.audio.microphone.muted: + self.icon.set_label(icons.mic_mute) + self.scale.add_style_class("muted") + self.icon.add_style_class("muted") + else: + if volume == 0: + self.scale.add_style_class("muted") + self.icon.add_style_class("muted") + else: + self.scale.remove_style_class("muted") + self.icon.remove_style_class("muted") + + if volume >= 67: + self.icon.set_label(icons.mic) + elif volume >= 34: + self.icon.set_label(icons.mic) + elif volume >= 1: + self.icon.set_label(icons.mic) + else: + self.icon.set_label(icons.mic_mute) + + def on_mute_changed(self, *_): + if self.audio.microphone: + self.update_icon(round(self.audio.microphone.volume)) + # Ensure we emit the signal immediately + GLib.idle_add(lambda: self.emit("mic-changed")) + + +class OSD(Window): + def __init__(self, **kwargs): + self.audio_container = AudioOSDContainer() + self.brightness_container = BrightnessOSDContainer() + self.microphone_container = MicrophoneOSDContainer() + + self.timeout = 1000 + + self.revealer = Revealer( + transition_type="slide-up", + transition_duration=100, + child_revealed=False, + ) + + self.main_box = Box( + orientation="v", + spacing=13, + children=[self.revealer], + ) + + super().__init__( + layer="overlay", + anchor="bottom", + child=self.main_box, + visible=False, + pass_through=True, + keyboard_mode="on-demand", + **kwargs, + ) + + self.last_activity_time = time.time() + + # Connect to audio and brightness signals + self.audio_container.audio.connect("notify::speaker", self.show_audio) + self.brightness_container.brightness_service.connect("screen", self.show_brightness) + self.audio_container.connect("volume-changed", self.show_audio) + self.microphone_container.connect("mic-changed", self.show_microphone) + + # Connect to speaker changes to handle new speakers + self.audio_container.audio.connect("speaker-changed", self.show_audio) + + # Connect to microphone changes to handle new microphones + self.microphone_container.audio.connect("microphone-changed", self.show_microphone) + + # Connect to the current speaker if available + if self.audio_container.audio.speaker: + self.audio_container.audio.speaker.connect("changed", self.show_audio) + + # Connect to the current microphone if available + if self.microphone_container.audio.microphone: + self.microphone_container.audio.microphone.connect("changed", self.show_microphone) + + GLib.timeout_add(100, self.check_inactivity) + + def show_audio(self, *_): + self.show_box(box_to_show="audio") + self.reset_inactivity_timer() + + def show_brightness(self, *_): + self.show_box(box_to_show="brightness") + self.reset_inactivity_timer() + + def show_microphone(self, *_): + self.show_box(box_to_show="microphone") + self.reset_inactivity_timer() + + def show_box(self, box_to_show: Literal["audio", "brightness", "microphone"]): + 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 + elif box_to_show == "microphone": + self.revealer.children = self.microphone_container + self.revealer.set_reveal_child(True) + self.reset_inactivity_timer() + + def start_hide_timer(self): + self.set_visible(False) + + def reset_inactivity_timer(self): + self.last_activity_time = time.time() + + def check_inactivity(self): + if time.time() - self.last_activity_time >= (self.timeout / 1000): + self.start_hide_timer() + return True diff --git a/services/brightness.py b/services/brightness.py index 49527b9..1cd2915 100644 --- a/services/brightness.py +++ b/services/brightness.py @@ -89,7 +89,6 @@ class Brightness(Service): try: exec_brightnessctl_async(f"--device '{screen_device}' set {value}") self.emit("screen", int((value / self.max_screen) * 100)) - logger.info(f"Set screen brightness to {value} (out of {self.max_screen})") except GLib.Error as e: logger.error(f"Error setting screen brightness: {e.message}") except Exception as e: diff --git a/styles/launcher.css b/styles/launcher.css deleted file mode 100644 index 0b78ccc..0000000 --- a/styles/launcher.css +++ /dev/null @@ -1,48 +0,0 @@ -#launcher { - transition: all 0.5s cubic-bezier(0.5, 0.25, 0, 1); - background-color: var(--shadow); -} - -#launcher entry { - transition: all 0.1s ease; - border-radius: 11px; - box-shadow: inset 0 0 0 1px var(--outline); - min-width: 400px; - padding: 10px; -} - -#slot-button { - border-radius: 40px; - padding: 0 16px; - animation: loadSlot 0.5s ease; - min-height: 52px; - transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes loadSlot { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -#slot-button:hover, -#slot-button:focus, -#slot-button:selected, -#slot-button.selected { - border-radius: 16px; - background-color: var(--surface); - padding-left: 20px; -} - -#slot-button:active { - background-color: var(--primary); -} - -#clip-icon { - font-size: 20px; - color: var(--primary); -} diff --git a/styles/osd.css b/styles/osd.css new file mode 100644 index 0000000..407af9e --- /dev/null +++ b/styles/osd.css @@ -0,0 +1,49 @@ +#osd { + background: var(--shadow); + padding: 12px 20px; + margin-bottom: 60px; +} + +#osd scale { + min-width: 150px; +} + +#osd trough { + background: var(--surface); + min-height: 10px; + margin-right: 4px; + transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +#osd trough highlight { + border-radius: 100px 0px 0px 100px; + background: var(--primary); + transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +#osd.muted trough highlight, +#osd.muted slider, +#osd scale.muted trough highlight, +#osd scale.muted slider { + background-color: var(--surface-bright); +} + +#osd slider { + background-color: var(--primary); + background-size: 20px 20px; + min-width: 4px; + min-height: 8px; + border-radius: 2px; + padding: 0px; + margin: -14px 0; + box-shadow: + -4px 0 0px 2px var(--shadow), + 4px 0 0px 2px var(--shadow); + transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +#brighntess-icons.muted, +#vol-icon.muted, +#mic-icon.muted { + color: var(--outline); +}