Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb1dd52833 | |||
| 8a78da1a28 | |||
| ec5893581e | |||
| 8afdadc263 | |||
| 5bb280b1da | |||
| 533c533034 |
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
|
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -4,20 +4,56 @@ from symconf import util
|
|||||||
from symconf.config import ConfigManager
|
from symconf.config import ConfigManager
|
||||||
|
|
||||||
|
|
||||||
def add_set_subparser(subparsers):
|
def add_install_subparser(subparsers):
|
||||||
def update_app_settings(args):
|
def install_apps(args):
|
||||||
cm = ConfigManager(args.config_dir)
|
cm = ConfigManager(args.config_dir)
|
||||||
cm.update_apps(
|
cm.install_apps(apps=args.apps)
|
||||||
|
|
||||||
|
parser = subparsers.add_parser(
|
||||||
|
'install',
|
||||||
|
description='Run install scripts for registered applications.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-a', '--apps',
|
||||||
|
required = False,
|
||||||
|
default = "*",
|
||||||
|
type = lambda s: s.split(',') if s != '*' else s,
|
||||||
|
help = 'Application target for theme. App must be present in the registry. ' \
|
||||||
|
+ 'Use "*" to apply to all registered apps'
|
||||||
|
)
|
||||||
|
parser.set_defaults(func=install_apps)
|
||||||
|
|
||||||
|
def add_update_subparser(subparsers):
|
||||||
|
def update_apps(args):
|
||||||
|
cm = ConfigManager(args.config_dir)
|
||||||
|
cm.update_apps(apps=args.apps)
|
||||||
|
|
||||||
|
parser = subparsers.add_parser(
|
||||||
|
'update',
|
||||||
|
description='Run update scripts for registered applications.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-a', '--apps',
|
||||||
|
required = False,
|
||||||
|
default = "*",
|
||||||
|
type = lambda s: s.split(',') if s != '*' else s,
|
||||||
|
help = 'Application target for theme. App must be present in the registry. ' \
|
||||||
|
+ 'Use "*" to apply to all registered apps'
|
||||||
|
)
|
||||||
|
parser.set_defaults(func=update_apps)
|
||||||
|
|
||||||
|
def add_config_subparser(subparsers):
|
||||||
|
def config_apps(args):
|
||||||
|
cm = ConfigManager(args.config_dir)
|
||||||
|
cm.config_apps(
|
||||||
apps=args.apps,
|
apps=args.apps,
|
||||||
scheme=args.scheme,
|
scheme=args.scheme,
|
||||||
palette=args.palette,
|
palette=args.palette,
|
||||||
)
|
)
|
||||||
|
|
||||||
parser = subparsers.add_parser(
|
parser = subparsers.add_parser(
|
||||||
'set',
|
'config',
|
||||||
description='Generate theme files for various applications. Uses a template (in TOML ' \
|
description='Set config files for registered applications.'
|
||||||
+ 'format) to map application-specific config keywords to colors (in JSON ' \
|
|
||||||
+ 'format).'
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-p', '--palette',
|
'-p', '--palette',
|
||||||
@@ -39,45 +75,20 @@ def add_set_subparser(subparsers):
|
|||||||
help = 'Application target for theme. App must be present in the registry. ' \
|
help = 'Application target for theme. App must be present in the registry. ' \
|
||||||
+ 'Use "*" to apply to all registered apps'
|
+ 'Use "*" to apply to all registered apps'
|
||||||
)
|
)
|
||||||
parser.set_defaults(func=update_app_settings)
|
|
||||||
|
|
||||||
def add_gen_subparser(subparsers):
|
|
||||||
parser = subparsers.add_parser(
|
|
||||||
'gen',
|
|
||||||
description='Generate theme files for various applications. Uses a template (in TOML ' \
|
|
||||||
+ 'format) to map application-specific config keywords to colors (in JSON ' \
|
|
||||||
+ 'format).'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-a', '--app',
|
'-T', '--template-vars',
|
||||||
required=True,
|
required = False,
|
||||||
help='Application target for theme. Supported: ["kitty"]'
|
nargs='+',
|
||||||
|
action=util.KVPair,
|
||||||
|
help='Groups to use when populating templates, in the form group=value'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.set_defaults(func=config_apps)
|
||||||
'-p', '--palette',
|
|
||||||
required=True,
|
|
||||||
help='Palette to use for template mappings. Uses local "theme/<palette>/colors.json".'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-t', '--template',
|
|
||||||
default=None,
|
|
||||||
help='Path to TOML template file. If omitted, app\'s default template path is used.' \
|
|
||||||
+ 'If a directory is provided, all TOML files in the folder will be used.'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-o', '--output',
|
|
||||||
default=None,
|
|
||||||
help='Output file path for theme. If omitted, app\'s default theme output path is used.'
|
|
||||||
)
|
|
||||||
parser.set_defaults(func=generate_theme_files)
|
|
||||||
|
|
||||||
|
|
||||||
# central argparse entry point
|
# central argparse entry point
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
'symconf',
|
'symconf',
|
||||||
description='Generate theme files for various applications. Uses a template (in TOML ' \
|
description='Manage application configuration with symlinks.'
|
||||||
+ 'format) to map application-specific config keywords to colors (in JSON ' \
|
|
||||||
+ 'format).'
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-c', '--config-dir',
|
'-c', '--config-dir',
|
||||||
@@ -88,14 +99,18 @@ parser.add_argument(
|
|||||||
|
|
||||||
# add subparsers
|
# add subparsers
|
||||||
subparsers = parser.add_subparsers(title='subcommand actions')
|
subparsers = parser.add_subparsers(title='subcommand actions')
|
||||||
#add_gen_subparser(subparsers)
|
add_install_subparser(subparsers)
|
||||||
add_set_subparser(subparsers)
|
add_update_subparser(subparsers)
|
||||||
|
add_config_subparser(subparsers)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
def main():
|
||||||
args = parser.parse_args()
|
args = parser.parse_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,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
import stat
|
||||||
import inspect
|
import inspect
|
||||||
import tomllib
|
import tomllib
|
||||||
import argparse
|
import argparse
|
||||||
@@ -13,6 +14,9 @@ from symconf import util
|
|||||||
from symconf.reader import DictReader
|
from symconf.reader import DictReader
|
||||||
|
|
||||||
|
|
||||||
|
def y(t):
|
||||||
|
return Style.RESET_ALL + Fore.BLUE + t + Style.RESET_ALL
|
||||||
|
|
||||||
class ConfigManager:
|
class ConfigManager:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -90,6 +94,30 @@ class ConfigManager:
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def _run_script(
|
||||||
|
self,
|
||||||
|
script,
|
||||||
|
):
|
||||||
|
script_path = Path(script)
|
||||||
|
if script_path.stat().st_mode & stat.S_IXUSR == 0:
|
||||||
|
print(
|
||||||
|
f'{y("│")}' + Fore.RED + Style.DIM \
|
||||||
|
+ f' > script "{script_path.relative_to(self.config_dir)}" missing execute permissions, skipping'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f'{y("│")}' + Fore.BLUE + Style.DIM + f' > running script "{script_path.relative_to(self.config_dir)}"')
|
||||||
|
|
||||||
|
output = subprocess.check_output(str(script_path), 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 app_config_map(self, app_name) -> dict[str, Path]:
|
def app_config_map(self, app_name) -> dict[str, Path]:
|
||||||
'''
|
'''
|
||||||
Get the config map for a provided app.
|
Get the config map for a provided app.
|
||||||
@@ -114,14 +142,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
|
||||||
@@ -215,8 +239,9 @@ class ConfigManager:
|
|||||||
ordered_matches = []
|
ordered_matches = []
|
||||||
for i, (palette_prefix, scheme_prefix) in enumerate(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'
|
||||||
@@ -232,6 +257,8 @@ class ConfigManager:
|
|||||||
'''
|
'''
|
||||||
Mostly to filter "any" matches, latching onto a particular result and getting
|
Mostly to filter "any" matches, latching onto a particular result and getting
|
||||||
only its relaxed variants.
|
only its relaxed variants.
|
||||||
|
|
||||||
|
Note that palette-scheme files can be named ``<variant>-<palette>-<scheme>``
|
||||||
'''
|
'''
|
||||||
if not match_list:
|
if not match_list:
|
||||||
return []
|
return []
|
||||||
@@ -242,8 +269,13 @@ class ConfigManager:
|
|||||||
|
|
||||||
relaxed_map = {}
|
relaxed_map = {}
|
||||||
for conf_part, theme_part, pathname, idx in match_list:
|
for conf_part, theme_part, pathname, idx in match_list:
|
||||||
theme_split = theme_part.split('-')
|
#theme_split = theme_part.split('-')[::-1]
|
||||||
palette_part, scheme_part = '-'.join(theme_split[:-1]), theme_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'
|
palette_match = palette_part == palette_tgt or palette_part == 'none'
|
||||||
scheme_match = scheme_part == scheme_tgt or scheme_part == 'none'
|
scheme_match = scheme_part == scheme_tgt or scheme_part == 'none'
|
||||||
@@ -297,11 +329,11 @@ class ConfigManager:
|
|||||||
for k, v in kw_groups.items()
|
for k, v in kw_groups.items()
|
||||||
}
|
}
|
||||||
# palette lookup will behave like other groups
|
# palette lookup will behave like other groups
|
||||||
groups['palette'] = palette
|
groups['palette'] = palette.split('-')[-1]
|
||||||
|
|
||||||
group_dir = Path(self.config_dir, 'groups')
|
group_dir = Path(self.config_dir, 'groups')
|
||||||
if not group_dir.exists():
|
if not group_dir.exists():
|
||||||
return {}
|
return {}, []
|
||||||
|
|
||||||
# handle non-palette-scheme groups
|
# handle non-palette-scheme groups
|
||||||
group_matches = {}
|
group_matches = {}
|
||||||
@@ -473,18 +505,18 @@ class ConfigManager:
|
|||||||
|
|
||||||
template_map = {}
|
template_map = {}
|
||||||
template_dir = Path(self.apps_dir, app_name, 'templates')
|
template_dir = Path(self.apps_dir, app_name, 'templates')
|
||||||
if template_dir.exists():
|
if template_dir.is_dir():
|
||||||
for template_file in template_dir.iterdir():
|
for template_file in template_dir.iterdir():
|
||||||
template_map[template_file.name] = template_file
|
template_map[template_file.name] = template_file
|
||||||
|
|
||||||
return template_map, template_dict, max_idx
|
return template_map, template_dict, relaxed_matches, max_idx
|
||||||
|
|
||||||
def get_matching_scripts(
|
def get_matching_scripts(
|
||||||
self,
|
self,
|
||||||
app_name,
|
app_name,
|
||||||
scheme='any',
|
scheme='any',
|
||||||
palette='any',
|
palette='any',
|
||||||
):
|
) -> list:
|
||||||
'''
|
'''
|
||||||
Execute matching scripts in the app's ``call/`` directory.
|
Execute matching scripts in the app's ``call/`` directory.
|
||||||
|
|
||||||
@@ -507,7 +539,7 @@ class ConfigManager:
|
|||||||
call_dir = Path(app_dir, 'call')
|
call_dir = Path(app_dir, 'call')
|
||||||
|
|
||||||
if not call_dir.is_dir():
|
if not call_dir.is_dir():
|
||||||
return
|
return []
|
||||||
|
|
||||||
prefix_order = [
|
prefix_order = [
|
||||||
('none' , 'none'),
|
('none' , 'none'),
|
||||||
@@ -558,7 +590,7 @@ class ConfigManager:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# merge templates and user-provided configs
|
# merge templates and user-provided configs
|
||||||
template_map, template_dict, tidx = self.get_matching_templates(
|
template_map, template_dict, template_matches, tidx = self.get_matching_templates(
|
||||||
app_name,
|
app_name,
|
||||||
scheme=scheme,
|
scheme=scheme,
|
||||||
palette=palette,
|
palette=palette,
|
||||||
@@ -576,10 +608,20 @@ class ConfigManager:
|
|||||||
generated_path = Path(self.apps_dir, app_name, 'generated')
|
generated_path = Path(self.apps_dir, app_name, 'generated')
|
||||||
generated_path.mkdir(parents=True, exist_ok=True)
|
generated_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# flatten matches
|
||||||
|
generated_paths = [m[2] for m in template_matches]
|
||||||
|
generated_config = {}
|
||||||
|
|
||||||
file_map = {}
|
file_map = {}
|
||||||
for tail, full_path in template_map.items():
|
for tail, full_path in template_map.items():
|
||||||
if tail in config_map and config_map[tail][2] >= tidx:
|
# use config only if strictly better match
|
||||||
file_map[tail] = config_map[tail]
|
# 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:
|
else:
|
||||||
template_str = full_path.open('r').read()
|
template_str = full_path.open('r').read()
|
||||||
filled_template = self.template_fill(template_str, template_dict)
|
filled_template = self.template_fill(template_str, template_dict)
|
||||||
@@ -588,6 +630,8 @@ class ConfigManager:
|
|||||||
config_path.write_text(filled_template)
|
config_path.write_text(filled_template)
|
||||||
file_map[tail] = config_path
|
file_map[tail] = config_path
|
||||||
|
|
||||||
|
generated_config[tail] = generated_paths
|
||||||
|
|
||||||
for tail, (full_path, idx) in config_map.items():
|
for tail, (full_path, idx) in config_map.items():
|
||||||
if tail not in file_map:
|
if tail not in file_map:
|
||||||
file_map[tail] = full_path
|
file_map[tail] = full_path
|
||||||
@@ -604,41 +648,10 @@ class ConfigManager:
|
|||||||
# app's config map points config tails to absolute paths
|
# app's config map points config tails to absolute paths
|
||||||
if config_tail in app_settings['config_map']:
|
if config_tail in app_settings['config_map']:
|
||||||
to_symlink.append((
|
to_symlink.append((
|
||||||
abs_pat(Path(app_settings['config_map'][config_tail])), # point from real config path
|
util.absolute_path(Path(app_settings['config_map'][config_tail])), # point from real config path
|
||||||
full_path, # to internal config location
|
full_path, # to internal config location
|
||||||
))
|
))
|
||||||
|
|
||||||
links_succ = []
|
|
||||||
links_fail = []
|
|
||||||
for from_path, to_path in to_symlink:
|
|
||||||
if not to_path.exists():
|
|
||||||
print(f'Internal config path "{to_path}" doesn\'t exist, skipping')
|
|
||||||
links_fail.append((from_path, to_path))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not from_path.parent.exists():
|
|
||||||
print(f'Target config parent directory for "{from_path}" doesn\'t exist, skipping')
|
|
||||||
links_fail.append((from_path, to_path))
|
|
||||||
continue
|
|
||||||
|
|
||||||
# if config file being symlinked exists & isn't already a symlink (i.e.,
|
|
||||||
# previously set by this script), throw an error.
|
|
||||||
if from_path.exists() and not from_path.is_symlink():
|
|
||||||
print(
|
|
||||||
f'Symlink target "{from_path}" exists and isn\'t a symlink, NOT overwriting;' \
|
|
||||||
+ ' please first manually remove this file so a symlink can be set.'
|
|
||||||
)
|
|
||||||
links_fail.append((from_path, to_path))
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
# if path doesn't exist, or exists and is symlink, remove the symlink in
|
|
||||||
# preparation for the new symlink setting
|
|
||||||
from_path.unlink(missing_ok=True)
|
|
||||||
|
|
||||||
#print(f'Linking [{from_path}] -> [{to_path}]')
|
|
||||||
from_path.symlink_to(to_path)
|
|
||||||
links_succ.append((from_path, to_path))
|
|
||||||
|
|
||||||
# run matching scripts for app-specific reload
|
# run matching scripts for app-specific reload
|
||||||
script_list = self.get_matching_scripts(
|
script_list = self.get_matching_scripts(
|
||||||
app_name,
|
app_name,
|
||||||
@@ -647,33 +660,60 @@ class ConfigManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
'├─ ' + Fore.YELLOW + f'{app_name} :: matched {len(to_symlink)} config files and {len(script_list)} scripts'
|
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_fail = []
|
||||||
|
for from_path, to_path in to_symlink:
|
||||||
|
if not to_path.exists():
|
||||||
|
print(f'Internal config path "{to_path}" doesn\'t exist, skipping')
|
||||||
|
links_fail.append((from_path, to_path))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# if config file being symlinked exists & isn't already a symlink (i.e.,
|
||||||
|
# previously set by this script), throw an error.
|
||||||
|
if from_path.exists() and not from_path.is_symlink():
|
||||||
|
print(
|
||||||
|
Fore.RED + \
|
||||||
|
f'Symlink target "{from_path}" exists and isn\'t a symlink, NOT overwriting;' \
|
||||||
|
+ ' please first manually remove this file so a symlink can be set.'
|
||||||
|
)
|
||||||
|
links_fail.append((from_path, to_path))
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# if path doesn't exist, or exists and is symlink, remove the symlink in
|
||||||
|
# preparation for the new symlink setting
|
||||||
|
from_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
#print(f'Linking [{from_path}] -> [{to_path}]')
|
||||||
|
|
||||||
|
# create parent directory if doesn't exist
|
||||||
|
from_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
from_path.symlink_to(to_path)
|
||||||
|
links_succ.append((from_path, to_path))
|
||||||
|
|
||||||
# link report
|
# link report
|
||||||
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'│ > linked {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'│ > failed to link {from_p} -> {to_p}')
|
print(f'{y("│")}' + Fore.RED + f' > failed to link {from_p} -> {to_p}')
|
||||||
|
|
||||||
|
|
||||||
for script in script_list:
|
for script in script_list:
|
||||||
print(Fore.BLUE + f'│ > running script "{script.relative_to(self.config_dir)}"')
|
self._run_script(script)
|
||||||
output = subprocess.check_output(str(script), shell=True)
|
|
||||||
if output:
|
|
||||||
fmt_output = output.decode().strip().replace('\n','\n│ ')
|
|
||||||
print(
|
|
||||||
Fore.BLUE + Style.DIM \
|
|
||||||
+ f'│ > captured script output "{fmt_output}"' \
|
|
||||||
+ Style.RESET_ALL
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_apps(
|
def config_apps(
|
||||||
self,
|
self,
|
||||||
apps: str | list[str] = '*',
|
apps: str | list[str] = '*',
|
||||||
scheme = 'any',
|
scheme = 'any',
|
||||||
@@ -698,6 +738,11 @@ class ConfigManager:
|
|||||||
print(' > scheme :: ' + Fore.YELLOW + f'{scheme}\n' + 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:
|
||||||
|
app_dir = Path(self.apps_dir, app_name)
|
||||||
|
if not app_dir.exists():
|
||||||
|
# app has no directory, skip it
|
||||||
|
continue
|
||||||
|
|
||||||
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],
|
||||||
@@ -706,3 +751,53 @@ class ConfigManager:
|
|||||||
strict=False,
|
strict=False,
|
||||||
**kw_groups,
|
**kw_groups,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def install_apps(
|
||||||
|
self,
|
||||||
|
apps: str | list[str] = '*',
|
||||||
|
):
|
||||||
|
if apps == '*':
|
||||||
|
# get all registered apps
|
||||||
|
app_list = list(self.app_registry.keys())
|
||||||
|
else:
|
||||||
|
# get requested apps that overlap with registry
|
||||||
|
app_list = [a for a in apps if a in self.app_registry]
|
||||||
|
|
||||||
|
if not app_list:
|
||||||
|
print(f'None of the apps "{apps}" are registered, exiting')
|
||||||
|
return
|
||||||
|
|
||||||
|
print('> symconf parameters: ')
|
||||||
|
print(' > registered apps :: ' + Fore.YELLOW + f'{app_list}' + Style.RESET_ALL)
|
||||||
|
|
||||||
|
for app_name in app_list:
|
||||||
|
install_script = Path(self.apps_dir, app_name, 'install.sh')
|
||||||
|
if not install_script.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._run_script(install_script)
|
||||||
|
|
||||||
|
def update_apps(
|
||||||
|
self,
|
||||||
|
apps: str | list[str] = '*',
|
||||||
|
):
|
||||||
|
if apps == '*':
|
||||||
|
# get all registered apps
|
||||||
|
app_list = list(self.app_registry.keys())
|
||||||
|
else:
|
||||||
|
# get requested apps that overlap with registry
|
||||||
|
app_list = [a for a in apps if a in self.app_registry]
|
||||||
|
|
||||||
|
if not app_list:
|
||||||
|
print(f'None of the apps "{apps}" are registered, exiting')
|
||||||
|
return
|
||||||
|
|
||||||
|
print('> symconf parameters: ')
|
||||||
|
print(' > registered apps :: ' + Fore.YELLOW + f'{app_list}' + Style.RESET_ALL)
|
||||||
|
|
||||||
|
for app_name in app_list:
|
||||||
|
update_script = Path(self.apps_dir, app_name, 'update.sh')
|
||||||
|
if not update_script.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._run_script(update_script)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from xdg import BaseDirectory
|
from xdg import BaseDirectory
|
||||||
|
|
||||||
@@ -19,3 +20,13 @@ def deep_update(mapping: dict, *updating_mappings: dict) -> dict:
|
|||||||
updated_mapping[k] = v
|
updated_mapping[k] = v
|
||||||
return updated_mapping
|
return updated_mapping
|
||||||
|
|
||||||
|
|
||||||
|
class KVPair(argparse.Action):
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
kv_dict = getattr(namespace, self.dest, {})
|
||||||
|
if kv_dict is None:
|
||||||
|
kv_dict = {}
|
||||||
|
for value in values:
|
||||||
|
key, val = value.split('=', 1)
|
||||||
|
kv_dict[key] = val
|
||||||
|
setattr(namespace, self.dest, kv_dict)
|
||||||
|
|||||||
Reference in New Issue
Block a user