Skip to content

Commit

Permalink
feat: verify prerequisites and show banners if missed
Browse files Browse the repository at this point in the history
  • Loading branch information
dynobo committed Apr 21, 2024
1 parent 89b2280 commit 0d54bf7
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 54 deletions.
80 changes: 29 additions & 51 deletions keyhint/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import re
import shutil
import subprocess
import sys
import traceback

logger = logging.getLogger("keyhint")

Expand All @@ -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.
Expand Down Expand Up @@ -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()
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
15 changes: 15 additions & 0 deletions keyhint/resources/window.ui
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@
<child>
<object class="GtkBox" id="container">
<property name="orientation">1</property>
<child>
<object class="AdwBanner" id="banner_window_calls">
<property name="button-label" translatable="true">Gnome Extension Webpage</property>
<property name="title" translatable="true">The Gnome Extension 'Window Calls' is required on Wayland!</property>
<property name="revealed">False</property>
<property name="action-name">win.visit_window_calls</property>
</object>
</child>
<child>
<object class="AdwBanner" id="banner_xprop">
<property name="title" translatable="true">The tool 'xprop' is required on Xorg. Install the system package 'x11-utils' (Debian/Ubuntu) or 'xprop' (Arch/Fedora)!</property>
<property name="revealed">False</property>
<property name="action-name">win.visit_xprop</property>
</object>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="propagate-natural-height">true</property>
Expand Down
56 changes: 53 additions & 3 deletions keyhint/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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 = ""
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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"<span foreground='#00A6FF'>{link or 'n/a'}</span>"
desktop_environment = context.get_desktop_environment()
desktop_environment = " ".join(context.get_desktop_environment_and_version())

return textwrap.dedent(
f"""
Expand Down

0 comments on commit 0d54bf7

Please sign in to comment.