This commit is contained in:
sankalp committed 2026-07-12 18:56:10 +05:45
1 parent 6b62051f93
commit dc60e20bd7
182 files changed
+18062 -277

No files matched your search

+334
View File
@@ -0,0 +1,334 @@
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()