From bb1e4e7027aa1337e6529206cf5057cfefc421dd Mon Sep 17 00:00:00 2001 From: Chintan Patel <216989679+Chintanpatel24@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:55:27 +0530 Subject: [PATCH 1/2] feat(shell): implement unified popout manager, pill anchoring, and fast IPC - Add UnifiedPopoutManager and PopoutScrim in snippets/popout_manager.py - Single persistent Wayland LayerShell popout surface per bar - Smooth pill tracking with 60fps cubic ease-out sliding animation - In-place hardware-accelerated crossfade stack transition - Active pill bonding with .bar-widget.active-popout accent illumination - Desktop dimming scrim with click-outside dismissal - Connect PopoutManager to Bar, WidgetWrapper, GroupWrapper, and BarManager.toggle in bar.py - Enhance Launcher with inline math evaluation (=) and terminal commands (>) in windows/launcher.py - Add dedicated subcommands to bin/agl: volume, brightness, launcher, cc - Update config/niri.kdl keybindings to use clean agl subcommands --- bar.py | 50 +++++- bin/agl | 69 ++++++- config/niri.kdl | 15 +- snippets/__init__.py | 4 +- snippets/popout_manager.py | 355 +++++++++++++++++++++++++++++++++++++ style/applet.css | 27 ++- style/bar.css | 17 +- style/launcher.css | 10 ++ windows/launcher.py | 111 ++++++++++++ 9 files changed, 642 insertions(+), 16 deletions(-) create mode 100644 snippets/popout_manager.py diff --git a/bar.py b/bar.py index 2fff910..a371b09 100644 --- a/bar.py +++ b/bar.py @@ -6,7 +6,7 @@ from fabric.widgets.box import Box from fabric.widgets.centerbox import CenterBox from fabric.widgets.eventbox import EventBox -from snippets import HackedRevealer, enable_blur, set_blur_regions_from_widget, disable_blur, free_blur, AppletReveal +from snippets import HackedRevealer, enable_blur, set_blur_regions_from_widget, disable_blur, free_blur, AppletReveal, UnifiedPopoutManager from gi.repository import Gdk, Gtk, GLib, GtkLayerShell from bar_widgets import ( LauncherButton, BluetoothButton, BatteryButton, CalendarButton, ClockButton, @@ -547,6 +547,11 @@ def _on_enter(self, widget, event): GLib.source_remove(self._leave_timer) self._leave_timer = None + bar = self._get_bar() + if bar and hasattr(bar, "popout_manager") and bar.popout_manager.is_open and self.widget_key in APPLET_WIDGETS: + bar.popout_manager.open(self.widget_key, self.event_box) + return False + if not _is_hover_enabled_for_key(self.widget_key): return False if self._hover_timer is not None: @@ -602,6 +607,11 @@ def _trigger_hover_open(self): singletons.bar_manager._dash.toggle(active_monitor) self._hook_dash_hover_leave() elif self.widget_key in APPLET_WIDGETS: + bar = self._get_bar() + if bar and hasattr(bar, "popout_manager"): + self._opened_by_hover = True + bar.popout_manager.open(self.widget_key, self.event_box) + return False popup = self._ensure_popup() if popup and not popup.is_visible(): self._opened_by_hover = True @@ -735,6 +745,10 @@ def _on_click(self, _widget, event: Gdk.EventButton): return False if event.button != 1: return False + bar = self._get_bar() + if bar and hasattr(bar, "popout_manager") and self.widget_key in APPLET_WIDGETS: + bar.popout_manager.toggle(self.widget_key, self.event_box) + return True popup = self._ensure_popup() if popup is None: return False @@ -1215,6 +1229,19 @@ def _on_child_click(self, _widget, event: Gdk.EventButton): return False if event.button != 1: return False + + bar = self._get_bar() + if bar and hasattr(bar, "popout_manager"): + try: + idx = self._event_boxes.index(_widget) + if 0 <= idx < len(self.widget_keys): + key = self.widget_keys[idx] + if key in APPLET_WIDGETS: + bar.popout_manager.toggle(key, _widget) + return True + except ValueError: + pass + popup = self._ensure_popup() if popup is None: return False @@ -1234,6 +1261,13 @@ def _on_outer_click(self, widget, event: Gdk.EventButton): if event.button != 1: return False + bar = self._get_bar() + if bar and hasattr(bar, "popout_manager"): + valid_keys = [k for k in self.widget_keys if k in APPLET_WIDGETS] + if valid_keys: + bar.popout_manager.toggle(valid_keys[0], self._outer_eb) + return True + popup = self._ensure_popup() if popup is None: return False @@ -1774,6 +1808,8 @@ def __init__(self, monitor_id: int = 0, bar_index: int = 0, bar_cfg: dict = None except Exception: pass + self.popout_manager = UnifiedPopoutManager(bar=self, applet_factories=APPLET_WIDGETS) + if not self.auto_hide: GLib.idle_add(self._revealer.set_reveal_child, True) else: @@ -2455,11 +2491,17 @@ def toggle(self, key: str): for section in bar.sections.values(): for child in section.get_children(): if isinstance(child, WidgetWrapper) and child.widget_key == key: + if hasattr(bar, "popout_manager") and bar.popout_manager: + bar.popout_manager.toggle(key, child.event_box) + return popup = child._ensure_popup() if popup: popup.toggle() return elif isinstance(child, GroupWrapper) and key in child.widget_keys: + if hasattr(bar, "popout_manager") and bar.popout_manager: + bar.popout_manager.toggle(key, child._outer_eb) + return popup = child._ensure_popup() if popup: popup.toggle() @@ -2468,6 +2510,12 @@ def toggle(self, key: str): if key not in APPLET_WIDGETS: return + for (monitor, _), bar in self._bars.items(): + if get_connector_from_monitor_id(bar.monitor_id) == active_output: + if hasattr(bar, "popout_manager") and bar.popout_manager: + bar.popout_manager.toggle(key, bar._centerbox) + return + if key not in self._fallback_popups: widget_class = APPLET_WIDGETS[key] self._fallback_popups[key] = AppletWindow( diff --git a/bin/agl b/bin/agl index 45ec3bf..f25062d 100755 --- a/bin/agl +++ b/bin/agl @@ -387,12 +387,30 @@ cmd_toggle() { dash) fabric-cli exec agility-shell "bar_manager.toggle('Dash')" 2>/dev/null || info "Shell not active." ;; - settings) - fabric-cli exec agility-shell "bar_manager.open_settings()" 2>/dev/null || info "Shell not active." + settings|control-center|cc|quicksettings|quick-settings) + fabric-cli exec agility-shell "bar_manager.toggle('Settings')" 2>/dev/null || info "Shell not active." ;; launcher) fabric-cli exec agility-shell "bar_manager.toggle('Launcher')" 2>/dev/null || info "Shell not active." ;; + calendar) + fabric-cli exec agility-shell "bar_manager.toggle('Calendar')" 2>/dev/null || info "Shell not active." + ;; + audio|volume) + fabric-cli exec agility-shell "bar_manager.toggle('Volume')" 2>/dev/null || info "Shell not active." + ;; + network|wifi) + fabric-cli exec agility-shell "bar_manager.toggle('Wifi')" 2>/dev/null || info "Shell not active." + ;; + bluetooth) + fabric-cli exec agility-shell "bar_manager.toggle('Bluetooth')" 2>/dev/null || info "Shell not active." + ;; + media) + fabric-cli exec agility-shell "bar_manager.toggle('Media')" 2>/dev/null || info "Shell not active." + ;; + weather) + fabric-cli exec agility-shell "bar_manager.toggle('Weather')" 2>/dev/null || info "Shell not active." + ;; suits) fabric-cli exec agility-shell "bar_manager.toggle('Suits')" 2>/dev/null || info "Shell not active." ;; @@ -408,6 +426,41 @@ cmd_toggle() { esac } +cmd_volume() { + local action="${1:-up}" + local step="${2:-0.05}" + case "${action,,}" in + up|raise|+) + fabric-cli exec agility-shell "import services.singletons as s; s.audio.speaker.volume = min(1.0, round(s.audio.speaker.volume + float($step), 2)) if s.audio and s.audio.speaker else None" 2>/dev/null || info "Shell not active." + ;; + down|lower|-) + fabric-cli exec agility-shell "import services.singletons as s; s.audio.speaker.volume = max(0.0, round(s.audio.speaker.volume - float($step), 2)) if s.audio and s.audio.speaker else None" 2>/dev/null || info "Shell not active." + ;; + mute|toggle-mute) + fabric-cli exec agility-shell "import services.singletons as s; s.audio.speaker.muted = not s.audio.speaker.muted if s.audio and s.audio.speaker else None" 2>/dev/null || info "Shell not active." + ;; + *) + echo "Usage: agl volume [step]" + ;; + esac +} + +cmd_brightness() { + local action="${1:-up}" + local step="${2:-5}" + case "${action,,}" in + up|raise|+) + fabric-cli exec agility-shell "import services.singletons as s; s.brightness.screen_brightness = min(100, int(s.brightness.screen_brightness + int($step))) if s.brightness and s.brightness.backend else None" 2>/dev/null || info "Shell not active." + ;; + down|lower|-) + fabric-cli exec agility-shell "import services.singletons as s; s.brightness.screen_brightness = max(0, int(s.brightness.screen_brightness - int($step))) if s.brightness and s.brightness.backend else None" 2>/dev/null || info "Shell not active." + ;; + *) + echo "Usage: agl brightness [step]" + ;; + esac +} + cmd_bar() { local action="${1:-height}" shift || true @@ -515,6 +568,18 @@ case "$COMMAND" in toggle) cmd_toggle "$@" ;; + volume) + cmd_volume "$@" + ;; + brightness) + cmd_brightness "$@" + ;; + launcher) + cmd_toggle "launcher" + ;; + cc|control-center) + cmd_toggle "control-center" + ;; -v|--version|version) echo -e "Agility Shell CLI (agl) v$VERSION" ;; diff --git a/config/niri.kdl b/config/niri.kdl index 5301aca..864cc71 100644 --- a/config/niri.kdl +++ b/config/niri.kdl @@ -16,10 +16,17 @@ layer-rule { } binds { - Mod+Space hotkey-overlay-title="Open Launcher" { spawn "fabric-cli" "exec" "agility-shell" "bar_manager.toggle('Launcher')"; } - Mod+D hotkey-overlay-title="Open Dash" { spawn "fabric-cli" "exec" "agility-shell" "bar_manager.toggle('Dash')"; } - Mod+Shift+W hotkey-overlay-title="Wallpaper Gallery" { spawn "fabric-cli" "exec" "agility-shell" "bar_manager.toggle('WallpaperPicker')"; } - Alt+C hotkey-overlay-title="Wallpaper Switcher" { spawn "fabric-cli" "exec" "agility-shell" "bar_manager.toggle('WallpaperDrawer')"; } + Mod+Space hotkey-overlay-title="Open Launcher" { spawn "agl" "toggle" "launcher"; } + Mod+C hotkey-overlay-title="Control Center" { spawn "agl" "toggle" "cc"; } + Mod+D hotkey-overlay-title="Open Dash" { spawn "agl" "toggle" "dash"; } + Mod+Shift+W hotkey-overlay-title="Wallpaper Gallery" { spawn "agl" "toggle" "WallpaperPicker"; } + Alt+C hotkey-overlay-title="Wallpaper Switcher" { spawn "agl" "toggle" "WallpaperDrawer"; } Mod+Alt+X hotkey-overlay-title="Random Wallpaper" { spawn "fabric-cli" "exec" "agility-shell" "wallpaper_service.random_wallpaper()"; } Alt+X hotkey-overlay-title="Random Wallpaper" { spawn "fabric-cli" "exec" "agility-shell" "wallpaper_service.random_wallpaper()"; } + + XF86AudioRaiseVolume allow-when-locked=true { spawn "agl" "volume" "up"; } + XF86AudioLowerVolume allow-when-locked=true { spawn "agl" "volume" "down"; } + XF86AudioMute allow-when-locked=true { spawn "agl" "volume" "mute"; } + XF86MonBrightnessUp allow-when-locked=true { spawn "agl" "brightness" "up"; } + XF86MonBrightnessDown allow-when-locked=true { spawn "agl" "brightness" "down"; } } diff --git a/snippets/__init__.py b/snippets/__init__.py index c289dad..9e97ef4 100644 --- a/snippets/__init__.py +++ b/snippets/__init__.py @@ -21,6 +21,7 @@ from .applet_reveal import AppletReveal from .dashreveal import DashReveal from .entry import StyleAwareEntry +from .popout_manager import UnifiedPopoutManager __all__ = [ "HackedRevealer", "HackedStack", @@ -50,5 +51,6 @@ "FlatScale", "AppletReveal", "DashReveal", - "StyleAwareEntry" + "StyleAwareEntry", + "UnifiedPopoutManager", ] diff --git a/snippets/popout_manager.py b/snippets/popout_manager.py new file mode 100644 index 0000000..4335874 --- /dev/null +++ b/snippets/popout_manager.py @@ -0,0 +1,355 @@ +from __future__ import annotations +import math +import time +from typing import TYPE_CHECKING, Callable +from loguru import logger +from fabric.widgets.wayland import WaylandWindow +from fabric.widgets.box import Box +from fabric.widgets.eventbox import EventBox +from gi.repository import Gtk, GtkLayerShell, Gdk, GLib +from snippets.blur.blur import enable_blur, is_blur_supported + +if TYPE_CHECKING: + from bar import Bar + +EDGE_MARGIN = 12 + +def _get_monitor_geometry(widget: Gtk.Widget | None) -> tuple[int, int, int]: + if widget is not None: + if hasattr(widget, "gdk_monitor") and widget.gdk_monitor: + geo = widget.gdk_monitor.get_geometry() + return geo.x, geo.width, geo.height + if hasattr(widget, "monitor_id") and widget.monitor_id is not None: + display = Gdk.Display.get_default() + if display and 0 <= widget.monitor_id < display.get_n_monitors(): + mon = display.get_monitor(widget.monitor_id) + if mon: + geo = mon.get_geometry() + return geo.x, geo.width, geo.height + + screen = Gdk.Screen.get_default() + if widget is not None: + toplevel = widget.get_toplevel() if hasattr(widget, "get_toplevel") else None + if toplevel and hasattr(toplevel, "get_window"): + window = toplevel.get_window() + if window is not None and screen is not None: + monitor_index = screen.get_monitor_at_window(window) + geo = screen.get_monitor_geometry(monitor_index) + return geo.x, geo.width, geo.height + + if screen is not None: + return 0, screen.get_width(), screen.get_height() + return 0, 1920, 1080 + +def _get_coords_for_widget(widget: Gtk.Widget) -> tuple[int, int]: + if not ((toplevel := widget.get_toplevel()) and toplevel.is_toplevel()): + return 0, 0 + coords = widget.translate_coordinates(toplevel, 0, 0) + if coords is not None: + return coords[0], coords[1] + allocation = widget.get_allocation() + return allocation.x, allocation.y + + +class PopoutScrim(WaylandWindow): + """ + Subtle desktop dimming scrim that captures clicks outside the popout and closes it. + Leaves the bar area uncovered so clicking other bar pills is immediately handled by the bar. + """ + def __init__(self, bar: "Bar", on_dismiss: Callable[[], None], **kwargs): + self._bar = bar + self._on_dismiss = on_dismiss + self._event_box = EventBox(style_classes=["desktop-scrim"]) + self._event_box.connect("button-release-event", lambda *_: self._on_dismiss()) + + monitor = bar.monitor_id if hasattr(bar, "monitor_id") else 0 + + super().__init__( + anchor="left right top bottom", + layer="top", + keyboard_mode="none", + monitor=monitor, + child=self._event_box, + visible=False, + **kwargs, + ) + self.update_margin() + bar.connect("size-allocate", lambda *_: self.update_margin()) + + def update_margin(self): + if not self._bar: + return + offset = self._bar.get_allocated_height() + if offset <= 1: + offset = self._bar.get_preferred_size()[1].height + if offset <= 1: + offset = 40 + + alignment = getattr(self._bar, "alignment", "top") + if alignment == "bottom": + self.margin = (0, 0, offset, 0) + else: + self.margin = (offset, 0, 0, 0) + + +class UnifiedPopoutManager: + """ + A single, persistent Wayland LayerShell popout surface for a given Bar. + Tracks active bar pills with smooth sliding physics, cross-fades content, + and manages the desktop dimming scrim. + """ + + def __init__(self, bar: "Bar", applet_factories: dict[str, type]): + self.bar = bar + self.applet_factories = applet_factories + self.is_open: bool = False + self._current_key: str | None = None + self._current_anchor: Gtk.Widget | None = None + self._current_left: int = 0 + self._slide_timer: int | None = None + self._fade_timer: int | None = None + self._cached_applets: dict[str, Gtk.Widget] = {} + + # Content Stack + self.stack = Gtk.Stack() + self.stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) + self.stack.set_transition_duration(180) + + self.container = Box( + style_classes=["applet-popout-container"], + children=[self.stack] + ) + + is_bottom = getattr(self.bar, "alignment", "top") == "bottom" + anchor = "left bottom" if is_bottom else "left top" + + self.window = WaylandWindow( + title="agility-shell-unified-popout", + anchor=anchor, + layer="top", + exclusivity="none", + keyboard_mode="on-demand", + monitor=bar.monitor_id if hasattr(bar, "monitor_id") else 0, + style_classes=["applet-popout-window", "applet-window"], + child=self.container, + visible=False, + ) + + try: + GtkLayerShell.set_exclusive_zone(self.window, -1) + except Exception: + pass + + self.window.add_keybinding("escape", lambda: self.close()) + self.window.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) + + # Scrim for outside click dismissal and subtle dimming + self.scrim = PopoutScrim(bar=self.bar, on_dismiss=self.close) + + # Enable compositor blur on the popout window if supported + try: + if is_blur_supported(): + enable_blur(self.window) + except Exception as e: + logger.debug(f"[PopoutManager] blur initialization: {e}") + + # Recalculate margins when bar or popout size changes + self.window.connect("size-allocate", self._on_size_allocate) + self.bar.connect("size-allocate", self._on_bar_size_allocate) + + def _on_bar_size_allocate(self, *_): + if self.is_open and self._current_anchor: + self._update_position(animate=False) + + def _on_size_allocate(self, *_): + if self.is_open and self._current_anchor: + self._update_position(animate=False) + + def _ensure_applet_widget(self, key: str) -> Gtk.Widget | None: + if key in self._cached_applets: + return self._cached_applets[key] + + factory = self.applet_factories.get(key) + if not factory: + return None + + try: + widget = factory(self.window) + wrapper = Box( + style_classes=["applet"], + children=[widget] + ) + wrapper.show_all() + self._cached_applets[key] = wrapper + self.stack.add_named(wrapper, key) + return wrapper + except Exception as e: + logger.error(f"[PopoutManager] Error creating applet for key '{key}': {e}") + return None + + def _calculate_target_left(self, anchor_widget: Gtk.Widget) -> int: + monitor_x, monitor_width, _ = _get_monitor_geometry(self.bar) + + popout_width = self.window.get_allocated_width() + if popout_width <= 1: + popout_width = self.window.get_preferred_size()[1].width + if popout_width <= 1: + popout_width = 340 + + bar_width = self.bar.get_allocated_width() + if bar_width <= 1: + bar_width = self.bar.get_preferred_size()[1].width + + parent_margin = getattr(self.bar, "margin", (0, 0, 0, 0)) + parent_margin_vals = tuple(parent_margin) if hasattr(parent_margin, "__iter__") else (0, 0, 0, 0) + parent_right_margin = parent_margin_vals[1] if len(parent_margin_vals) > 1 else 0 + parent_left_margin = parent_margin_vals[3] if len(parent_margin_vals) > 3 else 0 + + h_align = getattr(self.bar, "horizontal_alignment", "center") + min_width = getattr(self.bar, "min_width", False) + is_left = GtkLayerShell.get_anchor(self.bar, GtkLayerShell.Edge.LEFT) + is_right = GtkLayerShell.get_anchor(self.bar, GtkLayerShell.Edge.RIGHT) + + if not min_width or (is_left and is_right): + bar_screen_left = parent_left_margin + elif h_align == "left" or (is_left and not is_right): + bar_screen_left = parent_left_margin + elif h_align == "right" or (is_right and not is_left): + bar_screen_left = monitor_width - bar_width - parent_right_margin + else: + bar_screen_left = (monitor_width - bar_width) // 2 + + coords = _get_coords_for_widget(anchor_widget) + widget_width = anchor_widget.get_allocated_width() + widget_center_in_bar = coords[0] + (widget_width / 2.0) + + screen_widget_center_x = bar_screen_left + widget_center_in_bar + raw_margin_left = round(screen_widget_center_x - (popout_width / 2.0)) + + min_margin = EDGE_MARGIN + max_margin = max(EDGE_MARGIN, monitor_width - popout_width - EDGE_MARGIN) + return max(min_margin, min(raw_margin_left, max_margin)) + + def _get_vertical_offset(self) -> int: + offset = self.bar.get_allocated_height() + if offset <= 1: + offset = self.bar.get_preferred_size()[1].height + if offset <= 1: + offset = 40 + return offset + 8 + + def _set_left_margin(self, left: int): + self._current_left = left + is_bottom = getattr(self.bar, "alignment", "top") == "bottom" + offset = self._get_vertical_offset() + if is_bottom: + self.window.margin = (0, 0, offset, left) + else: + self.window.margin = (offset, 0, 0, left) + + def _animate_to_left(self, target_left: int, duration_ms: int = 180): + if self._slide_timer is not None: + GLib.source_remove(self._slide_timer) + self._slide_timer = None + + start_left = self._current_left + delta = target_left - start_left + if abs(delta) < 2: + self._set_left_margin(target_left) + return + + start_time = time.time() + duration_s = duration_ms / 1000.0 + + def _step(): + elapsed = time.time() - start_time + progress = min(1.0, elapsed / duration_s) + # Cubic ease-out: 1 - (1 - t)^3 + eased = 1.0 - math.pow(1.0 - progress, 3) + current = int(start_left + delta * eased) + self._set_left_margin(current) + + if progress >= 1.0: + self._slide_timer = None + self._set_left_margin(target_left) + return GLib.SOURCE_REMOVE + return GLib.SOURCE_CONTINUE + + self._slide_timer = GLib.timeout_add(16, _step) + + def _update_position(self, animate: bool = True): + if not self._current_anchor: + return + target_left = self._calculate_target_left(self._current_anchor) + if animate and self.is_open: + self._animate_to_left(target_left) + else: + self._set_left_margin(target_left) + + def toggle(self, key: str, anchor_widget: Gtk.Widget) -> bool: + """ + Toggle the popout for the given key and anchor. + If already open for this key, closes it. + If open for another key, seamlessly transitions to this one. + """ + if self.is_open and self._current_key == key: + self.close() + return False + self.open(key, anchor_widget) + return True + + def open(self, key: str, anchor_widget: Gtk.Widget): + """Open or smoothly transition to the given key and anchor.""" + widget = self._ensure_applet_widget(key) + if not widget: + return + + is_switching = self.is_open and self._current_key != key + + # Clear active class on previous anchor + if self._current_anchor and self._current_anchor != anchor_widget: + self._current_anchor.remove_style_class("active-popout") + self._current_anchor.remove_style_class("applet-open") + + self._current_key = key + self._current_anchor = anchor_widget + self.is_open = True + + # Add active classes to new anchor + anchor_widget.add_style_class("active-popout") + anchor_widget.add_style_class("applet-open") + + # Switch content in stack + self.stack.set_visible_child_name(key) + + if is_switching: + # Popout is already visible; smoothly slide to new position + self._update_position(animate=True) + else: + # First open: set position immediately, then reveal + self._update_position(animate=False) + self.scrim.update_margin() + self.scrim.set_visible(True) + self.scrim.show() + self.window.set_visible(True) + self.window.show_all() + + def close(self): + """Close the popout and scrim gracefully.""" + if not self.is_open: + return + + if self._slide_timer is not None: + GLib.source_remove(self._slide_timer) + self._slide_timer = None + + if self._current_anchor: + self._current_anchor.remove_style_class("active-popout") + self._current_anchor.remove_style_class("applet-open") + + self.is_open = False + self._current_key = None + self._current_anchor = None + + self.window.set_visible(False) + self.scrim.set_visible(False) diff --git a/style/applet.css b/style/applet.css index 62abfc1..f9a4de4 100644 --- a/style/applet.css +++ b/style/applet.css @@ -1,12 +1,27 @@ +.desktop-scrim { + background-color: rgba(0, 0, 0, 0.28); + transition: background-color 0.22s cubic-bezier(0.16, 1, 0.3, 1); +} + +.applet-popout-window { + background-color: alpha(var(--surface_container), 0.92); + border-radius: apply(radius-xl); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.12), + 0 16px 40px rgba(0, 0, 0, 0.45); + padding: 2px; +} + +.applet-popout-container { + border-radius: apply(radius-xl); +} + .applet { min-width: 324px; - background-color: var(--background); - margin: 8px; + background-color: transparent; + margin: 4px; border-radius: apply(radius-xl); - box-shadow: - inset 0 0 0 1px var(--outline_variant), - 0 4 4 0px rgba(0, 0, 0, 0.33); - padding: 18px; + padding: 14px; } .applet-misc-button { diff --git a/style/bar.css b/style/bar.css index 35fc044..57af0c2 100644 --- a/style/bar.css +++ b/style/bar.css @@ -376,12 +376,25 @@ background-color: var(--surface_container); } +.bar-widget.active-popout { + background-color: var(--primary); + color: var(--on_primary); + box-shadow: 0 0 14px alpha(var(--primary), 0.5); + transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); +} + +.bar-widget.active-popout * { + color: var(--on_primary); + transition: color 0.2s ease; +} + .bar-widget.applet-open { - background-color: var(--surface_container_highest); + background-color: var(--primary); + color: var(--on_primary); } .bar-widget.applet-open.hovered { - background-color: var(--surface_container_high); + background-color: mix(var(--primary), var(--surface_container_high), 0.2); } .bar-widget.group-drop-target { diff --git a/style/launcher.css b/style/launcher.css index d97d58b..11b91d9 100644 --- a/style/launcher.css +++ b/style/launcher.css @@ -62,6 +62,16 @@ transition: 0.3s ease; } +.launcher-action-item { + background-color: alpha(var(--primary_container), 0.35); + border: 1px solid alpha(var(--primary), 0.3); +} + +.launcher-action-item:hover { + background-color: alpha(var(--primary_container), 0.6); + border-color: var(--primary); +} + .launcher-app:hover { background-color: var(--surface_container_high); transition: 0.3s ease; diff --git a/windows/launcher.py b/windows/launcher.py index 6ca6ac0..190b832 100644 --- a/windows/launcher.py +++ b/windows/launcher.py @@ -106,6 +106,46 @@ def launch(self): self._launcher.toggle() +class LauncherActionItem(Button): + def __init__(self, title: str, subtitle: str, icon_name: str, on_action): + self._on_action = on_action + self.box = Box( + orientation="h", + spacing=10, + children=[ + Icon(icon_name=icon_name, icon_size=28), + Box( + orientation="v", + children=[ + Label( + label=title, + h_align="start", + ellipsization="end", + max_chars_width=30, + style="font-size: 14px; font-weight: bold;", + ), + Label( + label=subtitle, + h_align="start", + ellipsization="end", + max_chars_width=35, + style="font-size: 11px; opacity: 0.7;", + ), + ], + ), + ], + ) + super().__init__( + style_classes=["launcher-app", "launcher-action-item"], + on_clicked=lambda *_: self.launch(), + child=self.box, + ) + + def launch(self): + if self._on_action: + self._on_action() + + class LauncherGridItem(Button): def __init__(self, app: DesktopApp, launcher): self._app = app @@ -431,11 +471,82 @@ def commit_list(): threading.Thread(target=load, daemon=True).start() + def _render_calc_result(self, expr: str, val: str): + target = self._list_box + for child in target.get_children(): + child.destroy() + self._app_count.set_text("Calculator") + + def _copy_calc(): + from services.singletons import clipboard + if clipboard: + try: + clipboard.set_text(val) + except Exception: + pass + if hasattr(self.window, "toggle"): + self.window.toggle() + + item = LauncherActionItem( + title=f"= {val}", + subtitle=f"Result for '{expr}' · Press Enter to copy", + icon_name="calculator-duotone", + on_action=_copy_calc, + ) + target.add(item) + target.show_all() + self._view_stack.set_visible_child_name("list") + + def _render_command_item(self, cmd: str): + target = self._list_box + for child in target.get_children(): + child.destroy() + self._app_count.set_text("Run Command") + + def _run_cmd(): + import subprocess + try: + subprocess.Popen(cmd, shell=True) + except Exception: + pass + if hasattr(self.window, "toggle"): + self.window.toggle() + + item = LauncherActionItem( + title=f"Run: {cmd}", + subtitle="Execute in shell · Press Enter to run", + icon_name="terminal-window-duotone", + on_action=_run_cmd, + ) + target.add(item) + target.show_all() + self._view_stack.set_visible_child_name("list") + def _search(self, query: str): if not query: self._load_async(self._sorted_by_usage(self._all_apps), self._grid_mode) return + # Inline math evaluation: query starts with "=" + if query.startswith("="): + expr = query[1:].strip() + if expr: + try: + allowed = set("0123456789+-*/().,% eE") + if all(c in allowed for c in expr): + val = eval(expr, {"__builtins__": None}, {}) + self._render_calc_result(expr, str(val)) + return + except Exception: + pass + + # Terminal command execution: query starts with ">" + if query.startswith(">"): + cmd = query[1:].strip() + if cmd: + self._render_command_item(cmd) + return + usage = load_usage() raw_results = process.extract( query, From 7137b6cd402f6c6c7db8a7c15bc7e33ae32a3761 Mon Sep 17 00:00:00 2001 From: Chintan Patel <216989679+Chintanpatel24@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:04:34 +0530 Subject: [PATCH 2/2] fix(shell): remove unwanted search actions, add bar width CLI, fix caffeine sleep inhibitor, optimize popout speed --- bar.py | 9 ++ bin/agl | 51 +++++++- services/caffeine.py | 50 ++++++-- snippets/popout_manager.py | 4 +- style/launcher.css | 10 -- windows/launcher.py | 133 ++++----------------- windows/quick_settings/buttons/caffiene.py | 18 +-- 7 files changed, 131 insertions(+), 144 deletions(-) diff --git a/bar.py b/bar.py index a371b09..8ba9df1 100644 --- a/bar.py +++ b/bar.py @@ -2593,6 +2593,15 @@ def set_bar_alignment(self, alignment: str) -> None: for bar in self._bars.values(): bar._set_alignment(alignment) + def toggle_min_width(self) -> None: + for bar in self._bars.values(): + bar._toggle_min_width() + + def set_min_width(self, min_width: bool) -> None: + for bar in self._bars.values(): + if bar.min_width != min_width: + bar._toggle_min_width() + def reload_bars(self) -> None: for bar in list(self._bars.values()): bar.destroy() diff --git a/bin/agl b/bin/agl index f25062d..975500b 100755 --- a/bin/agl +++ b/bin/agl @@ -98,7 +98,7 @@ show_help() { echo -e " ${CYAN}uninstall${RESET} Uninstall Agility Shell (--purge to wipe user configs)" echo -e " ${CYAN}suits${RESET} Manage desktop suites (list, next, prev, toggle, switch )" echo -e " ${CYAN}profile${RESET} Switch/view performance profile (optimized, balanced, dedicated)" - echo -e " ${CYAN}bar${RESET} Adjust bar dimensions and thickness (agl bar height <26-48>)" + echo -e " ${CYAN}bar${RESET} Adjust bar dimensions (agl bar height <26-48> | agl bar width [toggle|min|full])" echo -e " ${CYAN}toggle${RESET} Toggle shell surfaces (dash, settings, launcher, suits, widgets)" echo -e "" echo -e "${BOLD}OPTIONS:${RESET}" @@ -519,8 +519,55 @@ if not found: echo -e "Usage: agl bar height <26-48>" fi ;; + width|min_width|min-width) + local mode="${1:-toggle}" + case "${mode,,}" in + min|minimal|compact) + if command -v fabric-cli &>/dev/null; then + fabric-cli exec agility-shell " +import services.singletons as s +try: + bm = s.bar_manager + if bm and hasattr(bm, 'set_min_width'): + bm.set_min_width(True) +except Exception: + pass +" 2>/dev/null || true + fi + success "Bar width set to minimal." + ;; + full|expand|max) + if command -v fabric-cli &>/dev/null; then + fabric-cli exec agility-shell " +import services.singletons as s +try: + bm = s.bar_manager + if bm and hasattr(bm, 'set_min_width'): + bm.set_min_width(False) +except Exception: + pass +" 2>/dev/null || true + fi + success "Bar width set to full width." + ;; + toggle|*) + if command -v fabric-cli &>/dev/null; then + fabric-cli exec agility-shell " +import services.singletons as s +try: + bm = s.bar_manager + if bm and hasattr(bm, 'toggle_min_width'): + bm.toggle_min_width() +except Exception: + pass +" 2>/dev/null || true + fi + success "Bar width toggled." + ;; + esac + ;; *) - echo -e "${BOLD}Usage:${RESET} agl bar height <26-48>" + echo -e "${BOLD}Usage:${RESET} agl bar | width [toggle|min|full]>" ;; esac } diff --git a/services/caffeine.py b/services/caffeine.py index aae823e..a4ac895 100644 --- a/services/caffeine.py +++ b/services/caffeine.py @@ -28,9 +28,29 @@ def toggle(self): self.enabled = not self._enabled def _start_inhibit(self): + try: + from services.singletons import idle + if idle: + idle.stop() + except Exception as e: + logger.debug(f"[CaffeineService] Error stopping idle service: {e}") + + try: + subprocess.run(["pkill", "-x", "swayidle"], check=False, capture_output=True) + except Exception: + pass + + try: + from services.idle import _get_screen_commands + _, screen_on = _get_screen_commands() + if screen_on: + subprocess.Popen(screen_on, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except Exception: + pass + try: self._process = subprocess.Popen( - ["systemd-inhibit", "--what=idle:sleep", "--why=Caffeine keep-awake", "--mode=block", "sleep", "infinity"], + ["systemd-inhibit", "--what=idle:sleep:handle-lid-switch", "--why=Caffeine keep-awake", "--mode=block", "sleep", "infinity"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) @@ -39,16 +59,22 @@ def _start_inhibit(self): logger.warning(f"[CaffeineService] Failed to start inhibitor: {e}") def _stop_inhibit(self): - if self._process is None: - return - try: - self._process.terminate() - self._process.wait(timeout=1) - except Exception: + if self._process is not None: try: - self._process.kill() + self._process.terminate() + self._process.wait(timeout=1) except Exception: - pass - finally: - self._process = None - logger.info("[CaffeineService] Inhibitor stopped") + try: + self._process.kill() + except Exception: + pass + finally: + self._process = None + logger.info("[CaffeineService] Inhibitor stopped") + + try: + from services.singletons import idle + if idle: + idle.start() + except Exception as e: + logger.debug(f"[CaffeineService] Error starting idle service: {e}") diff --git a/snippets/popout_manager.py b/snippets/popout_manager.py index 4335874..ec5699f 100644 --- a/snippets/popout_manager.py +++ b/snippets/popout_manager.py @@ -113,7 +113,7 @@ def __init__(self, bar: "Bar", applet_factories: dict[str, type]): # Content Stack self.stack = Gtk.Stack() self.stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) - self.stack.set_transition_duration(180) + self.stack.set_transition_duration(140) self.container = Box( style_classes=["applet-popout-container"], @@ -247,7 +247,7 @@ def _set_left_margin(self, left: int): else: self.window.margin = (offset, 0, 0, left) - def _animate_to_left(self, target_left: int, duration_ms: int = 180): + def _animate_to_left(self, target_left: int, duration_ms: int = 120): if self._slide_timer is not None: GLib.source_remove(self._slide_timer) self._slide_timer = None diff --git a/style/launcher.css b/style/launcher.css index 11b91d9..d97d58b 100644 --- a/style/launcher.css +++ b/style/launcher.css @@ -62,16 +62,6 @@ transition: 0.3s ease; } -.launcher-action-item { - background-color: alpha(var(--primary_container), 0.35); - border: 1px solid alpha(var(--primary), 0.3); -} - -.launcher-action-item:hover { - background-color: alpha(var(--primary_container), 0.6); - border-color: var(--primary); -} - .launcher-app:hover { background-color: var(--surface_container_high); transition: 0.3s ease; diff --git a/windows/launcher.py b/windows/launcher.py index 190b832..2707ce2 100644 --- a/windows/launcher.py +++ b/windows/launcher.py @@ -106,46 +106,6 @@ def launch(self): self._launcher.toggle() -class LauncherActionItem(Button): - def __init__(self, title: str, subtitle: str, icon_name: str, on_action): - self._on_action = on_action - self.box = Box( - orientation="h", - spacing=10, - children=[ - Icon(icon_name=icon_name, icon_size=28), - Box( - orientation="v", - children=[ - Label( - label=title, - h_align="start", - ellipsization="end", - max_chars_width=30, - style="font-size: 14px; font-weight: bold;", - ), - Label( - label=subtitle, - h_align="start", - ellipsization="end", - max_chars_width=35, - style="font-size: 11px; opacity: 0.7;", - ), - ], - ), - ], - ) - super().__init__( - style_classes=["launcher-app", "launcher-action-item"], - on_clicked=lambda *_: self.launch(), - child=self.box, - ) - - def launch(self): - if self._on_action: - self._on_action() - - class LauncherGridItem(Button): def __init__(self, app: DesktopApp, launcher): self._app = app @@ -471,83 +431,34 @@ def commit_list(): threading.Thread(target=load, daemon=True).start() - def _render_calc_result(self, expr: str, val: str): - target = self._list_box - for child in target.get_children(): - child.destroy() - self._app_count.set_text("Calculator") - - def _copy_calc(): - from services.singletons import clipboard - if clipboard: - try: - clipboard.set_text(val) - except Exception: - pass - if hasattr(self.window, "toggle"): - self.window.toggle() - - item = LauncherActionItem( - title=f"= {val}", - subtitle=f"Result for '{expr}' · Press Enter to copy", - icon_name="calculator-duotone", - on_action=_copy_calc, - ) - target.add(item) - target.show_all() - self._view_stack.set_visible_child_name("list") - - def _render_command_item(self, cmd: str): - target = self._list_box - for child in target.get_children(): - child.destroy() - self._app_count.set_text("Run Command") - - def _run_cmd(): - import subprocess - try: - subprocess.Popen(cmd, shell=True) - except Exception: - pass - if hasattr(self.window, "toggle"): - self.window.toggle() - - item = LauncherActionItem( - title=f"Run: {cmd}", - subtitle="Execute in shell · Press Enter to run", - icon_name="terminal-window-duotone", - on_action=_run_cmd, - ) - target.add(item) - target.show_all() - self._view_stack.set_visible_child_name("list") - def _search(self, query: str): if not query: self._load_async(self._sorted_by_usage(self._all_apps), self._grid_mode) return - # Inline math evaluation: query starts with "=" - if query.startswith("="): - expr = query[1:].strip() - if expr: - try: - allowed = set("0123456789+-*/().,% eE") - if all(c in allowed for c in expr): - val = eval(expr, {"__builtins__": None}, {}) - self._render_calc_result(expr, str(val)) - return - except Exception: - pass - - # Terminal command execution: query starts with ">" - if query.startswith(">"): - cmd = query[1:].strip() - if cmd: - self._render_command_item(cmd) - return - usage = load_usage() + query_clean = query.strip().lower() + + # Fast substring & prefix matching (instant sub-millisecond response) + prefix_matches = [] + substring_matches = [] + for app in self._all_apps: + display = (app.display_name or "").lower() + name = (app.name or "").lower() + if display.startswith(query_clean) or name.startswith(query_clean): + prefix_matches.append(app) + elif query_clean in display or query_clean in name: + substring_matches.append(app) + + if prefix_matches or substring_matches: + combined = prefix_matches + [a for a in substring_matches if a not in prefix_matches] + sorted_apps = sorted(combined, key=lambda a: get_usage_count(a, usage), reverse=True) + adj = self._scrolled_window.get_vadjustment() + adj.set_value(adj.get_lower()) + self._load_async(sorted_apps[:50], self._grid_mode) + return + + # Fallback to fuzzy matching if no direct substring match raw_results = process.extract( query, self._all_apps, diff --git a/windows/quick_settings/buttons/caffiene.py b/windows/quick_settings/buttons/caffiene.py index 2def5a0..1ef835f 100644 --- a/windows/quick_settings/buttons/caffiene.py +++ b/windows/quick_settings/buttons/caffiene.py @@ -1,18 +1,22 @@ from .button import QSButton from snippets import Icon -from services.singletons import idle +from services.singletons import caffeine class CaffieneButton(QSButton): def __init__(self, **kwargs): super().__init__( icon=Icon(icon_name="coffee-duotone"), - on_activate=lambda _: idle.stop(), - on_deactivate=lambda _: idle.start(), + on_activate=lambda _: self.set_active(True), + on_deactivate=lambda _: self.set_active(False), **kwargs, ) - idle.connect( - "notify::active", - lambda obj, _: setattr(self, "active", not obj.active), + caffeine.connect( + "notify::enabled", + lambda obj, _: setattr(self, "active", obj.enabled), ) - self.active = not idle.active \ No newline at end of file + self.active = caffeine.enabled + + def set_active(self, value: bool): + caffeine.enabled = value + setattr(self, "active", value) \ No newline at end of file