mabox-tools/bin/bl-reload-gtk23

160 lines
6.2 KiB
Plaintext
Raw Normal View History

#!/usr/bin/env python3
#
# bl-reload-gtk23: Make GTK2/3 reload settings file changes
# Copyright (C) 2020 2ion <twoion@bunsenlabs.org>
#
# 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 <http://www.gnu.org/licenses/>.
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())