Files
Modus/mine/modules/panel/components/taskbar.py
T
2026-07-12 18:56:10 +05:45

335 lines
12 KiB
Python

import json
from fabric.hyprland.service import Hyprland, HyprlandEvent
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.image import Image
import config.data as data
from services import has_config_changed, on_config_change
from utils.functions import is_special_workspace_id
from utils.icon_resolver import IconResolver
class TaskBar(Box):
def __init__(self, **kwargs):
super().__init__(
orientation="h",
spacing=6,
name="taskbar",
**kwargs,
)
self.icon_resolver = IconResolver()
self._clients = {}
self._buttons = {}
self._workspace_monitor_source_id = None
self._visibility_callback = None
self._hyprland_connection = Hyprland()
# Connect to Hyprland events
self._hyprland_connection.connect("event::openwindow", self._on_open_window)
self._hyprland_connection.connect("event::closewindow", self._on_close_window)
self._hyprland_connection.connect(
"event::activewindowv2", self._on_active_window
)
self._hyprland_connection.connect("event::movewindow", self._on_move_window)
self._hyprland_connection.connect(
"event::workspacev2", self._on_workspace_changed
)
on_config_change(self._on_config_change)
self._load_config()
self._init_clients()
self._update_visibility()
def _init_clients(self):
"""Initialize clients from current Hyprland state"""
res = self._hyprland_connection.send_command("j/clients")
if not res or not res.reply:
return
try:
clients = json.loads(res.reply.decode("utf-8"))
active_res = self._hyprland_connection.send_command("j/activewindow")
active_address = None
if active_res and active_res.reply:
active_window = json.loads(active_res.reply.decode("utf-8"))
active_address = active_window.get("address")
for client_data in clients:
address = client_data.get("address")
if address:
is_active = address == active_address
self._add_client(address, client_data, is_active)
except Exception as e:
print(f"Error initializing taskbar clients: {e}")
def _on_open_window(self, _, event):
if len(event.data) >= 1:
address = "0x" + event.data[0]
self._update_client_data(address)
def _on_close_window(self, _, event):
if len(event.data) >= 1:
address = "0x" + event.data[0]
self._remove_client(address)
def _on_active_window(self, _, event):
active_address = (
("0x" + event.data[0]) if event.data and event.data[0] else None
)
for key, btn_data in self._buttons.items():
button = btn_data["button"]
if self._group_apps:
# Grouped mode: active if any address in group is active
is_active = active_address in btn_data["addresses"]
else:
# Normal mode: active if address matches key
is_active = key == active_address
if is_active:
button.add_style_class("active")
else:
button.remove_style_class("active")
def _on_move_window(self, _, event):
if len(event.data) >= 1:
address = "0x" + event.data[0]
self._update_client_data(address)
def _update_client_data(self, address):
res = self._hyprland_connection.send_command("j/clients")
if not res or not res.reply:
return
try:
clients = json.loads(res.reply.decode("utf-8"))
for client_data in clients:
if client_data.get("address") == address:
if address in self._clients:
self._clients[address] = client_data
self._update_client_button_properties(address)
else:
self._add_client(address, client_data)
break
except Exception as e:
print(f"Error updating client data: {e}")
def _get_button_key(self, client_data):
if self._group_apps:
return client_data.get("class", "unknown")
return client_data.get("address")
def _add_client(self, address, client_data, is_active=False):
self._clients[address] = client_data
key = self._get_button_key(client_data)
if key in self._buttons:
# Button already exists (only in grouping mode)
if address not in self._buttons[key]["addresses"]:
self._buttons[key]["addresses"].append(address)
if is_active:
self._buttons[key]["button"].add_style_class("active")
else:
# Create new button
client_image = Image()
def on_click(k=key):
addresses = self._buttons[k]["addresses"]
if not addresses:
return
# Cycle behavior: focus the next one if the current active is in this group
active_res = self._hyprland_connection.send_command("j/activewindow")
current_active = None
if active_res and active_res.reply:
active_window = json.loads(active_res.reply.decode("utf-8"))
current_active = active_window.get("address")
target_address = addresses[0]
if current_active in addresses:
idx = (addresses.index(current_active) + 1) % len(addresses)
target_address = addresses[idx]
self._hyprland_connection.send_command(
f"/dispatch focuswindow address:{target_address}"
)
client_button = Button(
image=client_image, on_button_press_event=lambda *_: on_click()
)
if is_active:
client_button.add_style_class("active")
self._buttons[key] = {
"button": client_button,
"image": client_image,
"addresses": [address],
}
self._update_button_visuals(key)
self._update_visibility()
def _remove_client(self, address):
if address not in self._clients:
return
client_data = self._clients[address]
key = self._get_button_key(client_data)
if key in self._buttons:
if address in self._buttons[key]["addresses"]:
self._buttons[key]["addresses"].remove(address)
if not self._buttons[key]["addresses"]:
# Last window of the group/address closed
button = self._buttons[key]["button"]
if button.get_parent() == self:
self.remove(button)
button.destroy()
del self._buttons[key]
else:
# Update visuals (e.g. tooltip)
self._update_button_visuals(key)
del self._clients[address]
self._update_visibility()
def _update_client_button_properties(self, address):
"""Update properties of the button associated with this address"""
if address not in self._clients:
return
key = self._get_button_key(self._clients[address])
self._update_button_visuals(key)
def _update_button_visuals(self, key):
if key not in self._buttons:
return
btn_data = self._buttons[key]
button = btn_data["button"]
image = btn_data["image"]
addresses = btn_data["addresses"]
if not addresses:
return
# Use data from the first address for the icon/title
first_address = addresses[0]
client_data = self._clients.get(first_address, {})
app_id = client_data.get("class", "")
title = client_data.get("title", "")
if self._group_apps and len(addresses) > 1:
title = f"{title} ({len(addresses)})"
image.set_from_pixbuf(
self.icon_resolver.get_icon_pixbuf(app_id, self._icon_size)
)
button.set_tooltip_text(title)
# Also re-check visibility
self._update_button_visibility(key)
def _on_workspace_changed(self, _, event: HyprlandEvent):
for key in list(self._buttons.keys()):
self._update_button_visibility(key)
self._update_visibility()
def _update_button_visibility(self, key):
if key not in self._buttons:
return
btn_data = self._buttons[key]
button = btn_data["button"]
addresses = btn_data["addresses"]
# A grouped button should be visible if ANY of its windows should be visible
should_be_visible = False
for address in addresses:
client_data = self._clients.get(address)
if not client_data:
continue
workspace_id = self._get_workspace_id(client_data)
if workspace_id is not None:
is_special = is_special_workspace_id(workspace_id)
if not (self._hide_special and is_special):
should_be_visible = True
break
button_present = button.get_parent() is not None
if should_be_visible and not button_present:
self.add(button)
elif not should_be_visible and button_present:
self.remove(button)
def _get_workspace_id(self, client_data):
workspace_data = client_data.get("workspace", {})
if isinstance(workspace_data, dict):
return workspace_data.get("id")
elif isinstance(workspace_data, (int, str)):
return workspace_data
return None
def _load_config(self):
self._icon_size = data.DATA()["taskbar_icon_size"]
self._hide_special = data.DATA()["taskbar_hide_special"]
self._group_apps = data.DATA()["taskbar_group_apps"]
def _on_config_change(self, new_config, old_config):
config_paths = [
"panel.taskbar.icon_size",
"panel.taskbar.hide_empty",
"panel.taskbar.hide_special",
"panel.taskbar.group_apps",
]
config_changed = any(
has_config_changed(old_config, new_config, path) for path in config_paths
)
if config_changed:
self._load_config()
# Force full rebuild of buttons on mode change
self._rebuild_all()
def _rebuild_all(self):
"""Clear all and re-initialize based on current config"""
for child in self.get_children():
self.remove(child)
for btn_data in self._buttons.values():
btn_data["button"].destroy()
self._buttons = {}
current_clients = self._clients.copy()
self._clients = {}
for address, client_data in current_clients.items():
active_res = self._hyprland_connection.send_command("j/activewindow")
is_active = False
if active_res and active_res.reply:
active_window = json.loads(active_res.reply.decode("utf-8"))
is_active = address == active_window.get("address")
self._add_client(address, client_data, is_active)
self._update_visibility()
def force_update_for_workspace_change(self):
self._on_workspace_changed(None, None)
def set_visibility_callback(self, callback):
self._visibility_callback = callback
def _update_visibility(self):
has_visible_children = any(child.get_visible() for child in self.get_children())
self.set_visible(has_visible_children)
if self._visibility_callback:
self._visibility_callback()