115 lines
4.3 KiB
Python
115 lines
4.3 KiB
Python
"""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()
|