Skip to content

Instantly share code, notes, and snippets.

@James-E-A
Created December 11, 2025 21:13
Show Gist options
  • Select an option

  • Save James-E-A/2f0eb5fd6128b29f1deec78ae9128c31 to your computer and use it in GitHub Desktop.

Select an option

Save James-E-A/2f0eb5fd6128b29f1deec78ae9128c31 to your computer and use it in GitHub Desktop.
[WIP] script to automatically change OS theme between light/dark according to sunrise and sunset
from contextlib import contextmanager
import datetime
import hashlib
import inspect
import itertools
import json
import os
from pathlib import Path
import re
import sqlite3
import sys
import urllib.parse
import urllib.request
from textwrap import dedent
import zipfile
ID = hashlib.md5(inspect.getsource(sys.modules[__name__]).encode(), usedforsecurity=False).hexdigest().upper()[-8:]
DB_PATH = Path(__file__).parent / "{b9889727-d609-11f0-8788-a01d48f34869}.sqlite3"
COORDS = (34.73, -86.59)
def resolve_timezone(location):
import spherely # https://pypi.org/project/spherely/
import zoneinfo
if not isinstance(coords, spherely.Geometry):
coords = spherely.create_point(*reversed(coords))
if "Etc/UTC" not in zoneinfo.available_timezones():
# OS database is absent or deficient
import tzdata # https://pypi.org/project/tzdata/
assert "Etc/UTC" in zoneinfo.available_timezones()
with zipfile.PyZipFile(os.environ.get('TZ_GEO', Path('~/Downloads/timezones.geojson.zip').expanduser())) as f: # https://github.com/evansiroky/timezone-boundary-builder
with f.open(next(x for x in f.infolist() if x.filename.endswith('json'))) as f1:
data = json.load(f1)
assert data['type'] == 'FeatureCollection'
for feature in data['features']:
if (geom_type := feature['type']) == 'Polygon':
raise NotImplementedError
geometry = spherely.create_polygon(...)
elif geom_type == 'MultiPolygon':
raise NotImplementedError
geometry = spherely.create_multipolygon(...)
else:
raise RuntimeError(f"Unexpected geometry type: {geom_type}")
if spherely.covers(geometry, coords):
return zoneinfo.ZoneInfo(feature['properties']['tzid'])
else:
raise KeyError(coords)
def populate_db(start=None, stop=None):
if start is None:
start = datetime.datetime.now().astimezone() - datetime.timedelta(days=1)
if start.tzinfo is None:
start = start.astimezone()
if stop is None:
stop = start + datetime.timedelta(days=7)
if stop.tzinfo is None:
stop = stop.astimezone()
start_utc = start.astimezone(datetime.timezone.utc)
stop_utc = stop.astimezone(datetime.timezone.utc)
with sqlite3.connect(DB_PATH) as con:
cur = con.cursor()
def lookup_near(dt, coords):
if dt.tzinfo is None:
raise ValueError("Ambiguous naive datetime. Use .astimezone() if this is system local time, or .replace(datetime.timezone.utc) if it's UTC.")
u = (
"https://aa.usno.navy.mil/api/rstt/oneday?"
+ urllib.parse.urlencode({
'date': dt.strftime("%Y-%m-%d"),
'coords': f"{coords[0]},{coords[1]}",
'tz': dt.utcoffset().total_seconds() / 3600,
})
)
response = json.load(urllib.request.urlopen(u))
if 'error' in response:
raise Exception(response['error'])
data = [
(
(
datetime.datetime(
response['properties']['data']['year'],
response['properties']['data']['month'],
response['properties']['data']['day'],
int(record['time'].split(':', 1)[0]),
int(record['time'].split(':', 1)[1]),
tzinfo=datetime.timezone(
offset=datetime.timedelta(hours=response['properties']['data']['tz'])
)
)
.astimezone(datetime.timezone.utc)
.strftime('%Y-%m-%d %H:%M')
),
response['geometry']['coordinates'][0],
response['geometry']['coordinates'][1],
phen
)
for phen, record in (
(f"{k[:-4]}_{record['phen']}", record)
for k, v in response['properties']['data'].items()
if k.endswith('data')
for record in v
)
]
return data
def migrate_db():
with sqlite3.connect(DB_PATH) as con:
cur = con.cursor()
if not cur.execute("SELECT name FROM sqlite_schema WHERE type='table' AND name=?", ("rstt_cache",)).fetchone():
# migration 1
cur.executescript(dedent("""\
CREATE TABLE rstt_cache (
rowid INTEGER PRIMARY KEY,
geo_lat FLOAT,
geo_lon FLOAT,
phen VARCHAR(63),
datetime_utc VARCHAR(26)
);
CREATE INDEX rstt_cache_index ON rstt_cache (
datetime_utc,
geo_lon,
geo_lat,
phen
);
"""))
if sys.platform == 'win32':
import ctypes.wintypes
import winreg
LONG_PTR = (
{ctypes.sizeof(t): t for t in (getattr(ctypes, k) for k in dir(ctypes) if re.match(r'^c_int\d+$', k))}
[ctypes.sizeof(ctypes.c_void_p)]
)
LRESULT = LONG_PTR
HWND_BROADCAST = ctypes.wintypes.HANDLE(0xffff)
WM_SETTINGCHANGE = 0x001A
_SETTINGCHANGE_USER = 0
_SendNotifyMessageW = ctypes.oledll.User32['SendNotifyMessageW']
_SendNotifyMessageW.argtypes = [
ctypes.wintypes.HANDLE,
ctypes.wintypes.UINT,
ctypes.wintypes.WPARAM,
ctypes.wintypes.LPARAM
]
_SendNotifyMessageW.restype = LRESULT
@lambda func: setattr(_SendNotifyMessageW, 'errcheck', func) or func
def errcheck(result, func, args):
if result:
return
else:
raise ctypes.WinError()
def SendNotifyMessageW(hWnd, Msg, wParam, lParam):
if isinstance(lParam, str):
lParam = ctypes.create_unicode_buffer(lParam)
if isinstance(lParam, ctypes.Array):
buf_lParam = lParam
lParam = ctypes.addressof(buf_lParam)
_SendNotifyMessageW(hWnd, Msg, wParam, lParam)
def set_theme(name):
target_value_apps, target_value_system = {
'LIGHT': (1, 1),
'DARK': (0, 0),
'LIGHT_ON_DARK': (1, 0),
'DARK_ON_LIGHT': (0, 1),
}[name]
changed = False
# 1. Update the setting
with winreg.OpenKeyEx(
winreg.HKEY_CURRENT_USER,
'Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize',
0,
winreg.KEY_QUERY_VALUE | winreg.KEY_SET_VALUE
) as reg:
value_apps, _type = winreg.QueryValueEx(reg, 'AppsUseLightTheme')
assert _type == winreg.REG_DWORD
value_system, _type = winreg.QueryValueEx(reg, 'SystemUsesLightTheme')
assert _type == winreg.REG_DWORD
if value_apps != target_value_apps:
winreg.SetValueEx(
reg,
'AppsUseLightTheme',
0,
winreg.REG_DWORD,
target_value_apps
)
changed = True
if value_system != target_value_system:
winreg.SetValueEx(
reg,
'SystemUsesLightTheme',
0,
winreg.REG_DWORD,
target_value_system
)
changed = True
# 2. Notify other windows that they should double-check their color schemes
if changed:
SendNotifyMessageW(
HWND_BROADCAST,
WM_SETTINGCHANGE,
_SETTINGCHANGE_USER,
'ImmersiveColorSet' # https://stackoverflow.com/posts/comments/140898722
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment