import json import os from fabric.hyprland.service import HyprlandEvent from fabric.hyprland.widgets import HyprlandLanguage as Language from fabric.hyprland.widgets import HyprlandWorkspaces as Workspaces from fabric.hyprland.widgets import WorkspaceButton, get_hyprland_connection from fabric.utils.helpers import exec_shell_command_async from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox from fabric.widgets.datetime import DateTime from fabric.widgets.label import Label from fabric.widgets.revealer import Revealer from gi.repository import Gdk, GLib, Gtk 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 CHINESE_NUMERALS = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "〇"] # Tooltips tooltip_apps = f"""Launcher • Apps: Type to search. • Calculator [Prefix "="]: Solve a math expression. e.g. "=2+2" • Converter [Prefix ";"]: Convert between units. e.g. ";100 USD to EUR", ";10 km to miles" • Special Commands [Prefix ":"]: :update - Open {data.APP_NAME_CAP}'s updater. :d - Open Dashboard. :w - Open Wallpapers.""" tooltip_power = """Power Menu""" tooltip_tools = """Toolbox""" tooltip_overview = """Overview""" class Bar(Window): def __init__(self, monitor_id: int = 0, **kwargs): self.monitor_id = monitor_id super().__init__( name="bar", layer="top", exclusivity="auto", visible=True, all_visible=True, monitor=monitor_id, ) self.anchor_var = "" self.margin_var = "" match data.BAR_POSITION: case "Top": self.anchor_var = "left top right" case "Bottom": self.anchor_var = "left bottom right" case "Left": self.anchor_var = "left" if data.CENTERED_BAR else "left top bottom" case "Right": self.anchor_var = "right" if data.CENTERED_BAR else "top right bottom" case _: self.anchor_var = "left top right" if data.VERTICAL: match data.BAR_THEME: case "Edge": self.margin_var = "-8px -8px -8px -8px" case _: self.margin_var = "-4px -8px -4px -4px" else: match data.BAR_THEME: case "Edge": self.margin_var = "-8px -8px -8px -8px" case _: if data.BAR_POSITION == "Bottom": self.margin_var = "-8px -4px -4px -4px" else: self.margin_var = "-4px -4px -8px -4px" self.set_anchor(self.anchor_var) self.set_margin(self.margin_var) self.notch = kwargs.get("notch", None) self.component_visibility = data.BAR_COMPONENTS_VISIBILITY self.dock_instance = None self.integrated_dock_widget = None # Calculate workspace range based on monitor_id # Monitor 0: workspaces 1-10, Monitor 1: workspaces 11-20, etc. start_workspace = self.monitor_id * 10 + 1 end_workspace = start_workspace + 10 workspace_range = range(start_workspace, end_workspace) self.workspaces = Workspaces( name="workspaces", invert_scroll=True, empty_scroll=True, v_align="fill", orientation="h" if not data.VERTICAL else "v", spacing=8, buttons=[ WorkspaceButton( h_expand=False, v_expand=False, h_align="center", v_align="center", id=i, label=None, style_classes=["vertical"] if data.VERTICAL else None, ) for i in workspace_range ], buttons_factory=( None if data.BAR_HIDE_SPECIAL_WORKSPACE else Workspaces.default_buttons_factory ), ) self.workspaces_num = Workspaces( name="workspaces-num", invert_scroll=True, 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, buttons=[ WorkspaceButton( h_expand=False, v_expand=False, h_align="center", v_align="center", id=i, label=( CHINESE_NUMERALS[(i - start_workspace)] if data.BAR_WORKSPACE_USE_CHINESE_NUMERALS and 0 <= (i - start_workspace) < len(CHINESE_NUMERALS) else str(i) ), ) for i in workspace_range ], buttons_factory=( None if data.BAR_HIDE_SPECIAL_WORKSPACE else Workspaces.default_buttons_factory ), ) self.ws_container = Box( name="workspaces-container", children=( self.workspaces if not data.BAR_WORKSPACE_SHOW_NUMBER else self.workspaces_num ), ) self.button_tools = Button( name="button-bar", tooltip_markup=tooltip_tools, on_clicked=lambda *_: self.tools_menu(), child=Label(name="button-bar-label", markup=icons.toolbox), ) self.connection = get_hyprland_connection() self.button_tools.connect("enter_notify_event", self.on_button_enter) self.button_tools.connect("leave_notify_event", self.on_button_leave) self.systray = SystemTray() self.weather = Weather() self.sysprofiles = Systemprofiles() self.network = NetworkApplet() self.lang_label = Label(name="lang-label") self.language = Button( name="language", h_align="center", v_align="center", child=self.lang_label ) self.on_language_switch() self.connection.connect("event::activelayout", self.on_language_switch) # Determine date-time format based on the new setting if data.DATETIME_12H_FORMAT: time_format_horizontal = "%I:%M %p" time_format_vertical = "%I\n%M\n%p" else: time_format_horizontal = "%H:%M" time_format_vertical = "%H\n%M" self.date_time = DateTime( name="date-time", formatters=( [time_format_horizontal] if not data.VERTICAL else [time_format_vertical] ), h_align="center" if not data.VERTICAL else "fill", v_align="center", h_expand=True, v_expand=True, style_classes=["vertical"] if data.VERTICAL else [], ) self.button_apps = Button( name="button-bar", tooltip_markup=tooltip_apps, on_clicked=lambda *_: self.search_apps(), child=Label(name="button-bar-label", markup=icons.apps), ) self.button_apps.connect("enter_notify_event", self.on_button_enter) self.button_apps.connect("leave_notify_event", self.on_button_leave) self.button_power = Button( name="button-bar", tooltip_markup=tooltip_power, on_clicked=lambda *_: self.power_menu(), child=Label(name="button-bar-label", markup=icons.shutdown), ) self.button_power.connect("enter_notify_event", self.on_button_enter) self.button_power.connect("leave_notify_event", self.on_button_leave) self.button_overview = Button( name="button-bar", tooltip_markup=tooltip_overview, on_clicked=lambda *_: self.overview(), child=Label(name="button-bar-label", markup=icons.windows), ) self.button_overview.connect("enter_notify_event", self.on_button_enter) self.button_overview.connect("leave_notify_event", self.on_button_leave) self.control = ControlSmall() self.metrics = MetricsSmall() self.battery = Battery() self.apply_component_props() self.rev_right = [ self.metrics, self.control, ] self.revealer_right = Revealer( name="bar-revealer", transition_type="slide-left", child_revealed=True, child=Box( name="bar-revealer-box", orientation="h", spacing=4, children=self.rev_right if not data.VERTICAL else None, ), ) self.boxed_revealer_right = Box( name="boxed-revealer", children=[ self.revealer_right, ], ) self.rev_left = [ self.weather, self.sysprofiles, self.network, ] self.revealer_left = Revealer( name="bar-revealer", transition_type="slide-right", child_revealed=True, child=Box( name="bar-revealer-box", orientation="h", spacing=4, children=self.rev_left if not data.VERTICAL else None, ), ) self.boxed_revealer_left = Box( name="boxed-revealer", children=[ self.revealer_left, ], ) self.h_start_children = [ self.button_apps, self.ws_container, self.button_overview, self.boxed_revealer_left, ] self.h_end_children = [ self.boxed_revealer_right, self.battery, self.systray, self.button_tools, self.language, self.date_time, self.button_power, ] self.v_start_children = [ self.button_apps, self.systray, self.control, self.sysprofiles, self.network, self.button_tools, ] self.v_center_children = [ self.button_overview, self.ws_container, self.weather, ] self.v_end_children = [ self.battery, self.metrics, self.language, self.date_time, 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) # Create embedded dock when bar is in center position (regardless of DOCK_ENABLED setting) should_embed_dock = ( data.BAR_POSITION == "Bottom" or (data.PANEL_THEME == "Panel" and data.BAR_POSITION in ["Top", "Bottom"]) ) if should_embed_dock: 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: bar_center_actual_children = Box( orientation=Gtk.Orientation.VERTICAL, spacing=4, children=( self.v_all_children if is_centered_bar else self.v_center_children ), ) self.bar_inner = CenterBox( name="bar-inner", orientation=( Gtk.Orientation.HORIZONTAL if not data.VERTICAL else Gtk.Orientation.VERTICAL ), h_align="fill", v_align="fill", start_children=( None if is_centered_bar else Box( name="start-container", spacing=4, orientation=( Gtk.Orientation.HORIZONTAL if not data.VERTICAL else Gtk.Orientation.VERTICAL ), children=( self.h_start_children if not data.VERTICAL else self.v_start_children ), ) ), center_children=bar_center_actual_children, end_children=( None if is_centered_bar else Box( name="end-container", spacing=4, orientation=( Gtk.Orientation.HORIZONTAL if not data.VERTICAL else Gtk.Orientation.VERTICAL ), children=( self.h_end_children if not data.VERTICAL else self.v_end_children ), ) ), ) self.children = self.bar_inner self.hidden = False self.themed_children = [ self.button_apps, self.button_overview, self.button_power, self.button_tools, self.language, self.date_time, self.ws_container, self.weather, self.network, self.battery, self.metrics, self.systray, self.control, ] if self.integrated_dock_widget: self.themed_children.append(self.integrated_dock_widget) current_theme = data.BAR_THEME theme_classes = ["pills", "dense", "edge", "edgecenter"] for tc in theme_classes: self.bar_inner.remove_style_class(tc) self.style = None match current_theme: case "Pills": self.style = "pills" case "Dense": self.style = "dense" case "Edge": if data.VERTICAL and data.CENTERED_BAR: self.style = "edgecenter" else: self.style = "edge" case _: self.style = "pills" self.bar_inner.add_style_class(self.style) if self.integrated_dock_widget and hasattr( self.integrated_dock_widget, "add_style_class" ): for theme_class_to_remove in ["pills", "dense", "edge"]: style_context = self.integrated_dock_widget.get_style_context() if style_context.has_class(theme_class_to_remove): self.integrated_dock_widget.remove_style_class( theme_class_to_remove ) self.integrated_dock_widget.add_style_class(self.style) if data.BAR_THEME == "Dense" or data.BAR_THEME == "Edge": for child in self.themed_children: if hasattr(child, "add_style_class"): child.add_style_class("invert") match data.BAR_POSITION: case "Top": self.bar_inner.add_style_class("top") case "Bottom": self.bar_inner.add_style_class("bottom") case "Left": self.bar_inner.add_style_class("left") case "Right": self.bar_inner.add_style_class("right") case _: self.bar_inner.add_style_class("top") if data.VERTICAL: self.bar_inner.add_style_class("vertical") self.systray._update_visibility() self.chinese_numbers() def apply_component_props(self): components = { "button_apps": self.button_apps, "systray": self.systray, "control": self.control, "network": self.network, "button_tools": self.button_tools, "button_overview": self.button_overview, "ws_container": self.ws_container, "weather": self.weather, "battery": self.battery, "metrics": self.metrics, "language": self.language, "date_time": self.date_time, "button_power": self.button_power, "sysprofiles": self.sysprofiles, } for component_name, widget in components.items(): if component_name in self.component_visibility: widget.set_visible(self.component_visibility[component_name]) def toggle_component_visibility(self, component_name): components = { "button_apps": self.button_apps, "systray": self.systray, "control": self.control, "network": self.network, "button_tools": self.button_tools, "button_overview": self.button_overview, "ws_container": self.ws_container, "weather": self.weather, "battery": self.battery, "metrics": self.metrics, "language": self.language, "date_time": self.date_time, "button_power": self.button_power, "sysprofiles": self.sysprofiles, } if component_name in components and component_name in self.component_visibility: self.component_visibility[component_name] = not self.component_visibility[ component_name ] components[component_name].set_visible( self.component_visibility[component_name] ) config_file = os.path.expanduser( f"~/.config/{data.APP_NAME}/config/config.json" ) if os.path.exists(config_file): try: with open(config_file, "r") as f: config = json.load(f) config[f"bar_{component_name}_visible"] = self.component_visibility[ 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}") return self.component_visibility[component_name] return None def on_button_enter(self, widget, event): window = widget.get_window() if window: window.set_cursor(Gdk.Cursor.new_from_name(widget.get_display(), "hand2")) def on_button_leave(self, widget, event): window = widget.get_window() if window: window.set_cursor(None) def on_button_clicked(self, *args): exec_shell_command_async("notify-send 'Botón presionado' '¡Funciona!'") def search_apps(self): if self.notch: self.notch.open_notch("launcher") def overview(self): if self.notch: self.notch.open_notch("overview") def power_menu(self): if self.notch: self.notch.open_notch("power") def tools_menu(self): if self.notch: self.notch.open_notch("tools") def on_language_switch(self, _=None, event: HyprlandEvent = None): try: lang_data = ( event.data[1] if event and event.data and len(event.data) > 1 else Language().get_label() ) except json.JSONDecodeError: lang_data = "UNK" # Fallback to default language label self.language.set_tooltip_text(lang_data) if not data.VERTICAL: self.lang_label.set_label(lang_data[:3].upper()) else: self.lang_label.add_style_class("icon") self.lang_label.set_markup(icons.keyboard) def toggle_hidden(self): self.hidden = not self.hidden if self.hidden: self.bar_inner.add_style_class("hidden") else: self.bar_inner.remove_style_class("hidden") # Ensure notch is above bar when bar is shown if self.notch: # Focus the notch window to bring it to front GLib.idle_add(lambda: exec_shell_command_async("hyprctl dispatch focuswindow class:notch") if self.notch else None) def chinese_numbers(self): if data.BAR_WORKSPACE_USE_CHINESE_NUMERALS: self.workspaces_num.add_style_class("chinese") else: self.workspaces_num.remove_style_class("chinese")