From 77228a751cbe0009abba79796c2352f8664063e1 Mon Sep 17 00:00:00 2001 From: fdev31 Date: Fri, 25 Jul 2025 20:27:38 +0200 Subject: [PATCH 1/4] basic support for custom workspace icons --- config/data.py | 1 + modules/bar.py | 25 +++++++++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/config/data.py b/config/data.py index 8f770b4..73e22b0 100644 --- a/config/data.py +++ b/config/data.py @@ -75,6 +75,7 @@ def load_config(): "bar_workspace_use_chinese_numerals", False ) BAR_HIDE_SPECIAL_WORKSPACE = config.get("bar_hide_special_workspace", True) + BAR_WORKSPACE_ICONS = config.get("bar_workspace_icons", {}) BAR_THEME = config.get("bar_theme", "Pills") DOCK_THEME = config.get("dock_theme", "Pills") PANEL_THEME = config.get("panel_theme", "Pills") diff --git a/modules/bar.py b/modules/bar.py index ba5333e..1e9ee2b 100644 --- a/modules/bar.py +++ b/modules/bar.py @@ -45,6 +45,20 @@ tooltip_overview = """Overview""" +def build_caption(i: int): + """Build the label for a given workspace number""" + label = data.BAR_WORKSPACE_ICONS.get(str(i)) or data.BAR_WORKSPACE_ICONS.get('default') + if label is None: + return ( + CHINESE_NUMERALS[i - 1] + if data.BAR_WORKSPACE_USE_CHINESE_NUMERALS + and 1 <= i <= len(CHINESE_NUMERALS) + else str(i) + ) + else: + return label + + class Bar(Window): def __init__(self, **kwargs): super().__init__( @@ -135,12 +149,7 @@ def __init__(self, **kwargs): h_align="center", v_align="center", id=i, - label=( - CHINESE_NUMERALS[i - 1] - if data.BAR_WORKSPACE_USE_CHINESE_NUMERALS - and 1 <= i <= len(CHINESE_NUMERALS) - else str(i) - ), + label=build_caption(i), ) for i in range(1, 11) ], @@ -155,7 +164,7 @@ def __init__(self, **kwargs): name="workspaces-container", children=( self.workspaces - if not data.BAR_WORKSPACE_SHOW_NUMBER + if not (data.BAR_WORKSPACE_SHOW_NUMBER or data.BAR_WORKSPACE_ICONS) else self.workspaces_num ), ) @@ -603,7 +612,7 @@ def toggle_hidden(self): self.bar_inner.remove_style_class("hidden") def chinese_numbers(self): - if data.BAR_WORKSPACE_USE_CHINESE_NUMERALS: + if data.BAR_WORKSPACE_USE_CHINESE_NUMERALS or data.BAR_WORKSPACE_ICONS: self.workspaces_num.add_style_class("chinese") else: self.workspaces_num.remove_style_class("chinese") From c4b75b16e4cbb4408845bdcf556509a3af1f7fc9 Mon Sep 17 00:00:00 2001 From: fdev31 Date: Fri, 25 Jul 2025 22:56:46 +0200 Subject: [PATCH 2/4] Fancy animation attempt --- modules/bar.py | 448 ++++++++++++++++++++++++++++++++++++++---- scripts/screenshot.sh | 3 +- styles/workspaces.css | 15 +- 3 files changed, 422 insertions(+), 44 deletions(-) diff --git a/modules/bar.py b/modules/bar.py index 1e9ee2b..415247e 100644 --- a/modules/bar.py +++ b/modules/bar.py @@ -2,8 +2,12 @@ import os from fabric.hyprland.service import HyprlandEvent -from fabric.hyprland.widgets import (Language, WorkspaceButton, Workspaces, - get_hyprland_connection) +from fabric.hyprland.widgets import ( + Language, + WorkspaceButton, + Workspaces, + get_hyprland_connection, +) from fabric.utils.helpers import exec_shell_command_async from fabric.widgets.box import Box from fabric.widgets.button import Button @@ -11,18 +15,22 @@ from fabric.widgets.datetime import DateTime from fabric.widgets.label import Label from fabric.widgets.revealer import Revealer -from gi.repository import Gdk, Gtk +from gi.repository import Gdk, Gtk, GLib import config.data as data import modules.icons as icons from modules.controls import ControlSmall -from modules.dock import Dock from modules.metrics import Battery, MetricsSmall, NetworkApplet from modules.systemprofiles import Systemprofiles from modules.systemtray import SystemTray from modules.weather import Weather from widgets.wayland import WaylandWindow as Window +import logging + + +logger = logging.getLogger(__name__) + CHINESE_NUMERALS = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "〇"] # Tooltips @@ -47,7 +55,9 @@ def build_caption(i: int): """Build the label for a given workspace number""" - label = data.BAR_WORKSPACE_ICONS.get(str(i)) or data.BAR_WORKSPACE_ICONS.get('default') + label = data.BAR_WORKSPACE_ICONS.get(str(i)) or data.BAR_WORKSPACE_ICONS.get( + "default" + ) if label is None: return ( CHINESE_NUMERALS[i - 1] @@ -61,13 +71,12 @@ def build_caption(i: int): class Bar(Window): def __init__(self, **kwargs): - super().__init__( - name="bar", - layer="top", - exclusivity="auto", - visible=True, - all_visible=True, - ) + super().__init__(**kwargs) + self.name = "bar" + self.layer = "top" + self.exclusivity = "auto" + self.visible = True + self.all_visible = True self.anchor_var = "" self.margin_var = "" @@ -141,7 +150,7 @@ def __init__(self, **kwargs): empty_scroll=True, v_align="fill", orientation="h" if not data.VERTICAL else "v", - spacing=0 if not data.BAR_WORKSPACE_USE_CHINESE_NUMERALS else 4, + spacing=(4 if data.BAR_WORKSPACE_USE_CHINESE_NUMERALS else 0), buttons=[ WorkspaceButton( h_expand=False, @@ -159,16 +168,36 @@ def __init__(self, **kwargs): else Workspaces.default_buttons_factory ), ) + self.ws_rail = Box(name="workspace-rail", h_align="start", v_align="center") + self.current_rail_pos = 0 + self.current_rail_size = 0 + self.is_animating_rail = False + self.ws_rail_provider = Gtk.CssProvider() + self.ws_rail.get_style_context().add_provider( + self.ws_rail_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER + ) - self.ws_container = Box( - name="workspaces-container", - children=( - self.workspaces - if not (data.BAR_WORKSPACE_SHOW_NUMBER or data.BAR_WORKSPACE_ICONS) - else self.workspaces_num - ), + # Add initial styling for the rail + initial_css = """ + #workspace-rail { + background-color: rgba(255, 0, 0, 0.7); + border-radius: 999px; + } + """ + self.ws_rail_provider.load_from_data(initial_css.encode()) + logger.warning(f"Initialized workspace rail with vertical={data.VERTICAL}") + + workspaces_widget = ( + self.workspaces_num + if data.BAR_WORKSPACE_SHOW_NUMBER or data.BAR_WORKSPACE_ICONS + else self.workspaces ) + self.ws_container = Gtk.Grid() + self.ws_container.attach(self.ws_rail, 0, 0, 1, 1) + self.ws_container.attach(workspaces_widget, 0, 0, 1, 1) + self.ws_container.set_name("workspaces-container") + self.button_tools = Button( name="button-bar", tooltip_markup=tooltip_tools, @@ -180,6 +209,8 @@ def __init__(self, **kwargs): self.button_tools.connect("enter_notify_event", self.on_button_enter) self.button_tools.connect("leave_notify_event", self.on_button_leave) + self.connection.connect("event::workspace", self._on_workspace_changed) + self.systray = SystemTray() self.weather = Weather() @@ -338,25 +369,8 @@ def __init__(self, **kwargs): self.button_power, ] - self.v_all_children = [] - self.v_all_children.extend(self.v_start_children) - self.v_all_children.extend(self.v_center_children) - self.v_all_children.extend(self.v_end_children) - - if ( - data.DOCK_ENABLED - and data.BAR_POSITION == "Bottom" - or data.PANEL_THEME == "Panel" - and data.BAR_POSITION in ["Top", "Bottom"] - ): - if not data.VERTICAL: - self.dock_instance = Dock(integrated_mode=True) - self.integrated_dock_widget = self.dock_instance.wrapper - is_centered_bar = data.VERTICAL and getattr(data, "CENTERED_BAR", False) - bar_center_actual_children = None - if self.integrated_dock_widget is not None: bar_center_actual_children = self.integrated_dock_widget elif data.VERTICAL: @@ -364,7 +378,9 @@ def __init__(self, **kwargs): orientation=Gtk.Orientation.VERTICAL, spacing=4, children=( - self.v_all_children if is_centered_bar else self.v_center_children + self.v_start_children + self.v_center_children + self.v_end_children + if is_centered_bar + else self.v_center_children ), ) @@ -391,7 +407,11 @@ def __init__(self, **kwargs): children=( self.h_start_children if not data.VERTICAL - else self.v_start_children + else ( + self.v_all_children + if is_centered_bar + else self.v_start_children + ) ), ) ), @@ -493,6 +513,8 @@ def __init__(self, **kwargs): self.systray._update_visibility() self.chinese_numbers() + self.setup_workspaces() + def apply_component_props(self): components = { "button_apps": self.button_apps, @@ -556,7 +578,7 @@ def toggle_component_visibility(self, component_name): with open(config_file, "w") as f: json.dump(config, f, indent=4) except Exception as e: - print(f"Error updating config file: {e}") + logger.warning(f"Error updating config file: {e}") return self.component_visibility[component_name] @@ -616,3 +638,349 @@ def chinese_numbers(self): self.workspaces_num.add_style_class("chinese") else: self.workspaces_num.remove_style_class("chinese") + + def setup_workspaces(self): + """Set up workspace rail and initialize with current workspace""" + logger.info("Setting up workspaces") + try: + active_workspace = json.loads( + self.connection.send_command("j/activeworkspace").reply.decode() + )["id"] + self.update_rail(active_workspace, initial_setup=True) + except Exception as e: + logger.error(f"Error initializing workspace rail: {e}") + + def _on_workspace_changed(self, _, event): + """Handle workspace change events directly""" + if event is not None and isinstance(event, HyprlandEvent) and event.data: + try: + workspace_id = int(event.data[0]) + logger.info(f"Workspace changed to: {workspace_id}") + self.update_rail(workspace_id) + except (ValueError, IndexError) as e: + logger.error(f"Error processing workspace event: {e}") + else: + logger.warning(f"Invalid workspace event received: {event}") + + def update_rail(self, workspace_id, initial_setup=False): + """Update the workspace rail position based on the workspace button""" + if self.is_animating_rail and not initial_setup: + return + + logger.info(f"Updating rail for workspace {workspace_id}") + workspaces = self.children_workspaces + active_button = next( + ( + b + for b in workspaces + if isinstance(b, WorkspaceButton) and b.id == workspace_id + ), + None, + ) + + if not active_button: + logger.warning(f"No button found for workspace {workspace_id}") + return + + if initial_setup: + GLib.idle_add(self._position_rail_initially, active_button) + else: + self.is_animating_rail = True + GLib.idle_add(self._update_rail_with_animation, active_button) + + def _position_rail_initially(self, active_button): + allocation = active_button.get_allocation() + if allocation.width == 0 or allocation.height == 0: + return True + + diameter = 1 + if data.VERTICAL: + self.current_rail_pos = ( + allocation.y + (allocation.height / 2) - (diameter / 2) + ) + self.current_rail_size = diameter + css = f""" + #workspace-rail {{ + transition-property: none; + margin-top: {self.current_rail_pos}px; + min-height: {self.current_rail_size}px; + min-width: {self.current_rail_size}px; + }} + """ + else: + self.current_rail_pos = ( + allocation.x + (allocation.width / 2) - (diameter / 2) + ) + self.current_rail_size = diameter + css = f""" + #workspace-rail {{ + transition-property: none; + margin-left: {self.current_rail_pos}px; + min-width: {self.current_rail_size}px; + min-height: {self.current_rail_size}px; + }} + """ + self.ws_rail_provider.load_from_data(css.encode()) + logger.info( + f"Rail initialized at pos={self.current_rail_pos}, size={self.current_rail_size}" + ) + return False + + def _update_rail_with_animation(self, active_button): + """Position the rail at the active workspace button with a stretch animation.""" + target_allocation = active_button.get_allocation() + + if target_allocation.width == 0 or target_allocation.height == 0: + logger.info("Button allocation not ready, retrying...") + self.is_animating_rail = False + return True + + diameter = 24 + if data.VERTICAL: + pos_prop, size_prop = "margin-top", "min-height" + other_size_prop, other_size_val = "min-width", diameter + target_pos = ( + target_allocation.y + (target_allocation.height / 2) - (diameter / 2) + ) + else: + pos_prop, size_prop = "margin-left", "min-width" + other_size_prop, other_size_val = "min-height", diameter + target_pos = ( + target_allocation.x + (target_allocation.width / 2) - (diameter / 2) + ) + + if target_pos == self.current_rail_pos: + self.is_animating_rail = False + return False + + distance = target_pos - self.current_rail_pos + stretched_size = self.current_rail_size + abs(distance) + stretch_pos = target_pos if distance < 0 else self.current_rail_pos + + stretch_duration = 0.1 + shrink_duration = 0.15 + + stretch_css = f""" + #workspace-rail {{ + transition-property: {pos_prop}, {size_prop}; + transition-duration: {stretch_duration}s; + transition-timing-function: ease-out; + {pos_prop}: {stretch_pos}px; + {size_prop}: {stretched_size}px; + {other_size_prop}: {other_size_val}px; + }} + """ + self.ws_rail_provider.load_from_data(stretch_css.encode()) + + GLib.timeout_add( + int(stretch_duration * 1000), + self._shrink_rail, + target_pos, + diameter, + shrink_duration, + ) + return False + + def _shrink_rail(self, target_pos, target_size, duration): + """Shrink the rail to its final size and position.""" + if data.VERTICAL: + pos_prop = "margin-top" + size_props = "min-height, min-width" + else: + pos_prop = "margin-left" + size_props = "min-width, min-height" + + shrink_css = f""" + #workspace-rail {{ + transition-property: {pos_prop}, {size_props}; + transition-duration: {duration}s; + transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); + {pos_prop}: {target_pos}px; + min-width: {target_size}px; + min-height: {target_size}px; + }} + """ + self.ws_rail_provider.load_from_data(shrink_css.encode()) + + GLib.timeout_add( + int(duration * 1000), + self._finalize_rail_animation, + target_pos, + target_size, + ) + return False + + def _finalize_rail_animation(self, final_pos, final_size): + """Finalize animation and update state.""" + self.current_rail_pos = final_pos + self.current_rail_size = final_size + self.is_animating_rail = False + logger.info( + f"Rail animation finished at pos={self.current_rail_pos}, size={self.current_rail_size}" + ) + return False + + def _update_rail_position(self, active_button): + """Position the rail at the active workspace button""" + allocation = active_button.get_allocation() + + # If button isn't properly allocated yet, try again later + if allocation.width == 0 or allocation.height == 0: + logger.info("Button allocation not ready, retrying...") + return True # Keep retrying + + # Get the current position information to preserve it + current_css = self.ws_rail.get_style_context().get_property( + "margin-top" if data.VERTICAL else "margin-left", Gtk.StateFlags.NORMAL + ) + + # A trick to force animation: use a very short transition duration first + if data.VERTICAL: + css_preserve = f""" + #workspace-rail {{ + transition-property: margin-top; + transition-duration: 0.001s; + margin-top: {current_css.value}px; + min-height: {allocation.height}px; + min-width: {allocation.width}px; + }} + """ + else: + css_preserve = f""" + #workspace-rail {{ + transition-property: margin-left; + transition-duration: 0.001s; + margin-left: {current_css.value}px; + min-width: {allocation.width}px; + min-height: {allocation.height}px; + }} + """ + + # Apply the preservation CSS + self.ws_rail_provider.load_from_data(css_preserve.encode()) + + # Force a redraw and let the short transition complete + self.ws_rail.queue_draw() + + # Now add a small delay before applying the actual transition and new position + GLib.timeout_add(10, self._apply_rail_transition, active_button, allocation) + + return False # Don't call again + + def _apply_rail_transition(self, active_button, allocation): + """Apply the transition with proper timing""" + # Set CSS for either vertical or horizontal orientation with transition + if data.VERTICAL: + css = f""" + #workspace-rail {{ + transition-property: margin-top; + transition-duration: 0.15s; + transition-timing-function: ease-out; + margin-top: {allocation.y}px; + min-height: {allocation.height}px; + min-width: {allocation.width}px; + }} + """ + else: + css = f""" + #workspace-rail {{ + transition-property: margin-left; + transition-duration: 0.15s; + transition-timing-function: ease-out; + margin-left: {allocation.x}px; + min-width: {allocation.width}px; + min-height: {allocation.height}px; + }} + """ + + # Apply the CSS with transition + self.ws_rail_provider.load_from_data(css.encode()) + logger.info(f"Rail animating to x={allocation.x}, y={allocation.y}") + + return False # Don't call again + + @property + def children_workspaces(self): + workspaces_widget = None + for child in self.ws_container.get_children(): + if isinstance(child, Workspaces): + workspaces_widget = child + break + + if workspaces_widget: + try: + # The structure is Workspaces -> internal Box -> Buttons + internal_box = workspaces_widget.get_children()[0] + return internal_box.get_children() + except (IndexError, AttributeError): + logger.error( + "Failed to get workspace buttons due to unexpected widget structure." + ) + return [] + + logger.warning("Could not find the Workspaces widget in the container.") + return [] + + def _update_rail_idle(self, workspaces, button_id): + # Log initial state + logger.info(f"[Bar] Rail update idle for workspace {button_id}") + + active_button = next( + ( + b + for b in workspaces + if isinstance(b, WorkspaceButton) and b.id == button_id + ), + None, + ) + + logger.warning( + f"Active button {active_button} for workspace {button_id} {workspaces}" + ) + + if not active_button: + logger.warning(f"[Bar] No active button found for workspace {button_id}") + return GLib.SOURCE_REMOVE + + allocation = active_button.get_allocation() + if allocation.width == 0: + logger.info( + f"[Bar] Button allocation width is 0 for workspace {button_id}, retrying..." + ) + return True # Retry + + # Log allocation values + logger.info( + f"[Bar] Button allocation: x={allocation.x}, y={allocation.y}, width={allocation.width}, height={allocation.height}" + ) + + # Handle vertical vs horizontal orientation differently + if data.VERTICAL: + logger.info("[Bar] Using vertical CSS positioning") + css = f""" + #workspace-rail {{ + transition-duration: 0.15s; + margin-top: {allocation.y}px; + min-height: {allocation.height}px; + min-width: {allocation.width}px; + }} + """ + else: + logger.info("[Bar] Using horizontal CSS positioning") + css = f""" + #workspace-rail {{ + transition-duration: 0.15s; + margin-left: {allocation.x}px; + min-width: {allocation.width}px; + min-height: {allocation.height}px; + }} + """ + + logger.info(f"[Bar] Applying CSS: {css}") + self.ws_rail_provider.load_from_data(css.encode()) + + # Check the style properties after applying + style_context = self.ws_rail.get_style_context() + logger.info(f"[Bar] Rail style classes: {list(style_context.list_classes())}") + + return GLib.SOURCE_REMOVE diff --git a/scripts/screenshot.sh b/scripts/screenshot.sh index 8cef7ff..6222bee 100755 --- a/scripts/screenshot.sh +++ b/scripts/screenshot.sh @@ -38,7 +38,8 @@ case $1 in exit 1 ;; esac - +sleep 0.1 +sync if [ -f "$full_path" ]; then # Copiar al portapapeles si no es mockup if [ "$mockup_mode" != "mockup" ]; then diff --git a/styles/workspaces.css b/styles/workspaces.css index 179ec05..587a52e 100644 --- a/styles/workspaces.css +++ b/styles/workspaces.css @@ -8,6 +8,15 @@ #workspaces-container { background-color: var(--shadow); + padding: 0; + border-radius: 16px; +} + +#workspace-rail { + background-color: var(--primary); + border-radius: 16px; + transition: transform 0.5s cubic-bezier(0.15, 1, 0.3, 1), min-width 0.5s cubic-bezier(0.15, 1, 0.3, 1); + min-height: 34px; } #workspaces-container.invert { @@ -38,13 +47,13 @@ #workspaces > button.active { min-width: 48px; min-height: 8px; - background-color: var(--primary); + background-color: transparent; } #workspaces > button.active.vertical { min-width: 8px; min-height: 48px; - background-color: var(--primary); + background-color: transparent; } #workspaces > button.empty { @@ -77,7 +86,7 @@ } #workspaces-num > button.active { - background-color: var(--primary); + background-color: transparent; border-radius: 8px; } From 67954b1b50a10d18b9b8ead5581a2af0e3772ef3 Mon Sep 17 00:00:00 2001 From: fdev31 Date: Sat, 26 Jul 2025 15:40:57 +0200 Subject: [PATCH 3/4] more realistic animation --- modules/bar.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/bar.py b/modules/bar.py index 415247e..2380067 100644 --- a/modules/bar.py +++ b/modules/bar.py @@ -736,15 +736,16 @@ def _update_rail_with_animation(self, active_button): return True diameter = 24 + reduced_diameter = 8 # New variable for reduced size during stretch if data.VERTICAL: pos_prop, size_prop = "margin-top", "min-height" - other_size_prop, other_size_val = "min-width", diameter + other_size_prop, other_size_val = "min-width", reduced_diameter target_pos = ( target_allocation.y + (target_allocation.height / 2) - (diameter / 2) ) else: pos_prop, size_prop = "margin-left", "min-width" - other_size_prop, other_size_val = "min-height", diameter + other_size_prop, other_size_val = "min-height", reduced_diameter target_pos = ( target_allocation.x + (target_allocation.width / 2) - (diameter / 2) ) From 52d13809eda55fd36d256766af80ab52e31a6dbd Mon Sep 17 00:00:00 2001 From: fdev31 Date: Sat, 26 Jul 2025 15:54:56 +0200 Subject: [PATCH 4/4] Make shrinking proportional to the distance --- modules/bar.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/bar.py b/modules/bar.py index 2380067..be84ccb 100644 --- a/modules/bar.py +++ b/modules/bar.py @@ -736,16 +736,13 @@ def _update_rail_with_animation(self, active_button): return True diameter = 24 - reduced_diameter = 8 # New variable for reduced size during stretch if data.VERTICAL: pos_prop, size_prop = "margin-top", "min-height" - other_size_prop, other_size_val = "min-width", reduced_diameter target_pos = ( target_allocation.y + (target_allocation.height / 2) - (diameter / 2) ) else: pos_prop, size_prop = "margin-left", "min-width" - other_size_prop, other_size_val = "min-height", reduced_diameter target_pos = ( target_allocation.x + (target_allocation.width / 2) - (diameter / 2) ) @@ -761,6 +758,13 @@ def _update_rail_with_animation(self, active_button): stretch_duration = 0.1 shrink_duration = 0.15 + reduced_diameter = max(2, int(diameter - abs(distance / 10.0))) + + if data.VERTICAL: + other_size_prop, other_size_val = "min-width", reduced_diameter + else: + other_size_prop, other_size_val = "min-height", reduced_diameter + stretch_css = f""" #workspace-rail {{ transition-property: {pos_prop}, {size_prop};