(Init): Added shit
This commit is contained in:
209
nirimod/niri_ipc.py
Normal file
209
nirimod/niri_ipc.py
Normal file
@@ -0,0 +1,209 @@
|
||||
"""Wrappers around `niri msg` C. Low-level IPC operations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Callable
|
||||
|
||||
|
||||
|
||||
|
||||
# Internal: synchronous helper
|
||||
|
||||
|
||||
def _run_sync(args: list[str], timeout: float = 5.0) -> tuple[str, str, int]:
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
r = subprocess.run(
|
||||
args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
return r.stdout, r.stderr, r.returncode
|
||||
except FileNotFoundError:
|
||||
return "", "niri: command not found", 1
|
||||
except subprocess.TimeoutExpired:
|
||||
return "", "niri msg timed out", 1
|
||||
|
||||
|
||||
# Internal: non-blocking async dispatch
|
||||
|
||||
|
||||
def _run_async(
|
||||
args: list[str],
|
||||
callback: Callable[[str, str, int], None],
|
||||
) -> None:
|
||||
import gi
|
||||
gi.require_version("Gio", "2.0")
|
||||
gi.require_version("GLib", "2.0")
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
try:
|
||||
proc = Gio.Subprocess.new(
|
||||
args,
|
||||
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE,
|
||||
)
|
||||
except GLib.Error:
|
||||
GLib.idle_add(lambda: callback("", "niri: command not found", 1) or False)
|
||||
return
|
||||
|
||||
def _on_done(source: Gio.Subprocess, result: Gio.AsyncResult) -> None:
|
||||
try:
|
||||
ok, stdout_bytes, stderr_bytes = source.communicate_finish(result)
|
||||
stdout = stdout_bytes.get_data().decode("utf-8", errors="replace") if stdout_bytes else ""
|
||||
stderr = stderr_bytes.get_data().decode("utf-8", errors="replace") if stderr_bytes else ""
|
||||
if not ok:
|
||||
rc = 1
|
||||
else:
|
||||
rc = 0 if source.get_exit_status() == 0 else 1
|
||||
except GLib.Error as exc:
|
||||
stdout, stderr, rc = "", str(exc), 1
|
||||
callback(stdout, stderr, rc)
|
||||
|
||||
proc.communicate_async(None, None, _on_done)
|
||||
|
||||
|
||||
# IPC Getters
|
||||
|
||||
|
||||
def is_niri_running() -> bool:
|
||||
"""Return True if `niri msg version` succeeds. Called once at startup."""
|
||||
stdout, _, rc = _run_sync(["niri", "msg", "version"])
|
||||
return rc == 0 and bool(stdout.strip())
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
stdout, _, rc = _run_sync(["niri", "--version"])
|
||||
return stdout.strip() if rc == 0 else "unknown"
|
||||
|
||||
|
||||
_touchpad_cache: bool | None = None
|
||||
|
||||
|
||||
def has_touchpad() -> bool:
|
||||
import os
|
||||
|
||||
global _touchpad_cache
|
||||
if _touchpad_cache is not None:
|
||||
return _touchpad_cache
|
||||
|
||||
result = False
|
||||
try:
|
||||
for dev in os.listdir("/sys/class/input"):
|
||||
name_file = f"/sys/class/input/{dev}/device/name"
|
||||
if os.path.exists(name_file):
|
||||
with open(name_file) as fh:
|
||||
name = fh.read().lower()
|
||||
if "touchpad" in name or "trackpad" in name:
|
||||
result = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_touchpad_cache = result
|
||||
return result
|
||||
|
||||
|
||||
def validate_config(config_path: str | None = None) -> tuple[bool, str]:
|
||||
cmd = ["niri", "validate"]
|
||||
if config_path:
|
||||
cmd += ["--config", config_path]
|
||||
stdout, stderr, rc = _run_sync(cmd, timeout=10.0)
|
||||
if rc == 0:
|
||||
return True, stdout.strip() or "Config is valid."
|
||||
return False, stderr.strip() or stdout.strip() or "Unknown validation error."
|
||||
|
||||
|
||||
def load_config_file() -> tuple[bool, str]:
|
||||
stdout, stderr, rc = _run_sync(
|
||||
["niri", "msg", "action", "load-config-file"], timeout=10.0
|
||||
)
|
||||
if rc == 0:
|
||||
return True, stdout.strip() or "Config applied."
|
||||
return False, stderr.strip() or stdout.strip() or "Config reload failed."
|
||||
|
||||
|
||||
def get_outputs(callback: Callable[[list[dict]], None]) -> None:
|
||||
def _done(stdout: str, _stderr: str, rc: int) -> None:
|
||||
if rc != 0:
|
||||
callback([])
|
||||
return
|
||||
try:
|
||||
data = json.loads(stdout)
|
||||
callback(list(data.values()) if isinstance(data, dict) else data)
|
||||
except json.JSONDecodeError:
|
||||
callback([])
|
||||
|
||||
_run_async(["niri", "msg", "--json", "outputs"], _done)
|
||||
|
||||
|
||||
def get_workspaces(callback: Callable[[list[dict]], None]) -> None:
|
||||
def _done(stdout: str, _stderr: str, rc: int) -> None:
|
||||
if rc != 0:
|
||||
callback([])
|
||||
return
|
||||
try:
|
||||
callback(json.loads(stdout))
|
||||
except json.JSONDecodeError:
|
||||
callback([])
|
||||
|
||||
_run_async(["niri", "msg", "--json", "workspaces"], _done)
|
||||
|
||||
|
||||
def get_windows(callback: Callable[[list[dict]], None]) -> None:
|
||||
def _done(stdout: str, _stderr: str, rc: int) -> None:
|
||||
if rc != 0:
|
||||
callback([])
|
||||
return
|
||||
try:
|
||||
callback(json.loads(stdout))
|
||||
except json.JSONDecodeError:
|
||||
callback([])
|
||||
|
||||
_run_async(["niri", "msg", "--json", "windows"], _done)
|
||||
|
||||
|
||||
def get_focused_window(callback: Callable[[dict | None], None]) -> None:
|
||||
def _done(stdout: str, _stderr: str, rc: int) -> None:
|
||||
if rc != 0:
|
||||
callback(None)
|
||||
return
|
||||
try:
|
||||
callback(json.loads(stdout))
|
||||
except json.JSONDecodeError:
|
||||
callback(None)
|
||||
|
||||
_run_async(["niri", "msg", "--json", "focused-window"], _done)
|
||||
|
||||
|
||||
def action(action_name: str, *args: str, callback: Callable[[bool], None] | None = None) -> None:
|
||||
cmd = ["niri", "msg", "action", action_name] + list(args)
|
||||
|
||||
def _done(_stdout: str, _stderr: str, rc: int) -> None:
|
||||
if callback is not None:
|
||||
callback(rc == 0)
|
||||
|
||||
_run_async(cmd, _done)
|
||||
|
||||
|
||||
# Legacy thread shims
|
||||
|
||||
|
||||
def run_in_thread(fn: Callable, callback: Callable | None = None):
|
||||
|
||||
import threading
|
||||
|
||||
import gi
|
||||
gi.require_version("GLib", "2.0")
|
||||
from gi.repository import GLib
|
||||
|
||||
def _worker():
|
||||
result = fn()
|
||||
if callback is not None:
|
||||
GLib.idle_add(lambda: callback(result) or False)
|
||||
|
||||
t = threading.Thread(target=_worker, daemon=True)
|
||||
t.start()
|
||||
return t
|
||||
Reference in New Issue
Block a user