Files
sims/bar/modules/window_fuzzy.py

129 lines
4.4 KiB
Python

from fabric.i3 import I3, I3MessageType
from fabric.widgets.wayland import WaylandWindow as Window
from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.entry import Entry
from gi.repository import Gdk
from bar.services.fenster import get_i3_connection
class FuzzyWindowFinder(Window):
def __init__(
self,
monitor: int = 0,
):
super().__init__(
name="finder",
anchor="center",
monitor=monitor,
keyboard_mode="on-demand",
type="popup",
visible=False,
)
self._i3 = get_i3_connection()
self._all_windows = []
self._refresh_windows()
self.viewport = Box(name="viewport", spacing=4, orientation="v")
self.search_entry = Entry(
name="search-entry",
placeholder="Search Windows...",
h_expand=True,
editable=True,
notify_text=self.notify_text,
on_activate=lambda entry, *_: self.on_search_entry_activate(
entry.get_text()
),
on_key_press_event=self.on_search_entry_key_press,
)
self.picker_box = Box(
name="picker-box",
spacing=4,
orientation="v",
children=[self.search_entry, self.viewport],
)
self.add(self.picker_box)
self.arrange_viewport("")
def _refresh_windows(self):
"""Refresh the window list via GET_TREE"""
self._all_windows = []
tree_reply = I3.send_command("", I3MessageType.GET_TREE)
if not (tree_reply.is_ok and isinstance(tree_reply.reply, dict)):
return
tree = tree_reply.reply
# Traverse: root → outputs → workspaces → containers
for output_node in tree.get("nodes", []):
for ws_node in output_node.get("nodes", []):
ws_num = ws_node.get("num", 0)
for con in ws_node.get("nodes", []):
if con.get("type") == "con":
self._all_windows.append({
"id": con.get("id"),
"app_id": con.get("app_id", ""),
"title": con.get("name", ""),
"workspace": ws_num,
})
for con in ws_node.get("floating_nodes", []):
if con.get("type") == "con":
self._all_windows.append({
"id": con.get("id"),
"app_id": con.get("app_id", ""),
"title": con.get("name", ""),
"workspace": ws_num,
})
def show(self):
"""Override show to refresh windows before displaying"""
self._refresh_windows()
self.arrange_viewport(self.search_entry.get_text())
super().show()
def notify_text(self, entry, *_):
text = entry.get_text()
self.arrange_viewport(text)
def on_search_entry_key_press(self, widget, event):
if event.keyval in [Gdk.KEY_Escape, 103]:
self.hide()
return True
return False
def on_search_entry_activate(self, text):
"""Focus the first matching window"""
filtered = self._filter_windows(text)
if filtered:
window_id = filtered[0].get("id")
if window_id is not None:
I3.send_command(f"[con_id={window_id}] focus")
self.hide()
def _filter_windows(self, query: str) -> list:
"""Filter windows based on query matching title or app_id"""
if not query:
return self._all_windows
query_lower = query.lower()
return [
w for w in self._all_windows
if query_lower in w.get("title", "").lower()
or query_lower in w.get("app_id", "").lower()
]
def arrange_viewport(self, query: str = ""):
self.viewport.children = [] # Clear previous entries
filtered = self._filter_windows(query)
for window in filtered:
title = window.get("title", "")
app_id = window.get("app_id", "")
ws_num = window.get("workspace", 0)
display_text = f"[{ws_num}] {app_id}: {title}" if app_id else f"[{ws_num}] {title}"
self.viewport.add(
Box(name="slot-box", orientation="h", children=[Label(label=display_text)])
)