From 6470004e37cc1b1fdf193333eacead1a0f819e0c Mon Sep 17 00:00:00 2001 From: dynobo Date: Mon, 22 Apr 2024 15:41:30 +0200 Subject: [PATCH] feat: retrieve window information on KDE --- keyhint/app.py | 2 +- keyhint/context.py | 79 ++++++++++++++++++++++++++++++++-- keyhint/resources/headerbar.ui | 2 +- keyhint/window.py | 9 ++-- 4 files changed, 81 insertions(+), 11 deletions(-) diff --git a/keyhint/app.py b/keyhint/app.py index 41d04d9..80030f9 100644 --- a/keyhint/app.py +++ b/keyhint/app.py @@ -35,7 +35,7 @@ class Application(Adw.Application): def __init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003 """Initialize application with command line options.""" kwargs.update( - application_id="eu.dynobo.keyhint", + application_id="com.github.dynobo.keyhint", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, ) super().__init__( diff --git a/keyhint/context.py b/keyhint/context.py index 3fdc8c0..5ad6e28 100644 --- a/keyhint/context.py +++ b/keyhint/context.py @@ -6,6 +6,9 @@ import re import shutil import subprocess +import tempfile +import textwrap +from datetime import datetime logger = logging.getLogger("keyhint") @@ -63,16 +66,16 @@ def get_kde_version() -> str: Returns: Version string or '(n/a)'. """ - if not shutil.which("plasma-desktop"): + if not shutil.which("plasmashell"): return "(n/a)" try: output = subprocess.check_output( - ["plasma-desktop", "--version"], # noqa: S607 + ["plasmashell", "--version"], # noqa: S607 shell=False, # noqa: S603 text=True, ) - if result := re.search(r"Platform:\s+([\d+\.]+)", output.strip()): + if result := re.search(r"([\d+\.]+)", output.strip()): kde_version = result.groups()[0] except Exception as e: logger.warning("Exception when trying to get kde version from cli %s", e) @@ -124,7 +127,7 @@ def has_window_calls_extension() -> bool: def get_active_window_via_window_calls() -> tuple[str, str]: - """Retrieve active window class and active window title on Wayland. + """Retrieve active window class and active window title on Gnome + Wayland. Inspired by https://gist.github.com/rbreaves/257c3edfa301786e66e964d7ac036269 @@ -165,6 +168,74 @@ def _get_cmd_result(cmd: str) -> str: return wm_class, title +def get_active_window_via_kwin() -> tuple[str, str]: + """Retrieve active window class and active window title on KDE + Wayland. + + Returns: + Tuple(str, str): window class, window title + """ + kwin_script = textwrap.dedent(""" + console.info("keyhint test"); + client = workspace.activeClient; + title = client.caption; + wm_class = client.resourceClass; + console.info(`keyhint_out: wm_class=${wm_class}, window_title=${title}`); + """) + + with tempfile.NamedTemporaryFile(suffix=".js", delete=False) as fh: + fh.write(kwin_script.encode()) + cmd_load = ( + "gdbus call --session --dest org.kde.KWin " + "--object-path /Scripting " + f"--method org.kde.kwin.Scripting.loadScript '{fh.name}'" + ) + logger.debug("cmd_load: %s", cmd_load) + stdout = subprocess.check_output(cmd_load, shell=True).decode() # noqa: S602 + + logger.debug("loadScript output: %s", stdout) + script_id = stdout.strip().strip("()").split(",")[0] + + since = str(datetime.now()) + + cmd_run = ( + "gdbus call --session --dest org.kde.KWin " + f"--object-path /{script_id} " + "--method org.kde.kwin.Script.run" + ) + subprocess.check_output(cmd_run, shell=True) # noqa: S602 + + cmd_unload = ( + "gdbus call --session --dest org.kde.KWin " + "--object-path /Scripting " + f"--method org.kde.kwin.Scripting.unloadScript {script_id}" + ) + subprocess.check_output(cmd_unload, shell=True) # noqa: S602 + + # Unfortunately, we can read script output from stdout, because of a KDE bug: + # https://bugs.kde.org/show_bug.cgi?id=445058 + # The output has to be read through journalctl instead. A timestamp for + # filtering speeds up the process. + log_lines = ( + subprocess.check_output( + f'journalctl --user -o cat --since "{since}"', + shell=True, # noqa: S602 + ) + .decode() + .split("\n") + ) + logger.debug("Journal message: %s", log_lines) + result_line = [m for m in log_lines if "keyhint_out" in m][-1] + match = re.search(r"keyhint_out: wm_class=(.+), window_title=(.+)", result_line) + if match: + wm_class = match.group(1) + title = match.group(2) + else: + logger.warning("Could not extract window info from KWin log!") + wm_class = title = "" + + return wm_class, title + + def get_active_window_via_xprop() -> tuple[str, str]: """Retrieve active window class and active window title on Xorg desktops. diff --git a/keyhint/resources/headerbar.ui b/keyhint/resources/headerbar.ui index 757ed7c..86d1bb6 100644 --- a/keyhint/resources/headerbar.ui +++ b/keyhint/resources/headerbar.ui @@ -168,7 +168,7 @@ - view-pin-symbolic + pin Set to current cheatsheet diff --git a/keyhint/window.py b/keyhint/window.py index d7d2f42..e496c2d 100644 --- a/keyhint/window.py +++ b/keyhint/window.py @@ -86,7 +86,6 @@ def __init__(self, cli_args: dict) -> None: display=self.get_display(), css_file=RESOURCE_PATH / "style.css" ) self.zoom_css_provider = css.new_provider(display=self.get_display()) - self.set_icon_name("keyhint") self.set_titlebar(self.headerbars.normal) self.container.prepend(self.headerbars.fullscreen) @@ -133,7 +132,8 @@ def init_last_active_window_info(self) -> tuple[str, str]: else: self.banner_window_calls.set_revealed(True) logger.error("Window Calls extension not found!") - + case True, "kde": + wm_class, wm_title = context.get_active_window_via_kwin() case False, _: if context.has_xprop(): wm_class, wm_title = context.get_active_window_via_xprop() @@ -591,7 +591,7 @@ def on_create_new_sheet(self, _: Gio.SimpleAction, __: None) -> None: pad = 26 - len(title) template = f"""\ id = "{title}"{" " * pad } # Unique ID, used e.g. in cheatsheet dropdown - url = "" # (Optional) URL to keybinding docs + url = "" # (Optional) URL to keybinding docs [match] regex_wmclass = "{self.wm_class}" @@ -834,6 +834,5 @@ def get_debug_info_text(self) -> str: Wayland: {context.is_using_wayland()} Python: {platform.python_version()} Keyhint: v{__version__} - Flatpak: {context.is_flatpak_package()}\ - """ + Flatpak: {context.is_flatpak_package()}""" )