feat: power and screenshot launchers

This commit is contained in:
2026-05-03 21:53:10 +02:00
parent 7bdf23001f
commit afcf8d51fe
9 changed files with 159 additions and 45 deletions

View File

@@ -148,6 +148,13 @@
description = "Directory to save recordings into"; description = "Directory to save recordings into";
}; };
}; };
power = {
lock_command = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "waylock" ];
description = "argv for the Lock action in the power menu";
};
};
}; };
}; };
default = { default = {
@@ -170,6 +177,9 @@
enable = false; enable = false;
output_dir = "~/Videos/wl-screenrec"; output_dir = "~/Videos/wl-screenrec";
}; };
power = {
lock_command = [ "waylock" ];
};
}; };
}; };
}; };

View File

@@ -22,6 +22,8 @@ usage() {
usage: sims-cli <command> [args] usage: sims-cli <command> [args]
finder open window finder finder open window finder
apps open application launcher apps open application launcher
power open power menu
screenshot open screenshot menu
notmuch-refresh refresh unread mail count notmuch-refresh refresh unread mail count
screenrec menu open screenrec menu (auto-detects state) screenrec menu open screenrec menu (auto-detects state)
screenrec start-monitor start recording the focused monitor screenrec start-monitor start recording the focused monitor
@@ -35,6 +37,8 @@ EOF
case "${1:-}" in case "${1:-}" in
finder) invoke open-finder ;; finder) invoke open-finder ;;
apps) invoke open-app-launcher ;; apps) invoke open-app-launcher ;;
power) invoke open-power-menu ;;
screenshot) invoke open-screenshot-menu ;;
notmuch-refresh) invoke refresh-notmuch ;; notmuch-refresh) invoke refresh-notmuch ;;
screenrec) screenrec)
case "${2:-}" in case "${2:-}" in

View File

@@ -59,6 +59,9 @@ SCREENREC = app_config.get("screenrec", {
"enable": False, "enable": False,
"output_dir": "~/Videos/wl-screenrec", "output_dir": "~/Videos/wl-screenrec",
}) })
POWER = app_config.get("power", {
"lock_command": ["waylock"],
})
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)

View File

@@ -21,9 +21,11 @@ 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.apps import AppLauncher
from .modules.launcher.power import PowerMenu
from .modules.launcher.screenrec import ScreenrecMenu from .modules.launcher.screenrec import ScreenrecMenu
from .modules.launcher.screenshot import ScreenshotMenu
from .modules.stylix import get_stylix_css_path from .modules.stylix import get_stylix_css_path
from .config import SCREENREC, STYLIX from .config import POWER, SCREENREC, STYLIX
from .services.fenster import get_i3_connection from .services.fenster import get_i3_connection
from .services.screenrec import ScreenrecService from .services.screenrec import ScreenrecService
@@ -34,6 +36,8 @@ i3 = get_i3_connection()
dummy = Window(visible=False) dummy = Window(visible=False)
finder = FuzzyWindowFinder() finder = FuzzyWindowFinder()
app_launcher = AppLauncher() app_launcher = AppLauncher()
power_menu = PowerMenu(lock_command=POWER.get("lock_command", ["waylock"]))
screenshot_menu = ScreenshotMenu()
screenrec_service: ScreenrecService | None = None screenrec_service: ScreenrecService | None = None
screenrec_menu = None screenrec_menu = None
@@ -46,7 +50,7 @@ if SCREENREC.get("enable", False):
bar_windows = [] bar_windows = []
notmuch_widget = None notmuch_widget = None
_app_windows = [dummy, finder, app_launcher] _app_windows = [dummy, finder, app_launcher, power_menu, screenshot_menu]
if screenrec_menu is not None: if screenrec_menu is not None:
_app_windows.append(screenrec_menu) _app_windows.append(screenrec_menu)
app = Application("sims", *_app_windows) app = Application("sims", *_app_windows)
@@ -62,6 +66,16 @@ def open_app_launcher():
app_launcher.show() app_launcher.show()
@Application.action()
def open_power_menu():
power_menu.show()
@Application.action()
def open_screenshot_menu():
screenshot_menu.show()
@Application.action() @Application.action()
def refresh_notmuch(): def refresh_notmuch():
if notmuch_widget is not None: if notmuch_widget is not None:

View File

@@ -1,4 +1,10 @@
from .base import FuzzyMenu, LauncherProvider from .base import FuzzyMenu, LauncherProvider, StaticAction, StaticActionProvider
from .windows import WindowProvider from .windows import WindowProvider
__all__ = ["FuzzyMenu", "LauncherProvider", "WindowProvider"] __all__ = [
"FuzzyMenu",
"LauncherProvider",
"StaticAction",
"StaticActionProvider",
"WindowProvider",
]

View File

@@ -1,7 +1,9 @@
from typing import Any, Protocol from dataclasses import dataclass
from typing import Any, Callable, Protocol
from fabric.widgets.box import Box from fabric.widgets.box import Box
from fabric.widgets.entry import Entry from fabric.widgets.entry import Entry
from fabric.widgets.label import Label
from fabric.widgets.wayland import WaylandWindow as Window from fabric.widgets.wayland import WaylandWindow as Window
from gi.repository import Gdk, Gtk from gi.repository import Gdk, Gtk
@@ -13,6 +15,59 @@ class LauncherProvider(Protocol):
def activate(self, item: Any) -> None: ... def activate(self, item: Any) -> None: ...
@dataclass
class StaticAction:
label: str
handler: Callable[[], None]
class StaticActionProvider:
"""Provider for menus whose items are a fixed list of (label, handler) pairs.
Pass either StaticAction instances or (label, handler) tuples; tuples are
coerced. items_factory lets the list re-evaluate on each open (e.g. for
state-dependent menus) — otherwise the list is captured at construction.
"""
def __init__(
self,
actions: list[StaticAction | tuple[str, Callable[[], None]]] | None = None,
items_factory: Callable[[], list[StaticAction | tuple[str, Callable[[], None]]]] | None = None,
):
if (actions is None) == (items_factory is None):
raise ValueError("pass exactly one of actions or items_factory")
self._static = [_coerce(a) for a in actions] if actions is not None else None
self._factory = items_factory
def items(self) -> list[StaticAction]:
if self._factory is not None:
return [_coerce(a) for a in self._factory()]
return list(self._static or [])
def filter(self, items: list[StaticAction], query: str) -> list[StaticAction]:
if not query:
return items
q = query.lower()
return [i for i in items if q in i.label.lower()]
def render(self, item: StaticAction) -> Gtk.Widget:
return Box(
name="slot-box",
orientation="h",
children=[Label(label=item.label, h_align="start")],
)
def activate(self, item: StaticAction) -> None:
item.handler()
def _coerce(a: StaticAction | tuple[str, Callable[[], None]]) -> StaticAction:
if isinstance(a, StaticAction):
return a
label, handler = a
return StaticAction(label=label, handler=handler)
class FuzzyMenu(Window): class FuzzyMenu(Window):
def __init__( def __init__(
self, self,

View File

@@ -0,0 +1,25 @@
import subprocess
from .base import FuzzyMenu, StaticActionProvider
def _spawn(argv: list[str]) -> None:
subprocess.Popen(argv, start_new_session=True)
def PowerMenu(monitor: int = 0, lock_command: list[str] | None = None) -> FuzzyMenu:
lock = lock_command or ["waylock"]
provider = StaticActionProvider(
actions=[
("⏻ Poweroff", lambda: _spawn(["systemctl", "poweroff"])),
("🔁 Reboot", lambda: _spawn(["systemctl", "reboot"])),
("⏾ Suspend", lambda: _spawn(["systemctl", "suspend"])),
("Lock", lambda: _spawn(lock)),
]
)
return FuzzyMenu(
provider=provider,
monitor=monitor,
placeholder="Power Menu...",
window_name="power-menu",
)

View File

@@ -1,50 +1,24 @@
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 sims.services.screenrec import ScreenrecService
from .base import FuzzyMenu from .base import FuzzyMenu, StaticActionProvider
@dataclass def _idle_actions(service: ScreenrecService):
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 [ return [
_Item("Monitor → Videos", lambda: self._service.start_monitor("videos")), ("Monitor → Videos", lambda: service.start_monitor("videos")),
_Item("Region → Videos", lambda: self._service.start_region("videos")), ("Region → Videos", lambda: service.start_region("videos")),
_Item("Monitor → Clipboard", lambda: self._service.start_monitor("clipboard")), ("Monitor → Clipboard", lambda: service.start_monitor("clipboard")),
_Item("Region → Clipboard", lambda: self._service.start_region("clipboard")), ("Region → Clipboard", lambda: 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: def ScreenrecProvider(service: ScreenrecService) -> StaticActionProvider:
return Box( def items():
name="slot-box", if service.recording:
orientation="h", return [("Stop Recording", service.stop)]
children=[Label(label=item.label, h_align="start")], return _idle_actions(service)
)
def activate(self, item: _Item) -> None: return StaticActionProvider(items_factory=items)
item.handler()
def ScreenrecMenu(service: ScreenrecService, monitor: int = 0) -> FuzzyMenu: def ScreenrecMenu(service: ScreenrecService, monitor: int = 0) -> FuzzyMenu:

View File

@@ -0,0 +1,23 @@
import subprocess
from .base import FuzzyMenu, StaticActionProvider
def _spawn(argv: list[str]) -> None:
subprocess.Popen(argv, start_new_session=True)
def ScreenshotMenu(monitor: int = 0) -> FuzzyMenu:
provider = StaticActionProvider(
actions=[
("Normal", lambda: _spawn(["grimnorm"])),
("To Clipboard", lambda: _spawn(["grim2clip"])),
("To Imv", lambda: _spawn(["grim2imv"])),
]
)
return FuzzyMenu(
provider=provider,
monitor=monitor,
placeholder="Screenshot...",
window_name="screenshot-menu",
)