628 lines
21 KiB
Python
628 lines
21 KiB
Python
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"""<b><u>Launcher</u></b>
|
||
<b>• Apps:</b> Type to search.
|
||
|
||
<b>• Calculator [Prefix "="]:</b> Solve a math expression.
|
||
e.g. "=2+2"
|
||
|
||
<b>• Converter [Prefix ";"]:</b> Convert between units.
|
||
e.g. ";100 USD to EUR", ";10 km to miles"
|
||
|
||
<b>• Special Commands [Prefix ":"]:</b>
|
||
:update - Open {data.APP_NAME_CAP}'s updater.
|
||
:d - Open Dashboard.
|
||
:w - Open Wallpapers."""
|
||
|
||
tooltip_power = """<b>Power Menu</b>"""
|
||
tooltip_tools = """<b>Toolbox</b>"""
|
||
tooltip_overview = """<b>Overview</b>"""
|
||
|
||
|
||
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")
|