Last active
October 12, 2021 23:34
-
-
Save hhsprings/a673ffba3d321529859fd811f082a93e to your computer and use it in GitHub Desktop.
A Simple Windows Themepack Builder
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
# -*- 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