diff --git a/keyhint/context.py b/keyhint/context.py index ff31b24..3fdc8c0 100644 --- a/keyhint/context.py +++ b/keyhint/context.py @@ -6,8 +6,6 @@ import re import shutil import subprocess -import sys -import traceback logger = logging.getLogger("keyhint") @@ -21,6 +19,15 @@ def is_using_wayland() -> bool: return "WAYLAND_DISPLAY" in os.environ +def has_xprop() -> bool: + """Check if xprop is installed. + + Returns: + [bool] -- {True} if xprop is installed + """ + return shutil.which("xprop") is not None + + def get_gnome_version() -> str: """Detect Gnome version of current session. @@ -74,7 +81,7 @@ def get_kde_version() -> str: return kde_version -def get_desktop_environment() -> str: +def get_desktop_environment_and_version() -> tuple[str, str]: """Detect used desktop environment.""" kde_full_session = os.environ.get("KDE_FULL_SESSION", "").lower() xdg_current_desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower() @@ -85,12 +92,15 @@ def get_desktop_environment() -> str: if gnome_desktop_session_id == "this-is-deprecated": gnome_desktop_session_id = "" - de = "not detected" + de = "(DE not detected)" + version = "(version not detected)" if gnome_desktop_session_id or "gnome" in xdg_current_desktop: - de = f"Gnome v{get_gnome_version()}" + de = "Gnome" + version = get_gnome_version() if kde_full_session or "kde-plasma" in desktop_session: - de = f"KDE v{get_kde_version()}" + de = "KDE" + version = get_kde_version() if "sway" in xdg_current_desktop or "sway" in desktop_session: de = "Sway" if "unity" in xdg_current_desktop: @@ -100,10 +110,20 @@ def get_desktop_environment() -> str: if "awesome" in xdg_current_desktop: de = "Awesome" - return de + return de, version -def get_active_window_info_wayland() -> tuple[str, str]: +def has_window_calls_extension() -> bool: + cmd_introspect = ( + "gdbus introspect --session --dest org.gnome.Shell " + "--object-path /org/gnome/Shell/Extensions/Windows " + ) + stdout_bytes = subprocess.check_output(cmd_introspect, shell=True) # noqa: S602 + stdout = stdout_bytes.decode("utf-8") + return all(["List" in stdout, "GetTitle" in stdout]) + + +def get_active_window_via_window_calls() -> tuple[str, str]: """Retrieve active window class and active window title on Wayland. Inspired by https://gist.github.com/rbreaves/257c3edfa301786e66e964d7ac036269 @@ -145,7 +165,7 @@ def _get_cmd_result(cmd: str) -> str: return wm_class, title -def get_active_window_info_x() -> tuple[str, str]: +def get_active_window_via_xprop() -> tuple[str, str]: """Retrieve active window class and active window title on Xorg desktops. Returns: @@ -184,45 +204,3 @@ def get_active_window_info_x() -> tuple[str, str]: wm_class = match.group("class") return wm_class, title - - -def detect_active_window() -> tuple[str, str]: - """Get class and title of active window. - - Identify the OS and display server and pick the method accordingly. - - Returns: - Tuple[str, str]: [description] - """ - wm_class = window_title = "" - - try: - if is_using_wayland(): - wm_class, window_title = get_active_window_info_wayland() - else: - wm_class, window_title = get_active_window_info_x() - except Exception: - traceback.print_stack() - logger.error( # noqa: TRY400 # the stacktrace should be before message - "Couldn't detect active application window.\n" - "KeyHint supports Wayland and Xorg.\n" - "For Wayland, the installation of the 'Window Calls' gnome extension is " - "required:\nhttps://extensions.gnome.org/extension/4724/window-calls\n" - "For Xorg, the 'xprop' command is required. Check your system repository " - "to identify its package.\n" - "If you met the prerequisites but still see this, please create an issue " - "incl. the traceback above on:\nhttps://github.com/dynobo/keyhint/issues" - ) - sys.exit(1) - - logger.debug("Detected wm_class: '%s'.", wm_class) - logger.debug("Detected window_title: '%s'.", window_title) - - if "" in [wm_class, window_title]: - logger.error( - "Couldn't detect active window! Please report this error " - "together with information about your OS and display server on " - "https://github.com/dynobo/keyhint/issues" - ) - - return wm_class, window_title diff --git a/keyhint/resources/window.ui b/keyhint/resources/window.ui index 5966a63..280a542 100644 --- a/keyhint/resources/window.ui +++ b/keyhint/resources/window.ui @@ -13,6 +13,21 @@ 1 + + + + + + true diff --git a/keyhint/window.py b/keyhint/window.py index d538ab8..d7d2f42 100644 --- a/keyhint/window.py +++ b/keyhint/window.py @@ -58,6 +58,8 @@ class KeyhintWindow(Gtk.ApplicationWindow): __gtype_name__ = "main_window" overlay = cast(Adw.ToastOverlay, Gtk.Template.Child()) + banner_window_calls = cast(Adw.Banner, Gtk.Template.Child()) + banner_xprop = cast(Adw.Banner, Gtk.Template.Child()) scrolled_window = cast(Gtk.ScrolledWindow, Gtk.Template.Child()) container = cast(Gtk.Box, Gtk.Template.Child()) sheet_container_box = cast(Gtk.FlowBox, Gtk.Template.Child()) @@ -75,7 +77,7 @@ def __init__(self, cli_args: dict) -> None: self.cli_args = cli_args self.config = config.load() self.sheets = sheets.load_sheets() - self.wm_class, self.window_title = context.detect_active_window() + self.wm_class, self.window_title = self.init_last_active_window_info() self.skip_search_changed: bool = False self.search_text: str = "" @@ -105,11 +107,48 @@ def __init__(self, cli_args: dict) -> None: self.init_action_fallback_sheet() self.init_actions_for_menu_entries() self.init_actions_for_toasts() + self.init_actions_for_banners() self.init_search_entry() self.init_key_event_controllers() self.focus_search_entry() + def init_last_active_window_info(self) -> tuple[str, str]: + """Get class and title of active window. + + Identify the OS and display server and pick the method accordingly. + + Returns: + Tuple[str, str]: wm_class, window title + """ + wm_class = wm_title = "" + + on_wayland = context.is_using_wayland() + desktop_environment = context.get_desktop_environment_and_version()[0].lower() + + match (on_wayland, desktop_environment): + case True, "gnome": + if context.has_window_calls_extension(): + wm_class, wm_title = context.get_active_window_via_window_calls() + else: + self.banner_window_calls.set_revealed(True) + logger.error("Window Calls extension not found!") + + case False, _: + if context.has_xprop(): + wm_class, wm_title = context.get_active_window_via_xprop() + else: + self.banner_xprop.set_revealed(True) + logger.error("xprop not found!") + + logger.debug("Detected wm_class: '%s'.", wm_class) + logger.debug("Detected window_title: '%s'.", wm_title) + + if "" in [wm_class, wm_title]: + logger.warning("Couldn't detect active window!") + + return wm_class, wm_title + def init_action_sort_by(self) -> None: action = Gio.SimpleAction.new_stateful( name="sort_by", @@ -256,11 +295,22 @@ def init_actions_for_menu_entries(self) -> None: self.add_action(action) def init_actions_for_toasts(self) -> None: - """Register actions which can be triggered from toast notifications.""" + """Register actions which are triggered from toast notifications.""" action = Gio.SimpleAction.new("create_new_sheet", None) action.connect("activate", self.on_create_new_sheet) self.add_action(action) + def init_actions_for_banners(self) -> None: + """Register actions which are triggered from banners.""" + action = Gio.SimpleAction.new("visit_window_calls", None) + action.connect( + "activate", + lambda *args: Gio.AppInfo.launch_default_for_uri( + "https://extensions.gnome.org/extension/4724/window-calls/" + ), + ) + self.add_action(action) + def init_key_event_controllers(self) -> None: """Register key press handlers.""" evk = Gtk.EventControllerKey() @@ -761,7 +811,7 @@ def get_debug_info_text(self) -> str: regex_title = sheet.get("match", {}).get("regex_title", "n/a") link = sheet.get("url", "") link_text = f"{link or 'n/a'}" - desktop_environment = context.get_desktop_environment() + desktop_environment = " ".join(context.get_desktop_environment_and_version()) return textwrap.dedent( f"""