Created
March 6, 2019 17:27
-
-
Save ldotlopez/5e87dc5e2d16a6349c09224d57913dad to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import argparse | |
import difflib | |
import enum | |
import os | |
import re | |
import sys | |
from os import path | |
from urllib import parse | |
if sys.version_info < (3, 4, 0): | |
msg = "python < 3.4.0" | |
raise SystemError(msg) | |
import gi # noqa | |
gi.require_version('GLib', '2.0') | |
from gi.repository import GLib, Gio # noqa | |
import pydbus # noqa | |
class Implementations(enum.Enum): | |
GTK_2 = 'gtk-2' | |
GTK_3 = 'gtk-3' | |
GNOME_SHELL = 'gnome-shell' | |
BACKGROUND = 'background' | |
class Modes(enum.Enum): | |
LIGHT = 'light' | |
DARK = 'dark' | |
class Theme(object): | |
def __init__(self, name, implements=None): | |
self.name = name | |
self.implements = set(implements or []) | |
def __hash__(self): | |
return hash(self.name) | |
def __str__(self): | |
return self.name | |
def __repr__(self): | |
return '{name} ({impls})'.format( | |
name=self.name, | |
impls=', '.join(x.name for x in self.implements)) | |
class Nightmode(object): | |
def __init__(self): | |
super(Nightmode, self).__init__() | |
self.themes = {x.name: x for x in self.get_all_themes()} | |
def switch(self, mode=None): | |
for impl in Implementations: | |
current = self.get_current_theme(impl) | |
if current is None: | |
msg = "Current theme for {impl} is unknow" | |
msg = msg.format(impl=impl) | |
print(msg) | |
continue | |
light, dark = self.get_pair(current, impl) | |
if not light or not dark: | |
msg = "Current theme {name} for {impl} has no variant" | |
msg = msg.format(name=current.name, impl=impl) | |
print(msg) | |
continue | |
if mode is None: | |
switch_to = light if current is dark else dark | |
elif mode is Modes.LIGHT: | |
switch_to = light | |
elif mode is Modes.DARK: | |
switch_to = dark | |
else: | |
raise NotImplementedError(mode) | |
if current is switch_to: | |
msg = "Theme '{name}' is current theme for {impl} already" | |
msg = msg.format(name=current.name, impl=impl) | |
print(msg) | |
continue | |
msg = "Switch from '{a}' to '{b}' for {impl}" | |
msg = msg.format(a=current.name, b=switch_to.name, impl=impl) | |
print(msg) | |
self.set_current_theme(switch_to, impl) | |
def get_all_themes(self): | |
prefixes = [ | |
'/usr/share/themes', | |
path.expanduser('~/.themes') | |
] | |
ret = {} | |
for prefix in prefixes: | |
for name in os.listdir(prefix): | |
try: | |
theme = ret[name] | |
except KeyError: | |
theme = ret[name] = Theme(name) | |
basedir = prefix + '/' + name | |
if path.exists(basedir + '/gtk-3.0/gtk.css'): | |
theme.implements.add(Implementations.GTK_3) | |
if path.exists(basedir + '/gtk-2.0/gtkrc'): | |
theme.implements.add(Implementations.GTK_2) | |
if path.exists(basedir + '/gnome-shell/gnome-shell.css'): | |
theme.implements.add(Implementations.GNOME_SHELL) | |
# Hack to integrate background themes | |
background = self._gsettings_get('org.gnome.desktop.background', | |
'picture-uri') | |
if background.startswith('file://'): | |
background = background[len('file://'):] | |
background = parse.unquote(background) | |
name, ext = path.splitext(background) | |
rawname = re.sub(r'-dark', '', name) | |
lightname = rawname + '-light' | |
darkname = rawname + '-dark' | |
for name in [rawname, lightname, darkname]: | |
filepath = name + ext | |
if path.exists(filepath): | |
fileuri = 'file://' + parse.quote_plus(name) | |
theme = Theme(fileuri, | |
implements=[Implementations.BACKGROUND]) | |
ret[fileuri] = theme | |
else: | |
print("Non local backgrounds not implemented") | |
return list(ret.values()) | |
def get_themes(self, impl): | |
return [theme for theme in | |
self.themes if impl in theme.implements] | |
def get_current_theme(self, impl): | |
if impl is Implementations.GTK_3: | |
name = self._gsettings_get('org.gnome.desktop.interface', | |
'gtk-theme') | |
elif impl is Implementations.GNOME_SHELL: | |
name = self._gsettings_get('org.gnome.shell.extensions.user-theme', | |
'name') | |
elif impl is Implementations.BACKGROUND: | |
name = self._gsettings_get('org.gnome.desktop.background', | |
'picture-uri') | |
else: | |
return None | |
# raise NotImplementedError(impl) | |
return self.themes.get(name, None) | |
def set_current_theme(self, theme, impl): | |
if impl is Implementations.GTK_3: | |
self._gsettings_set('org.gnome.desktop.interface', | |
'gtk-theme', theme.name) | |
elif impl is Implementations.GNOME_SHELL: | |
self._gsettings_set('org.gnome.shell.extensions.user-theme', | |
'name', theme.name) | |
elif impl is Implementations.BACKGROUND: | |
self._gsettings_get('org.gnome.desktop.background', | |
'picture-uri', theme.name) | |
else: | |
raise NotImplementedError(impl) | |
def get_pair(self, theme, impl): | |
tbl = [] | |
# Strip dark from theme name | |
name = re.sub(r'-dark$', '', theme.name.replace('-dark-', '-')) | |
for candidate in self.themes.values(): | |
if candidate.name == name: | |
continue | |
if impl not in candidate.implements: | |
continue | |
if ( | |
not candidate.name.endswith('-dark') and | |
'-dark-' not in candidate.name): | |
continue | |
tbl.append(( | |
candidate, | |
difflib.SequenceMatcher(a=name, b=candidate.name).ratio())) | |
candidates = [theme for theme, _ in | |
sorted(tbl, key=lambda x: x[1], reverse=True)] | |
try: | |
return self.themes[name], candidates[0] | |
except IndexError: | |
return None | |
def _gsettings_get(self, schema, key): | |
settings = Gio.Settings(schema=schema) | |
value = settings.get_value(key).unpack() | |
return value | |
def _gsettings_set(self, schema, key, value): | |
signs = { | |
bool: 'b', | |
int: 'i', | |
float: 'd', | |
str: 's' | |
} | |
value = GLib.Variant(signs[type(value)], value) | |
settings = Gio.Settings(schema=schema) | |
settings.set_value(key, value) | |
class IntegratedApp(Nightmode): | |
def __init__(self): | |
super(IntegratedApp, self).__init__() | |
self.bus = pydbus.SessionBus() | |
self.color = self.bus.get('org.gnome.SettingsDaemon.Color') | |
def update(self, is_dark_active): | |
self.switch(Modes.DARK if is_dark_active else Modes.LIGHT) | |
@property | |
def enabled(self): | |
val = self._gsettings_get("org.gnome.settings-daemon.plugins.color", | |
'night-light-enabled') | |
return val == 'true' | |
def watch(self): | |
# Initial update | |
self.update(self.color.NightLightActive) | |
# (':1.58', '/org/gnome/SettingsDaemon/Color', 'org.freedesktop.DBus.Properties', 'PropertiesChanged', ('org.gnome.SettingsDaemon.Color', {'NightLightActive': False}, [])) {} | |
# (sender=None, iface=None, signal=None, object=None, arg0=None, flags=0, signal_fired=None | |
self.bus.subscribe(sender='org.gnome.SettingsDaemon.Color', | |
iface='org.freedesktop.DBus.Properties', | |
signal='PropertiesChanged', | |
signal_fired=self.on_color_prop_change) | |
loop = GLib.MainLoop() | |
loop.run() | |
def on_color_prop_change(self, sender, object, iface, signal, params): | |
color, props, strings = params | |
if 'NightLightActive' in props: | |
self.update(props['NightLightActive']) | |
def autostart(): | |
contents = """ | |
[Desktop Entry] | |
Name=Dark mode | |
GenericName=Dark mode | |
Exec={me} --watch | |
Terminal=false | |
Type=Application | |
StartupNotify=false | |
NotShowIn=KDE; | |
X-GNOME-Autostart-enabled=true | |
""" | |
contents = '\n'.join([x.strip() for x in contents.split('\n')]).strip() | |
contents = contents.format(me=' '.join(sys.argv[:-1])) | |
print(contents) | |
def main(): | |
parser = argparse.ArgumentParser() | |
mode_args = parser.add_mutually_exclusive_group() | |
mode_args.add_argument('--light', action='store_true') | |
mode_args.add_argument('--dark', action='store_true') | |
mode_args.add_argument('--switch', action='store_true') | |
mode_args.add_argument('--watch', action='store_true') | |
mode_args.add_argument('--enable-autostart', action='store_true') | |
mode_args.add_argument('--uninstall', action='store_true') | |
args = parser.parse_args(sys.argv[1:]) | |
app = IntegratedApp() | |
if args.light: | |
app.switch(Modes.LIGHT) | |
elif args.dark: | |
app.switch(Modes.DARK) | |
elif args.switch: | |
app.switch() | |
elif args.watch: | |
app.watch() | |
elif args.enable_autostart: | |
autostart() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment