Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bb280b1da | |||
| 533c533034 | |||
| 3120638ef3 | |||
| 2f78fa0527 | |||
| b72de8e28f |
6
TODO.md
6
TODO.md
@@ -1,5 +1 @@
|
|||||||
- Push scheme generation to `~/.config/autoconf`
|
- Add local app caching for `auto` logic
|
||||||
- Copy default app registry to the config location
|
|
||||||
- Formalize the theme spec (JSON) and `autoconf gen` to produce color configs
|
|
||||||
- Likely need to formalize the `sync.sh` script logic better, possibly associated directly
|
|
||||||
with registry TOML
|
|
||||||
|
|||||||
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line, and also
|
||||||
|
# from the environment for the first two.
|
||||||
|
SPHINXOPTS ?=
|
||||||
|
SPHINXBUILD ?= sphinx-build
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
@@ -28,6 +28,9 @@ dependencies = [
|
|||||||
"colorama",
|
"colorama",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
symconf = "symconf.__main__:main"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
tests = ["pytest"]
|
tests = ["pytest"]
|
||||||
docs = [
|
docs = [
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from symconf.config import ConfigManager
|
from symconf.config import ConfigManager
|
||||||
|
from symconf.reader import DictReader
|
||||||
|
|
||||||
from symconf import config
|
from symconf import config
|
||||||
|
from symconf import reader
|
||||||
from symconf import theme
|
from symconf import theme
|
||||||
from symconf import util
|
from symconf import util
|
||||||
|
|||||||
@@ -92,11 +92,13 @@ subparsers = parser.add_subparsers(title='subcommand actions')
|
|||||||
add_set_subparser(subparsers)
|
add_set_subparser(subparsers)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
def main():
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
print(args)
|
|
||||||
|
|
||||||
if 'func' in args:
|
if 'func' in args:
|
||||||
args.func(args)
|
args.func(args)
|
||||||
else:
|
else:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import json
|
import json
|
||||||
import inspect
|
import inspect
|
||||||
import tomllib
|
import tomllib
|
||||||
@@ -9,7 +10,10 @@ from pathlib import Path
|
|||||||
from colorama import Fore, Back, Style
|
from colorama import Fore, Back, Style
|
||||||
|
|
||||||
from symconf import util
|
from symconf import util
|
||||||
|
from symconf.reader import DictReader
|
||||||
|
|
||||||
|
def y(t):
|
||||||
|
return Style.RESET_ALL + Fore.YELLOW + t + Style.RESET_ALL
|
||||||
|
|
||||||
class ConfigManager:
|
class ConfigManager:
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -81,34 +85,12 @@ class ConfigManager:
|
|||||||
|
|
||||||
self.app_registry = app_registry.get('app', {})
|
self.app_registry = app_registry.get('app', {})
|
||||||
|
|
||||||
def _resolve_scheme(self, scheme):
|
def _resolve_group(self, group, value='auto'):
|
||||||
# if scheme == 'auto':
|
if value == 'auto':
|
||||||
# os_cmd_groups = {
|
# look group up in app cache and set to current value
|
||||||
# 'Linux': (
|
|
||||||
# "gsettings get org.gnome.desktop.interface color-scheme",
|
|
||||||
# lambda r: r.split('-')[1][:-1],
|
|
||||||
# ),
|
|
||||||
# 'Darwin': (),
|
|
||||||
# }
|
|
||||||
|
|
||||||
# osname = os.uname().sysname
|
|
||||||
# os_group = os_cmd_groups.get(osname, [])
|
|
||||||
|
|
||||||
# for cmd in cmd_list:
|
|
||||||
# subprocess.check_call(cmd.format(scheme=scheme).split())
|
|
||||||
|
|
||||||
# return scheme
|
|
||||||
|
|
||||||
if scheme == 'auto':
|
|
||||||
return 'any'
|
return 'any'
|
||||||
|
|
||||||
return scheme
|
return value
|
||||||
|
|
||||||
def _resolve_palette(self, palette):
|
|
||||||
if palette == 'auto':
|
|
||||||
return 'any'
|
|
||||||
|
|
||||||
return palette
|
|
||||||
|
|
||||||
def app_config_map(self, app_name) -> dict[str, Path]:
|
def app_config_map(self, app_name) -> dict[str, Path]:
|
||||||
'''
|
'''
|
||||||
@@ -134,14 +116,10 @@ class ConfigManager:
|
|||||||
'''
|
'''
|
||||||
# first look in "generated", then overwrite with "user"
|
# first look in "generated", then overwrite with "user"
|
||||||
file_map = {}
|
file_map = {}
|
||||||
app_dir = Path(self.apps_dir, app_name)
|
user_app_dir = Path(self.apps_dir, app_name, 'user')
|
||||||
for subdir in ['generated', 'user']:
|
|
||||||
subdir_path = Path(app_dir, subdir)
|
|
||||||
|
|
||||||
if not subdir_path.is_dir():
|
if user_app_dir.is_dir():
|
||||||
continue
|
for conf_file in user_app_dir.iterdir():
|
||||||
|
|
||||||
for conf_file in subdir_path.iterdir():
|
|
||||||
file_map[conf_file.name] = conf_file
|
file_map[conf_file.name] = conf_file
|
||||||
|
|
||||||
return file_map
|
return file_map
|
||||||
@@ -220,6 +198,9 @@ class ConfigManager:
|
|||||||
prefix_order=None,
|
prefix_order=None,
|
||||||
strict=False,
|
strict=False,
|
||||||
):
|
):
|
||||||
|
'''
|
||||||
|
Find and return matches along the "match trajectory."
|
||||||
|
'''
|
||||||
file_parts = self._get_file_parts(pathnames)
|
file_parts = self._get_file_parts(pathnames)
|
||||||
|
|
||||||
if prefix_order is None:
|
if prefix_order is None:
|
||||||
@@ -230,18 +211,170 @@ class ConfigManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
ordered_matches = []
|
ordered_matches = []
|
||||||
for palette_prefix, scheme_prefix in prefix_order:
|
for i, (palette_prefix, scheme_prefix) in enumerate(prefix_order):
|
||||||
for theme_part, conf_part, pathname in file_parts:
|
for theme_part, conf_part, pathname in file_parts:
|
||||||
theme_split = theme_part.split('-')
|
theme_split = theme_part.split('-')
|
||||||
palette_part, scheme_part = '-'.join(theme_split[:-1]), theme_split[-1]
|
scheme_part = theme_split[-1]
|
||||||
|
palette_part = '-'.join(theme_split[:-1])
|
||||||
|
|
||||||
palette_match = palette_prefix == palette_part or palette_prefix == 'any'
|
palette_match = palette_prefix == palette_part or palette_prefix == 'any'
|
||||||
scheme_match = scheme_prefix == scheme_part or scheme_prefix == 'any'
|
scheme_match = scheme_prefix == scheme_part or scheme_prefix == 'any'
|
||||||
if palette_match and scheme_match:
|
if palette_match and scheme_match:
|
||||||
ordered_matches.append((conf_part, theme_part, pathname))
|
ordered_matches.append((conf_part, theme_part, pathname, i+1))
|
||||||
|
|
||||||
return ordered_matches
|
return ordered_matches
|
||||||
|
|
||||||
|
def _get_relaxed_set(
|
||||||
|
self,
|
||||||
|
match_list
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Mostly to filter "any" matches, latching onto a particular result and getting
|
||||||
|
only its relaxed variants.
|
||||||
|
|
||||||
|
Note that palette-scheme files can be named ``<variant>-<palette>-<scheme>``
|
||||||
|
'''
|
||||||
|
if not match_list:
|
||||||
|
return []
|
||||||
|
|
||||||
|
match = match_list[-1]
|
||||||
|
theme_split = match[1].split('-')
|
||||||
|
palette_tgt, scheme_tgt = '-'.join(theme_split[:-1]), theme_split[-1]
|
||||||
|
|
||||||
|
relaxed_map = {}
|
||||||
|
for conf_part, theme_part, pathname, idx in match_list:
|
||||||
|
#theme_split = theme_part.split('-')[::-1]
|
||||||
|
#scheme_part = theme_split[0]
|
||||||
|
#palette_part = theme_split[1]
|
||||||
|
theme_split = theme_part.split('-')
|
||||||
|
scheme_part = theme_split[-1]
|
||||||
|
palette_part = '-'.join(theme_split[:-1])
|
||||||
|
#pvar_part = '-'.join(theme_split[2:])
|
||||||
|
|
||||||
|
palette_match = palette_part == palette_tgt or palette_part == 'none'
|
||||||
|
scheme_match = scheme_part == scheme_tgt or scheme_part == 'none'
|
||||||
|
|
||||||
|
if palette_match and scheme_match:
|
||||||
|
relaxed_map[pathname] = (conf_part, theme_part, pathname, idx)
|
||||||
|
|
||||||
|
return list(relaxed_map.values())
|
||||||
|
|
||||||
|
def _stack_toml(
|
||||||
|
self,
|
||||||
|
path_list
|
||||||
|
):
|
||||||
|
stacked_dict = {}
|
||||||
|
for toml_path in path_list:
|
||||||
|
updated_map = tomllib.load(toml_path.open('rb'))
|
||||||
|
stacked_dict = util.deep_update(stacked_dict, updated_map)
|
||||||
|
|
||||||
|
return stacked_dict
|
||||||
|
|
||||||
|
def template_fill(
|
||||||
|
self,
|
||||||
|
template_str : str,
|
||||||
|
template_dict : dict,
|
||||||
|
pattern : str = r'f{{(\S+)}}',
|
||||||
|
):
|
||||||
|
dr = DictReader.from_dict(template_dict)
|
||||||
|
return re.sub(
|
||||||
|
pattern,
|
||||||
|
lambda m:str(dr.get(m.group(1))),
|
||||||
|
template_str
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_matching_group_dict(
|
||||||
|
self,
|
||||||
|
scheme='auto',
|
||||||
|
palette='auto',
|
||||||
|
**kw_groups,
|
||||||
|
) -> dict:
|
||||||
|
'''
|
||||||
|
Note that "strictness" doesn't really apply in this setting. In the config
|
||||||
|
matching setting, setting strict means there's no relaxation to "none," but here,
|
||||||
|
any "none" group files just help fill any gaps (but are otherwise totally
|
||||||
|
overwritten, even if matched, by more precise matches). You can match ``nones``
|
||||||
|
directly if you want as well. ``get_matching_scripts()`` is similar in this sense.
|
||||||
|
'''
|
||||||
|
scheme = self._resolve_group('scheme', scheme)
|
||||||
|
palette = self._resolve_group('palette', palette)
|
||||||
|
groups = {
|
||||||
|
k : self._resolve_group(k, v)
|
||||||
|
for k, v in kw_groups.items()
|
||||||
|
}
|
||||||
|
# palette lookup will behave like other groups
|
||||||
|
groups['palette'] = palette.split('-')[-1]
|
||||||
|
|
||||||
|
group_dir = Path(self.config_dir, 'groups')
|
||||||
|
if not group_dir.exists():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# handle non-palette-scheme groups
|
||||||
|
group_matches = {}
|
||||||
|
for fkey, fval in groups.items():
|
||||||
|
key_group_dir = Path(group_dir, fkey)
|
||||||
|
|
||||||
|
if not key_group_dir.exists():
|
||||||
|
print(f'Group directory {fkey} doesn\'t exist, skipping')
|
||||||
|
continue
|
||||||
|
|
||||||
|
# mirror matching scheme: 1) prefix order, 2) full enumeration, 3) select
|
||||||
|
# best, 4) make unique, 5) ordered relaxation
|
||||||
|
stem_map = {path.stem : path for path in key_group_dir.iterdir()}
|
||||||
|
|
||||||
|
if fval == 'any':
|
||||||
|
prefix_order = [fval, 'none']
|
||||||
|
else:
|
||||||
|
prefix_order = ['none', fval]
|
||||||
|
|
||||||
|
matches = []
|
||||||
|
for prefix in prefix_order:
|
||||||
|
for stem in stem_map:
|
||||||
|
if prefix == stem or prefix == 'any':
|
||||||
|
matches.append(stem)
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
# no matches for group, skip
|
||||||
|
continue
|
||||||
|
|
||||||
|
match_dict = {}
|
||||||
|
tgt = matches[-1] # select best based on order, make new target
|
||||||
|
for stem in matches:
|
||||||
|
if stem == tgt or stem == 'none':
|
||||||
|
match_dict[stem] = stem_map[stem]
|
||||||
|
|
||||||
|
group_matches[fkey] = list(match_dict.values())
|
||||||
|
|
||||||
|
# first handle scheme maps; matching palette files should already be found in the
|
||||||
|
# regular group matching process
|
||||||
|
palette_dict = self._stack_toml(group_matches.get('palette', []))
|
||||||
|
|
||||||
|
# then palette-scheme groups (require 2-combo logic)
|
||||||
|
scheme_group_dir = Path(group_dir, 'scheme')
|
||||||
|
scheme_pathnames = [path.name for path in scheme_group_dir.iterdir()]
|
||||||
|
ordered_matches = self.match_pathnames(
|
||||||
|
scheme_pathnames,
|
||||||
|
scheme,
|
||||||
|
palette,
|
||||||
|
)
|
||||||
|
relaxed_matches = self._get_relaxed_set(ordered_matches)
|
||||||
|
|
||||||
|
scheme_dict = {}
|
||||||
|
for conf_part, theme_part, toml_path, _ in relaxed_matches:
|
||||||
|
toml_str = Path(scheme_group_dir, toml_path).open('r').read()
|
||||||
|
filled_toml = self.template_fill(toml_str, palette_dict)
|
||||||
|
|
||||||
|
toml_dict = tomllib.loads(filled_toml)
|
||||||
|
scheme_dict = util.deep_update(scheme_dict, toml_dict)
|
||||||
|
|
||||||
|
template_dict = {
|
||||||
|
group : self._stack_toml(ordered_matches)
|
||||||
|
for group, ordered_matches in group_matches.items()
|
||||||
|
}
|
||||||
|
template_dict['scheme'] = scheme_dict
|
||||||
|
|
||||||
|
return template_dict, relaxed_matches
|
||||||
|
|
||||||
def get_matching_configs(
|
def get_matching_configs(
|
||||||
self,
|
self,
|
||||||
app_name,
|
app_name,
|
||||||
@@ -310,8 +443,8 @@ class ConfigManager:
|
|||||||
'''
|
'''
|
||||||
app_dir = Path(self.apps_dir, app_name)
|
app_dir = Path(self.apps_dir, app_name)
|
||||||
|
|
||||||
scheme = self._resolve_scheme(scheme)
|
scheme = self._resolve_group('scheme', scheme)
|
||||||
palette = self._resolve_palette(palette)
|
palette = self._resolve_group('palette', palette)
|
||||||
|
|
||||||
app_config_map = self.app_config_map(app_name)
|
app_config_map = self.app_config_map(app_name)
|
||||||
|
|
||||||
@@ -323,11 +456,35 @@ class ConfigManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
matching_file_map = {}
|
matching_file_map = {}
|
||||||
for conf_part, theme_part, pathname in ordered_matches:
|
for conf_part, theme_part, pathname, idx in ordered_matches:
|
||||||
matching_file_map[conf_part] = app_config_map[pathname]
|
matching_file_map[conf_part] = (app_config_map[pathname], idx)
|
||||||
|
|
||||||
return matching_file_map
|
return matching_file_map
|
||||||
|
|
||||||
|
def get_matching_templates(
|
||||||
|
self,
|
||||||
|
app_name,
|
||||||
|
scheme='auto',
|
||||||
|
palette='auto',
|
||||||
|
**kw_groups,
|
||||||
|
) -> dict:
|
||||||
|
template_dict, relaxed_matches = self.get_matching_group_dict(
|
||||||
|
scheme=scheme,
|
||||||
|
palette=palette,
|
||||||
|
**kw_groups,
|
||||||
|
)
|
||||||
|
max_idx = 0
|
||||||
|
if relaxed_matches:
|
||||||
|
max_idx = max([m[3] for m in relaxed_matches])
|
||||||
|
|
||||||
|
template_map = {}
|
||||||
|
template_dir = Path(self.apps_dir, app_name, 'templates')
|
||||||
|
if template_dir.is_dir():
|
||||||
|
for template_file in template_dir.iterdir():
|
||||||
|
template_map[template_file.name] = template_file
|
||||||
|
|
||||||
|
return template_map, template_dict, relaxed_matches, max_idx
|
||||||
|
|
||||||
def get_matching_scripts(
|
def get_matching_scripts(
|
||||||
self,
|
self,
|
||||||
app_name,
|
app_name,
|
||||||
@@ -372,16 +529,19 @@ class ConfigManager:
|
|||||||
palette,
|
palette,
|
||||||
prefix_order=prefix_order
|
prefix_order=prefix_order
|
||||||
)
|
)
|
||||||
|
relaxed_matches = self._get_relaxed_set(ordered_matches)
|
||||||
|
|
||||||
# flip list to execute by decreasing specificity
|
# flip list to execute by decreasing specificity
|
||||||
return list(dict.fromkeys(map(lambda x:Path(call_dir, x[2]), ordered_matches)))[::-1]
|
return list(map(lambda x:Path(call_dir, x[2]), relaxed_matches))[::-1]
|
||||||
|
|
||||||
def update_app_config(
|
def update_app_config(
|
||||||
self,
|
self,
|
||||||
app_name,
|
app_name,
|
||||||
app_settings = None,
|
app_settings = None,
|
||||||
|
strict = False,
|
||||||
scheme = 'any',
|
scheme = 'any',
|
||||||
palette = 'any',
|
palette = 'any',
|
||||||
|
**kw_groups,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Perform full app config update process, applying symlinks and running scripts.
|
Perform full app config update process, applying symlinks and running scripts.
|
||||||
@@ -403,12 +563,54 @@ class ConfigManager:
|
|||||||
print(f'App "{app_name}" incorrectly configured, skipping')
|
print(f'App "{app_name}" incorrectly configured, skipping')
|
||||||
return
|
return
|
||||||
|
|
||||||
to_symlink: list[tuple[Path, Path]] = []
|
# merge templates and user-provided configs
|
||||||
file_map = self.get_matching_configs(
|
template_map, template_dict, template_matches, tidx = self.get_matching_templates(
|
||||||
app_name,
|
app_name,
|
||||||
scheme=scheme,
|
scheme=scheme,
|
||||||
palette=palette,
|
palette=palette,
|
||||||
|
**kw_groups
|
||||||
)
|
)
|
||||||
|
# set file map to user configs if yields a strictly better match
|
||||||
|
config_map = self.get_matching_configs(
|
||||||
|
app_name,
|
||||||
|
scheme=scheme,
|
||||||
|
palette=palette,
|
||||||
|
strict=strict,
|
||||||
|
)
|
||||||
|
|
||||||
|
# tuples of 1) full paths and 2) whether to fill template
|
||||||
|
generated_path = Path(self.apps_dir, app_name, 'generated')
|
||||||
|
generated_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# flatten matches
|
||||||
|
generated_paths = [m[2] for m in template_matches]
|
||||||
|
generated_config = {}
|
||||||
|
|
||||||
|
file_map = {}
|
||||||
|
for tail, full_path in template_map.items():
|
||||||
|
# use config only if strictly better match
|
||||||
|
# the P-S match forms rules the match quality; if additional args from
|
||||||
|
# templates (e.g. "font") match available groups but there is still a better
|
||||||
|
# P-S match in "user/", it will beat out the template (b/c the "user" config
|
||||||
|
# is concrete). If they're on the same level, prefer the template match for
|
||||||
|
# flexibility (guarantees same P-S match and extra group customization).
|
||||||
|
if tail in config_map and config_map[tail][1] > tidx:
|
||||||
|
file_map[tail] = config_map[tail][0]
|
||||||
|
else:
|
||||||
|
template_str = full_path.open('r').read()
|
||||||
|
filled_template = self.template_fill(template_str, template_dict)
|
||||||
|
|
||||||
|
config_path = Path(generated_path, tail)
|
||||||
|
config_path.write_text(filled_template)
|
||||||
|
file_map[tail] = config_path
|
||||||
|
|
||||||
|
generated_config[tail] = generated_paths
|
||||||
|
|
||||||
|
for tail, (full_path, idx) in config_map.items():
|
||||||
|
if tail not in file_map:
|
||||||
|
file_map[tail] = full_path
|
||||||
|
|
||||||
|
to_symlink: list[tuple[Path, Path]] = []
|
||||||
if 'config_dir' in app_settings:
|
if 'config_dir' in app_settings:
|
||||||
for config_tail, full_path in file_map.items():
|
for config_tail, full_path in file_map.items():
|
||||||
to_symlink.append((
|
to_symlink.append((
|
||||||
@@ -424,6 +626,22 @@ class ConfigManager:
|
|||||||
full_path, # to internal config location
|
full_path, # to internal config location
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# run matching scripts for app-specific reload
|
||||||
|
script_list = self.get_matching_scripts(
|
||||||
|
app_name,
|
||||||
|
scheme=scheme,
|
||||||
|
palette=palette,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f'{y("├─")} ' + Fore.YELLOW + f'{app_name} :: matched ({len(to_symlink)}) config files and ({len(script_list)}) scripts'
|
||||||
|
)
|
||||||
|
for tail, gen_paths in generated_config.items():
|
||||||
|
print(
|
||||||
|
f'{y("│")}' + Fore.GREEN + Style.DIM + \
|
||||||
|
f' > generating config "{tail}" from {gen_paths}' + Style.RESET_ALL
|
||||||
|
)
|
||||||
|
|
||||||
links_succ = []
|
links_succ = []
|
||||||
links_fail = []
|
links_fail = []
|
||||||
for from_path, to_path in to_symlink:
|
for from_path, to_path in to_symlink:
|
||||||
@@ -455,35 +673,37 @@ class ConfigManager:
|
|||||||
from_path.symlink_to(to_path)
|
from_path.symlink_to(to_path)
|
||||||
links_succ.append((from_path, to_path))
|
links_succ.append((from_path, to_path))
|
||||||
|
|
||||||
# run matching scripts for app-specific reload
|
# link report
|
||||||
script_list = self.get_matching_scripts(
|
|
||||||
app_name,
|
|
||||||
scheme=scheme,
|
|
||||||
palette=palette,
|
|
||||||
)
|
|
||||||
|
|
||||||
for script in script_list:
|
|
||||||
print(Fore.BLUE + f'> Running script "{script.relative_to(self.config_dir)}"')
|
|
||||||
output = subprocess.check_output(str(script), shell=True)
|
|
||||||
print(
|
|
||||||
Fore.BLUE + Style.DIM + f'-> Captured script output "{output.decode().strip()}"' + Style.RESET_ALL
|
|
||||||
)
|
|
||||||
|
|
||||||
for from_p, to_p in links_succ:
|
for from_p, to_p in links_succ:
|
||||||
from_p = from_p
|
from_p = from_p
|
||||||
to_p = to_p.relative_to(self.config_dir)
|
to_p = to_p.relative_to(self.config_dir)
|
||||||
print(Fore.GREEN + f'> {app_name} :: {from_p} -> {to_p}')
|
print(f'{y("│")}' + Fore.GREEN + f' > linked {from_p} -> {to_p}')
|
||||||
|
|
||||||
for from_p, to_p in links_fail:
|
for from_p, to_p in links_fail:
|
||||||
from_p = from_p
|
from_p = from_p
|
||||||
to_p = to_p.relative_to(self.config_dir)
|
to_p = to_p.relative_to(self.config_dir)
|
||||||
print(Fore.RED + f'> {app_name} :: {from_p} -> {to_p}')
|
print(f'{y("│")}' + Fore.RED + f' > failed to link {from_p} -> {to_p}')
|
||||||
|
|
||||||
|
|
||||||
|
for script in script_list:
|
||||||
|
print(f'{y("│")}' + Fore.BLUE + f' > running script "{script.relative_to(self.config_dir)}"')
|
||||||
|
output = subprocess.check_output(str(script), shell=True)
|
||||||
|
if output:
|
||||||
|
fmt_output = output.decode().strip().replace('\n',f'\n{y("│")} ')
|
||||||
|
print(
|
||||||
|
f'{y("│")}' + \
|
||||||
|
Fore.BLUE + Style.DIM + \
|
||||||
|
f' > captured script output "{fmt_output}"' \
|
||||||
|
+ Style.RESET_ALL
|
||||||
|
)
|
||||||
|
|
||||||
def update_apps(
|
def update_apps(
|
||||||
self,
|
self,
|
||||||
apps: str | list[str] = '*',
|
apps: str | list[str] = '*',
|
||||||
scheme = 'any',
|
scheme = 'any',
|
||||||
palette = 'any',
|
palette = 'any',
|
||||||
|
strict=False,
|
||||||
|
**kw_groups,
|
||||||
):
|
):
|
||||||
if apps == '*':
|
if apps == '*':
|
||||||
# get all registered apps
|
# get all registered apps
|
||||||
@@ -496,10 +716,17 @@ class ConfigManager:
|
|||||||
print(f'None of the apps "{apps}" are registered, exiting')
|
print(f'None of the apps "{apps}" are registered, exiting')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
print('> symconf parameters: ')
|
||||||
|
print(' > registered apps :: ' + Fore.YELLOW + f'{app_list}' + Style.RESET_ALL)
|
||||||
|
print(' > palette :: ' + Fore.YELLOW + f'{palette}' + Style.RESET_ALL)
|
||||||
|
print(' > scheme :: ' + Fore.YELLOW + f'{scheme}\n' + Style.RESET_ALL)
|
||||||
|
|
||||||
for app_name in app_list:
|
for app_name in app_list:
|
||||||
self.update_app_config(
|
self.update_app_config(
|
||||||
app_name,
|
app_name,
|
||||||
app_settings=self.app_registry[app_name],
|
app_settings=self.app_registry[app_name],
|
||||||
scheme=scheme,
|
scheme=scheme,
|
||||||
palette=palette,
|
palette=palette,
|
||||||
|
strict=False,
|
||||||
|
**kw_groups,
|
||||||
)
|
)
|
||||||
|
|||||||
94
symconf/reader.py
Normal file
94
symconf/reader.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import copy
|
||||||
|
import pprint
|
||||||
|
import tomllib
|
||||||
|
import hashlib
|
||||||
|
from typing import Any
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from symconf.util import deep_update
|
||||||
|
|
||||||
|
|
||||||
|
class DictReader:
|
||||||
|
def __init__(self, toml_path=None):
|
||||||
|
self._config = {}
|
||||||
|
self.toml_path = toml_path
|
||||||
|
|
||||||
|
if toml_path is not None:
|
||||||
|
self._config = self._load_toml(toml_path)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return pprint.pformat(self._config, indent=4)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_toml(toml_path) -> dict[str, Any]:
|
||||||
|
return tomllib.loads(Path(toml_path).read_text())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, config_dict):
|
||||||
|
new_instance = cls()
|
||||||
|
new_instance._config = copy.deepcopy(config_dict)
|
||||||
|
return new_instance
|
||||||
|
|
||||||
|
def update(self, config, in_place=False):
|
||||||
|
new_config = deep_update(self._config, config._config)
|
||||||
|
|
||||||
|
if in_place:
|
||||||
|
self._config = new_config
|
||||||
|
return self
|
||||||
|
|
||||||
|
return self.from_dict(new_config)
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
return self.from_dict(copy.deepcopy(self._config))
|
||||||
|
|
||||||
|
def get_subconfig(self, key): pass
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
keys = key.split('.')
|
||||||
|
|
||||||
|
subconfig = self._config
|
||||||
|
for subkey in keys[:-1]:
|
||||||
|
subconfig = subconfig.get(subkey)
|
||||||
|
|
||||||
|
if type(subconfig) is not dict:
|
||||||
|
return default
|
||||||
|
|
||||||
|
return subconfig.get(keys[-1], default)
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
keys = key.split('.')
|
||||||
|
|
||||||
|
subconfig = self._config
|
||||||
|
for subkey in keys[:-1]:
|
||||||
|
if subkey in subconfig:
|
||||||
|
subconfig = subconfig[subkey]
|
||||||
|
|
||||||
|
if type(subconfig) is not dict:
|
||||||
|
logger.debug(
|
||||||
|
'Attempting to set nested key with an existing non-dict parent'
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
subdict = {}
|
||||||
|
subconfig[subkey] = subdict
|
||||||
|
subconfig = subdict
|
||||||
|
|
||||||
|
subconfig.update({ keys[-1]: value })
|
||||||
|
return True
|
||||||
|
|
||||||
|
def generate_hash(self, exclude_keys=None):
|
||||||
|
inst_copy = self.copy()
|
||||||
|
|
||||||
|
if exclude_keys is not None:
|
||||||
|
for key in exclude_keys:
|
||||||
|
inst_copy.set(key, None)
|
||||||
|
|
||||||
|
items = inst_copy._config.items()
|
||||||
|
|
||||||
|
# create hash from config options
|
||||||
|
config_str = str(sorted(items))
|
||||||
|
return hashlib.md5(config_str.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
@@ -7,3 +7,15 @@ def absolute_path(path: str | Path) -> Path:
|
|||||||
|
|
||||||
def xdg_config_path():
|
def xdg_config_path():
|
||||||
return Path(BaseDirectory.save_config_path('symconf'))
|
return Path(BaseDirectory.save_config_path('symconf'))
|
||||||
|
|
||||||
|
def deep_update(mapping: dict, *updating_mappings: dict) -> dict:
|
||||||
|
'''Code adapted from pydantic'''
|
||||||
|
updated_mapping = mapping.copy()
|
||||||
|
for updating_mapping in updating_mappings:
|
||||||
|
for k, v in updating_mapping.items():
|
||||||
|
if k in updated_mapping and isinstance(updated_mapping[k], dict) and isinstance(v, dict):
|
||||||
|
updated_mapping[k] = deep_update(updated_mapping[k], v)
|
||||||
|
else:
|
||||||
|
updated_mapping[k] = v
|
||||||
|
return updated_mapping
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ def test_matching_scripts():
|
|||||||
3. (palette, none) :: test-none.sh
|
3. (palette, none) :: test-none.sh
|
||||||
4. (palette, scheme) :: (nothing)
|
4. (palette, scheme) :: (nothing)
|
||||||
|
|
||||||
Yielding (ordered by dec specificity) "test-none.sh", "none-light.sh", "none-none.sh".
|
Yielding (ordered by dec specificity) "test-none.sh" as primary match, then relaxation
|
||||||
|
match "none-none.sh".
|
||||||
'''
|
'''
|
||||||
test_any = cm.get_matching_scripts(
|
test_any = cm.get_matching_scripts(
|
||||||
'test',
|
'test',
|
||||||
@@ -109,8 +110,8 @@ def test_matching_scripts():
|
|||||||
scheme='any',
|
scheme='any',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(test_any) == 3
|
assert len(test_any) == 2
|
||||||
assert test_any == ['test-none.sh', 'none-light.sh', 'none-none.sh']
|
assert list(map(lambda p:p.name, test_any)) == ['test-none.sh', 'none-none.sh']
|
||||||
|
|
||||||
any_light = cm.get_matching_scripts(
|
any_light = cm.get_matching_scripts(
|
||||||
'test',
|
'test',
|
||||||
@@ -118,8 +119,8 @@ def test_matching_scripts():
|
|||||||
scheme='light',
|
scheme='light',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(any_light) == 3
|
assert len(any_light) == 2
|
||||||
assert any_light == ['test-none.sh', 'none-light.sh', 'none-none.sh']
|
assert list(map(lambda p:p.name, any_light)) == ['none-light.sh', 'none-none.sh']
|
||||||
|
|
||||||
any_dark = cm.get_matching_scripts(
|
any_dark = cm.get_matching_scripts(
|
||||||
'test',
|
'test',
|
||||||
@@ -128,4 +129,4 @@ def test_matching_scripts():
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert len(any_dark) == 2
|
assert len(any_dark) == 2
|
||||||
assert any_dark == ['test-none.sh', 'none-none.sh']
|
assert list(map(lambda p:p.name, any_dark)) == ['test-none.sh', 'none-none.sh']
|
||||||
|
|||||||
Reference in New Issue
Block a user