diff --git a/hints/backends/atspi.py b/hints/backends/atspi.py index 837e02d..f4c4fb3 100644 --- a/hints/backends/atspi.py +++ b/hints/backends/atspi.py @@ -296,6 +296,8 @@ def get_atspi_active_window(self) -> Atspi.Accessible | None: continue for window_index in range(window.get_child_count()): current_window = window.get_child_at_index(window_index) + if current_window is None: + continue # Some hidden windows that are minimized to status trays # (like discord) will still have the Atspi.StateType.Active # state, so the pid from the window manger allows us to filter diff --git a/hints/hints.py b/hints/hints.py index 4939b13..e84fdff 100644 --- a/hints/hints.py +++ b/hints/hints.py @@ -280,6 +280,10 @@ def get_window_system_class( from hints.window_systems.plasmashell import Plasmashell as window_system case "gnome-shell": from hints.window_systems.gnome import Gnome as window_system + case "niri": + from hints.window_systems.niri import Niri as window_system + case "mango": + from hints.window_systems.mango import Mango as window_system return window_system @@ -301,7 +305,7 @@ def get_window_system(window_system_id: str = "") -> Type[WindowSystem]: if window_system_type == WindowSystemType.WAYLAND: # add new waland wms here, then add a match case below to import the class - supported_wayland_wms = {"sway", "Hyprland", "plasmashell", "gnome-shell"} + supported_wayland_wms = {"sway", "Hyprland", "plasmashell", "gnome-shell", "niri", "mango"} # Check if there is a process running that matches the supported_wayland_wms window_system_id = ( diff --git a/hints/window_systems/mango.py b/hints/window_systems/mango.py new file mode 100644 index 0000000..be2adc4 --- /dev/null +++ b/hints/window_systems/mango.py @@ -0,0 +1,56 @@ +"""Mango window system.""" + +from subprocess import run + +from hints.window_systems.window_system import WindowSystem + + +def _parse_mmsg(output: str) -> dict[str, str]: + result = {} + for line in output.strip().splitlines(): + parts = line.split(None, 2) + if len(parts) == 3: + result[parts[1]] = parts[2] + return result + + +class Mango(WindowSystem): + """Mango (MangoWM) window system class.""" + + def __init__(self): + super().__init__() + self._state = self._query() + + def _query(self) -> dict[str, str]: + result = run( + ["mmsg", "-g", "-c", "-x"], + capture_output=True, + check=True, + ) + return _parse_mmsg(result.stdout.decode("utf-8")) + + @property + def window_system_name(self) -> str: + return "mango" + + @property + def focused_window_extents(self) -> tuple[int, int, int, int]: + return ( + int(self._state["x"]), + int(self._state["y"]), + int(self._state["width"]), + int(self._state["height"]), + ) + + @property + def focused_window_pid(self) -> int: + appid = self._state.get("appid", "") + result = run( + ["pgrep", "-n", appid], + capture_output=True, + ) + return int(result.stdout.strip()) if result.returncode == 0 else -1 + + @property + def focused_applicaiton_name(self) -> str: + return self._state.get("appid", "") diff --git a/hints/window_systems/niri.py b/hints/window_systems/niri.py new file mode 100644 index 0000000..e682e30 --- /dev/null +++ b/hints/window_systems/niri.py @@ -0,0 +1,85 @@ +"""Niri window system.""" + +from json import loads +from subprocess import run + +from hints.window_systems.window_system import WindowSystem + + +class Niri(WindowSystem): + """Niri Window system class.""" + + def __init__(self): + super().__init__() + self._focused_window = self._get_focused_window() + self._focused_output = self._get_focused_output() + + def _get_focused_window(self): + result = run( + ["niri", "msg", "-j", "focused-window"], + capture_output=True, + check=True, + ) + return loads(result.stdout.decode("utf-8")) + + def _get_focused_output(self): + result = run( + ["niri", "msg", "-j", "focused-output"], + capture_output=True, + check=True, + ) + return loads(result.stdout.decode("utf-8")) + + @property + def window_system_name(self) -> str: + """Get the name of the window system. + + :return: The window system name + """ + return "niri" + + @property + def focused_window_extents(self) -> tuple[int, int, int, int]: + """Get active window extents. + + :return: Active window extents (x, y, width, height). + """ + output_logical = self._focused_output["logical"] + layout = self._focused_window["layout"] + + tile_pos = layout.get("tile_pos_in_workspace_view") + if tile_pos is not None: + # Floating windows: niri populates tile_pos_in_workspace_view + x = output_logical["x"] + int(tile_pos[0]) + y = output_logical["y"] + int(tile_pos[1]) + width = layout["window_size"][0] + height = layout["window_size"][1] + else: + # Tiled windows: niri IPC does not expose the screen position + # (https://github.com/niri-wm/niri/issues/2381). Fall back to + # the full output geometry so hints still scans the screen. + x = output_logical["x"] + y = output_logical["y"] + width = output_logical["width"] + height = output_logical["height"] + + return (x, y, width, height) + + @property + def focused_window_pid(self) -> int: + """Get Process ID corresponding to the focused window. + + :return: Process ID of focused window. + """ + return self._focused_window["pid"] + + @property + def focused_applicaiton_name(self) -> str: + """Get focused application name. + + This name is the name used to identify applications for per- + application rules. + + :return: Focused application name. + """ + return self._focused_window["app_id"] diff --git a/hints/window_systems/window_system_type.py b/hints/window_systems/window_system_type.py index 309ed18..ecdc20c 100644 --- a/hints/window_systems/window_system_type.py +++ b/hints/window_systems/window_system_type.py @@ -12,7 +12,7 @@ class WindowSystemType(Enum): WAYLAND = "wayland" -SupportedWindowSystems = Literal["x11", "sway", "hyprland"] +SupportedWindowSystems = Literal["x11", "sway", "hyprland", "niri"] def get_window_system_type() -> WindowSystemType: