129 lines
4.4 KiB
Python
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)])
|
|
)
|