Skip to content

Instantly share code, notes, and snippets.

@ldotlopez
Created March 6, 2019 17:27
Show Gist options
  • Save ldotlopez/5e87dc5e2d16a6349c09224d57913dad to your computer and use it in GitHub Desktop.
Save ldotlopez/5e87dc5e2d16a6349c09224d57913dad to your computer and use it in GitHub Desktop.
#!/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