160 lines
6.2 KiB
Python
Executable File
160 lines
6.2 KiB
Python
Executable File
#!/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())
|