#!/usr/bin/env python3 # # bl-reload-gtk23: Make GTK2/3 reload settings file changes # Copyright (C) 2020 2ion # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from argparse import ArgumentParser, Namespace import Xlib.display # type: ignore import Xlib.protocol # type: ignore import logging import os import psutil # type: ignore import shutil import signal import subprocess import sys DESCRIPTION = (""" After changing GTK2 and GTK3 configuration files, notify running GTK2 and GTK3 clients to apply those changes. The notification mechanism used by GTK3 requires that xsettingsd is running. If it is installed and not running, the program will launch it. Note that xsettingsd does not read settings from GTK3's settings.ini - if information is changed, it must be changed in xsettingsd config files as well. EXIT CODES: 0 - both gtk2 and gtk3 clients notified successfully 1 - failed to notify gtk2 clients 2 - failed to notify gtk3 clients 3 - failed to notify gtk2 and gtk3 clients """) LOG_FORMAT = "%(asctime)s %(levelname)s %(module)s %(funcName)s() : %(message)s" def getopts() -> Namespace: ap = ArgumentParser(description=DESCRIPTION) ap.add_argument("-d", "--debug", action="store_true", default=False, help="print debug information") ap.add_argument("-f", "--force", action="store_true", default=False, help="ignore all errors") opts = ap.parse_args() if opts.debug: logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT) else: logging.basicConfig(level=logging.WARN, format=LOG_FORMAT) return opts def sync_gtk2() -> None: """ Tell GTK2 X11 clients to reload the GTK RC files and update their appearance/settings if required. This implements this process without GTK/GDK in order to be able to drop the dependency on the obsolete pygtk library. GTK3/pygobject does not support GTK2. This function will always fail on non-X11 platforms as the GTK2 client notification mechanism is based on X. This implementation is based on the following resources: * From libgtk2 2.24.18: * gdk_event_send_client_message_to_all_recurse() * gdk_screen_broadcast_client_message() * From kde-gtk-config https://github.com/KDE/kde-gtk-config/blob/a5d4ddb3b1a27ec2ee4e1b6957a98a57ad56d39c/gtkproxies/reload.c """ display = Xlib.display.Display(display=os.getenv("DISPLAY")) wm_state_atom = display.intern_atom("WM_STATE", False) gtkrc_atom = display.intern_atom("_GTK_READ_RCFILES", False) def send_event(window) -> bool: """ Send a _GTK_READ_RCFILES client message to the given X window. Returns true unless an exception occurs. """ window.send_event( Xlib.protocol.event.ClientMessage( window = window, client_type = gtkrc_atom, data = (8, b"\0" * 20), ), propagate = 0, event_mask = 0 ) return True def recurse_windows(window, parents) -> bool: """ Given a X window, recurse over all its children and selectively apply the send_event function to them. Returns true if an event got sent to at least one window equal or below the given one. """ sent = False wm_state = window.get_property(wm_state_atom, wm_state_atom, 0, 0) name = window.get_wm_name() level = len(parents) if wm_state is not None: sent = send_event(window) else: tree = window.query_tree() for child in tree.children: if not recurse_windows(child, parents + [window.id]) and level == 1: sent = send_event(window) logging.debug("%10s %s %s [%24s] [%s]", hex(window.id), "W" if not not wm_state else " ", "S" if sent else " ", name[:24] if name else "", ",".join(map(hex, parents))) return sent for sno in range(0, display.screen_count()): screen = display.screen(sno) recurse_windows(screen.root, []) def sync_gtk3(): """ GTK3 applications can be notified of changes to their theming via xsettingsd. This requires that the GTK3 theming information has been updated in settings.ini as well as the gsettings schema, managed either by a standalone xettingsd implementation or the gnome-settings-daemon. As for now, we only support reloading `xsettingsd`: Send SIGHUP if we find it running, or start it if it is installed and not running. * https://github.com/swaywm/sway/wiki/GTK-3-settings-on-Wayland * https://github.com/KDE/kde-gtk-config/blob/a5d4ddb3b1a27ec2ee4e1b6957a98a57ad56d39c/kded/configeditor.cpp#L285 """ found = False for proc in psutil.process_iter(): if proc.name() == "xsettingsd": proc.send_signal(signal.SIGHUP) found = True logging.debug("Found xsettingsd process and sent SIGHUP: PID %d", proc.pid) break if not found: xsettingsd_binary = shutil.which("xsettingsd") if xsettingsd_binary is not None: proc = subprocess.Popen([xsettingsd_binary], cwd="/", start_new_session=True) logging.debug("xsettingsd not running; started from %s; PID: %d", xsettingsd_binary, proc.pid) else: raise RuntimeError("xsettingsd not running and not in PATH, no settings changes propagated") def main() -> int: opts = getopts() ret = 0 try: sync_gtk2() except Exception as err: logging.warning("Failed to reload GTK2 settings: %s", err) ret |= 1<<0 try: sync_gtk3() except Exception as err: logging.warning("Failed to reload GTK3 settings: %s", err) ret |= 1<<1 return 0 if opts.force else ret if __name__ == "__main__": sys.exit(main())