"""Full-width taskbar strip: jump to any open window. Reads open windows from `hyprctl clients -j`, groups them by app (class). A group with a single window focuses it directly; a group with several windows opens a pop-out list so you can pick a specific instance. The strip scrolls sideways when many apps are open. Focusing a window closes the menu. """ from __future__ import annotations import gi gi.require_version("Gtk", "4.0") gi.require_version("AstalApps", "0.1") from gi.repository import AstalApps, Gtk # noqa: E402 from lib.proc import run_json, run_text class Taskbar(Gtk.Box): def __init__(self, on_activate): super().__init__(orientation=Gtk.Orientation.VERTICAL) self.add_css_class("taskbar") self._on_activate = on_activate self._apps = AstalApps.Apps() self._wm_index = self._build_wm_index() header = Gtk.Label(label="Open windows", xalign=0.0) header.add_css_class("section-title") self.append(header) self._row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) self._row.add_css_class("taskbar-row") scroller = Gtk.ScrolledWindow( vscrollbar_policy=Gtk.PolicyType.NEVER, hscrollbar_policy=Gtk.PolicyType.AUTOMATIC) scroller.set_child(self._row) self.append(scroller) def _build_wm_index(self) -> dict: idx = {} for app in self._apps.get_list(): for key in (app.get_wm_class(), app.get_executable(), app.get_name()): if key: idx.setdefault(key.lower(), app) return idx def _icon_for(self, cls: str) -> str: app = self._wm_index.get((cls or "").lower()) if app and app.get_icon_name(): return app.get_icon_name() return (cls or "application-x-executable").lower() # -- populate ---------------------------------------------------------- def refresh(self) -> None: run_json(["hyprctl", "clients", "-j"], self._on_clients) def _on_clients(self, ok: bool, data) -> None: child = self._row.get_first_child() while child: self._row.remove(child) child = self._row.get_first_child() if not ok or not isinstance(data, list): self._row.append(Gtk.Label(label="no window data")) return groups: dict[str, list] = {} for w in data: if not w.get("mapped", True) or not w.get("class"): continue groups.setdefault(w["class"], []).append(w) if not groups: self._row.append(Gtk.Label(label="No open windows")) return for cls, wins in sorted(groups.items()): self._row.append(self._group_button(cls, wins)) def _group_button(self, cls: str, wins: list) -> Gtk.Widget: icon = Gtk.Image.new_from_icon_name(self._icon_for(cls)) icon.set_pixel_size(32) if len(wins) == 1: btn = Gtk.Button() btn.add_css_class("task-tile") btn.set_child(icon) btn.set_tooltip_text(wins[0].get("title") or cls) btn.connect("clicked", lambda *_a, w=wins[0]: self._focus(w)) return btn # multiple windows -> pop-out list of specific instances btn = Gtk.MenuButton() btn.add_css_class("task-tile") box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) box.append(icon) badge = Gtk.Label(label=str(len(wins))) badge.add_css_class("task-badge") box.append(badge) btn.set_child(box) btn.set_tooltip_text(f"{cls} ({len(wins)})") pop = Gtk.Popover() pop.add_css_class("task-popover") plist = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) for w in wins: item = Gtk.Button(label=w.get("title") or cls) item.add_css_class("task-window") item.connect("clicked", lambda *_a, ww=w: (pop.popdown(), self._focus(ww))) plist.append(item) pop.set_child(plist) btn.set_popover(pop) return btn def _focus(self, w) -> None: addr = w.get("address") if addr: run_text(["hyprctl", "dispatch", "focuswindow", f"address:{addr}"], lambda *_a: None) self._on_activate()