feat: add app launcher
This commit is contained in:
@@ -13,6 +13,9 @@ notmuch:
|
|||||||
enable: true
|
enable: true
|
||||||
notmuch_path: "notmuch" # or full path like "/home/user/.nix-profile/bin/notmuch"
|
notmuch_path: "notmuch" # or full path like "/home/user/.nix-profile/bin/notmuch"
|
||||||
emacsclient_command: "emacsclient" # or full path like "/home/user/.nix-profile/bin/emacsclient"
|
emacsclient_command: "emacsclient" # or full path like "/home/user/.nix-profile/bin/emacsclient"
|
||||||
|
screenrec:
|
||||||
|
enable: true
|
||||||
|
output_dir: "~/Videos/wl-screenrec"
|
||||||
stylix:
|
stylix:
|
||||||
enable: true
|
enable: true
|
||||||
colors:
|
colors:
|
||||||
|
|||||||
16
flake.nix
16
flake.nix
@@ -136,6 +136,18 @@
|
|||||||
description = "Path to the emacsclient binary";
|
description = "Path to the emacsclient binary";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
screenrec = {
|
||||||
|
enable = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Whether to enable the screenrec widget and menu";
|
||||||
|
};
|
||||||
|
output_dir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "~/Videos/wl-screenrec";
|
||||||
|
description = "Directory to save recordings into";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
default = {
|
default = {
|
||||||
@@ -154,6 +166,10 @@
|
|||||||
notmuch_path = "notmuch";
|
notmuch_path = "notmuch";
|
||||||
emacsclient_command = "emacsclient";
|
emacsclient_command = "emacsclient";
|
||||||
};
|
};
|
||||||
|
screenrec = {
|
||||||
|
enable = false;
|
||||||
|
output_dir = "~/Videos/wl-screenrec";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ usage() {
|
|||||||
cat <<'EOF' >&2
|
cat <<'EOF' >&2
|
||||||
usage: sims-cli <command> [args]
|
usage: sims-cli <command> [args]
|
||||||
finder open window finder
|
finder open window finder
|
||||||
|
apps open application launcher
|
||||||
notmuch-refresh refresh unread mail count
|
notmuch-refresh refresh unread mail count
|
||||||
|
screenrec menu open screenrec menu (auto-detects state)
|
||||||
|
screenrec start-monitor start recording the focused monitor
|
||||||
|
screenrec start-region start recording a slurp-selected region
|
||||||
|
screenrec stop stop active recording
|
||||||
list list registered actions
|
list list registered actions
|
||||||
EOF
|
EOF
|
||||||
exit 2
|
exit 2
|
||||||
@@ -29,7 +34,16 @@ EOF
|
|||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
finder) invoke open-finder ;;
|
finder) invoke open-finder ;;
|
||||||
|
apps) invoke open-app-launcher ;;
|
||||||
notmuch-refresh) invoke refresh-notmuch ;;
|
notmuch-refresh) invoke refresh-notmuch ;;
|
||||||
|
screenrec)
|
||||||
|
case "${2:-}" in
|
||||||
|
menu) invoke open-screenrec-menu ;;
|
||||||
|
start-monitor) invoke screenrec-start-monitor ;;
|
||||||
|
start-region) invoke screenrec-start-region ;;
|
||||||
|
stop) invoke screenrec-stop ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac ;;
|
||||||
list) list_actions ;;
|
list) list_actions ;;
|
||||||
*) usage ;;
|
*) usage ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ WINDOW_TITLE = app_config.get("window_title", {"enable": True})
|
|||||||
STYLIX = app_config.get("stylix", {"enable": False})
|
STYLIX = app_config.get("stylix", {"enable": False})
|
||||||
CALENDAR = app_config.get("calendar", {"enable": True, "khal_path": "khal"})
|
CALENDAR = app_config.get("calendar", {"enable": True, "khal_path": "khal"})
|
||||||
NOTMUCH = app_config.get("notmuch", {"enable": True, "notmuch_path": "notmuch", "emacsclient_command": "emacsclient"})
|
NOTMUCH = app_config.get("notmuch", {"enable": True, "notmuch_path": "notmuch", "emacsclient_command": "emacsclient"})
|
||||||
|
SCREENREC = app_config.get("screenrec", {
|
||||||
|
"enable": False,
|
||||||
|
"output_dir": "~/Videos/wl-screenrec",
|
||||||
|
})
|
||||||
BAR_HEIGHT = app_config.get("height", 40)
|
BAR_HEIGHT = app_config.get("height", 40)
|
||||||
LOG_LEVEL = app_config.get("logLevel", "WARNING")
|
LOG_LEVEL = app_config.get("logLevel", "WARNING")
|
||||||
DEV = app_config.get("dev", False)
|
DEV = app_config.get("dev", False)
|
||||||
|
|||||||
57
sims/main.py
57
sims/main.py
@@ -20,9 +20,12 @@ from fabric.utils import (
|
|||||||
)
|
)
|
||||||
from .modules.bar import StatusBar
|
from .modules.bar import StatusBar
|
||||||
from .modules.window_fuzzy import FuzzyWindowFinder
|
from .modules.window_fuzzy import FuzzyWindowFinder
|
||||||
|
from .modules.launcher.apps import AppLauncher
|
||||||
|
from .modules.launcher.screenrec import ScreenrecMenu
|
||||||
from .modules.stylix import get_stylix_css_path
|
from .modules.stylix import get_stylix_css_path
|
||||||
from .config import STYLIX
|
from .config import SCREENREC, STYLIX
|
||||||
from .services.fenster import get_i3_connection
|
from .services.fenster import get_i3_connection
|
||||||
|
from .services.screenrec import ScreenrecService
|
||||||
|
|
||||||
|
|
||||||
tray = SystemTray(name="system-tray", spacing=4)
|
tray = SystemTray(name="system-tray", spacing=4)
|
||||||
@@ -30,11 +33,23 @@ i3 = get_i3_connection()
|
|||||||
|
|
||||||
dummy = Window(visible=False)
|
dummy = Window(visible=False)
|
||||||
finder = FuzzyWindowFinder()
|
finder = FuzzyWindowFinder()
|
||||||
|
app_launcher = AppLauncher()
|
||||||
|
|
||||||
|
screenrec_service: ScreenrecService | None = None
|
||||||
|
screenrec_menu = None
|
||||||
|
if SCREENREC.get("enable", False):
|
||||||
|
screenrec_service = ScreenrecService(
|
||||||
|
output_dir=SCREENREC.get("output_dir", "~/Videos/wl-screenrec")
|
||||||
|
)
|
||||||
|
screenrec_menu = ScreenrecMenu(screenrec_service)
|
||||||
|
|
||||||
bar_windows = []
|
bar_windows = []
|
||||||
notmuch_widget = None
|
notmuch_widget = None
|
||||||
|
|
||||||
app = Application("sims", dummy, finder)
|
_app_windows = [dummy, finder, app_launcher]
|
||||||
|
if screenrec_menu is not None:
|
||||||
|
_app_windows.append(screenrec_menu)
|
||||||
|
app = Application("sims", *_app_windows)
|
||||||
|
|
||||||
|
|
||||||
@Application.action()
|
@Application.action()
|
||||||
@@ -42,19 +57,46 @@ def open_finder():
|
|||||||
finder.show()
|
finder.show()
|
||||||
|
|
||||||
|
|
||||||
|
@Application.action()
|
||||||
|
def open_app_launcher():
|
||||||
|
app_launcher.show()
|
||||||
|
|
||||||
|
|
||||||
@Application.action()
|
@Application.action()
|
||||||
def refresh_notmuch():
|
def refresh_notmuch():
|
||||||
if notmuch_widget is not None:
|
if notmuch_widget is not None:
|
||||||
notmuch_widget.service.update_unread_count()
|
notmuch_widget.service.update_unread_count()
|
||||||
|
|
||||||
|
|
||||||
|
@Application.action()
|
||||||
|
def open_screenrec_menu():
|
||||||
|
if screenrec_menu is not None:
|
||||||
|
screenrec_menu.show()
|
||||||
|
|
||||||
|
|
||||||
|
@Application.action()
|
||||||
|
def screenrec_start_monitor():
|
||||||
|
if screenrec_service is not None:
|
||||||
|
screenrec_service.start_monitor("videos")
|
||||||
|
|
||||||
|
|
||||||
|
@Application.action()
|
||||||
|
def screenrec_start_region():
|
||||||
|
if screenrec_service is not None:
|
||||||
|
screenrec_service.start_region("videos")
|
||||||
|
|
||||||
|
|
||||||
|
@Application.action()
|
||||||
|
def screenrec_stop():
|
||||||
|
if screenrec_service is not None:
|
||||||
|
screenrec_service.stop()
|
||||||
|
|
||||||
# Load CSS - use Stylix if enabled, otherwise use default
|
# Load CSS - use Stylix if enabled, otherwise use default
|
||||||
if STYLIX.get("enable", False):
|
if STYLIX.get("enable", False):
|
||||||
stylix_css_path = get_stylix_css_path()
|
stylix_css_path = get_stylix_css_path()
|
||||||
if stylix_css_path:
|
if stylix_css_path:
|
||||||
logger.info("[Bar] Using Stylix CSS")
|
logger.info("[Bar] Using Stylix CSS")
|
||||||
# Load base styles first for structure
|
|
||||||
app.set_stylesheet_from_file(get_relative_path("styles/main.css"))
|
app.set_stylesheet_from_file(get_relative_path("styles/main.css"))
|
||||||
# Then apply Stylix theme colors
|
|
||||||
app.set_stylesheet_from_file(stylix_css_path)
|
app.set_stylesheet_from_file(stylix_css_path)
|
||||||
else:
|
else:
|
||||||
logger.warning("[Bar] Stylix enabled but CSS generation failed, falling back to default")
|
logger.warning("[Bar] Stylix enabled but CSS generation failed, falling back to default")
|
||||||
@@ -81,7 +123,12 @@ def spawn_bars():
|
|||||||
|
|
||||||
for i, output in enumerate(outputs):
|
for i, output in enumerate(outputs):
|
||||||
output_name = output.get("name", f"Unknown-{i}")
|
output_name = output.get("name", f"Unknown-{i}")
|
||||||
bar = StatusBar(display=output_name, tray=tray if i == 0 else None, monitor=i)
|
bar = StatusBar(
|
||||||
|
display=output_name,
|
||||||
|
tray=tray if i == 0 else None,
|
||||||
|
monitor=i,
|
||||||
|
screenrec_service=screenrec_service if i == 0 else None,
|
||||||
|
)
|
||||||
bar_windows.append(bar)
|
bar_windows.append(bar)
|
||||||
if i == 0 and bar.notmuch:
|
if i == 0 and bar.notmuch:
|
||||||
notmuch_widget = bar.notmuch
|
notmuch_widget = bar.notmuch
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ from sims.modules.quick_menu import QuickMenuOpener
|
|||||||
from sims.modules.battery import Battery
|
from sims.modules.battery import Battery
|
||||||
from sims.modules.calendar import CalendarService, CalendarPopup
|
from sims.modules.calendar import CalendarService, CalendarPopup
|
||||||
from sims.modules.notmuch import NotmuchWidget
|
from sims.modules.notmuch import NotmuchWidget
|
||||||
|
from sims.modules.screenrec import ScreenrecWidget
|
||||||
from fabric.widgets.wayland import WaylandWindow as Window
|
from fabric.widgets.wayland import WaylandWindow as Window
|
||||||
from fabric.system_tray.widgets import SystemTray
|
from fabric.system_tray.widgets import SystemTray
|
||||||
from sims.widgets.fenster import FensterWorkspaces, FensterWorkspaceButton, FensterActiveWindow
|
from sims.widgets.fenster import FensterWorkspaces, FensterWorkspaceButton, FensterActiveWindow
|
||||||
from sims.services.fenster import get_i3_connection
|
from sims.services.fenster import get_i3_connection
|
||||||
|
from sims.services.screenrec import ScreenrecService
|
||||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||||
from sims.services.system_stats import SystemStatsService
|
from sims.services.system_stats import SystemStatsService
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ class StatusBar(Window):
|
|||||||
display: str,
|
display: str,
|
||||||
tray: SystemTray | None = None,
|
tray: SystemTray | None = None,
|
||||||
monitor: int = 1,
|
monitor: int = 1,
|
||||||
|
screenrec_service: ScreenrecService | None = None,
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name="sims",
|
name="sims",
|
||||||
@@ -102,6 +105,10 @@ class StatusBar(Window):
|
|||||||
if NOTMUCH["enable"]:
|
if NOTMUCH["enable"]:
|
||||||
self.notmuch = NotmuchWidget()
|
self.notmuch = NotmuchWidget()
|
||||||
|
|
||||||
|
self.screenrec = None
|
||||||
|
if screenrec_service is not None:
|
||||||
|
self.screenrec = ScreenrecWidget(screenrec_service)
|
||||||
|
|
||||||
self.status_container = Box(
|
self.status_container = Box(
|
||||||
name="widgets-container",
|
name="widgets-container",
|
||||||
spacing=4,
|
spacing=4,
|
||||||
@@ -121,6 +128,9 @@ class StatusBar(Window):
|
|||||||
if self.notmuch:
|
if self.notmuch:
|
||||||
end_container_children.append(self.notmuch)
|
end_container_children.append(self.notmuch)
|
||||||
|
|
||||||
|
if self.screenrec:
|
||||||
|
end_container_children.append(self.screenrec)
|
||||||
|
|
||||||
# Add quick menu button next to time
|
# Add quick menu button next to time
|
||||||
end_container_children.append(self.quick_menu)
|
end_container_children.append(self.quick_menu)
|
||||||
end_container_children.append(self.date_time)
|
end_container_children.append(self.date_time)
|
||||||
|
|||||||
59
sims/modules/launcher/apps.py
Normal file
59
sims/modules/launcher/apps.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
from fabric.utils.helpers import DesktopApp, get_desktop_applications
|
||||||
|
from fabric.widgets.box import Box
|
||||||
|
from fabric.widgets.image import Image
|
||||||
|
from fabric.widgets.label import Label
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
from .base import FuzzyMenu
|
||||||
|
|
||||||
|
|
||||||
|
ICON_SIZE = 32
|
||||||
|
|
||||||
|
|
||||||
|
class AppProvider:
|
||||||
|
def items(self) -> list[DesktopApp]:
|
||||||
|
return get_desktop_applications()
|
||||||
|
|
||||||
|
def filter(self, items: list[DesktopApp], query: str) -> list[DesktopApp]:
|
||||||
|
if not query:
|
||||||
|
return items
|
||||||
|
q = query.lower()
|
||||||
|
return [a for a in items if _matches(a, q)]
|
||||||
|
|
||||||
|
def render(self, item: DesktopApp) -> Gtk.Widget:
|
||||||
|
children: list[Gtk.Widget] = []
|
||||||
|
|
||||||
|
pixbuf = item.get_icon_pixbuf(size=ICON_SIZE)
|
||||||
|
if pixbuf is not None:
|
||||||
|
children.append(Image(pixbuf=pixbuf, name="app-icon"))
|
||||||
|
|
||||||
|
primary = item.display_name or item.name or ""
|
||||||
|
text_box = Box(name="app-text", orientation="v", spacing=0)
|
||||||
|
text_box.add(Label(label=primary, name="app-name", h_align="start"))
|
||||||
|
if item.generic_name and item.generic_name != primary:
|
||||||
|
text_box.add(
|
||||||
|
Label(label=item.generic_name, name="app-generic", h_align="start")
|
||||||
|
)
|
||||||
|
children.append(text_box)
|
||||||
|
|
||||||
|
return Box(name="slot-box", orientation="h", spacing=10, children=children)
|
||||||
|
|
||||||
|
def activate(self, item: DesktopApp) -> None:
|
||||||
|
item.launch()
|
||||||
|
|
||||||
|
|
||||||
|
def _matches(app: DesktopApp, q: str) -> bool:
|
||||||
|
for field in (app.name, app.display_name, app.generic_name, app.executable):
|
||||||
|
if field and q in field.lower():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def AppLauncher(monitor: int = 0) -> FuzzyMenu:
|
||||||
|
return FuzzyMenu(
|
||||||
|
provider=AppProvider(),
|
||||||
|
monitor=monitor,
|
||||||
|
placeholder="Search Apps...",
|
||||||
|
window_name="app-launcher",
|
||||||
|
max_results=8,
|
||||||
|
)
|
||||||
@@ -19,9 +19,12 @@ class FuzzyMenu(Window):
|
|||||||
provider: LauncherProvider,
|
provider: LauncherProvider,
|
||||||
monitor: int = 0,
|
monitor: int = 0,
|
||||||
placeholder: str = "Search...",
|
placeholder: str = "Search...",
|
||||||
|
window_name: str = "finder",
|
||||||
|
max_results: int | None = None,
|
||||||
):
|
):
|
||||||
|
self._max_results = max_results
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name="finder",
|
name=window_name,
|
||||||
anchor="center",
|
anchor="center",
|
||||||
monitor=monitor,
|
monitor=monitor,
|
||||||
keyboard_mode="on-demand",
|
keyboard_mode="on-demand",
|
||||||
@@ -41,8 +44,6 @@ class FuzzyMenu(Window):
|
|||||||
h_expand=True,
|
h_expand=True,
|
||||||
editable=True,
|
editable=True,
|
||||||
notify_text=self._on_text_changed,
|
notify_text=self._on_text_changed,
|
||||||
on_activate=lambda *_: self._activate_selected(),
|
|
||||||
on_key_press_event=self._on_key_press,
|
|
||||||
)
|
)
|
||||||
self.picker_box = Box(
|
self.picker_box = Box(
|
||||||
name="picker-box",
|
name="picker-box",
|
||||||
@@ -52,6 +53,7 @@ class FuzzyMenu(Window):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.add(self.picker_box)
|
self.add(self.picker_box)
|
||||||
|
self.connect("key-press-event", self._on_key_press)
|
||||||
self._refresh_viewport("")
|
self._refresh_viewport("")
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
@@ -60,29 +62,40 @@ class FuzzyMenu(Window):
|
|||||||
self._selected_index = 0
|
self._selected_index = 0
|
||||||
self._refresh_viewport("")
|
self._refresh_viewport("")
|
||||||
super().show()
|
super().show()
|
||||||
|
self.search_entry.grab_focus()
|
||||||
|
|
||||||
def _on_text_changed(self, entry, *_):
|
def _on_text_changed(self, entry, *_):
|
||||||
self._selected_index = 0
|
self._selected_index = 0
|
||||||
self._refresh_viewport(entry.get_text())
|
self._refresh_viewport(entry.get_text())
|
||||||
|
|
||||||
def _on_key_press(self, _widget, event):
|
def _on_key_press(self, _widget, event):
|
||||||
if event.keyval == Gdk.KEY_Escape:
|
ctrl = bool(event.state & Gdk.ModifierType.CONTROL_MASK)
|
||||||
|
keyval = event.keyval
|
||||||
|
|
||||||
|
if keyval == Gdk.KEY_Escape:
|
||||||
self.hide()
|
self.hide()
|
||||||
return True
|
return True
|
||||||
if event.keyval == Gdk.KEY_Down:
|
if keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
|
||||||
if self._filtered:
|
self._activate_selected()
|
||||||
self._selected_index = (self._selected_index + 1) % len(self._filtered)
|
|
||||||
self._update_selection_highlight()
|
|
||||||
return True
|
return True
|
||||||
if event.keyval == Gdk.KEY_Up:
|
if keyval == Gdk.KEY_Down or (ctrl and keyval in (Gdk.KEY_n, Gdk.KEY_N)):
|
||||||
if self._filtered:
|
self._move_selection(1)
|
||||||
self._selected_index = (self._selected_index - 1) % len(self._filtered)
|
return True
|
||||||
self._update_selection_highlight()
|
if keyval == Gdk.KEY_Up or (ctrl and keyval in (Gdk.KEY_p, Gdk.KEY_P)):
|
||||||
|
self._move_selection(-1)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _move_selection(self, delta: int):
|
||||||
|
if not self._filtered:
|
||||||
|
return
|
||||||
|
self._selected_index = (self._selected_index + delta) % len(self._filtered)
|
||||||
|
self._update_selection_highlight()
|
||||||
|
|
||||||
def _refresh_viewport(self, query: str):
|
def _refresh_viewport(self, query: str):
|
||||||
self._filtered = self._provider.filter(self._items, query)
|
self._filtered = self._provider.filter(self._items, query)
|
||||||
|
if self._max_results is not None:
|
||||||
|
self._filtered = self._filtered[: self._max_results]
|
||||||
if self._selected_index >= len(self._filtered):
|
if self._selected_index >= len(self._filtered):
|
||||||
self._selected_index = 0
|
self._selected_index = 0
|
||||||
self.viewport.children = []
|
self.viewport.children = []
|
||||||
|
|||||||
56
sims/modules/launcher/screenrec.py
Normal file
56
sims/modules/launcher/screenrec.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from fabric.widgets.box import Box
|
||||||
|
from fabric.widgets.label import Label
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
from sims.services.screenrec import ScreenrecService
|
||||||
|
|
||||||
|
from .base import FuzzyMenu
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _Item:
|
||||||
|
label: str
|
||||||
|
handler: Callable[[], None]
|
||||||
|
|
||||||
|
|
||||||
|
class ScreenrecProvider:
|
||||||
|
def __init__(self, service: ScreenrecService):
|
||||||
|
self._service = service
|
||||||
|
|
||||||
|
def items(self) -> list[_Item]:
|
||||||
|
if self._service.recording:
|
||||||
|
return [_Item("Stop Recording", self._service.stop)]
|
||||||
|
return [
|
||||||
|
_Item("Monitor → Videos", lambda: self._service.start_monitor("videos")),
|
||||||
|
_Item("Region → Videos", lambda: self._service.start_region("videos")),
|
||||||
|
_Item("Monitor → Clipboard", lambda: self._service.start_monitor("clipboard")),
|
||||||
|
_Item("Region → Clipboard", lambda: self._service.start_region("clipboard")),
|
||||||
|
]
|
||||||
|
|
||||||
|
def filter(self, items: list[_Item], query: str) -> list[_Item]:
|
||||||
|
if not query:
|
||||||
|
return items
|
||||||
|
q = query.lower()
|
||||||
|
return [i for i in items if q in i.label.lower()]
|
||||||
|
|
||||||
|
def render(self, item: _Item) -> Gtk.Widget:
|
||||||
|
return Box(
|
||||||
|
name="slot-box",
|
||||||
|
orientation="h",
|
||||||
|
children=[Label(label=item.label, h_align="start")],
|
||||||
|
)
|
||||||
|
|
||||||
|
def activate(self, item: _Item) -> None:
|
||||||
|
item.handler()
|
||||||
|
|
||||||
|
|
||||||
|
def ScreenrecMenu(service: ScreenrecService, monitor: int = 0) -> FuzzyMenu:
|
||||||
|
return FuzzyMenu(
|
||||||
|
provider=ScreenrecProvider(service),
|
||||||
|
monitor=monitor,
|
||||||
|
placeholder="Screen Recording...",
|
||||||
|
window_name="screenrec-menu",
|
||||||
|
)
|
||||||
@@ -19,16 +19,16 @@ class WindowProvider:
|
|||||||
if con.get("type") == "con":
|
if con.get("type") == "con":
|
||||||
windows.append({
|
windows.append({
|
||||||
"id": con.get("id"),
|
"id": con.get("id"),
|
||||||
"app_id": con.get("app_id", ""),
|
"app_id": con.get("app_id") or "",
|
||||||
"title": con.get("name", ""),
|
"title": con.get("name") or "",
|
||||||
"workspace": ws_num,
|
"workspace": ws_num,
|
||||||
})
|
})
|
||||||
for con in ws_node.get("floating_nodes", []):
|
for con in ws_node.get("floating_nodes", []):
|
||||||
if con.get("type") == "con":
|
if con.get("type") == "con":
|
||||||
windows.append({
|
windows.append({
|
||||||
"id": con.get("id"),
|
"id": con.get("id"),
|
||||||
"app_id": con.get("app_id", ""),
|
"app_id": con.get("app_id") or "",
|
||||||
"title": con.get("name", ""),
|
"title": con.get("name") or "",
|
||||||
"workspace": ws_num,
|
"workspace": ws_num,
|
||||||
})
|
})
|
||||||
return windows
|
return windows
|
||||||
|
|||||||
58
sims/modules/screenrec.py
Normal file
58
sims/modules/screenrec.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from fabric.utils import invoke_repeater
|
||||||
|
from fabric.widgets.box import Box
|
||||||
|
from fabric.widgets.button import Button
|
||||||
|
from fabric.widgets.label import Label
|
||||||
|
from gi.repository import GLib
|
||||||
|
|
||||||
|
from sims.services.screenrec import ScreenrecService
|
||||||
|
|
||||||
|
|
||||||
|
class ScreenrecWidget(Button):
|
||||||
|
def __init__(self, service: ScreenrecService, **kwargs):
|
||||||
|
self._service = service
|
||||||
|
self._timer_id: int | None = None
|
||||||
|
|
||||||
|
self._dot = Label(name="screenrec-dot", label="●")
|
||||||
|
self._elapsed = Label(name="screenrec-elapsed", label="00:00")
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
name="screenrec-widget",
|
||||||
|
child=Box(
|
||||||
|
orientation="h",
|
||||||
|
spacing=6,
|
||||||
|
children=[self._dot, self._elapsed],
|
||||||
|
),
|
||||||
|
on_clicked=lambda *_: self._service.stop(),
|
||||||
|
visible=False,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._service.connect("recording-changed", self._on_recording_changed)
|
||||||
|
if self._service.recording:
|
||||||
|
self._on_recording_changed(self._service, True)
|
||||||
|
|
||||||
|
def _on_recording_changed(self, _service, recording: bool):
|
||||||
|
if recording:
|
||||||
|
self._update_elapsed()
|
||||||
|
self.set_visible(True)
|
||||||
|
if self._timer_id is None:
|
||||||
|
self._timer_id = invoke_repeater(1000, self._update_elapsed)
|
||||||
|
else:
|
||||||
|
self.set_visible(False)
|
||||||
|
if self._timer_id is not None:
|
||||||
|
try:
|
||||||
|
GLib.source_remove(self._timer_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._timer_id = None
|
||||||
|
|
||||||
|
def _update_elapsed(self) -> bool:
|
||||||
|
started = self._service.started_at
|
||||||
|
if started is None:
|
||||||
|
self._elapsed.set_text("00:00")
|
||||||
|
return True
|
||||||
|
secs = int(time.monotonic() - started)
|
||||||
|
self._elapsed.set_text(f"{secs // 60:02d}:{secs % 60:02d}")
|
||||||
|
return True
|
||||||
@@ -361,6 +361,64 @@ tooltip>* {{
|
|||||||
.toggle-inactive {{
|
.toggle-inactive {{
|
||||||
background-color: #{colors["base02"]};
|
background-color: #{colors["base02"]};
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
/* Launcher (FuzzyMenu — finder, app launcher, screenrec menu) */
|
||||||
|
#picker-box {{
|
||||||
|
background-color: #{colors["base00"]};
|
||||||
|
border: solid 1px #{colors["base02"]};
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
#search-entry {{
|
||||||
|
background-color: #{colors["base01"]};
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
border: solid 1px #{colors["base02"]};
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#viewport {{
|
||||||
|
background-color: #{colors["base00"]};
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px;
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
#viewport > * {{
|
||||||
|
background-color: #{colors["base01"]};
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
transition: background-color 80ms ease, border-color 80ms ease;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#viewport > *.selected {{
|
||||||
|
background-color: alpha(#{colors["base0D"]}, 0.28);
|
||||||
|
border-left-color: #{colors["base0D"]};
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
font-weight: 600;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#viewport > *.selected label {{
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
}}
|
||||||
|
|
||||||
|
#app-icon {{
|
||||||
|
margin-right: 4px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#app-name {{
|
||||||
|
color: #{colors["base05"]};
|
||||||
|
font-weight: 500;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#app-generic {{
|
||||||
|
color: #{colors["base04"]};
|
||||||
|
font-size: {small_font}px;
|
||||||
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Write to temporary file
|
# Write to temporary file
|
||||||
|
|||||||
230
sims/services/screenrec.py
Normal file
230
sims/services/screenrec.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
"""wl-screenrec process manager.
|
||||||
|
|
||||||
|
Owns the wl-screenrec subprocess so the bar reflects real recording state
|
||||||
|
without polling. Survives bar restarts via orphan adoption.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from fabric.core.service import Service, Signal
|
||||||
|
from fabric.i3 import I3, I3MessageType
|
||||||
|
from gi.repository import GLib
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
Destination = Literal["videos", "clipboard"]
|
||||||
|
|
||||||
|
|
||||||
|
class ScreenrecService(Service):
|
||||||
|
@Signal
|
||||||
|
def recording_changed(self, recording: bool) -> None: ...
|
||||||
|
|
||||||
|
def __init__(self, output_dir: str = "~/Videos/wl-screenrec"):
|
||||||
|
super().__init__()
|
||||||
|
self._output_dir = os.path.expanduser(output_dir)
|
||||||
|
self._proc: subprocess.Popen | None = None
|
||||||
|
self._adopted_pid: int | None = None
|
||||||
|
self._started_at: float | None = None
|
||||||
|
self._output_path: str | None = None
|
||||||
|
self._destination: Destination | None = None
|
||||||
|
self._watch_id: int | None = None
|
||||||
|
|
||||||
|
self._adopt_orphan_if_running()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recording(self) -> bool:
|
||||||
|
return self._proc is not None or self._adopted_pid is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def started_at(self) -> float | None:
|
||||||
|
return self._started_at
|
||||||
|
|
||||||
|
def start_monitor(self, dest: Destination = "videos", output: str | None = None) -> None:
|
||||||
|
if self.recording:
|
||||||
|
logger.warning("[Screenrec] start_monitor: already recording")
|
||||||
|
return
|
||||||
|
path = self._make_output_path(dest)
|
||||||
|
|
||||||
|
# Prefer an explicit output name when the caller passes one, but fenster's
|
||||||
|
# IPC reports synthetic names ("Unknown-XXXX") that wl-screenrec can't
|
||||||
|
# resolve. Fall back to the focused output's geometry, which works
|
||||||
|
# across i3/sway/fenster.
|
||||||
|
if output and not output.startswith("Unknown-"):
|
||||||
|
self._spawn(["wl-screenrec", "-o", output, "-f", path], path, dest)
|
||||||
|
return
|
||||||
|
|
||||||
|
geom = self._focused_output_geometry()
|
||||||
|
if not geom:
|
||||||
|
logger.error("[Screenrec] no focused output found")
|
||||||
|
return
|
||||||
|
self._spawn(["wl-screenrec", "-g", geom, "-f", path], path, dest)
|
||||||
|
|
||||||
|
def start_region(self, dest: Destination = "videos") -> None:
|
||||||
|
if self.recording:
|
||||||
|
logger.warning("[Screenrec] start_region: already recording")
|
||||||
|
return
|
||||||
|
geom = self._slurp_region()
|
||||||
|
if not geom:
|
||||||
|
logger.info("[Screenrec] region selection cancelled")
|
||||||
|
return
|
||||||
|
path = self._make_output_path(dest)
|
||||||
|
self._spawn(["wl-screenrec", "-g", geom, "-f", path], path, dest)
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
if self._proc is not None:
|
||||||
|
logger.info("[Screenrec] sending SIGINT to wl-screenrec")
|
||||||
|
try:
|
||||||
|
self._proc.send_signal(signal.SIGINT)
|
||||||
|
except ProcessLookupError:
|
||||||
|
self._on_exit(self._proc.pid, 0)
|
||||||
|
return
|
||||||
|
if self._adopted_pid is not None:
|
||||||
|
logger.info(f"[Screenrec] sending SIGINT to adopted pid {self._adopted_pid}")
|
||||||
|
try:
|
||||||
|
os.kill(self._adopted_pid, signal.SIGINT)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
self._poll_adopted_until_gone()
|
||||||
|
|
||||||
|
def _spawn(self, argv: list[str], path: str, dest: Destination) -> None:
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
logger.info(f"[Screenrec] spawning: {' '.join(argv)}")
|
||||||
|
try:
|
||||||
|
self._proc = subprocess.Popen(
|
||||||
|
argv,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True,
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error("[Screenrec] wl-screenrec not found on PATH")
|
||||||
|
return
|
||||||
|
self._output_path = path
|
||||||
|
self._destination = dest
|
||||||
|
self._started_at = time.monotonic()
|
||||||
|
self._watch_id = GLib.child_watch_add(
|
||||||
|
GLib.PRIORITY_DEFAULT, self._proc.pid, self._on_exit
|
||||||
|
)
|
||||||
|
self.recording_changed(True)
|
||||||
|
|
||||||
|
def _on_exit(self, pid: int, status: int) -> None:
|
||||||
|
logger.info(f"[Screenrec] wl-screenrec exited (pid={pid} status={status})")
|
||||||
|
path = self._output_path
|
||||||
|
dest = self._destination
|
||||||
|
self._reset_state()
|
||||||
|
if dest == "clipboard" and path and os.path.exists(path):
|
||||||
|
self._copy_to_clipboard(path)
|
||||||
|
self.recording_changed(False)
|
||||||
|
|
||||||
|
def _reset_state(self) -> None:
|
||||||
|
self._proc = None
|
||||||
|
self._adopted_pid = None
|
||||||
|
self._started_at = None
|
||||||
|
self._output_path = None
|
||||||
|
self._destination = None
|
||||||
|
if self._watch_id is not None:
|
||||||
|
try:
|
||||||
|
GLib.source_remove(self._watch_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._watch_id = None
|
||||||
|
|
||||||
|
def _copy_to_clipboard(self, path: str) -> None:
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
subprocess.Popen(
|
||||||
|
["wl-copy", "-t", "video/mp4"],
|
||||||
|
stdin=f,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True,
|
||||||
|
)
|
||||||
|
logger.info(f"[Screenrec] copied {path} to clipboard as video/mp4")
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error("[Screenrec] wl-copy not found on PATH")
|
||||||
|
|
||||||
|
def _slurp_region(self) -> str | None:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["slurp"], capture_output=True, text=True, check=False
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error("[Screenrec] slurp not found on PATH")
|
||||||
|
return None
|
||||||
|
geom = result.stdout.strip()
|
||||||
|
return geom or None
|
||||||
|
|
||||||
|
def _focused_output(self) -> str | None:
|
||||||
|
reply = I3.send_command("", I3MessageType.GET_WORKSPACES)
|
||||||
|
if not (reply.is_ok and isinstance(reply.reply, list)):
|
||||||
|
return None
|
||||||
|
for ws in reply.reply:
|
||||||
|
if ws.get("focused"):
|
||||||
|
return ws.get("output")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _focused_output_geometry(self) -> str | None:
|
||||||
|
"""Return the focused output's geometry as 'X,Y WxH' (slurp format).
|
||||||
|
|
||||||
|
Used as a portable fallback when the IPC's output names aren't real
|
||||||
|
wl_output names (e.g. fenster reports 'Unknown-XXXX').
|
||||||
|
"""
|
||||||
|
reply = I3.send_command("", I3MessageType.GET_WORKSPACES)
|
||||||
|
if not (reply.is_ok and isinstance(reply.reply, list)):
|
||||||
|
return None
|
||||||
|
for ws in reply.reply:
|
||||||
|
if ws.get("focused"):
|
||||||
|
rect = ws.get("rect") or {}
|
||||||
|
x = rect.get("x")
|
||||||
|
y = rect.get("y")
|
||||||
|
w = rect.get("width")
|
||||||
|
h = rect.get("height")
|
||||||
|
if None in (x, y, w, h) or w <= 0 or h <= 0:
|
||||||
|
return None
|
||||||
|
return f"{x},{y} {w}x{h}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _make_output_path(self, dest: Destination) -> str:
|
||||||
|
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
|
if dest == "clipboard":
|
||||||
|
tmp = os.path.join(GLib.get_tmp_dir(), f"sims-screenrec-{stamp}.mp4")
|
||||||
|
return tmp
|
||||||
|
return os.path.join(self._output_dir, f"{stamp}.mp4")
|
||||||
|
|
||||||
|
def _adopt_orphan_if_running(self) -> None:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["pgrep", "-x", "wl-screenrec"], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return
|
||||||
|
pid_str = result.stdout.strip().split("\n")[0] if result.stdout.strip() else ""
|
||||||
|
if not pid_str:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._adopted_pid = int(pid_str)
|
||||||
|
except ValueError:
|
||||||
|
return
|
||||||
|
logger.info(f"[Screenrec] adopted orphan wl-screenrec pid={self._adopted_pid}")
|
||||||
|
self._started_at = time.monotonic()
|
||||||
|
self.recording_changed(True)
|
||||||
|
|
||||||
|
def _poll_adopted_until_gone(self) -> None:
|
||||||
|
def check() -> bool:
|
||||||
|
if self._adopted_pid is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
os.kill(self._adopted_pid, 0)
|
||||||
|
except ProcessLookupError:
|
||||||
|
logger.info("[Screenrec] adopted wl-screenrec finished")
|
||||||
|
self._reset_state()
|
||||||
|
self.recording_changed(False)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
GLib.timeout_add(250, check)
|
||||||
@@ -60,6 +60,24 @@
|
|||||||
color: var(--blue);
|
color: var(--blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#screenrec-widget {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0 6px;
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#screenrec-dot {
|
||||||
|
color: #ff4444;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#screenrec-elapsed {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
tooltip {
|
tooltip {
|
||||||
border: solid 2px;
|
border: solid 2px;
|
||||||
border-color: var(--border-color);
|
border-color: var(--border-color);
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
#picker-box {
|
|
||||||
padding: 12px;
|
|
||||||
background-color: rgba(40, 40, 40, 0.95); /* darker for contrast */
|
|
||||||
border-radius: 8px;
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#viewport {
|
|
||||||
padding: 8px;
|
|
||||||
background-color: rgba(30, 30, 30, 0.9); /* dark background for contrast */
|
|
||||||
border-radius: 6px;
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
color: white; /* ensure contrast */
|
|
||||||
}
|
|
||||||
|
|
||||||
#viewport > * {
|
|
||||||
padding: 6px 10px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
#viewport > *.selected {
|
|
||||||
background-color: rgba(255, 255, 255, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
#viewport:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.15); /* hover feedback */
|
|
||||||
}
|
|
||||||
55
sims/styles/launcher.css
Normal file
55
sims/styles/launcher.css
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/* Shared styles for the fuzzy-menu launcher (window finder, app launcher,
|
||||||
|
screenrec menu, and any future provider built on FuzzyMenu).
|
||||||
|
Element IDs come from sims/modules/launcher/base.py. */
|
||||||
|
|
||||||
|
#picker-box {
|
||||||
|
padding: 12px;
|
||||||
|
background-color: rgba(40, 40, 40, 0.95);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewport {
|
||||||
|
padding: 8px;
|
||||||
|
background-color: rgba(30, 30, 30, 0.9);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewport > * {
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.04);
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
transition: background-color 80ms ease, border-color 80ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewport > *.selected {
|
||||||
|
background-color: rgba(137, 180, 250, 0.28);
|
||||||
|
border-left-color: #89b4fa;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewport > *.selected label {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Provider-specific tweaks (apps launcher) */
|
||||||
|
#app-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app-name {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app-generic {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
@import url("./menu.css");
|
@import url("./menu.css");
|
||||||
@import url("./vinyl.css");
|
@import url("./vinyl.css");
|
||||||
@import url("./bar.css");
|
@import url("./bar.css");
|
||||||
@import url("./finder.css");
|
@import url("./launcher.css");
|
||||||
@import url("./calendar.css");
|
@import url("./calendar.css");
|
||||||
@import url("./notmuch.css");
|
@import url("./notmuch.css");
|
||||||
|
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ class FensterActiveWindow(Label):
|
|||||||
if tree_reply.is_ok and isinstance(tree_reply.reply, dict):
|
if tree_reply.is_ok and isinstance(tree_reply.reply, dict):
|
||||||
focused = self._find_focused(tree_reply.reply)
|
focused = self._find_focused(tree_reply.reply)
|
||||||
if focused:
|
if focused:
|
||||||
self._set_title(focused.get("name", ""))
|
self._set_title(focused.get("name") or "")
|
||||||
return
|
return
|
||||||
self.set_label("")
|
self.set_label("")
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ class FensterActiveWindow(Label):
|
|||||||
|
|
||||||
def _on_window_event(self, _, event: I3Event):
|
def _on_window_event(self, _, event: I3Event):
|
||||||
container = event.data.get("container", {})
|
container = event.data.get("container", {})
|
||||||
self._set_title(container.get("name", ""))
|
self._set_title(container.get("name") or "")
|
||||||
|
|
||||||
def _on_window_close(self, _, event: I3Event):
|
def _on_window_close(self, _, event: I3Event):
|
||||||
self._initialize()
|
self._initialize()
|
||||||
|
|||||||
Reference in New Issue
Block a user