Compare commits
No commits in common. "68b3f2bdd09c38bc571868beef8c36985acd004e" and "d6cb0909ae674619a5ada8925b470a9b6dd28ea4" have entirely different histories.
68b3f2bdd0
...
d6cb0909ae
|
|
@ -55,11 +55,6 @@ class Taskbar(Gtk.Box):
|
||||||
self._dir_dds = {}
|
self._dir_dds = {}
|
||||||
self._fit_sws = {}
|
self._fit_sws = {}
|
||||||
self._poll_id: int | None = None
|
self._poll_id: int | None = None
|
||||||
# Content signatures, so a poll that found nothing new rebuilds nothing
|
|
||||||
# (see _build_panel_scaffold for why that matters). None = never built.
|
|
||||||
self._strip_sig: list | None = None
|
|
||||||
self._list_order: list | None = None
|
|
||||||
self._rows: dict = {} # window address -> row widgets, for in-place updates
|
|
||||||
|
|
||||||
# The workspace/window panel. It is NOT appended here: the menu window mounts
|
# The workspace/window panel. It is NOT appended here: the menu window mounts
|
||||||
# `panel_widget` into the quad region so that expanding COLLAPSES this strip's
|
# `panel_widget` into the quad region so that expanding COLLAPSES this strip's
|
||||||
|
|
@ -69,7 +64,6 @@ class Taskbar(Gtk.Box):
|
||||||
self._panel_scroll = Gtk.ScrolledWindow(
|
self._panel_scroll = Gtk.ScrolledWindow(
|
||||||
hscrollbar_policy=Gtk.PolicyType.NEVER, vexpand=True)
|
hscrollbar_policy=Gtk.PolicyType.NEVER, vexpand=True)
|
||||||
self._panel_scroll.set_child(self._panel)
|
self._panel_scroll.set_child(self._panel)
|
||||||
self._build_panel_scaffold()
|
|
||||||
|
|
||||||
header = Gtk.CenterBox()
|
header = Gtk.CenterBox()
|
||||||
title = Gtk.Label(label="Open windows", xalign=0.0)
|
title = Gtk.Label(label="Open windows", xalign=0.0)
|
||||||
|
|
@ -172,31 +166,21 @@ class Taskbar(Gtk.Box):
|
||||||
def _on_ws(self, ok: bool, data) -> None:
|
def _on_ws(self, ok: bool, data) -> None:
|
||||||
if ok and isinstance(data, dict):
|
if ok and isinstance(data, dict):
|
||||||
self._active_ws = data.get("id")
|
self._active_ws = data.get("id")
|
||||||
self._update_panel()
|
self._rebuild_panel()
|
||||||
|
|
||||||
def _on_clients(self, ok: bool, data) -> None:
|
def _on_clients(self, ok: bool, data) -> None:
|
||||||
self._clients = data if ok and isinstance(data, list) else []
|
self._clients = data if ok and isinstance(data, list) else []
|
||||||
self._rebuild_strip()
|
self._rebuild_strip()
|
||||||
self._update_panel()
|
self._rebuild_panel()
|
||||||
|
|
||||||
# -- compact strip -----------------------------------------------------
|
# -- compact strip -----------------------------------------------------
|
||||||
def _rebuild_strip(self) -> None:
|
def _rebuild_strip(self) -> None:
|
||||||
|
self._clear(self._row)
|
||||||
groups: dict[str, list] = {}
|
groups: dict[str, list] = {}
|
||||||
for w in self._clients:
|
for w in self._clients:
|
||||||
if not w.get("mapped", True) or not w.get("class"):
|
if not w.get("mapped", True) or not w.get("class"):
|
||||||
continue
|
continue
|
||||||
groups.setdefault(w["class"], []).append(w)
|
groups.setdefault(w["class"], []).append(w)
|
||||||
# Same reasoning as the panel list: rebuild only when the strip's contents
|
|
||||||
# actually change, so a poll can't reset its horizontal scroll — or tear down
|
|
||||||
# a grouped app's instance popover while it is open. Keyed on addresses only:
|
|
||||||
# a retitled window doesn't change anything visible here (titles appear in the
|
|
||||||
# tooltip/popover, refreshed on the next real change).
|
|
||||||
sig = [(cls, tuple(w.get("address") for w in wins))
|
|
||||||
for cls, wins in sorted(groups.items())]
|
|
||||||
if sig == self._strip_sig:
|
|
||||||
return
|
|
||||||
self._strip_sig = sig
|
|
||||||
self._clear(self._row)
|
|
||||||
if not groups:
|
if not groups:
|
||||||
self._row.append(Gtk.Label(label="No open windows"))
|
self._row.append(Gtk.Label(label="No open windows"))
|
||||||
return
|
return
|
||||||
|
|
@ -235,22 +219,16 @@ class Taskbar(Gtk.Box):
|
||||||
return btn
|
return btn
|
||||||
|
|
||||||
# -- pop-open panel: layout controls + window list ---------------------
|
# -- pop-open panel: layout controls + window list ---------------------
|
||||||
# The panel scaffolding is built ONCE, here, and every later refresh updates it in
|
def _rebuild_panel(self) -> None:
|
||||||
# place. It used to be rebuilt from scratch on each refresh (_clear(self._panel) +
|
self._clear(self._panel)
|
||||||
# fresh widgets, including a fresh ScrolledWindow for the window list) — and since
|
|
||||||
# start_polling() refreshes once a second while the menu is open, that made the
|
|
||||||
# list impossible to scroll: emptying the scrolled content collapses the scroll
|
|
||||||
# adjustment's upper bound, so its value is clamped back to 0 and the viewport
|
|
||||||
# snapped to the top roughly once a second.
|
|
||||||
def _build_panel_scaffold(self) -> None:
|
|
||||||
# tabbed layout selector for the active workspace: a tab per layout, and a
|
# tabbed layout selector for the active workspace: a tab per layout, and a
|
||||||
# per-layout options page underneath that switches with the tab. Selecting a
|
# per-layout options page underneath that switches with the tab. Selecting a
|
||||||
# tab applies that layout to the currently focused workspace. The tabs are the
|
# tab applies that layout to the currently focused workspace.
|
||||||
# same for every workspace — only the *selection* is per-workspace, and that is
|
ws = self._active_ws
|
||||||
# what _sync_layout_controls() refreshes — so this is built once too.
|
hdr = Gtk.Label(label=f"Workspace {ws if ws is not None else '?'} layout", xalign=0.0)
|
||||||
self._ws_hdr = Gtk.Label(label="Workspace ? layout", xalign=0.0)
|
hdr.add_css_class("section-title")
|
||||||
self._ws_hdr.add_css_class("section-title")
|
self._panel.append(hdr)
|
||||||
self._panel.append(self._ws_hdr)
|
|
||||||
|
|
||||||
self._dir_dds = {} # layout name -> (Gtk.DropDown, [dir values])
|
self._dir_dds = {} # layout name -> (Gtk.DropDown, [dir values])
|
||||||
self._fit_sws = {} # layout name -> Gtk.Switch
|
self._fit_sws = {} # layout name -> Gtk.Switch
|
||||||
|
|
@ -262,72 +240,23 @@ class Taskbar(Gtk.Box):
|
||||||
self._panel.append(switcher)
|
self._panel.append(switcher)
|
||||||
self._panel.append(self._layout_stack)
|
self._panel.append(self._layout_stack)
|
||||||
self._layout_stack.connect("notify::visible-child-name", self._on_tab_switch)
|
self._layout_stack.connect("notify::visible-child-name", self._on_tab_switch)
|
||||||
|
self._sync_layout_controls()
|
||||||
|
|
||||||
self._panel.append(Gtk.Separator())
|
self._panel.append(Gtk.Separator())
|
||||||
|
|
||||||
# per-window rows: [focus/jump] [pull here]
|
# per-window rows: [focus/jump] [pull here]
|
||||||
self._listing = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
|
||||||
self._list_scroll = Gtk.ScrolledWindow(
|
|
||||||
vexpand=True, hscrollbar_policy=Gtk.PolicyType.NEVER)
|
|
||||||
self._list_scroll.set_child(self._listing)
|
|
||||||
self._panel.append(self._list_scroll)
|
|
||||||
|
|
||||||
def _update_panel(self) -> None:
|
|
||||||
ws = self._active_ws
|
|
||||||
self._ws_hdr.set_label(f"Workspace {ws if ws is not None else '?'} layout")
|
|
||||||
self._sync_layout_controls()
|
|
||||||
if getattr(self, "_cols_lbl", None) is not None:
|
|
||||||
self._cols_lbl.set_label(str(self._read_cols()))
|
|
||||||
self._update_window_list()
|
|
||||||
|
|
||||||
def _update_window_list(self) -> None:
|
|
||||||
wins = [w for w in self._clients if w.get("mapped", True) and w.get("class")]
|
wins = [w for w in self._clients if w.get("mapped", True) and w.get("class")]
|
||||||
wins.sort(key=lambda w: (w.get("workspace", {}).get("id", 0),
|
wins.sort(key=lambda w: (w.get("workspace", {}).get("id", 0),
|
||||||
(w.get("title") or w.get("class") or "").lower()))
|
(w.get("title") or w.get("class") or "").lower()))
|
||||||
order = [w.get("address") for w in wins]
|
|
||||||
if order == self._list_order:
|
|
||||||
# Same windows in the same order: refresh the row contents in place, so a
|
|
||||||
# retitled window (browser tab switch, shell cwd) costs no rebuild and
|
|
||||||
# therefore cannot move the scroll position.
|
|
||||||
for w in wins:
|
|
||||||
refs = self._rows.get(w.get("address"))
|
|
||||||
if refs is not None:
|
|
||||||
self._set_row_content(refs, w)
|
|
||||||
return
|
|
||||||
|
|
||||||
# The list really did change shape. Rebuild just the rows (not the scroller)
|
|
||||||
# and put the viewport back where the user left it, once the new rows have been
|
|
||||||
# allocated — until then the adjustment's upper bound is still stale. Both
|
|
||||||
# scrollers are saved: which one actually scrolls depends on how much height
|
|
||||||
# the mounted panel gets, and restoring an already-zero offset is a no-op.
|
|
||||||
saved = [(sw.get_vadjustment(), sw.get_vadjustment().get_value())
|
|
||||||
for sw in (self._panel_scroll, self._list_scroll)]
|
|
||||||
self._list_order = order
|
|
||||||
self._rows = {}
|
|
||||||
self._clear(self._listing)
|
|
||||||
if not wins:
|
if not wins:
|
||||||
self._listing.append(Gtk.Label(label="No open windows", xalign=0.0))
|
self._panel.append(Gtk.Label(label="No open windows", xalign=0.0))
|
||||||
|
return
|
||||||
|
listing = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||||
|
scroller = Gtk.ScrolledWindow(vexpand=True, hscrollbar_policy=Gtk.PolicyType.NEVER)
|
||||||
|
scroller.set_child(listing)
|
||||||
for w in wins:
|
for w in wins:
|
||||||
self._listing.append(self._window_row(w))
|
listing.append(self._window_row(w))
|
||||||
GLib.idle_add(self._restore_offsets, saved)
|
self._panel.append(scroller)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _restore_offsets(saved) -> bool:
|
|
||||||
for adj, value in saved:
|
|
||||||
adj.set_value(min(value, max(0.0, adj.get_upper() - adj.get_page_size())))
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _set_row_content(self, refs, w) -> None:
|
|
||||||
icon, title_lbl, ws_lbl = refs
|
|
||||||
title = w.get("title") or w.get("class") or "?"
|
|
||||||
if title_lbl.get_label() != title:
|
|
||||||
title_lbl.set_label(title)
|
|
||||||
ws_text = f"ws {w.get('workspace', {}).get('id')}"
|
|
||||||
if ws_lbl.get_label() != ws_text:
|
|
||||||
ws_lbl.set_label(ws_text)
|
|
||||||
icon_name = self._icon_for(w.get("class", ""))
|
|
||||||
if icon.get_icon_name() != icon_name:
|
|
||||||
icon.set_from_icon_name(icon_name)
|
|
||||||
|
|
||||||
def _window_row(self, w) -> Gtk.Widget:
|
def _window_row(self, w) -> Gtk.Widget:
|
||||||
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||||
|
|
@ -342,12 +271,8 @@ class Taskbar(Gtk.Box):
|
||||||
lbl.append(icon)
|
lbl.append(icon)
|
||||||
wl = Gtk.Label(label=f"{title}", xalign=0.0, hexpand=True, ellipsize=3, max_width_chars=32)
|
wl = Gtk.Label(label=f"{title}", xalign=0.0, hexpand=True, ellipsize=3, max_width_chars=32)
|
||||||
lbl.append(wl)
|
lbl.append(wl)
|
||||||
ws_lbl = Gtk.Label(label=f"ws {wsid}", xalign=1.0)
|
lbl.append(Gtk.Label(label=f"ws {wsid}", xalign=1.0))
|
||||||
lbl.append(ws_lbl)
|
|
||||||
name.set_child(lbl)
|
name.set_child(lbl)
|
||||||
# Keep the mutable bits reachable so a refresh can update this row instead of
|
|
||||||
# replacing it (the address is the row's identity and never changes).
|
|
||||||
self._rows[w.get("address")] = (icon, wl, ws_lbl)
|
|
||||||
name.set_tooltip_text("Jump to window")
|
name.set_tooltip_text("Jump to window")
|
||||||
name.connect("clicked", lambda *_a: self._focus(w))
|
name.connect("clicked", lambda *_a: self._focus(w))
|
||||||
row.append(name)
|
row.append(name)
|
||||||
|
|
@ -442,21 +367,21 @@ class Taskbar(Gtk.Box):
|
||||||
# Reflect the current per-ws layout / direction / focus_fit in the tabs+options.
|
# Reflect the current per-ws layout / direction / focus_fit in the tabs+options.
|
||||||
#
|
#
|
||||||
# This fires 3 concurrent hyprctl subprocess calls with no guaranteed resolution
|
# This fires 3 concurrent hyprctl subprocess calls with no guaranteed resolution
|
||||||
# order, and refresh() can trigger _update_panel() (hence this) twice in a row
|
# order, and refresh() can trigger _rebuild_panel() (hence this) twice in a row
|
||||||
# (once from the clients query, once from the activeworkspace one), so several
|
# (once from the clients query, once from the activeworkspace one) — each
|
||||||
# syncs can be in flight at once. Two failure modes existed:
|
# rebuilding a fresh _layout_stack/_dir_dds/_fit_sws. Two failure modes existed:
|
||||||
# 1. The 3 callbacks used to each toggle the shared `self._syncing` flag
|
# 1. The 3 callbacks used to each toggle the shared `self._syncing` flag
|
||||||
# independently (True at start, False at end). Whichever resolved *first*
|
# independently (True at start, False at end). Whichever resolved *first*
|
||||||
# dropped the guard while the others — in particular the tab-selection one,
|
# dropped the guard while the others — in particular the tab-selection one,
|
||||||
# which programmatically flips the visible stack child — were still in
|
# which programmatically flips the visible stack child — were still in
|
||||||
# flight, letting `_on_tab_switch`/`_on_opt_change` misfire as if the user
|
# flight, letting `_on_tab_switch`/`_on_opt_change` misfire as if the user
|
||||||
# had clicked and re-apply stale data via `layouts.set(...)`.
|
# had clicked and re-apply stale data via `layouts.set(...)`.
|
||||||
# 2. A second `_sync_layout_controls()` call (from the redundant refresh) could
|
# 2. A second `_sync_layout_controls()` call (from the redundant rebuild) could
|
||||||
# land while the first's callbacks were still pending, so a stale callback
|
# land while the first's callbacks were still pending, so a stale callback
|
||||||
# from sync #1 could decrement sync #2's pending-count or apply sync #1's
|
# from sync #1 could decrement sync #2's pending-count or apply sync #1's
|
||||||
# (possibly wrong-workspace) data into sync #2's widgets.
|
# (possibly wrong-workspace) data into sync #2's freshly built widgets.
|
||||||
# A generation token invalidates any sync superseded by a newer one outright —
|
# A generation token invalidates any sync superseded by a newer one outright —
|
||||||
# if the user switches workspaces (or a redundant refresh fires) mid-flight, only
|
# if the user switches workspaces (or a redundant rebuild fires) mid-flight, only
|
||||||
# the latest call's callbacks are allowed to touch anything, and self._syncing
|
# the latest call's callbacks are allowed to touch anything, and self._syncing
|
||||||
# only lifts once *that* generation's 3 calls have all resolved.
|
# only lifts once *that* generation's 3 calls have all resolved.
|
||||||
self._sync_generation = getattr(self, "_sync_generation", 0) + 1
|
self._sync_generation = getattr(self, "_sync_generation", 0) + 1
|
||||||
|
|
|
||||||
|
|
@ -77,18 +77,15 @@ hl.bind(mainMod .. " + RETURN", hl.dsp.exec_cmd(menu))
|
||||||
hl.bind(mainMod .. " + SHIFT + RETURN", hl.dsp.exec_cmd(winswitch))
|
hl.bind(mainMod .. " + SHIFT + RETURN", hl.dsp.exec_cmd(winswitch))
|
||||||
hl.bind("CTRL + SHIFT + R", hl.dsp.exec_cmd(menu))
|
hl.bind("CTRL + SHIFT + R", hl.dsp.exec_cmd(menu))
|
||||||
|
|
||||||
hl.bind(mainMod .. " + F", hl.dsp.exec_cmd(fileManager))
|
|
||||||
|
|
||||||
-- File picker dialog -> clipboard (path / contents)
|
-- File picker dialog -> clipboard (path / contents)
|
||||||
hl.bind(mainMod .. " + CTRL + SHIFT + F",hl.dsp.exec_cmd("[tag +filepicker] ~/.config/scripts/filepicker-clipboard.sh path"))
|
hl.bind(mainMod .. " + F", hl.dsp.exec_cmd("[tag +filepicker] ~/.config/scripts/filepicker-clipboard.sh path"))
|
||||||
hl.bind(mainMod .. " + SHIFT + F", hl.dsp.exec_cmd("[tag +filepicker] ~/.config/scripts/filepicker-clipboard.sh content"))
|
hl.bind(mainMod .. " + SHIFT + F", hl.dsp.exec_cmd("[tag +filepicker] ~/.config/scripts/filepicker-clipboard.sh content"))
|
||||||
|
|
||||||
-- Super+S is the supersonic-booster mixer (see below); pavucontrol is the fallback.
|
hl.bind(mainMod .. " + S", hl.dsp.exec_cmd("[tag +mixer] pavucontrol"))
|
||||||
hl.bind(mainMod .. " + SHIFT + S", hl.dsp.exec_cmd("[tag +mixer] pavucontrol"))
|
|
||||||
hl.bind(mainMod .. " + U", hl.dsp.exec_cmd("[tag +centered-L] kitty btop"))
|
hl.bind(mainMod .. " + U", hl.dsp.exec_cmd("[tag +centered-L] kitty btop"))
|
||||||
hl.bind(mainMod .. " + W", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/wallpaper-picker ~/Pictures"))
|
hl.bind(mainMod .. " + W", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/wallpaper-picker ~/Pictures"))
|
||||||
hl.bind(mainMod .. " + SHIFT + M", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/monitor-manager"))
|
hl.bind(mainMod .. " + SHIFT + M", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/monitor-manager"))
|
||||||
hl.bind(mainMod .. " + CTRL + S", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/amssh"))
|
hl.bind(mainMod .. " + CTRL + R", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/amssh"))
|
||||||
hl.bind(mainMod .. " + F1", hl.dsp.exec_cmd("[tag +centered] kitty ~/.config/scripts/helpmenu.sh"))
|
hl.bind(mainMod .. " + F1", hl.dsp.exec_cmd("[tag +centered] kitty ~/.config/scripts/helpmenu.sh"))
|
||||||
hl.bind(mainMod .. " + CTRL + T", hl.dsp.exec_cmd("[tag +centered-S] kitty bash ~/.config/scripts/timer-pick"))
|
hl.bind(mainMod .. " + CTRL + T", hl.dsp.exec_cmd("[tag +centered-S] kitty bash ~/.config/scripts/timer-pick"))
|
||||||
hl.bind(mainMod .. " + SHIFT + F1", hl.dsp.exec_cmd("[tag +centered-L] kitty nvim ~/.config/hypr/usr/binds.lua"))
|
hl.bind(mainMod .. " + SHIFT + F1", hl.dsp.exec_cmd("[tag +centered-L] kitty nvim ~/.config/hypr/usr/binds.lua"))
|
||||||
|
|
@ -346,8 +343,8 @@ hl.bind(mainMod .. " + Z", hl.dsp.exec_cmd("~/.config/scripts/station-bar.sh tog
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
-- PipeWire mixer panel: Applications / Output / Input / General tabs.
|
-- PipeWire mixer panel: Applications / Output / Input / General tabs.
|
||||||
-- The primary mixer, on Super+S; pavucontrol is the Super+Shift+S fallback.
|
-- Super+S is already the pavucontrol fallback mixer; this is the primary one.
|
||||||
hl.bind(mainMod .. " + S", hl.dsp.exec_cmd("~/.config/scripts/supersonic-booster.sh"), { release = true })
|
hl.bind(mainMod .. " + SHIFT + S", hl.dsp.exec_cmd("~/.config/scripts/supersonic-booster.sh"), { release = true })
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
---- SCREENSHOT ----
|
---- SCREENSHOT ----
|
||||||
|
|
@ -380,8 +377,7 @@ hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("bri --down"), { locked = true,
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
hl.bind(mainMod .. " + CTRL + W", hl.dsp.exec_cmd("hyprctl hyprsunset gamma +10"))
|
hl.bind(mainMod .. " + CTRL + W", hl.dsp.exec_cmd("hyprctl hyprsunset gamma +10"))
|
||||||
-- Super+Ctrl+S is the ssh manager, so gamma-down takes the extra SHIFT.
|
hl.bind(mainMod .. " + CTRL + S", hl.dsp.exec_cmd("hyprctl hyprsunset gamma -10"))
|
||||||
hl.bind(mainMod .. " + CTRL + SHIFT + S", hl.dsp.exec_cmd("hyprctl hyprsunset gamma -10"))
|
|
||||||
hl.bind(mainMod .. " + CTRL + A", hl.dsp.exec_cmd("hyprctl hyprsunset temperature +450"))
|
hl.bind(mainMod .. " + CTRL + A", hl.dsp.exec_cmd("hyprctl hyprsunset temperature +450"))
|
||||||
hl.bind(mainMod .. " + CTRL + Q", hl.dsp.exec_cmd("hyprctl hyprsunset temperature -450"))
|
hl.bind(mainMod .. " + CTRL + Q", hl.dsp.exec_cmd("hyprctl hyprsunset temperature -450"))
|
||||||
hl.bind(mainMod .. " + CTRL + X", hl.dsp.exec_cmd("hyprctl hyprsunset identity"))
|
hl.bind(mainMod .. " + CTRL + X", hl.dsp.exec_cmd("hyprctl hyprsunset identity"))
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,6 @@ class Taskbar(Gtk.Box):
|
||||||
self._dir_dds = {}
|
self._dir_dds = {}
|
||||||
self._fit_sws = {}
|
self._fit_sws = {}
|
||||||
self._poll_id: int | None = None
|
self._poll_id: int | None = None
|
||||||
# Content signatures, so a poll that found nothing new rebuilds nothing
|
|
||||||
# (see _build_panel_scaffold for why that matters). None = never built.
|
|
||||||
self._strip_sig: list | None = None
|
|
||||||
self._list_order: list | None = None
|
|
||||||
self._rows: dict = {} # window address -> row widgets, for in-place updates
|
|
||||||
|
|
||||||
# The workspace/window panel. It is NOT appended here: the menu window mounts
|
# The workspace/window panel. It is NOT appended here: the menu window mounts
|
||||||
# `panel_widget` into the quad region so that expanding COLLAPSES this strip's
|
# `panel_widget` into the quad region so that expanding COLLAPSES this strip's
|
||||||
|
|
@ -69,7 +64,6 @@ class Taskbar(Gtk.Box):
|
||||||
self._panel_scroll = Gtk.ScrolledWindow(
|
self._panel_scroll = Gtk.ScrolledWindow(
|
||||||
hscrollbar_policy=Gtk.PolicyType.NEVER, vexpand=True)
|
hscrollbar_policy=Gtk.PolicyType.NEVER, vexpand=True)
|
||||||
self._panel_scroll.set_child(self._panel)
|
self._panel_scroll.set_child(self._panel)
|
||||||
self._build_panel_scaffold()
|
|
||||||
|
|
||||||
header = Gtk.CenterBox()
|
header = Gtk.CenterBox()
|
||||||
title = Gtk.Label(label="Open windows", xalign=0.0)
|
title = Gtk.Label(label="Open windows", xalign=0.0)
|
||||||
|
|
@ -172,31 +166,21 @@ class Taskbar(Gtk.Box):
|
||||||
def _on_ws(self, ok: bool, data) -> None:
|
def _on_ws(self, ok: bool, data) -> None:
|
||||||
if ok and isinstance(data, dict):
|
if ok and isinstance(data, dict):
|
||||||
self._active_ws = data.get("id")
|
self._active_ws = data.get("id")
|
||||||
self._update_panel()
|
self._rebuild_panel()
|
||||||
|
|
||||||
def _on_clients(self, ok: bool, data) -> None:
|
def _on_clients(self, ok: bool, data) -> None:
|
||||||
self._clients = data if ok and isinstance(data, list) else []
|
self._clients = data if ok and isinstance(data, list) else []
|
||||||
self._rebuild_strip()
|
self._rebuild_strip()
|
||||||
self._update_panel()
|
self._rebuild_panel()
|
||||||
|
|
||||||
# -- compact strip -----------------------------------------------------
|
# -- compact strip -----------------------------------------------------
|
||||||
def _rebuild_strip(self) -> None:
|
def _rebuild_strip(self) -> None:
|
||||||
|
self._clear(self._row)
|
||||||
groups: dict[str, list] = {}
|
groups: dict[str, list] = {}
|
||||||
for w in self._clients:
|
for w in self._clients:
|
||||||
if not w.get("mapped", True) or not w.get("class"):
|
if not w.get("mapped", True) or not w.get("class"):
|
||||||
continue
|
continue
|
||||||
groups.setdefault(w["class"], []).append(w)
|
groups.setdefault(w["class"], []).append(w)
|
||||||
# Same reasoning as the panel list: rebuild only when the strip's contents
|
|
||||||
# actually change, so a poll can't reset its horizontal scroll — or tear down
|
|
||||||
# a grouped app's instance popover while it is open. Keyed on addresses only:
|
|
||||||
# a retitled window doesn't change anything visible here (titles appear in the
|
|
||||||
# tooltip/popover, refreshed on the next real change).
|
|
||||||
sig = [(cls, tuple(w.get("address") for w in wins))
|
|
||||||
for cls, wins in sorted(groups.items())]
|
|
||||||
if sig == self._strip_sig:
|
|
||||||
return
|
|
||||||
self._strip_sig = sig
|
|
||||||
self._clear(self._row)
|
|
||||||
if not groups:
|
if not groups:
|
||||||
self._row.append(Gtk.Label(label="No open windows"))
|
self._row.append(Gtk.Label(label="No open windows"))
|
||||||
return
|
return
|
||||||
|
|
@ -235,22 +219,16 @@ class Taskbar(Gtk.Box):
|
||||||
return btn
|
return btn
|
||||||
|
|
||||||
# -- pop-open panel: layout controls + window list ---------------------
|
# -- pop-open panel: layout controls + window list ---------------------
|
||||||
# The panel scaffolding is built ONCE, here, and every later refresh updates it in
|
def _rebuild_panel(self) -> None:
|
||||||
# place. It used to be rebuilt from scratch on each refresh (_clear(self._panel) +
|
self._clear(self._panel)
|
||||||
# fresh widgets, including a fresh ScrolledWindow for the window list) — and since
|
|
||||||
# start_polling() refreshes once a second while the menu is open, that made the
|
|
||||||
# list impossible to scroll: emptying the scrolled content collapses the scroll
|
|
||||||
# adjustment's upper bound, so its value is clamped back to 0 and the viewport
|
|
||||||
# snapped to the top roughly once a second.
|
|
||||||
def _build_panel_scaffold(self) -> None:
|
|
||||||
# tabbed layout selector for the active workspace: a tab per layout, and a
|
# tabbed layout selector for the active workspace: a tab per layout, and a
|
||||||
# per-layout options page underneath that switches with the tab. Selecting a
|
# per-layout options page underneath that switches with the tab. Selecting a
|
||||||
# tab applies that layout to the currently focused workspace. The tabs are the
|
# tab applies that layout to the currently focused workspace.
|
||||||
# same for every workspace — only the *selection* is per-workspace, and that is
|
ws = self._active_ws
|
||||||
# what _sync_layout_controls() refreshes — so this is built once too.
|
hdr = Gtk.Label(label=f"Workspace {ws if ws is not None else '?'} layout", xalign=0.0)
|
||||||
self._ws_hdr = Gtk.Label(label="Workspace ? layout", xalign=0.0)
|
hdr.add_css_class("section-title")
|
||||||
self._ws_hdr.add_css_class("section-title")
|
self._panel.append(hdr)
|
||||||
self._panel.append(self._ws_hdr)
|
|
||||||
|
|
||||||
self._dir_dds = {} # layout name -> (Gtk.DropDown, [dir values])
|
self._dir_dds = {} # layout name -> (Gtk.DropDown, [dir values])
|
||||||
self._fit_sws = {} # layout name -> Gtk.Switch
|
self._fit_sws = {} # layout name -> Gtk.Switch
|
||||||
|
|
@ -262,72 +240,23 @@ class Taskbar(Gtk.Box):
|
||||||
self._panel.append(switcher)
|
self._panel.append(switcher)
|
||||||
self._panel.append(self._layout_stack)
|
self._panel.append(self._layout_stack)
|
||||||
self._layout_stack.connect("notify::visible-child-name", self._on_tab_switch)
|
self._layout_stack.connect("notify::visible-child-name", self._on_tab_switch)
|
||||||
|
self._sync_layout_controls()
|
||||||
|
|
||||||
self._panel.append(Gtk.Separator())
|
self._panel.append(Gtk.Separator())
|
||||||
|
|
||||||
# per-window rows: [focus/jump] [pull here]
|
# per-window rows: [focus/jump] [pull here]
|
||||||
self._listing = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
|
||||||
self._list_scroll = Gtk.ScrolledWindow(
|
|
||||||
vexpand=True, hscrollbar_policy=Gtk.PolicyType.NEVER)
|
|
||||||
self._list_scroll.set_child(self._listing)
|
|
||||||
self._panel.append(self._list_scroll)
|
|
||||||
|
|
||||||
def _update_panel(self) -> None:
|
|
||||||
ws = self._active_ws
|
|
||||||
self._ws_hdr.set_label(f"Workspace {ws if ws is not None else '?'} layout")
|
|
||||||
self._sync_layout_controls()
|
|
||||||
if getattr(self, "_cols_lbl", None) is not None:
|
|
||||||
self._cols_lbl.set_label(str(self._read_cols()))
|
|
||||||
self._update_window_list()
|
|
||||||
|
|
||||||
def _update_window_list(self) -> None:
|
|
||||||
wins = [w for w in self._clients if w.get("mapped", True) and w.get("class")]
|
wins = [w for w in self._clients if w.get("mapped", True) and w.get("class")]
|
||||||
wins.sort(key=lambda w: (w.get("workspace", {}).get("id", 0),
|
wins.sort(key=lambda w: (w.get("workspace", {}).get("id", 0),
|
||||||
(w.get("title") or w.get("class") or "").lower()))
|
(w.get("title") or w.get("class") or "").lower()))
|
||||||
order = [w.get("address") for w in wins]
|
|
||||||
if order == self._list_order:
|
|
||||||
# Same windows in the same order: refresh the row contents in place, so a
|
|
||||||
# retitled window (browser tab switch, shell cwd) costs no rebuild and
|
|
||||||
# therefore cannot move the scroll position.
|
|
||||||
for w in wins:
|
|
||||||
refs = self._rows.get(w.get("address"))
|
|
||||||
if refs is not None:
|
|
||||||
self._set_row_content(refs, w)
|
|
||||||
return
|
|
||||||
|
|
||||||
# The list really did change shape. Rebuild just the rows (not the scroller)
|
|
||||||
# and put the viewport back where the user left it, once the new rows have been
|
|
||||||
# allocated — until then the adjustment's upper bound is still stale. Both
|
|
||||||
# scrollers are saved: which one actually scrolls depends on how much height
|
|
||||||
# the mounted panel gets, and restoring an already-zero offset is a no-op.
|
|
||||||
saved = [(sw.get_vadjustment(), sw.get_vadjustment().get_value())
|
|
||||||
for sw in (self._panel_scroll, self._list_scroll)]
|
|
||||||
self._list_order = order
|
|
||||||
self._rows = {}
|
|
||||||
self._clear(self._listing)
|
|
||||||
if not wins:
|
if not wins:
|
||||||
self._listing.append(Gtk.Label(label="No open windows", xalign=0.0))
|
self._panel.append(Gtk.Label(label="No open windows", xalign=0.0))
|
||||||
|
return
|
||||||
|
listing = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||||
|
scroller = Gtk.ScrolledWindow(vexpand=True, hscrollbar_policy=Gtk.PolicyType.NEVER)
|
||||||
|
scroller.set_child(listing)
|
||||||
for w in wins:
|
for w in wins:
|
||||||
self._listing.append(self._window_row(w))
|
listing.append(self._window_row(w))
|
||||||
GLib.idle_add(self._restore_offsets, saved)
|
self._panel.append(scroller)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _restore_offsets(saved) -> bool:
|
|
||||||
for adj, value in saved:
|
|
||||||
adj.set_value(min(value, max(0.0, adj.get_upper() - adj.get_page_size())))
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _set_row_content(self, refs, w) -> None:
|
|
||||||
icon, title_lbl, ws_lbl = refs
|
|
||||||
title = w.get("title") or w.get("class") or "?"
|
|
||||||
if title_lbl.get_label() != title:
|
|
||||||
title_lbl.set_label(title)
|
|
||||||
ws_text = f"ws {w.get('workspace', {}).get('id')}"
|
|
||||||
if ws_lbl.get_label() != ws_text:
|
|
||||||
ws_lbl.set_label(ws_text)
|
|
||||||
icon_name = self._icon_for(w.get("class", ""))
|
|
||||||
if icon.get_icon_name() != icon_name:
|
|
||||||
icon.set_from_icon_name(icon_name)
|
|
||||||
|
|
||||||
def _window_row(self, w) -> Gtk.Widget:
|
def _window_row(self, w) -> Gtk.Widget:
|
||||||
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||||
|
|
@ -342,12 +271,8 @@ class Taskbar(Gtk.Box):
|
||||||
lbl.append(icon)
|
lbl.append(icon)
|
||||||
wl = Gtk.Label(label=f"{title}", xalign=0.0, hexpand=True, ellipsize=3, max_width_chars=32)
|
wl = Gtk.Label(label=f"{title}", xalign=0.0, hexpand=True, ellipsize=3, max_width_chars=32)
|
||||||
lbl.append(wl)
|
lbl.append(wl)
|
||||||
ws_lbl = Gtk.Label(label=f"ws {wsid}", xalign=1.0)
|
lbl.append(Gtk.Label(label=f"ws {wsid}", xalign=1.0))
|
||||||
lbl.append(ws_lbl)
|
|
||||||
name.set_child(lbl)
|
name.set_child(lbl)
|
||||||
# Keep the mutable bits reachable so a refresh can update this row instead of
|
|
||||||
# replacing it (the address is the row's identity and never changes).
|
|
||||||
self._rows[w.get("address")] = (icon, wl, ws_lbl)
|
|
||||||
name.set_tooltip_text("Jump to window")
|
name.set_tooltip_text("Jump to window")
|
||||||
name.connect("clicked", lambda *_a: self._focus(w))
|
name.connect("clicked", lambda *_a: self._focus(w))
|
||||||
row.append(name)
|
row.append(name)
|
||||||
|
|
@ -442,21 +367,21 @@ class Taskbar(Gtk.Box):
|
||||||
# Reflect the current per-ws layout / direction / focus_fit in the tabs+options.
|
# Reflect the current per-ws layout / direction / focus_fit in the tabs+options.
|
||||||
#
|
#
|
||||||
# This fires 3 concurrent hyprctl subprocess calls with no guaranteed resolution
|
# This fires 3 concurrent hyprctl subprocess calls with no guaranteed resolution
|
||||||
# order, and refresh() can trigger _update_panel() (hence this) twice in a row
|
# order, and refresh() can trigger _rebuild_panel() (hence this) twice in a row
|
||||||
# (once from the clients query, once from the activeworkspace one), so several
|
# (once from the clients query, once from the activeworkspace one) — each
|
||||||
# syncs can be in flight at once. Two failure modes existed:
|
# rebuilding a fresh _layout_stack/_dir_dds/_fit_sws. Two failure modes existed:
|
||||||
# 1. The 3 callbacks used to each toggle the shared `self._syncing` flag
|
# 1. The 3 callbacks used to each toggle the shared `self._syncing` flag
|
||||||
# independently (True at start, False at end). Whichever resolved *first*
|
# independently (True at start, False at end). Whichever resolved *first*
|
||||||
# dropped the guard while the others — in particular the tab-selection one,
|
# dropped the guard while the others — in particular the tab-selection one,
|
||||||
# which programmatically flips the visible stack child — were still in
|
# which programmatically flips the visible stack child — were still in
|
||||||
# flight, letting `_on_tab_switch`/`_on_opt_change` misfire as if the user
|
# flight, letting `_on_tab_switch`/`_on_opt_change` misfire as if the user
|
||||||
# had clicked and re-apply stale data via `layouts.set(...)`.
|
# had clicked and re-apply stale data via `layouts.set(...)`.
|
||||||
# 2. A second `_sync_layout_controls()` call (from the redundant refresh) could
|
# 2. A second `_sync_layout_controls()` call (from the redundant rebuild) could
|
||||||
# land while the first's callbacks were still pending, so a stale callback
|
# land while the first's callbacks were still pending, so a stale callback
|
||||||
# from sync #1 could decrement sync #2's pending-count or apply sync #1's
|
# from sync #1 could decrement sync #2's pending-count or apply sync #1's
|
||||||
# (possibly wrong-workspace) data into sync #2's widgets.
|
# (possibly wrong-workspace) data into sync #2's freshly built widgets.
|
||||||
# A generation token invalidates any sync superseded by a newer one outright —
|
# A generation token invalidates any sync superseded by a newer one outright —
|
||||||
# if the user switches workspaces (or a redundant refresh fires) mid-flight, only
|
# if the user switches workspaces (or a redundant rebuild fires) mid-flight, only
|
||||||
# the latest call's callbacks are allowed to touch anything, and self._syncing
|
# the latest call's callbacks are allowed to touch anything, and self._syncing
|
||||||
# only lifts once *that* generation's 3 calls have all resolved.
|
# only lifts once *that* generation's 3 calls have all resolved.
|
||||||
self._sync_generation = getattr(self, "_sync_generation", 0) + 1
|
self._sync_generation = getattr(self, "_sync_generation", 0) + 1
|
||||||
|
|
|
||||||
|
|
@ -78,21 +78,18 @@ hl.bind(mainMod .. " + SHIFT + RETURN", hl.dsp.exec_cmd(winswitch))
|
||||||
hl.bind(mainMod .. " + SHIFT + R", hl.dsp.exec_cmd("wofi --show drun"))
|
hl.bind(mainMod .. " + SHIFT + R", hl.dsp.exec_cmd("wofi --show drun"))
|
||||||
hl.bind("CTRL + SHIFT + R", hl.dsp.exec_cmd(menu))
|
hl.bind("CTRL + SHIFT + R", hl.dsp.exec_cmd(menu))
|
||||||
|
|
||||||
hl.bind(mainMod .. " + F", hl.dsp.exec_cmd(fileManager))
|
|
||||||
|
|
||||||
-- File picker dialog -> clipboard (path / contents)
|
-- File picker dialog -> clipboard (path / contents)
|
||||||
hl.bind(mainMod .. " + CTRL + SHIFT + F",hl.dsp.exec_cmd("[tag +filepicker] ~/.config/scripts/filepicker-clipboard.sh path"))
|
hl.bind(mainMod .. " + F", hl.dsp.exec_cmd("[tag +filepicker] ~/.config/scripts/filepicker-clipboard.sh path"))
|
||||||
hl.bind(mainMod .. " + SHIFT + F", hl.dsp.exec_cmd("[tag +filepicker] ~/.config/scripts/filepicker-clipboard.sh content"))
|
hl.bind(mainMod .. " + SHIFT + F", hl.dsp.exec_cmd("[tag +filepicker] ~/.config/scripts/filepicker-clipboard.sh content"))
|
||||||
-- old:
|
-- old:
|
||||||
-- hl.bind(mainMod .. " + F", hl.dsp.exec_cmd("~/.config/scripts/wofi-file-search.sh"))
|
-- hl.bind(mainMod .. " + F", hl.dsp.exec_cmd("~/.config/scripts/wofi-file-search.sh"))
|
||||||
-- hl.bind(mainMod .. " + SHIFT + F", hl.dsp.exec_cmd("~/.config/scripts/foldersearch.sh"))
|
-- hl.bind(mainMod .. " + SHIFT + F", hl.dsp.exec_cmd("~/.config/scripts/foldersearch.sh"))
|
||||||
|
|
||||||
-- Super+S is the audio-panel mixer (see AUDIO-PANEL below); pavucontrol is the fallback.
|
hl.bind(mainMod .. " + S", hl.dsp.exec_cmd("[tag +mixer] pavucontrol"))
|
||||||
hl.bind(mainMod .. " + SHIFT + S", hl.dsp.exec_cmd("[tag +mixer] pavucontrol"))
|
|
||||||
hl.bind(mainMod .. " + U", hl.dsp.exec_cmd("[tag +centered-L] kitty btop"))
|
hl.bind(mainMod .. " + U", hl.dsp.exec_cmd("[tag +centered-L] kitty btop"))
|
||||||
hl.bind(mainMod .. " + W", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/wallpaper-picker ~/Pictures"))
|
hl.bind(mainMod .. " + W", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/wallpaper-picker ~/Pictures"))
|
||||||
hl.bind(mainMod .. " + SHIFT + M", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/monitor-manager"))
|
hl.bind(mainMod .. " + SHIFT + M", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/monitor-manager"))
|
||||||
hl.bind(mainMod .. " + CTRL + S", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/amssh"))
|
hl.bind(mainMod .. " + CTRL + R", hl.dsp.exec_cmd("[tag +centered-L] kitty -e ~/.config/scripts/amssh"))
|
||||||
hl.bind(mainMod .. " + F1", hl.dsp.exec_cmd("[tag +centered] kitty ~/.config/scripts/helpmenu.sh"))
|
hl.bind(mainMod .. " + F1", hl.dsp.exec_cmd("[tag +centered] kitty ~/.config/scripts/helpmenu.sh"))
|
||||||
hl.bind(mainMod .. " + CTRL + T", hl.dsp.exec_cmd("[tag +centered-S] kitty bash ~/.config/scripts/timer-pick"))
|
hl.bind(mainMod .. " + CTRL + T", hl.dsp.exec_cmd("[tag +centered-S] kitty bash ~/.config/scripts/timer-pick"))
|
||||||
hl.bind(mainMod .. " + SHIFT + F1", hl.dsp.exec_cmd("[tag +centered-L] kitty nvim ~/.config/hypr/usr/binds.lua"))
|
hl.bind(mainMod .. " + SHIFT + F1", hl.dsp.exec_cmd("[tag +centered-L] kitty nvim ~/.config/hypr/usr/binds.lua"))
|
||||||
|
|
@ -326,8 +323,8 @@ hl.bind(mainMod .. " + SHIFT + A", hl.dsp.exec_cmd("~/.config/scripts/menu-toggl
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
-- PipeWire mixer panel: Applications / Output / Input / General tabs.
|
-- PipeWire mixer panel: Applications / Output / Input / General tabs.
|
||||||
-- The primary mixer, on Super+S; pavucontrol is the Super+Shift+S fallback.
|
-- Super+S is already the pavucontrol fallback mixer; this is the primary one.
|
||||||
hl.bind(mainMod .. " + S", hl.dsp.exec_cmd("~/.config/scripts/audio-panel.sh"), { release = true })
|
hl.bind(mainMod .. " + SHIFT + S", hl.dsp.exec_cmd("~/.config/scripts/audio-panel.sh"), { release = true })
|
||||||
|
|
||||||
--------------------
|
--------------------
|
||||||
---- SCREENSHOT ----
|
---- SCREENSHOT ----
|
||||||
|
|
@ -360,8 +357,7 @@ hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("bri --down"), { locked = true,
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
hl.bind(mainMod .. " + CTRL + W", hl.dsp.exec_cmd("hyprctl hyprsunset gamma +10"))
|
hl.bind(mainMod .. " + CTRL + W", hl.dsp.exec_cmd("hyprctl hyprsunset gamma +10"))
|
||||||
-- Super+Ctrl+S is the ssh manager, so gamma-down takes the extra SHIFT.
|
hl.bind(mainMod .. " + CTRL + S", hl.dsp.exec_cmd("hyprctl hyprsunset gamma -10"))
|
||||||
hl.bind(mainMod .. " + CTRL + SHIFT + S", hl.dsp.exec_cmd("hyprctl hyprsunset gamma -10"))
|
|
||||||
hl.bind(mainMod .. " + CTRL + A", hl.dsp.exec_cmd("hyprctl hyprsunset temperature +450"))
|
hl.bind(mainMod .. " + CTRL + A", hl.dsp.exec_cmd("hyprctl hyprsunset temperature +450"))
|
||||||
hl.bind(mainMod .. " + CTRL + Q", hl.dsp.exec_cmd("hyprctl hyprsunset temperature -450"))
|
hl.bind(mainMod .. " + CTRL + Q", hl.dsp.exec_cmd("hyprctl hyprsunset temperature -450"))
|
||||||
hl.bind(mainMod .. " + CTRL + X", hl.dsp.exec_cmd("hyprctl hyprsunset identity"))
|
hl.bind(mainMod .. " + CTRL + X", hl.dsp.exec_cmd("hyprctl hyprsunset identity"))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue