diff options
Diffstat (limited to 'auto_xsettingsd.py')
| -rw-r--r-- | auto_xsettingsd.py | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/auto_xsettingsd.py b/auto_xsettingsd.py new file mode 100644 index 0000000..b6acd36 --- /dev/null +++ b/auto_xsettingsd.py @@ -0,0 +1,151 @@ +"""Automatically update xsettings based on gsettings""" + +__version__ = "0.1.0" + +import os +import signal +import struct +import sys +import tempfile +from collections.abc import Iterator +from pathlib import Path +from subprocess import Popen + +from gi.repository import Gio, GLib + +DESKTOP_INTERFACE_SCHEMA = "org.gnome.desktop.interface" +GTK_THEME_KEY = "gtk-theme" +HEADER_FORMAT = ">H" + + +def start_gsettings_listener() -> tuple[int, int]: + def on_change(settings, key): + if settings.props.schema_id != DESKTOP_INTERFACE_SCHEMA: + return + if key != GTK_THEME_KEY: + return + gtk_theme = settings.get_string(key).encode() + data = struct.pack(HEADER_FORMAT, len(gtk_theme)) + os.write(write, data + gtk_theme) + + read, write = os.pipe() + os.set_inheritable(write, True) + pid = os.fork() + if pid != 0: + os.close(write) + return read, pid + + settings = Gio.Settings.new(DESKTOP_INTERFACE_SCHEMA) + settings.connect("changed", on_change) + on_change(settings, GTK_THEME_KEY) + + try: + GLib.MainLoop().run() + exit(1) + except KeyboardInterrupt: + os._exit(0) + + +def start_xsettingsd(conf: str, args: list[str]) -> Popen: + return Popen(["xsettingsd", "--config", conf] + args) + + +def read_theme(fd: int) -> str: + header = os.read(fd, 2) + (length,) = struct.unpack(HEADER_FORMAT, header) + return os.read(fd, length).decode() + + +def xsettingsd_escape(s: str) -> str: + return s.replace("\n", r"\n").replace('"', r"\"") + + +def get_xsettingsd_config_paths() -> Iterator[Path]: + yield Path.home() / ".xsettingsd" + conf_home = os.environ.get("XDG_CONFIG_HOME") + conf_home = Path(conf_home) if conf_home else Path.home() / ".config" + xdg_dirs = [conf_home] + conf_dirs = os.environ.get("XDG_CONFIG_DIRS") + if conf_dirs: + for d in conf_dirs.split(":"): + xdg_dirs.append(Path(d)) + for d in xdg_dirs: + yield d / "xsettingsd" / "xsettingsd.conf" + + +XSETTINGSD_CONFIG_PATHS = list(get_xsettingsd_config_paths()) + + +# It would make more sense to only read the config once every HUP +def get_xsettingsd_config() -> str: + for path in XSETTINGSD_CONFIG_PATHS: + try: + with path.open() as f: + return f.read() + except FileNotFoundError: + pass + return "" + + +def write_merged_settings(fd: int, gtk_theme: str) -> None: + os.lseek(fd, 0, os.SEEK_SET) + os.ftruncate(fd, 0) + os.write(fd, f'Net/ThemeName "{xsettingsd_escape(gtk_theme)}"\n'.encode()) + os.write(fd, get_xsettingsd_config().encode()) + + +def start_reconfigurator(theme_fd: int, conf_fd: int, xsettingsd_pid: int) -> int: + os.set_inheritable(theme_fd, True) + os.set_inheritable(conf_fd, True) + pid = os.fork() + if pid != 0: + os.close(theme_fd) + return pid + + try: + while True: + gtk_theme = read_theme(theme_fd) + write_merged_settings(conf_fd, gtk_theme) + os.kill(xsettingsd_pid, signal.SIGHUP) + except KeyboardInterrupt: + os._exit(0) + + +def kill_and_wait(pid: int) -> None: + os.kill(pid, signal.SIGTERM) + os.kill(pid, signal.SIGCONT) + try: + os.waitpid(pid, 0) + except ChildProcessError as e: + print(e) + pass + + +def main(): + xsettingsd = None + gsettings_pid = None + reconf_pid = None + conf_fd, conf_name = tempfile.mkstemp(prefix="xsettings", suffix=".conf") + try: + notify_fd, gsettings_pid = start_gsettings_listener() + gtk_theme = read_theme(notify_fd) + write_merged_settings(conf_fd, gtk_theme) + xsettingsd = start_xsettingsd(conf_name, sys.argv[1:]) + reconf_pid = start_reconfigurator(notify_fd, conf_fd, xsettingsd.pid) + pid, _ = os.wait() + except KeyboardInterrupt: + pid = None + pass + finally: + os.close(conf_fd) + os.unlink(conf_name) + if xsettingsd and pid != xsettingsd.pid: + xsettingsd.terminate() + if gsettings_pid and pid != gsettings_pid: + kill_and_wait(gsettings_pid) + if reconf_pid and pid != reconf_pid: + kill_and_wait(reconf_pid) + + +if __name__ == "__main__": + main() |
