summaryrefslogtreecommitdiffstats
path: root/auto_xsettingsd.py
diff options
context:
space:
mode:
Diffstat (limited to 'auto_xsettingsd.py')
-rw-r--r--auto_xsettingsd.py151
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()