Skip to content

Instantly share code, notes, and snippets.

@hhsprings
Last active October 12, 2021 23:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hhsprings/a673ffba3d321529859fd811f082a93e to your computer and use it in GitHub Desktop.
Save hhsprings/a673ffba3d321529859fd811f082a93e to your computer and use it in GitHub Desktop.
A Simple Windows Themepack Builder
# -*- coding: utf-8 -*-
# only for windows (7+)
# require: python 3.x, pillow
"""
This script file is assumed to be managed by three aliases:
* windows_themepack_builder.py
* windows_soundscheme_pcglobal.py
* windows_cursorscheme_pcglobal.py
Hard link should be done:
$ ln windows_themepack_builder.py windows_soundscheme_pcglobal.py
$ ln windows_themepack_builder.py windows_cursorscheme_pcglobal.py
windows_themepack_builder.py:
A Simple Windows Themepack Builder.
windows_soundscheme_pcglobal.py:
A tool that turns a sound schema dedicated to a specific theme into
a sound schema that can be reused regardless of the theme. (Just
for windows.)
windows_cursorscheme_pcglobal.py:
A tool that turns a cursor schema dedicated to a specific theme into
a cursor schema that can be reused regardless of the theme. (Just
for windows.)
"""
import io
import os
import sys
import re
import logging
import shutil
import ctypes
import msilib
import json
import tempfile
import atexit
import functools
import mimetypes
import subprocess
import time
import winreg
import zipfile
import ssl
import urllib.request
import xml.etree.ElementTree as ElementTree
from glob import glob
from collections import OrderedDict
from textwrap import dedent
from urllib.parse import urlsplit
from urllib.request import urlretrieve
from urllib.request import quote as urllib_quote
from configparser import RawConfigParser
from multiprocessing import shared_memory
from PIL import Image
try:
from py7zr import py7zr
except ImportError:
py7zr = None
try:
from unrar import rarfile
except ImportError:
rarfile = None
# ========================================================
#
# Common Part
#
__MYNAME__, _ = os.path.splitext(
os.path.basename(sys.modules[__name__].__file__))
_log = logging.getLogger(__MYNAME__)
_log.setLevel(logging.INFO)
__USER_AGENT__ = "\
Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/91.0.4472.124 Safari/537.36"
_htctxssl = ssl.create_default_context()
_htctxssl.check_hostname = False
_htctxssl.verify_mode = ssl.CERT_NONE
https_handler = urllib.request.HTTPSHandler(context=_htctxssl)
opener = urllib.request.build_opener(https_handler)
opener.addheaders = [('User-Agent', __USER_AGENT__)]
urllib.request.install_opener(opener)
class _chdir(object):
def __init__(self, trgdir):
self._curdir = os.path.abspath(os.curdir)
self._trgdir = os.path.abspath(trgdir)
os.stat(self._trgdir)
_log.info("Entering: %r", self._trgdir)
os.chdir(trgdir)
def __enter__(self):
return self
def __exit__(self, t, v, tb):
_log.info("Leaving : %r", self._trgdir)
os.chdir(self._curdir)
def _remove_silently(fun, *fnl):
for fn in fnl:
try:
fun(fn)
except Exception:
pass
_cbenvs = None
def _pathbackvars(v):
global _cbenvs
if not _cbenvs:
_cbenvs = [
"%TEMP%",
"%ALLUSERSPROFILE%",
"%LOCALAPPDATA%",
"%APPDATA%",
"%COMMONPROGRAMFILES%",
"%PROGRAMDATA%",
"%PROGRAMFILES%",
"%PROGRAMFILES(x86)%",
"%PUBLIC%",
"%USERPROFILE%",
"%WINDIR%"]
_cbenvs = {os.path.expandvars(k).lower(): k for k in _cbenvs}
p = os.path.normpath(v)
for fw, rw in _cbenvs.items():
if p.lower().startswith(fw):
p = rw + p[len(fw):]
break
return p.replace("\\", "/")
def _urlretrieve(ifn):
ifn = ifn.replace("\\", "/")
if not os.path.exists(ifn):
_, ext = os.path.splitext(ifn.lower())
try:
res, _ = urlretrieve(ifn)
except UnicodeError:
scheme, pathpart = "", ifn
m = re.match(r"([a-z]+://)(.*)", ifn)
if m:
scheme, pathpart = m.group(1, 2)
pathpart = "/".join([
urllib_quote(sp, encoding="utf-8")
for sp in pathpart.split("/")])
res, _ = urlretrieve(scheme + pathpart)
ofn = os.path.abspath(res).replace("\\", "/") + ext
if not ext:
try:
# assuming it is image
timg = Image.open(ofn)
ity = re.sub(r"ImageFile$", r"", timg.__class__.__name__).lower()
timg.close()
ext = "." + {"jpeg": "jpg"}.get(ity, ity)
ofn += ext
except Exception:
pass
os.rename(res, ofn)
_log.info("urlretrieve %r -> %r", ifn, _pathbackvars(ofn))
atexit.register(
functools.partial(
_remove_silently, os.unlink, ofn,))
return ofn
return ifn
def _localfile(fn):
scheme, netloc, path, query, fragment = urlsplit(fn)
if len(scheme) == 1 or scheme in ("", "file"):
return os.path.abspath(fn).replace("\\", "/")
try:
return _urlretrieve(fn)
except Exception as e:
_log.error("cannot get %r: %r", fn, e)
def _detect_newfn(base):
nn = base
i = 1
while os.path.exists(nn):
nn = "{}.{}".format(base, i)
i += 1
return nn
def _link(ifn, ofn):
ifnp = _pathbackvars(ifn)
ofnp = _pathbackvars(ofn)
if os.path.exists(ofn):
_log.warning("%r is already existing.", ofnp)
return
try:
os.link(ifn, ofn)
_log.info("ln %r -> %r", ifnp, ofnp)
except Exception:
_log.info("copy %r -> %r", ifnp, ofnp)
shutil.copy(ifn, ofn)
def _is_fn_in_categ(fn, types):
ty, _ = mimetypes.guess_type(fn)
if not ty:
return False
return ty.split("/")[0] in types
def _findsubdir(pardir, subdir):
res = os.path.join(pardir, subdir)
if os.path.exists(res):
return res
cands = list(glob(res))
if len(cands) == 1:
return cands[0]
elif len(cands) > 1:
raise ValueError(
"There are multiple files that match the " +
"specified pattern {}.: {}".format(repr(subdir), repr(cands)))
os.stat(res)
def _looseloadWinConfig(fn, encoding, **cpiniopts):
cpiniopts["strict"] = False # allow duplicating
cpiniopts["allow_no_value"] = True
cp = RawConfigParser(**cpiniopts)
cont = io.open(fn, encoding=encoding).read()
# skip before first section.
m = re.search(r"^\[.*\]\s*$", cont, flags=re.M)
cont = cont[m.span()[0]:]
cp.read_string(cont)
return cp
class _arcunpacker(object):
_dejav = {}
def __init__(self, fn):
self._fn = fn
self._ext = os.path.splitext(fn.lower())[1]
# There are some odd variants such as .curzip, .deskthemepack, etc.
self._is_target = re.search(r"(zip|themepack|cab)$", self._ext) is not None
self._is_target = self._is_target or (
py7zr is not None and self._ext == ".7z")
self._is_target = self._is_target or (
rarfile is not None and self._ext == ".rar")
def is_target(self):
return self._is_target
def unpack(self, cleanimmed=False):
if not self.is_target():
return
if re.search("(themepack|cab)$", self._ext):
d = self._expand_wincab(cleanimmed)
else:
if self._ext == ".7z":
p = py7zr.SevenZipFile
elif self._ext == ".rar":
p = rarfile.RarFile
else:
p = zipfile.ZipFile
d = self._unzip(p, cleanimmed)
return d
def _expand_wincab(self, cleanimmed=False):
if self._fn in self._dejav:
return self._dejav[self._fn]
expand_exe = os.path.join(
os.path.dirname(os.environ.get("COMSPEC", "")), "expand")
td = tempfile.mkdtemp()
if cleanimmed:
atexit.register(functools.partial(shutil.rmtree, td))
subprocess.check_call(
[expand_exe, "-F:*", self._fn, td])
self._dejav[self._fn] = td
return self._dejav[self._fn]
def _unzip(self, unpacker, cleanimmed=False):
if self._fn in self._dejav:
return self._dejav[self._fn]
fn = os.path.abspath(self._fn)
td = os.path.abspath(tempfile.mkdtemp(suffix=__MYNAME__))
if cleanimmed:
atexit.register(functools.partial(shutil.rmtree, td))
bn, _ = os.path.splitext(os.path.basename(self._fn))
tds = os.path.join(td, bn)
if not os.path.exists(tds):
os.mkdir(tds)
with _chdir(tds):
zf = unpacker(fn)
if isinstance(zf, py7zr.SevenZipFile):
from py7zr.callbacks import ExtractCallback
class _U7ZCB(ExtractCallback):
def report_start_preparation(self): pass
def report_start(self, processing_file_path, processing_bytes): pass
def report_end(self, processing_file_path, wrote_bytes):
_log.info("extracted %r", processing_file_path)
def report_warning(self, message):
_log.warning(message)
def report_postprocess(self): pass
zf.extractall(callback=_U7ZCB())
else:
for zi in zf.namelist():
zf.extract(zi)
_log.info("extracted %r", zi)
self._dejav[self._fn] = tds
return self._dejav[self._fn]
_DEPLOYSCR_TMPL = """\
# -*- coding: utf-8 -*-
import os
import sys
import shutil
import logging
from multiprocessing import shared_memory
_log = logging.getLogger()
_log.setLevel(logging.INFO)
_cbenvs = None
def _pathbackvars(v):
global _cbenvs
if not _cbenvs:
_cbenvs = [
"%TEMP%",
"%ALLUSERSPROFILE%",
"%LOCALAPPDATA%",
"%APPDATA%",
"%COMMONPROGRAMFILES%",
"%PROGRAMDATA%",
"%PROGRAMFILES%",
"%PROGRAMFILES(x86)%",
"%PUBLIC%",
"%USERPROFILE%",
"%WINDIR%"]
_cbenvs = {{os.path.expandvars(k).lower(): k for k in _cbenvs}}
p = os.path.normpath(v)
for fw, rw in _cbenvs.items():
if p.lower().startswith(fw):
p = rw + p[len(fw):]
break
return p.replace("\\\\", "/")
def _p(p):
return p.replace(os.path.sep, "/")
def _fix_srcdest(ifn, ofn):
ifn, dest = _p(ifn), _p(ofn)
if dest.endswith("/") or os.path.isdir(dest):
dest = _p(os.path.join(ofn, os.path.basename(ifn)))
destdir = _p(os.path.dirname(dest))
return ifn, dest, destdir
def _link(ifn, ofn):
ifn, dest, destdir = _fix_srcdest(ifn, ofn)
if os.path.exists(dest):
_log.warning("%r is already existing.", _pathbackvars(dest))
return
if not os.path.exists(destdir):
try:
os.makedirs(destdir)
_log.info("create directory: %r", _pathbackvars(destdir))
except Exception as e:
_log.error(
"could not create a directory %r: %r",
_pathbackvars(destdir), e)
try:
os.link(ifn, dest)
_log.info("ln: %r -> %r", _pathbackvars(ifn), _pathbackvars(dest))
except Exception:
try:
shutil.copy(ifn, dest)
_log.info("copy: %r -> %r", _pathbackvars(ifn), _pathbackvars(dest))
except Exception as e:
_log.error(
"copying %r to %r failed: %r",
_pathbackvars(ifn), _pathbackvars(dest), e)
def _unlink(ifn, ofn):
ifn, dest, destdir = _fix_srcdest(ifn, ofn)
if not os.path.exists(dest):
_log.warning("%r doesn't seem to be installed.", _pathbackvars(dest))
elif _p(destdir.lower()) in (_p(os.path.expandvars(
"%SYSTEMROOT%/Media")).lower(), _p(os.path.expandvars(
"%SYSTEMROOT%/Cursors")).lower()):
_log.warning(
"You are trying to modify a content %r in your Windows " +
"system folder. It's not safe for scripts to do this " +
"automatically, so if that's your intention, do it manually.",
_pathbackvars(dest))
else:
try:
os.unlink(dest)
_log.info("unlink: %r", _pathbackvars(dest))
except Exception as e:
_log.error("unlink %r failed: %r", _pathbackvars(dest), e)
if os.path.exists(destdir):
if not os.listdir(destdir):
try:
os.rmdir(destdir)
_log.info("rmdir: %r", _pathbackvars(destdir))
except Exception as e:
_log.error(
"could not remove a directory %r: %r",
_pathbackvars(destdir), e)
if __name__ == '__main__':
shm = shared_memory.SharedMemory(name="{shm_name}")
me = os.path.abspath(sys.argv[0])
fh = logging.FileHandler(me + '.log', encoding="utf-8")
fmt = logging.Formatter("%(asctime)s:%(levelname)-7s:%(process)04x:%(message)s")
fh.setFormatter(fmt)
_log.addHandler(fh)
{cplines}
shm.buf[0] = 1
shm.close()
"""
class _ResourcesPCGlobalizer(object):
def __init__(self, args):
self._mode = "install"
if hasattr(args, "mode"):
self._mode = args.mode
self._shm = shared_memory.SharedMemory(create=True, size=1)
self._shm.buf[0] = 0
def _build_deployscript(self, destdir, files):
srcl = []
dejav = set()
cmd = "_link" if self._mode == "install" else "_unlink"
for fn in files:
if "%systemroot%" not in fn.lower():
s = os.path.expandvars(fn)
if s and s not in dejav:
srcl.append("{}({}, {})".format(
cmd, repr(s), repr(destdir + "/")))
dejav.add(s)
if not srcl:
return
pyfn = tempfile.mktemp(suffix=".py")
with io.open(pyfn, "w", encoding="utf-8") as fo:
fo.write(
_DEPLOYSCR_TMPL.format(
shm_name=self._shm.name,
cplines="\n ".join(srcl)))
return pyfn
def deploy_files(self, destdir, files):
pyfn = self._build_deployscript(
destdir, files)
if not pyfn:
return
#subprocess.check_call(["py", "-3", pyfn])
ctypes.windll.shell32.ShellExecuteW(
None, "runas", sys.executable,
" ".join([pyfn]),
None,
0 # 0: SW_HIDE, 1: SW_NORMAL
)
while not self._shm.buf[0]:
time.sleep(1)
self._shm.close()
self._shm.unlink()
print(io.open(pyfn + ".log", encoding="utf-8").read())
[os.remove(f) for f in glob(pyfn + "*")]
class WindowsThemeConfigBuilder(object):
class _RawConfigParser(RawConfigParser):
def optionxform(self, optionstr):
return optionstr
_kmap = [
("Theme", "Theme"),
(re.compile(r"^CLSID\.*", flags=re.I), "Theme(icons)"), # internal name
(r"Control Panel\Colors", r"Control Panel\Colors"),
(r"Control Panel\Cursors", r"Control Panel\Cursors"),
(r"Control Panel\Desktop", r"Control Panel\Desktop"),
(r"Slideshow", r"Slideshow"),
(r"Control Panel\Desktop\WindowMetrics", r"Control Panel\Desktop\WindowMetrics"),
(r"Metrics", r"Metrics"),
(r"VisualStyles", r"VisualStyles"),
(re.compile(r"^AppEvents\.*", flags=re.I), "Sounds(mapping)"), # internal name
(r"Sounds", "Sounds"),
(r"Boot", "Boot"),
("MasterThemeSelector", "MasterThemeSelector"),
]
_igns = [
("theme", "themeid"),
("slideshow", "imagesrootpidl"),
]
def __init__(self, args):
display_name, inheritfrom = args.theme_name, args.inherit_theme
self._args = args
self._sections = OrderedDict()
for _, sn in self._kmap:
self._sections[sn] = OrderedDict()
if inheritfrom:
for parthm in inheritfrom:
thunp = _arcunpacker(parthm)
if thunp.is_target():
cands = list(glob(os.path.join(thunp.unpack(), "*.theme")))
if cands:
parthm = cands[0]
os.stat(parthm)
self._load(parthm)
self._set("Theme", ("", "DisplayName"), display_name)
self._set("MasterThemeSelector", ("", "MTSM"), "DABJDKT")
def set_brand_image(self, brand_image):
if brand_image:
self._set("Theme", ("", "BrandImage"), brand_image)
def _load(self, theme):
cp = WindowsThemeConfigBuilder._RawConfigParser(
dict_type=OrderedDict)
cp.read([theme])
for sect in cp.sections():
dk = None
sub = ""
for pat, dkcand in self._kmap:
if isinstance(pat, (re.Pattern,)):
if pat.match(sect):
dk, sub = dkcand, sect
break
elif pat.lower() == sect.lower():
dk = dkcand
break
else:
continue
if dk not in self._sections:
self._sections[dk] = OrderedDict()
for k, v in cp.items(sect):
if (sect.lower(), k.lower()) in self._igns:
continue
self._sections[dk][(sub, k)] = v
def _set(self, sec, k=None, v=None):
tdic, tsec = None, None
for s in self._sections.keys():
if s.lower() == sec.lower():
tdic, tsec = self._sections[s], s
break
else:
tdic = self._sections[sec] = OrderedDict()
tsec = sec
if not k:
self._sections[tsec] = OrderedDict()
return
k1, k2 = k
for ko1, ko2 in tdic.keys():
if (ko1.lower(), ko2.lower()) == (k1.lower(), k2.lower()):
tdic[(ko1, ko2)] = v
else:
tdic[(k1, k2)] = v
def _copy_mapping_gen(self, sec, mapping):
for k, v in mapping.items():
self._set(sec, ("", k), v)
def set_visualstyles(self, mapping):
self._copy_mapping_gen(r"VisualStyles", mapping)
def set_cursors(self, scheme_name, mapping):
sec = r"Control Panel\Cursors"
self._copy_mapping_gen(sec, mapping)
self._set(sec, ("", "DefaultValue"), scheme_name)
def set_desktop(self, mapping):
self._copy_mapping_gen(r"Control Panel\Desktop", mapping)
def set_slideshow(
self,
interval,
shuffle="1",
imagesrootpath="DesktopBackground",
rssfeed=""):
sec = "Slideshow"
self._set(sec, ("", "Interval"), interval)
self._set(sec, ("", "Shuffle"), shuffle)
if rssfeed:
m = re.match(r"([a-z]{2,}://)(.*)", rssfeed)
if not m or m.group(1) == "file://":
_log.warning(
"%r: ignored because it is not valid url for RSSFeed.", rssfeed)
rssfeed = ""
if rssfeed:
self._set(sec, ("", "ImagesRootPath"), None)
self._set(sec, ("", "RSSFeed"), rssfeed)
else:
self._set(sec, ("", "ImagesRootPath"), imagesrootpath)
self._set(sec, ("", "RSSFeed"), None)
def set_sounds(self, scheme_name, mapping):
for k, v in mapping.items():
self._set("Sounds(mapping)", (k, "DefaultValue"), v)
self._set("Sounds", ("", "SchemeName"), scheme_name)
def rearrange(self):
if "boot" in list(map(lambda s: s.lower(), self._sections.keys())):
# Screen Savers are deprecated in the Windows 10 Anniversary Update and
# beyond.
if not self._args.keep_boot:
self._set(r"Boot")
if "path" in [k2.lower() for k1, k2 in self._sections["VisualStyles"].keys()]:
# If you supply a path, you should remove the metrics and color sections
# from the .theme file.
if not self._args.keep_metrics:
for dt in ("Metrics", r"Control Panel\Desktop\WindowMetrics"):
self._set(dt)
if not self._args.keep_cp_colors:
self._set(r"Control Panel\Colors")
if "RSSFeed" in [k2.lower() for k1, k2 in self._sections["Slideshow"].keys()]:
# You cannot specify both an RSSFeed and ImagesRootPath.
self._set("Slideshow", ("", "ImagesRootPath"))
def dump(self, fo=sys.stdout):
for sec in self._sections:
if not self._sections[sec]:
continue
if "(" not in sec:
print("[{}]".format(sec), file=fo)
for k1, k2 in self._sections[sec]:
v = self._sections[sec][(k1, k2)]
if v is None:
continue
if k1: # mappings of icon or sound
print("[{}]\n{}={}\n".format(k1, k2, v), file=fo)
else:
print("{}={}".format(k2, v), file=fo)
print("", file=fo)
# ========================================================
#
# CursorPcGlobal Part
#
_CURSOR_STDNAMES = [
"Arrow",
"Help",
"AppStarting",
"Wait",
"NWPen",
"No",
"SizeNS",
"SizeWE",
"Crosshair",
"IBeam",
"SizeNWSE",
"SizeNESW",
"SizeAll",
"UpArrow",
"Hand",
"Pin",
"Person",
]
_CURSOR_WINDOWS_DEFAULT_SCHEMES = { # SchemeSource=0
"Windows Default": OrderedDict(zip(_CURSOR_STDNAMES, [
"aero_arrow.cur", "aero_helpsel.cur", "aero_working.ani", "aero_busy.ani", "", "", "aero_pen.cur",
"aero_unavail.cur", "aero_ns.cur", "aero_ew.cur", "aero_nwse.cur", "aero_nesw.cur", "aero_move.cur",
"aero_up.cur", "aero_link.cur", "aero_pin.cur", "aero_person.cur"])),
"Windows Default (large)": OrderedDict(zip(_CURSOR_STDNAMES, [
"aero_arrow_l.cur", "aero_helpsel_l.cur", "aero_working_l.ani", "aero_busy_l.ani", "", "",
"aero_pen_l.cur", "aero_unavail_l.cur", "aero_ns_l.cur", "aero_ew_l.cur", "aero_nwse_l.cur",
"aero_nesw_l.cur", "aero_move_l.cur",
"aero_up_l.cur", "aero_link_l.cur", "aero_pin_l.cur", "aero_person_l.cur"])),
"Windows Default (extra large)": OrderedDict(zip(_CURSOR_STDNAMES, [
"aero_arrow_xl.cur", "aero_helpsel_xl.cur", "aero_working_xl.ani", "aero_busy_xl.ani", "", "",
"aero_pen_xl.cur", "aero_unavail_xl.cur", "aero_ns_xl.cur", "aero_ew_xl.cur", "aero_nwse_xl.cur",
"aero_nesw_xl.cur", "aero_move_xl.cur",
"aero_up_xl.cur", "aero_link_xl.cur", "aero_pin_xl.cur", "aero_person_xl.cur"])),
"Windows Standard": OrderedDict(zip(_CURSOR_STDNAMES, [
"arrow.cur", "help.cur", "wait.cur", "busy.cur", "cross.cur", "beam.cur", "pen.cur",
"no.cur", "size4.cur", "size3.cur", "size2.cur", "size1.cur", "move.cur", "up.cur",
"", "pin.cur", "person.cur"])),
"Windows Standard (large)": OrderedDict(zip(_CURSOR_STDNAMES, [
"arrow_m.cur", "help_m.cur", "wait_m.cur", "busy_m.cur", "cross_m.cur", "beam_m.cur", "pen_m.cur",
"no_m.cur", "size4_m.cur", "size3_m.cur", "size2_m.cur", "size1_m.cur", "move_m.cur", "up_m.cur",
"", "pin_m.cur", "person_m.cur"])),
"Windows Standard (extra large)": OrderedDict(zip(_CURSOR_STDNAMES, [
"arrow_l.cur", "help_l.cur", "wait_l.cur", "busy_l.cur", "cross_l.cur", "beam_l.cur", "pen_l.cur",
"no_l.cur", "size4_l.cur", "size3_l.cur", "size2_l.cur", "size1_l.cur", "move_l.cur", "up_l.cur",
"", "pin_l.cur", "person_l.cur"])),
"Windows Black": OrderedDict(zip(_CURSOR_STDNAMES, [
"arrow_r.cur", "help_r.cur", "wait_r.cur", "busy_r.cur", "cross_r.cur", "beam_r.cur", "pen_r.cur",
"no_r.cur", "size4_r.cur", "size3_r.cur", "size2_r.cur", "size1_r.cur", "move_r.cur", "up_r.cur",
"", "pin_r.cur", "person_r.cur"])),
"Windows Black (large)": OrderedDict(zip(_CURSOR_STDNAMES, [
"arrow_rm.cur", "help_rm.cur", "wait_rm.cur", "busy_rm.cur", "cross_rm.cur", "beam_rm.cur", "pen_rm.cur",
"no_rm.cur", "size4_rm.cur", "size3_rm.cur", "size2_rm.cur", "size1_rm.cur", "move_rm.cur", "up_rm.cur",
"", "pin_rm.cur", "person_rm.cur"])),
"Windows Black (extra large)": OrderedDict(zip(_CURSOR_STDNAMES, [
"arrow_rl.cur", "help_rl.cur", "wait_rl.cur", "busy_rl.cur", "cross_rl.cur", "beam_rl.cur", "pen_rl.cur",
"no_rl.cur", "size4_rl.cur", "size3_rl.cur", "size2_rl.cur", "size1_rl.cur", "move_rl.cur", "up_rl.cur",
"", "pin_rl.cur", "person_rl.cur"])),
"Windows Inverted": OrderedDict(zip(_CURSOR_STDNAMES, [
"arrow_i.cur", "help_i.cur", "wait_i.cur", "busy_i.cur", "cross_i.cur", "beam_i.cur", "pen_i.cur",
"no_i.cur", "size4_i.cur", "size3_i.cur", "size2_i.cur", "size1_i.cur", "move_i.cur", "up_i.cur",
"aero_link_i.cur", "pin_i.cur", "person_i.cur"])),
"Windows Inverted (large)": OrderedDict(zip(_CURSOR_STDNAMES, [
"arrow_im.cur", "help_im.cur", "wait_im.cur", "busy_im.cur", "cross_im.cur", "beam_im.cur", "pen_im.cur",
"no_im.cur", "size4_im.cur", "size3_im.cur", "size2_im.cur", "size1_im.cur", "move_im.cur", "up_im.cur",
"aero_link_im.cur", "pin_im.cur", "person_im.cur"])),
"Windows Inverted (extra large)": OrderedDict(zip(_CURSOR_STDNAMES, [
"arrow_il.cur", "help_il.cur", "wait_il.cur", "busy_il.cur", "cross_il.cur", "beam_il.cur", "pen_il.cur",
"no_il.cur", "size4_il.cur", "size3_il.cur", "size2_il.cur", "size1_il.cur", "move_il.cur", "up_il.cur",
"aero_link_il.cur", "pin_il.cur", "person_il.cur"])),
"Magnified": OrderedDict(zip(_CURSOR_STDNAMES, [
"larrow.cur", "", "lappstrt.cur", "lwait.cur", "lcross.cur", "libeam.cur", "", "lnodrop.cur", "lns.cur",
"lwe.cur", "lnwse.cur", "lnesw.cur", "lmove.cur", "", "", "lpin.cur", "lperson.cur"])),
}
_EXTODR_MAP = {
".inf": 0,
".crs": 1,
".curtheme": 1,
".theme": 1,
}
def _list_cursors(d):
tmp = []
for fn in os.listdir(d):
_, ext = os.path.splitext(fn.lower())
tmp.append((_EXTODR_MAP.get(ext, 2), os.path.join(d, fn)))
return (fn for _, fn in sorted(tmp))
class WinCursorConfigBuilder(_ResourcesPCGlobalizer):
_nmap = {
"pointer": "Arrow",
"help": "Help",
"work": "AppStarting",
"busy": "Wait",
"cross": "NWPen",
"text": "No",
"hand": "SizeNS",
"unavailiable": "SizeWE",
"unavailable": "SizeWE",
"vert": "Crosshair",
"precision": "Crosshair",
"horz": "IBeam",
"dgn1": "SizeNWSE",
"dgn2": "SizeNESW",
"move": "SizeAll",
"alternate": "UpArrow",
"link": "Hand",
"pin": "Pin", # ??
"person": "Person", # ??
}
def __init__(self, args):
super().__init__(args)
self._SCHEME_NAME = ""
if hasattr(args, "cursor_scheme_name"):
if args.cursor_scheme_name:
self._SCHEME_NAME = args.cursor_scheme_name
elif hasattr(args, "scheme_name"):
if args.scheme_name:
self._SCHEME_NAME = args.scheme_name
self._CUR_DIR = ""
self._mapping = OrderedDict()
for n in _CURSOR_STDNAMES:
self._mapping[n] = ""
self._inf = ""
self._curtheme = ""
self._crs = ""
def update(self, fn):
unp = _arcunpacker(fn)
_, ext = os.path.splitext(fn.lower())
if fn.endswith(".inf"):
if not self._inf:
self._load_inf(fn)
self._inf = os.path.abspath(fn)
elif fn.endswith(".curtheme"):
if not self._inf and not self._curtheme:
self._load_curtheme(fn)
self._curtheme = os.path.abspath(fn)
elif fn.endswith(".crs"):
if not self._inf and not self._crs:
self._load_crs(fn)
self._crs = os.path.abspath(fn)
elif fn.endswith(".theme"):
self._load_theme(fn)
elif unp.is_target():
td = unp.unpack(True)
for fn in _list_cursors(td):
self.update(fn)
elif ext in (".cur", ".ani",):
def _cnt(lhs, rhs):
if not lhs or not rhs:
return False
l = lhs.lower()
r = rhs.lower()
return l in r or r in l
keys = list(self._nmap.items())
for k1, k2 in keys:
k3 = k2.lower().replace("size", "")
k4 = k2.lower().replace("arrow", "")
k5 = k2.lower().replace("nw", "")
ks = [k1, k2, k3, k4, k5]
if any([_cnt(fn, k) for k in ks]):
if not self._mapping[k2]:
self._mapping[k2] = fn
# print(k2, fn)
break
def _load_curtheme(self, fn):
encs = [
"utf-8",
"ISO-8859-1",
sys.getfilesystemencoding()]
et = None
for enc in encs:
try:
cont = io.open(fn, encoding=enc).read()
et = ElementTree.XML(cont)
break
except Exception:
pass
if not et:
return
for ch in list(et.find("CurTheme")):
t = ch.tag
if t not in self._mapping:
continue
if not self._mapping[t]:
self._mapping[t] = ch.attrib["V"]
def _load_inf(self, fn):
cp = _looseloadWinConfig(fn, encoding="iso-8859-1")
for t, v in cp.items("Strings"):
if '"' in v or "'" in v:
v = eval(v.replace("\\", "/"))
t = self._nmap.get(t, t)
if t not in self._mapping:
if t == "scheme_name":
an = "_" + t.upper()
if not self._SCHEME_NAME:
self._SCHEME_NAME = v
elif t == "cur_dir":
if not self._CUR_DIR:
self._CUR_DIR = os.path.relpath(v, "Cursors")
continue
if not self._mapping[t]:
self._mapping[t] = v
def _load_theme(self, fn):
cp = _looseloadWinConfig(fn, encoding="iso-8859-1")
if not cp.has_section(r"Control Panel\Cursors"):
return
if cp.has_section("Theme"):
self._SCHEME_NAME = cp.get("Theme", "displayname")
for t, v in cp.items(r"Control Panel\Cursors"):
for k in self._mapping.keys():
if t == k.lower():
t = k
break
else:
continue
if not self._mapping[t]:
if "%SystemRoot%" in v:
v = os.path.relpath(v, "%SystemRoot%/Cursors")
self._mapping[t] = v
def _load_crs(self, fn):
cp = _looseloadWinConfig(fn, encoding="utf-8-sig")
for k in self._mapping.keys():
kcp = k # kcp = {"Hand": "Hand"}.get(k, k)
if cp.has_section(kcp):
self._mapping[k] = cp.get(kcp, "path")
def _getdestdir(self):
if self._curtheme:
return os.path.dirname(self._curtheme)
elif self._inf:
return os.path.dirname(self._inf)
elif self._crs:
return os.path.dirname(self._crs)
return os.path.abspath(os.path.commonpath([
v for v in self._mapping.values() if v]))
def _getschemename(self):
scheme_name = self._SCHEME_NAME
if not scheme_name:
if self._curtheme:
scheme_name, _ = os.path.splitext(
os.path.basename(self._curtheme))
elif self._crs:
scheme_name, _ = os.path.splitext(
os.path.basename(self._crs))
else:
scheme_name, _ = os.path.splitext(
list(self._mapping.values())[0])
scheme_name = os.path.basename(os.path.dirname(scheme_name))
# if not scheme_name:
# scheme_name = os.path.basename(scheme_name)
return scheme_name
def build_curtheme(self):
if not self._curtheme:
destdir = self._getdestdir()
scheme_name = self._getschemename()
ofn = os.path.join(destdir, scheme_name + ".curtheme")
lines = [
"<?xml version='1.0'?>",
"<Root>",
"<CurTheme>",
] + [
'<{} V="{}"/>'.format(k, v)
for k, v in self._mapping.items() if v] + [
"</CurTheme>",
"</Root>",
]
with io.open(ofn, "w", encoding="utf-8") as fo:
fo.write("\n".join(lines) + "\n")
self._curtheme = ofn
return self._curtheme
def build_inf(self):
destdir = self._getdestdir()
scheme_name = self._getschemename()
if not scheme_name or not scheme_name.strip():
raise ValueError(
"scheme_name is empty! please specify it via --scheme_name")
cur_dir = self._CUR_DIR
if not cur_dir:
cur_dir = scheme_name
cur_dir = os.path.join("Cursors", cur_dir)
lines = [
'[Version]',
'signature="$CHICAGO$"',
'',
'[DefaultInstall]',
'CopyFiles = Scheme.Cur,',
'AddReg = Scheme.Reg',
'',
'[DestinationDirs]',
'Scheme.Cur = 10,"%CUR_DIR%"',
'Scheme.Txt = 10,"%CUR_DIR%"',
'',
]
nmap_r = {v: k for k, v in self._nmap.items()}
lines.append('[Scheme.Reg]')
items = [
(k, v) for k, v in self._mapping.items()
if not (k in ("Pin", "Person") and not v)
]
lines.append(
'HKCU,"Control Panel\Cursors\Schemes","%SCHEME_NAME%",,"' +
",".join(
[(r"%10%\%CUR_DIR%\%{}%".format(nmap_r[k]) if v else "")
for k, v in items]) +
'"')
lines.append(
'HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion\Runonce\Setup\",' +
'"",,"rundll32.exe shell32.dll,Control_RunDLL main.cpl @0"')
lines.append('')
lines.append('[Scheme.Cur]')
for k, v in self._mapping.items():
if v:
lines.append('"{}"'.format(os.path.basename(v)))
lines.append('')
lines.append('[Strings]')
lines.append('CUR_DIR = "{}"'.format(cur_dir))
lines.append('SCHEME_NAME = "{}"'.format(scheme_name))
for k, v in self._mapping.items():
if v:
lines.append('{} = "{}"'.format(
nmap_r[k], os.path.basename(v)))
ofn = os.path.join(destdir, scheme_name + ".inf")
with io.open(ofn, "w", encoding="utf-8") as fo:
fo.write("\n".join(lines) + "\n")
self._inf = ofn
return self._inf
def build_crs(self):
if not self._crs:
destdir = self._getdestdir()
scheme_name = self._getschemename()
lines = []
for k, v in self._mapping.items():
# k = {"Hand": "Hand"}.get(k, k)
if v:
lines.extend([
'[{}]'.format(k),
'Path={}'.format(v),
'',
])
ofn = os.path.join(destdir, scheme_name + ".crs")
with io.open(ofn, "w", encoding="utf-8") as fo:
fo.write("\n".join(lines) + "\n")
self._crs = ofn
return self._crs
def setup_inf(self):
if all([(not v) for v in self._mapping.values()]):
_log.warning("No cursors found.")
return
inf = self.build_inf()
infdir = os.path.dirname(inf)
# I don't know a reasonable approach if RUNDLL32.EXE fails with a file
# path that contains spaces. And apparently we need to give RUNDLL the
# full path to inf. We'll deal with copying the whole thing to a
# temporary folder and running it there.
td = os.path.abspath(tempfile.mkdtemp())
atexit.register(functools.partial(shutil.rmtree, td))
for fn in self._mapping.values():
if fn:
bn = os.path.basename(fn)
_link(
os.path.join(infdir, bn),
os.path.join(td, bn))
ninfbase = __MYNAME__ + ".inf"
ninf = os.path.join(td, ninfbase)
infcont = io.open(inf).read().replace(os.path.basename(inf), ninfbase)
io.open(ninf, "w").write(infcont)
_log.info("replace %r -> %r", inf, ninf)
if self._mode == "install":
with _chdir(td):
cmdl = ["RUNDLL32.EXE", "SETUPAPI.DLL,InstallHinfSection",
"DefaultInstall", "132", ninf]
subprocess.check_call(cmdl)
else:
# In the past, InstallHinfSection had a mode named "DefaultUninstall",
# but starting with Windows 10 version 1903, the DefaultUninstall and
# DefaultUninstall.Services INF sections are prohibited (with exception).
d = os.path.expandvars(
os.path.join("%SYSTEMROOT%/Cursors", self._getschemename()))
self.deploy_files(d, self._mapping.values())
regfn = tempfile.mktemp(suffix=".reg")
regeditscript = [
"REGEDIT4", "",
r"[HKEY_CURRENT_USER\Control Panel\Cursors\Schemes]",
'"{}"=-'.format(self._getschemename())
]
with io.open(regfn, "w", encoding=sys.getfilesystemencoding()) as fo:
fo.write("\n".join(regeditscript) + "\n")
ctypes.windll.shell32.ShellExecuteW(
None, "runas", "regedit",
" ".join([regfn]),
None,
1 # 0: SW_HIDE, 1: SW_NORMAL
)
def from_system_installed(self):
sch = self._getschemename()
if sch in _CURSOR_WINDOWS_DEFAULT_SCHEMES:
self._mapping = _CURSOR_WINDOWS_DEFAULT_SCHEMES[sch]
elif sch:
_SCHEMES = r"Control Panel\Cursors\Schemes"
topk = winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, _SCHEMES)
files, _ = winreg.QueryValueEx(
topk, sch)
self._mapping.update(dict(zip(_CURSOR_STDNAMES, files.split(","))))
def mapping_for_theme(self):
sch = self._getschemename()
if all([(not v) for v in self._mapping.values()]):
return sch, {}
schdir = "" if sch in _CURSOR_WINDOWS_DEFAULT_SCHEMES else sch
res = OrderedDict()
for k, v in self._mapping.items():
if v:
dirnm = os.path.join(
"%SystemRoot%/Cursors", schdir)
try:
# if v is already in dirnm
v = os.path.relpath(v, dirnm)
except ValueError:
pass
curspath = os.path.join(
"%SystemRoot%/Cursors",
schdir, v).replace("/", "\\")
res[k] = curspath
return sch, res
def _cursorscheme_from_folder(folder, args):
curbldr = WinCursorConfigBuilder(args)
for fn in _list_cursors(folder):
curbldr.update(fn)
curbldr.setup_inf()
def themelocal_cursorscheme_to_pcglobal(theme, args):
_, ext = os.path.splitext(theme.lower())
unp = _arcunpacker(theme)
if unp.is_target():
tds = unp.unpack()
if args.dive_zipsubdir:
tds = _findsubdir(tds, args.dive_zipsubdir)
_cursorscheme_from_folder(tds, args)
elif os.path.exists(theme) and os.path.isdir(theme):
_cursorscheme_from_folder(theme, args)
else:
curbldr = WinCursorConfigBuilder(args)
curbldr.update(theme)
curbldr.setup_inf()
def windows_cursorscheme_pcglobal_cuimain():
"""
A tool that turns a cursor schema dedicated to a specific theme into
a cursor schema that can be reused regardless of the theme. (Just
for windows.)
"""
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("theme_or_themepack")
ap.add_argument("--scheme_name")
ap.add_argument(
"--mode",
choices=["install", "uninstall"],
default="install")
ap.add_argument("--dive_zipsubdir")
args = ap.parse_args()
themelocal_cursorscheme_to_pcglobal(args.theme_or_themepack, args)
# ========================================================
#
# SoundSchemePcGlobal Part
#
class WindowsSoundSchemeAsPCGlobal(_ResourcesPCGlobalizer):
def __init__(self, media_subdir, args):
super().__init__(args)
if args.scheme_name:
self._media_subdir = args.scheme_name
else:
self._media_subdir = media_subdir
if not self._media_subdir:
raise ValueError("require scheme_name!")
self._sndlist = []
def add_sound(self, sound, evt):
bn, ext = os.path.splitext(sound)
sound = bn + ext.lower()
if not evt:
_log.warning(
"%r is a sound that is not tied to an event.",
_pathbackvars(sound))
self._sndlist.append((evt, sound))
def has_sound(self):
return len(self._sndlist) > 0
def deploy_sound(self):
if not self.has_sound():
_log.warning("theme has no sounds.")
return
d = os.path.expandvars(
os.path.join("%SYSTEMROOT%/Media", self._media_subdir))
self.deploy_files(d, [snd for _, snd in self._sndlist])
def update_reg(self):
if not self.has_sound():
_log.warning("theme has no sounds.")
return
regeditscript = ["REGEDIT4", ""]
regdelmk = "-" if self._mode == "uninstall" else ""
schmrn = ".{}".format(re.sub(r"\s+", r"", self._media_subdir))
regeditscript.append(
r"[{}HKEY_CURRENT_USER\AppEvents\Schemes\Names\{}]".format(
regdelmk,
schmrn))
if self._mode == "install":
regeditscript.append('@="{}"'.format(self._media_subdir))
regeditscript.append("")
for evt, sound in self._sndlist:
if not evt:
continue
regeditscript.append(
"[{}HKEY_CURRENT_USER\\{}\\{}]".format(regdelmk, evt, schmrn))
if self._mode == "install":
if "%systemroot%" in sound.lower():
path = sound
else:
path = os.path.join(
"%SYSTEMROOT%\\Media",
self._media_subdir, os.path.basename(sound))
path = os.path.expandvars(path).replace(os.path.sep, "\\\\")
regeditscript.append(r'@="{}"'.format(path))
regeditscript.append("")
regfn = tempfile.mktemp(suffix=".reg")
with io.open(regfn, "w", encoding=sys.getfilesystemencoding()) as fo:
fo.write("\n".join(regeditscript) + "\n")
ctypes.windll.shell32.ShellExecuteW(
None, "runas", "regedit",
" ".join([regfn]),
None,
1 # 0: SW_HIDE, 1: SW_NORMAL
)
def _soundscheme_from_themefile(theme, args):
themedir = os.path.dirname(theme)
with io.open(theme, encoding="iso-8859-1") as fi:
sn = ""
cs = ""
msr = re.compile(r"\[(.*)\]")
mir = re.compile(r"([^=]+)\s*=\s*(.*)")
lines = iter(re.split(r"\s*\r?\n", fi.read()))
for line in lines:
ms, mi = msr.match(line), mir.match(line)
if ms:
cs = ms.group(1)
elif mi:
k, v = mi.group(1, 2)
if cs.lower() == "sounds" and k.lower() == "schemename":
sn = v
break
wss = WindowsSoundSchemeAsPCGlobal(sn, args)
for line in lines:
ms, mi = msr.match(line), mir.match(line)
if ms:
cs = ms.group(1)
elif mi:
k, v = mi.group(1, 2)
if cs.lower().startswith("appevents") and k.lower() == "defaultvalue":
if os.path.exists(os.path.join(themedir, v)):
v = os.path.join(themedir, v)
wss.add_sound(v, cs)
if wss.has_sound():
wss.deploy_sound()
wss.update_reg()
else:
_log.warning("theme has no sounds.")
_APPEVENTS_MAPPING_REPLACES = [
(re.compile(r"^windows[\s_-]"), ""),
(re.compile(r"(hardware|usb)"), r"device"),
(re.compile(r"power"), r"battery"),
(re.compile(r"op\-"), "op"),
# common mispells...
(re.compile(r"excla.?mation"), "exclamation"),
(re.compile(r"asterik"), "asterisk"),
]
_APPEVENTS_DEFAULT_MAPPING = {
r"AppEvents\Schemes\Apps\.Default\.Default": [
"default beep",
"default",
"ding",
"background",
],
r"AppEvents\Schemes\Apps\.Default\ChangeTheme": [
"logon",
"startup",
"start",
"change theme",
],
r"AppEvents\Schemes\Apps\.Default\CriticalBatteryAlarm": [
"battery critical",
"critical battery",
"foreground",
],
r"AppEvents\Schemes\Apps\.Default\DeviceConnect": [
"device connect",
"device insert",
"device",
],
r"AppEvents\Schemes\Apps\.Default\DeviceDisconnect": [
"device disconnect",
"device remove",
"device",
],
r"AppEvents\Schemes\Apps\.Default\DeviceFail": [
"device error",
"device fail",
],
r"AppEvents\Schemes\Apps\.Default\FaxBeep": [
"new mail",
"notify email",
"notify",
],
r"AppEvents\Schemes\Apps\.Default\LowBatteryAlarm": [
"battery low",
"low battery",
"background",
],
r"AppEvents\Schemes\Apps\.Default\MailBeep": [
"new mail",
"notify email",
"notify",
"mail beep",
],
r"AppEvents\Schemes\Apps\.Default\Maximize": [
"maximize",
],
r"AppEvents\Schemes\Apps\.Default\MenuCommand": [
"menu command",
],
r"AppEvents\Schemes\Apps\.Default\MenuPopup": [
"menu popup",
],
r"AppEvents\Schemes\Apps\.Default\MessageNudge": [
"message nudge",
],
r"AppEvents\Schemes\Apps\.Default\Minimize": [
"minimize",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Default": [
"notify system generic",
],
r"AppEvents\Schemes\Apps\.Default\Notification.IM": [
"notify messaging",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Alarm": [
"alarm01",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Alarm2": [
"alarm02",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Alarm3": [
"alarm03",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Alarm4": [
"alarm04",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Alarm5": [
"alarm05",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Alarm6": [
"alarm06",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Alarm7": [
"alarm07",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Alarm8": [
"alarm08",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Alarm9": [
"alarm09",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Alarm10": [
"alarm10",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Call": [
"ring01",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Call2": [
"ring02",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Call3": [
"ring03",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Call4": [
"ring04",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Call5": [
"ring05",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Call6": [
"ring06",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Call7": [
"ring07",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Call8": [
"ring08",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Call9": [
"ring09",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Looping.Call10": [
"ring10",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Mail": [
"notify email",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Proximity": [
"proximity notification",
],
r"AppEvents\Schemes\Apps\.Default\Notification.Reminder": [
"notify calendar",
],
r"AppEvents\Schemes\Apps\.Default\Notification.SMS": [
"notify messaging",
],
r"AppEvents\Schemes\Apps\.Default\PrintComplete": [
"print complete",
],
r"AppEvents\Schemes\Apps\.Default\ProximityConnection": [
"proximity connection",
],
r"AppEvents\Schemes\Apps\.Default\RestoreDown": [
"restore down",
"restore",
],
r"AppEvents\Schemes\Apps\.Default\RestoreUp": [
"restore up",
"restore",
],
r"AppEvents\Schemes\Apps\.Default\SystemAsterisk": [
"asterisk",
"background",
"error",
],
r"AppEvents\Schemes\Apps\.Default\SystemExclamation": [
"exclamation",
"background",
],
r"AppEvents\Schemes\Apps\.Default\SystemExit": [
"shutdown",
"exit windows",
"system exit",
],
r"AppEvents\Schemes\Apps\.Default\SystemHand": [
"critical stop",
"foreground",
"system hand",
],
r"AppEvents\Schemes\Apps\.Default\SystemNotification": [
"balloon",
"background",
"system notification",
],
r"AppEvents\Schemes\Apps\.Default\SystemQuestion": [
"system question",
"question sound",
],
r"AppEvents\Schemes\Apps\.Default\WindowsLogoff": [
"logoff sound",
"logoff",
"shutdown",
],
r"AppEvents\Schemes\Apps\.Default\WindowsLogon": [
"logon sound",
"logon",
],
r"AppEvents\Schemes\Apps\.Default\WindowsUAC": [
"uac",
"user account control",
"user account",
],
r"AppEvents\Schemes\Apps\.Default\WindowsUnlock": [
"unlock",
],
r"AppEvents\Schemes\Apps\Explorer\BlockedPopup": [
"popup blocked",
"popup block",
"blocked popup",
"block popup",
],
r"AppEvents\Schemes\Apps\Explorer\EmptyRecycleBin": [
"empty recycle",
"recycle",
],
r"AppEvents\Schemes\Apps\Explorer\FaxError": [
"fax error ding",
],
r"AppEvents\Schemes\Apps\Explorer\FaxLineRings": [
"ringin",
],
r"AppEvents\Schemes\Apps\Explorer\FaxSent": [
"fax sent tada",
"fax sent",
"tada",
"ringout",
],
r"AppEvents\Schemes\Apps\Explorer\FeedDiscovered": [
"feed discovered",
"discover feed",
"discoverfeed",
],
r"AppEvents\Schemes\Apps\Explorer\MoveMenuItem": [
"move menu item",
],
r"AppEvents\Schemes\Apps\Explorer\Navigating": [
"navigation start",
"navigate",
"navigating",
],
r"AppEvents\Schemes\Apps\Explorer\SecurityBand": [
"information bar",
"security band",
],
r"AppEvents\Schemes\Apps\sapisvr\DisNumbersSound": [
"speech disambiguation",
],
r"AppEvents\Schemes\Apps\sapisvr\HubOffSound": [
"speech off",
],
r"AppEvents\Schemes\Apps\sapisvr\HubOnSound": [
"speech on",
],
r"AppEvents\Schemes\Apps\sapisvr\HubSleepSound": [
"speech sleep",
],
r"AppEvents\Schemes\Apps\sapisvr\MisrecoSound": [
"speech misrecognition",
],
r"AppEvents\Schemes\Apps\sapisvr\PanelSound": [
"speech disambiguation",
],
}
def _soundscheme_from_folder(themedir, args):
sn = os.path.basename(os.path.normpath(themedir))
wss = WindowsSoundSchemeAsPCGlobal(sn, args)
for fn in os.listdir(themedir):
if not _is_fn_in_categ(fn, ("audio",)):
continue
bn, ext = os.path.splitext(fn)
ext = ext.lower()
if _is_fn_in_categ(bn, ("audio",)):
# fix strange filename such as ".mp3.wav"
orn = os.path.join(themedir, fn)
nwn = os.path.join(themedir, os.path.splitext(bn)[0] + ext)
if not os.path.exists(nwn):
os.link(orn, nwn)
_log.warning(
"fixed strange filename %r -> %r",
_pathbackvars(orn), _pathbackvars(nwn))
fn = os.path.basename(nwn)
bn, ext = os.path.splitext(fn)
ext = ext.lower()
bns = re.sub(r"(\w)([A-Z][a-z0-9])", r"\1_\2", bn).lower()
for rgx, rep in _APPEVENTS_MAPPING_REPLACES:
bns = rgx.sub(rep, bns)
bns = re.sub(
r"(battery|device|critical|change|print|user|popup)",
r"_\1_", bns)
bns = re.sub(r"[\s_-]\d+$", "", bns)
bns = list(filter(None, re.split(r"[\s_-]+", bns)))
pt = os.path.join(themedir, fn)
t = False
for k, cands in _APPEVENTS_DEFAULT_MAPPING.items():
if any([
lhs in rhs or rhs in lhs
for lhs, rhs in [
("_".join(bns), "_".join(re.split(r"[\s_-]+", c)))
for c in cands]]):
wss.add_sound(pt, k)
t = True
if not t and ext == ".wav":
wss.add_sound(pt, "")
if wss.has_sound():
wss.deploy_sound()
wss.update_reg()
else:
_log.warning("this folder has no sounds.")
def themelocal_soundscheme_to_pcglobal(theme, args):
_, ext = os.path.splitext(theme.lower())
unp = _arcunpacker(theme)
if unp.is_target():
tds = unp.unpack()
if args.dive_zipsubdir:
tds = _findsubdir(tds, args.dive_zipsubdir)
if any([_is_fn_in_categ(f, ("audio",)) for f in os.listdir(tds)]):
themelocal_soundscheme_to_pcglobal(tds, args)
else:
dirs = [f for f in os.listdir(tds) if os.path.isdir(os.path.join(tds, f))]
if len(dirs) == 1: # has root
themelocal_soundscheme_to_pcglobal(
os.path.join(tds, dirs[0]), args)
else:
_log.warning("this folder has no sounds.")
return
elif os.path.exists(theme) and os.path.isdir(theme):
cands = list(glob(os.path.join(theme, "*.theme")))
if cands:
theme = cands[0]
else:
_soundscheme_from_folder(theme, args)
return
_soundscheme_from_themefile(theme, args)
def windows_soundscheme_pcglobal_cuimain():
"""
A tool that turns a sound schema dedicated to a specific theme into
a sound schema that can be reused regardless of the theme. (Just
for windows.)
"""
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("theme_or_themepack")
ap.add_argument("--scheme_name")
ap.add_argument(
"--mode",
choices=["install", "uninstall"],
default="install")
ap.add_argument("--dive_zipsubdir")
args = ap.parse_args()
themelocal_soundscheme_to_pcglobal(args.theme_or_themepack, args)
# ========================================================
#
# Themepack Builder Part
#
class ThemeDesktopBackground(object):
def __init__(self, args):
self._args = args
self._sources = []
def accept(self, ifn):
if not ifn:
return False
_, ext = os.path.splitext(ifn.lower())
if not _is_fn_in_categ(ifn, ("image",)):
return False
try:
im = Image.open(ifn)
except Exception as e:
_log.warning("ignoring %r (%r)", _pathbackvars(ifn), e)
return False
tfs = [] # temp files
if ext not in (
".jpg", ".jpeg", ".bmp",
".dib", ".tif", ".png"):
newext = ".tif" if ext == ".tiff" else ".jpg"
nifn = os.path.abspath(
os.path.splitext(ifn)[0] + newext).replace("\\", "/")
im.save(nifn)
_log.info(
"converted: %r -> %r",
_pathbackvars(ifn), _pathbackvars(nifn))
tfs.append(nifn)
ifn = nifn
#
orig_width, orig_height = width, height = im.size
ar = (orig_width / float(orig_height))
for ignf in json.loads(self._args.image_ignore_size_filter):
if eval("{} {} {}".format(*re.split(r"\s*,\s*", ignf))):
_log.warning(
"ignoring %r (%r matches %r)",
_pathbackvars(ifn), im.size, ignf)
return False
for cond in json.loads(self._args.image_resize_filter):
# "height,<,480"
t, op, v = re.split(r"\s*,\s*", cond)
if eval("{} {} {}".format(t, op, v)):
if t == "height":
height = int(v)
else: # width
width = int(v)
if height != orig_height:
width = int(height * ar)
else:
height = int(width * (1 / ar))
im = im.resize((width, height))
nifn = tempfile.mktemp(suffix=ext)
tfs.append(os.path.abspath(nifn))
_log.info(
"resized: %r (%r) -> %r (%r)",
_pathbackvars(ifn),
(orig_width, orig_height),
_pathbackvars(nifn),
im.size)
im.save(nifn)
ifn, (orig_width, orig_height) = nifn, (width, height)
break
if tfs:
atexit.register(
functools.partial(_remove_silently, os.unlink, *tfs,))
#
self._sources.append(ifn)
def export(self, destdir):
ofnlist = []
for ifn in self._sources:
# we can't use slash as path delimitor...
ifnbn = os.path.basename(ifn)
_, ext = os.path.splitext(ifnbn)
ofn = os.path.join(
destdir, "{}{}".format(len(ofnlist), ext))
_link(ifn, ofn)
ofnlist.append(ofn)
return ofnlist[0], ofnlist
def _enum_appevents_by_schemename(schemename):
_NAMES = r"AppEvents\Schemes\Names"
topk = winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, _NAMES)
idx = 0
schnm = None
while not schnm:
try:
n1 = n = winreg.EnumKey(topk, idx)
v = winreg.QueryValue(topk, n)
if n.startswith("."):
n1 = n[1:]
if schemename in (n, n1, v):
schnm = n
break
idx += 1
except OSError:
break
topk.Close()
if schnm:
#
_APPS = r"AppEvents\Schemes\Apps"
topk = winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, _APPS)
idx_o = 0
while True:
try:
app = winreg.EnumKey(topk, idx_o) # .Default, devenv, Explorer, sapisvr, ...
appk = winreg.OpenKeyEx(topk, app)
idx_o += 1
idx_i = 0
while True:
try:
evt = winreg.EnumKey(appk, idx_i)
sndk = winreg.OpenKeyEx(appk, evt + "\\" + schnm)
snd, _ = winreg.QueryValueEx(sndk, "")
if snd:
yield (
"{}".format(_APPS + "\\" + app + "\\" + evt),
_pathbackvars(snd))
sndk.Close()
idx_i += 1
except OSError:
break
appk.Close()
except OSError:
break
topk.Close()
def _to_brandimage(fn, destdir):
fn = _localfile(fn)
base, ext = os.path.splitext(fn)
if ext.lower() != ".png":
img = Image.open(fn)
ofn = os.path.join(destdir, os.path.basename(base + ".png"))
img.save(ofn)
_log.info("converted: %r -> %r", fn, ofn)
else:
ofn = os.path.join(destdir, os.path.basename(fn))
_link(fn, ofn)
return ofn
def windows_themepack_builder_main(args):
thmbld = WindowsThemeConfigBuilder(args)
vs = json.loads(args.visual_style)
if args.extend_visual_style:
for st in args.extend_visual_style:
vs.update(json.loads(st))
thmbld.set_visualstyles(vs)
#
dbgcont = ThemeDesktopBackground(args)
curbldr = WinCursorConfigBuilder(args)
lfns = args.desktopimages_listfile
if not lfns:
lfns = ["-"]
for lfn in lfns:
fi = sys.stdin if lfn == "-" else io.open(lfn, encoding="utf-8")
for fn in filter(None, re.split(r"\s*\r?\n", fi.read())):
fn = _localfile(fn)
dbgcont.accept(fn)
#
wkdir = _detect_newfn(".wk")
wkdir = os.path.abspath(wkdir).replace("\\", "/")
cwd = os.path.abspath(os.curdir).replace("\\", "/")
dbdir = os.path.join(wkdir, "DesktopBackground")
os.makedirs(dbdir)
#
brand_image = ""
if args.brand_image:
brand_image = os.path.relpath(_to_brandimage(args.brand_image, wkdir), wkdir)
#
atexit.register(
functools.partial(_remove_silently, shutil.rmtree, wkdir))
thmbld.set_brand_image(brand_image)
#
curbldr.from_system_installed()
csch, cma = curbldr.mapping_for_theme()
if csch and cma:
thmbld.set_cursors(csch, cma)
#
if args.sound_scheme_name:
sndma = OrderedDict()
for evt, snd in _enum_appevents_by_schemename(args.sound_scheme_name):
sndma[evt] = snd
thmbld.set_sounds(args.sound_scheme_name, sndma)
#
with _chdir(wkdir):
_, ofnlist = dbgcont.export("DesktopBackground")
thmbld.set_desktop(
OrderedDict(
Wallpaper=ofnlist[0],
TileWallpaper=args.tile_wallpaper,
WallpaperStyle=args.wallpaper_style,
Pattern="",
MultimonBackgrounds=0,
))
thmbld.set_slideshow(
interval=args.slideshow_interval_sec * 1000,
rssfeed=args.slideshow_rssfeed)
theme_fn = args.theme_name + ".theme"
with io.open(theme_fn, "w", encoding="utf-8") as fo:
thmbld.rearrange()
thmbld.dump(fo)
#
tn = "{theme_name}.themepack".format(theme_name=args.theme_name)
dtn = os.path.join(cwd, tn)
arfiles = []
for root, _, files in os.walk("."):
arfiles.extend([
(fn, fn)
for fn in [os.path.normpath(os.path.join(root, f)) for f in files]])
msilib.FCICreate(dtn, arfiles)
_log.info("built %r", _pathbackvars(tn))
if not args.no_shellexecute:
shellexecute = ctypes.windll.shell32.ShellExecuteW
_log.info("executing %r", _pathbackvars(dtn))
shellexecute(
0,
"open",
dtn,
"",
"",
1) # 1: SW_SHOWNORMAL
shellexecute(
0,
"open",
os.path.expandvars("%USERPROFILE%/AppData/Local/Microsoft/Windows/Themes"),
"",
"",
1) # 1: SW_SHOWNORMAL
def windows_themepack_builder_cuimain():
"""
A Simple Windows Themepack Builder.
The main responsibility of this script for the generated Windows theme is
the desktop background image. The image used for the desktop background can
be specified by giving a file list to the script. Besides, you can give a
cursor and a sound. This script doesn't "directly" support everything that
the "windows theme" supports, but actually you can control it all. For example,
if you want to include [Control Panel\\Colors] in the theme, prepare a ".theme"
file that includes it in advance, and then inherit it with "--inherit_theme".
For the ".theme" specification, see:
https://docs.microsoft.com/en-us/windows/win32/controls/themesfileformat-overview
"""
import argparse
ap = argparse.ArgumentParser(
description=windows_themepack_builder_cuimain.__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("theme_name")
ap.add_argument("desktopimages_listfile", nargs="*")
ap.add_argument("--brand_image")
ap.add_argument("--inherit_theme", action="append")
vstyle_default = dict(
Path="%SystemRoot%\\resources\\themes\\Aero\\Aero.msstyles",
ColorStyle="NormalColor",
Size="NormalSize")
ap.add_argument(
"--visual_style",
default=json.dumps(vstyle_default),
help="settings for 'VisualStyles'. default=%(default)s")
ap.add_argument(
"--extend_visual_style",
help="additional settings for 'VisualStyles'", action="append")
ap.add_argument(
"--keep_cp_colors",
action="store_true",
help="keep [Control Panel\\Colors]")
ap.add_argument(
"--keep_metrics",
action="store_true",
help="keep [Metrics], and [Control Panel\\Desktop\\WindowMetrics]")
ap.add_argument(
"--keep_boot",
action="store_true",
help="keep [Boot]")
ap.add_argument("--tile_wallpaper", choices=["0", "1"], default="0")
# tile_wallpaper:
# 0: The wallpaper picture should not be tiled
# 1: The wallpaper picture should be tiled
ap.add_argument("--wallpaper_style", choices=["0", "2", "6", "10"], default="0")
# wallpaper_style:
# 0: The image is centered if TileWallpaper=0 or tiled if TileWallpaper=1
# 2: The image is stretched to fill the screen
# 6: The image is resized to fit the screen while maintaining the aspect
# ratio. (Windows 7 and later)
# 10: The image is resized and cropped to fill the screen while maintaining
# the aspect ratio. (Windows 7 and later)
ap.add_argument("--slideshow_interval_sec", type=int, default=60)
ap.add_argument("--slideshow_rssfeed", help="for example: http://feeds.feedburner.com/bingimages")
ap.add_argument("--no_shellexecute", action="store_true")
image_ignore_size_filter_default = [
"height,<,270"
]
ap.add_argument(
"--image_ignore_size_filter",
default=json.dumps(image_ignore_size_filter_default))
image_resize_filter_default = [
"height,<,{}".format(i * 10 * 9)
for i in range(6, 13, 2)] + ["height,>,1080"]
ap.add_argument(
"--image_resize_filter",
default=json.dumps(image_resize_filter_default))
ap.add_argument("--cursor_scheme_name")
ap.add_argument("--sound_scheme_name")
args = ap.parse_args()
windows_themepack_builder_main(args)
# ========================================================
#
# run module as main
#
if __name__ == '__main__':
loglevel = logging.INFO
logging.basicConfig(
stream=sys.stderr,
level=loglevel,
format="%(levelname)-5s:%(process)04x %(name)s(%(lineno)d):%(message)s")
if __MYNAME__.endswith("themepack_builder"):
windows_themepack_builder_cuimain()
elif __MYNAME__.endswith("soundscheme_pcglobal"):
windows_soundscheme_pcglobal_cuimain()
elif __MYNAME__.endswith("cursorscheme_pcglobal"):
windows_cursorscheme_pcglobal_cuimain()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment