add py execution support to templates, loosen generate matches

This commit is contained in:
Sam G. 2024-08-18 11:56:28 -07:00
parent 017f1c5b1c
commit 2553bc14af
9 changed files with 195 additions and 79 deletions

View File

@ -19,8 +19,9 @@ used when populating config file templates. Specifically, in this example, invok
are responsive to `prefers-color-scheme`) are responsive to `prefers-color-scheme`)
- **kitty**: theme template is re-generated using the specified palette, and `kitty` - **kitty**: theme template is re-generated using the specified palette, and `kitty`
processes are sent a message to live-reload the new config file processes are sent a message to live-reload the new config file
- **neovim**: a `vim` theme file is generated from the chosen palette, and running - **neovim**: a `vim` theme file (along with a statusline theme) is generated from the
instances of `neovim` are sent a message to re-source this theme chosen palette, and running instances of `neovim` are sent a message to re-source this
theme (via `nvim --remote-send`)
- **waybar**: bar styles are updated to match the mode setting - **waybar**: bar styles are updated to match the mode setting
- **sway**: the background color and window borders are dynamically set to base palette - **sway**: the background color and window borders are dynamically set to base palette
colors, and `swaymsg reload` is called colors, and `swaymsg reload` is called
@ -73,13 +74,25 @@ You can also install via `pip`, or clone and install locally.
all registered apps. all registered apps.
* `-m --mode`: preferred lightness mode/scheme, either `light`, `dark`, `any`, or * `-m --mode`: preferred lightness mode/scheme, either `light`, `dark`, `any`, or
`none`. `none`.
* `-s --style`: style indicate, often the name of a color palette, capturing thematic * `-s --style`: style indicator, often the name of a color palette, capturing thematic
details in a config file to be matched. `any` or `none` are reserved keywords (see details in a config file to be matched. `any` or `none` are reserved keywords (see
below). below).
* `-T --template-vars`: additional groups to use when populating templates, in the form * `-T --template-vars`: additional groups to use when populating templates, in the form
`<group>=<value>`, where `<group>` is a template group with a folder `<group>=<value>`, where `<group>` is a template group with a folder
`$CONFIG_HOME/groups/<group>/` and `<value>` should correspond to a TOML file in this `$CONFIG_HOME/groups/<group>/` and `<value>` should correspond to a TOML file in this
folder (i.e., `<value>.toml`). folder (i.e., `<value>.toml`).
- `symconf generate` is a subcommand that can be used for batch generation of config
files. It accepts the same arguments as `symconf config`, but rather than selecting the
best match to be used for the system setting, all matching templates are generated.
There is one additional required argument:
* `-o --output-dir`: the directory under which generated config files should be written.
App-specific subdirectories are created to house config files for each provided app.
- `symconf install`: runs install scripts for matching apps that specify one
* `-a --apps`: comma-separate list of registered apps, or `"*"` (default) to consider
all registered apps.
- `symconf update`: runs update scripts for matching apps that specify one
* `-a --apps`: comma-separate list of registered apps, or `"*"` (default) to consider
all registered apps.
The keywords `any` and `none` can be used when specifying `--mode`, `--style`, or as a The keywords `any` and `none` can be used when specifying `--mode`, `--style`, or as a
value in `--template-vars` (and we refer to each of these variables as _factors_ that help value in `--template-vars` (and we refer to each of these variables as _factors_ that help

View File

@ -3,22 +3,29 @@
{ref}`modindex` {ref}`modindex`
{ref}`search` {ref}`search`
## Top-level module overview
```{eval-rst} ```{eval-rst}
.. autosummary:: .. autosummary::
:nosignatures: :nosignatures:
:recursive:
# list modules here for quick links symconf.config
symconf.template
symconf.matching
symconf.reader
symconf.runner
``` ```
## Auto-reference contents
```{toctree} ```{toctree}
:maxdepth: 3 :maxdepth: 3
:caption: Autoref
_autoref/symconf.rst _autoref/symconf.rst
``` ```
```{toctree} ```{toctree}
:maxdepth: 3 :maxdepth: 2
:caption: Contents :caption: Contents
reference/configuring reference/configuring
@ -28,4 +35,7 @@ reference/documentation/index
``` ```
```{include} ../README.md ```{include} ../README.md
:relative-docs: docs/
:relative-images:
``` ```

View File

@ -50,6 +50,7 @@ def add_config_subparser(subparsers):
apps=args.apps, apps=args.apps,
scheme=args.mode, scheme=args.mode,
style=args.style, style=args.style,
**args.template_vars
) )
parser = subparsers.add_parser( parser = subparsers.add_parser(
@ -81,6 +82,7 @@ def add_config_subparser(subparsers):
'-T', '--template-vars', '-T', '--template-vars',
required = False, required = False,
nargs='+', nargs='+',
default = {},
action=util.KVPair, action=util.KVPair,
help='Groups to use when populating templates, in the form group=value' help='Groups to use when populating templates, in the form group=value'
) )
@ -90,8 +92,11 @@ def add_generate_subparser(subparsers):
def generate_apps(args): def generate_apps(args):
cm = ConfigManager(args.config_dir) cm = ConfigManager(args.config_dir)
cm.generate_app_templates( cm.generate_app_templates(
gen_dir=args.gen_dir, gen_dir=args.output_dir,
apps=args.apps, apps=args.apps,
scheme=args.mode,
style=args.style,
**args.template_vars
) )
parser = subparsers.add_parser( parser = subparsers.add_parser(
@ -99,11 +104,24 @@ def add_generate_subparser(subparsers):
description='Generate all template config files for specified apps' description='Generate all template config files for specified apps'
) )
parser.add_argument( parser.add_argument(
'-g', '--gen-dir', '-o', '--output-dir',
required = True, required = True,
type = util.absolute_path, type = util.absolute_path,
help = 'Path to write generated template files' help = 'Path to write generated template files'
) )
parser.add_argument(
'-s', '--style',
required = False,
default = 'any',
help = 'Style indicator (often a color palette) capturing thematic details in '
'a config file'
)
parser.add_argument(
'-m', '--mode',
required = False,
default = "any",
help = 'Preferred lightness mode/scheme, either "light," "dark," "any," or "none."'
)
parser.add_argument( parser.add_argument(
'-a', '--apps', '-a', '--apps',
required = False, required = False,
@ -112,6 +130,14 @@ def add_generate_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.add_argument(
'-T', '--template-vars',
required = False,
nargs = '+',
default = {},
action = util.KVPair,
help = 'Groups to use when populating templates, in the form group=value'
)
parser.set_defaults(func=generate_apps) parser.set_defaults(func=generate_apps)

View File

@ -1,19 +1,19 @@
''' '''
Get the config map for a provided app. Primary config management abstractions
The config map is a dict mapping from config file **path names** to their absolute The config map is a dict mapping from config file **path names** to their absolute
path locations. That is, path locations. That is,
```sh .. code-block:: sh
<config_path_name> -> <config_dir>/apps/<app_name>/<subdir>/<palette>-<scheme>.<config_path_name>
``` <config_path_name> -> <config_dir>/apps/<app_name>/<subdir>/<palette>-<scheme>.<config_path_name>
For example, For example,
``` .. code-block:: sh
palette1-light.conf.ini -> ~/.config/symconf/apps/user/palette1-light.conf.ini
palette2-dark.app.conf -> ~/.config/symconf/apps/generated/palette2-dark.app.conf palette1-light.conf.ini -> ~/.config/symconf/apps/user/palette1-light.conf.ini
``` palette2-dark.app.conf -> ~/.config/symconf/apps/generated/palette2-dark.app.conf
This ensures we have unique config names pointing to appropriate locations (which This ensures we have unique config names pointing to appropriate locations (which
is mostly important when the same config file names are present across ``user`` is mostly important when the same config file names are present across ``user``
@ -220,16 +220,16 @@ class ConfigManager:
files just the same as we do for non-template config files. That is, we will look files just the same as we do for non-template config files. That is, we will look
for files of the format for files of the format
```sh .. code-block:: sh
<style>-<scheme>.toml
``` <style>-<scheme>.toml
The only difference is that, while ``style`` can still include arbitrary style The only difference is that, while ``style`` can still include arbitrary style
variants, it *must* have the form variants, it *must* have the form
```sh .. code-block:: sh
<variant-1>-...-<variant-N>-<palette>
``` <variant-1>-...-<variant-N>-<palette>
if you want to match a ``palette`` template. Palettes are like regular template if you want to match a ``palette`` template. Palettes are like regular template
groups, and should be placed in their own template folder. But when applying those groups, and should be placed in their own template folder. But when applying those
@ -346,7 +346,11 @@ class ConfigManager:
return template_dict, relaxed_theme_matches return template_dict, relaxed_theme_matches
def _prepare_all_templates(self) -> dict[str, dict]: def _prepare_all_templates(
self,
scheme = 'any',
style = 'any',
) -> dict[str, dict]:
palette_map = {} palette_map = {}
palette_group_dir = Path(self.group_dir, 'palette') palette_group_dir = Path(self.group_dir, 'palette')
if palette_group_dir.exists(): if palette_group_dir.exists():
@ -358,30 +362,37 @@ class ConfigManager:
palette_base.append(palette_map['none']) palette_base.append(palette_map['none'])
# then palette-scheme groups (require 2-combo logic) # then palette-scheme groups (require 2-combo logic)
theme_map = {} theme_matches = []
theme_group_dir = Path(self.group_dir, 'theme') theme_group_dir = Path(self.group_dir, 'theme')
if theme_group_dir.exists(): if theme_group_dir.exists():
for theme_toml in theme_group_dir.iterdir(): theme_matches = self.matcher.match_paths(
fp = FilePart(theme_toml) theme_group_dir.iterdir(), # match files in groups/theme/
self.matcher.prefix_order( # reg non-template order
theme_matches = self.matcher.match_paths( scheme, style, strict=True # set strict=True to ignore "nones"
theme_group_dir.iterdir(), # match files in groups/theme/
self.matcher.prefix_order(fp.scheme, fp.style) # reg non-template order
) )
relaxed_theme_matches = self.matcher.relaxed_match(theme_matches) )
palette = fp.style.split('-')[-1] theme_map = {}
palette_paths = [*palette_base] for fp in theme_matches:
if palette in palette_map: # still look through whole theme dir here (eg to match nones)
palette_paths.append(palette_map[palette]) theme_matches = self.matcher.match_paths(
theme_group_dir.iterdir(), # match files in groups/theme/
self.matcher.prefix_order(fp.scheme, fp.style) # reg non-template order
)
relaxed_theme_matches = self.matcher.relaxed_match(theme_matches)
theme_dict = {} palette = fp.style.split('-')[-1]
palette_dict = TOMLTemplate.stack_toml(palette_paths) palette_paths = [*palette_base]
for file_part in relaxed_theme_matches: if palette in palette_map:
toml_dict = TOMLTemplate(file_part.path).fill(palette_dict) palette_paths.append(palette_map[palette])
theme_dict = util.deep_update(theme_dict, toml_dict)
theme_map[fp.path.stem] = {'theme': theme_dict} theme_dict = {}
palette_dict = TOMLTemplate.stack_toml(palette_paths)
for file_part in relaxed_theme_matches:
toml_dict = TOMLTemplate(file_part.path).fill(palette_dict)
theme_dict = util.deep_update(theme_dict, toml_dict)
theme_map[fp.path.stem] = {'theme': theme_dict}
return theme_map return theme_map
@ -406,7 +417,7 @@ class ConfigManager:
``none-<scheme>`` and ``<style>-none`` are both available, in which case the latter ``none-<scheme>`` and ``<style>-none`` are both available, in which case the latter
will overwrite the former). will overwrite the former).
.. admonition: Edge cases .. admonition:: Edge cases
There are a few quirks to this matching scheme that yield potentially There are a few quirks to this matching scheme that yield potentially
unintuitive results. As a recap: unintuitive results. As a recap:
@ -515,9 +526,9 @@ class ConfigManager:
Scripts need to be placed in Scripts need to be placed in
```sh .. code-block:: sh
<config_dir>/apps/<app_name>/call/<style>-<scheme>.sh
``` <config_dir>/apps/<app_name>/call/<style>-<scheme>.sh
and are matched using the same heuristic employed by config file symlinking and are matched using the same heuristic employed by config file symlinking
procedure (see ``get_matching_configs()``), albeit with a forced ``prefix_order``, procedure (see ``get_matching_configs()``), albeit with a forced ``prefix_order``,
@ -808,6 +819,9 @@ class ConfigManager:
self, self,
gen_dir : str | Path, gen_dir : str | Path,
apps : str | list[str] = '*', apps : str | list[str] = '*',
scheme : str = 'any',
style : str = 'any',
**kw_groups,
): ):
if apps == '*': if apps == '*':
app_list = list(self.app_registry.keys()) app_list = list(self.app_registry.keys())
@ -823,7 +837,7 @@ class ConfigManager:
print(f'> Writing templates...') print(f'> Writing templates...')
gen_dir = util.absolute_path(gen_dir) gen_dir = util.absolute_path(gen_dir)
theme_map = self._prepare_all_templates() theme_map = self._prepare_all_templates(scheme, style)
for app_name in app_list: for app_name in app_list:
app_template_dir = Path(self.apps_dir, app_name, 'templates') app_template_dir = Path(self.apps_dir, app_name, 'templates')
@ -831,6 +845,12 @@ class ConfigManager:
continue continue
app_template_files = list(app_template_dir.iterdir()) app_template_files = list(app_template_dir.iterdir())
self.get_matching_templates(
app_name,
scheme=scheme,
style=style,
**kw_groups
)
num_temps = len(app_template_files) num_temps = len(app_template_files)
num_themes = len(theme_map) num_themes = len(theme_map)
@ -855,5 +875,5 @@ class ConfigManager:
print( print(
color_text("", Fore.BLUE), color_text("", Fore.BLUE),
f'> generating "{tgt_template_path.name}"' f'> generating "{tgt_template_path.name}"'
) )

View File

@ -1,9 +1,11 @@
''' '''
Top-level definitions Generic combinatorial name-matching subsystem
Config files are expected to have names matching the following spec: Config files are expected to have names matching the following spec:
<style>-<scheme>.<config_pathname> .. code-block:: sh
<style>-<scheme>.<config_pathname>
- ``config_pathname``: refers to a concrete filename, typically that which is expected by - ``config_pathname``: refers to a concrete filename, typically that which is expected by
the target app (e.g., ``kitty.conf``). In the context of ``config_map`` in the registry, the target app (e.g., ``kitty.conf``). In the context of ``config_map`` in the registry,
@ -15,17 +17,17 @@ Config files are expected to have names matching the following spec:
For example For example
```sh .. code-block:: sh
soft-gruvbox-dark.kitty.conf
``` soft-gruvbox-dark.kitty.conf
gets mapped to gets mapped to
```sh .. code-block:: sh
style -> "soft-gruvbox"
scheme -> "dark" style -> "soft-gruvbox"
pathname -> "kitty.conf" scheme -> "dark"
``` pathname -> "kitty.conf"
''' '''
from pathlib import Path from pathlib import Path
@ -64,9 +66,9 @@ class Matcher:
Pathnames should be of the format Pathnames should be of the format
```sh .. code-block:: sh
<style>-<scheme>.<config_pathname>
``` <style>-<scheme>.<config_pathname>
where ``style`` is typically itself of the form ``<variant>-<palette>``. where ``style`` is typically itself of the form ``<variant>-<palette>``.
''' '''
@ -151,12 +153,12 @@ class Matcher:
"consistent" with some user input (and is computed external to this method). For "consistent" with some user input (and is computed external to this method). For
example, it could be example, it could be
```py .. code-block:: python
[
('none', 'none') [
('none', 'dark') ('none', 'none')
] ('none', 'dark')
``` ]
indicating that either ``none-none.<config>`` or ``none-dark.<config>`` would be indicating that either ``none-none.<config>`` or ``none-dark.<config>`` would be
considered matching pathnames, with the latter being preferred. considered matching pathnames, with the latter being preferred.
@ -166,7 +168,7 @@ class Matcher:
will be available, so we consider each file for each of the prefixes, and take the will be available, so we consider each file for each of the prefixes, and take the
latest/best match for each unique config pathname (allowing for a "soft" match). latest/best match for each unique config pathname (allowing for a "soft" match).
.. admonition: Checking for matches .. admonition:: Checking for matches
When thinking about how best to structure this method, it initially felt like When thinking about how best to structure this method, it initially felt like
indexing factors of the FileParts would make the most sense, preventing the indexing factors of the FileParts would make the most sense, preventing the

View File

@ -1,3 +1,6 @@
'''
Simplified management for nested dictionaries
'''
import copy import copy
import pprint import pprint
import tomllib import tomllib

View File

@ -1,3 +1,6 @@
'''
Handle job/script execution
'''
import stat import stat
import subprocess import subprocess
from pathlib import Path from pathlib import Path

View File

@ -1,3 +1,6 @@
'''
Support for basic config templates
'''
import re import re
import tomllib import tomllib
from pathlib import Path from pathlib import Path
@ -10,10 +13,12 @@ class Template:
def __init__( def __init__(
self, self,
template_str : str, template_str : str,
pattern : str = r'f{{(\S+?)}}', key_pattern : str = r'f{{(\S+?)}}',
exe_pattern : str = r'x{{(.*)}}',
): ):
self.template_str = template_str self.template_str = template_str
self.pattern = pattern self.key_pattern = key_pattern
self.exe_pattern = exe_pattern
def fill( def fill(
self, self,
@ -21,32 +26,66 @@ class Template:
) -> str: ) -> str:
dr = DictReader.from_dict(template_dict) dr = DictReader.from_dict(template_dict)
return re.sub( exe_filled = re.sub(
self.pattern, self.exe_pattern,
lambda m: str(dr.get(m.group(1))), lambda m: self._exe_fill(m, dr),
self.template_str self.template_str
) )
key_filled = re.sub(
self.key_pattern,
lambda m: self._key_fill(m, dr),
exe_filled
)
return key_filled
def _key_fill(
self,
match,
dict_reader: DictReader,
) -> str:
key = match.group(1)
return str(dict_reader.get(key))
def _exe_fill(
self,
match,
dict_reader: DictReader,
) -> str:
key_fill = re.sub(
self.key_pattern,
lambda m: f'"{self._key_fill(m, dict_reader)}"',
match.group(1)
)
return str(eval(key_fill))
class FileTemplate(Template): class FileTemplate(Template):
def __init__( def __init__(
self, self,
path : Path, path: Path,
pattern : str = r'f{{(\S+)}}', key_pattern: str = r'f{{(\S+?)}}',
exe_pattern: str = r'x{{(.*)}}',
): ):
super().__init__( super().__init__(
path.open('r').read(), path.open('r').read(),
pattern=pattern key_pattern=key_pattern,
exe_pattern=exe_pattern,
) )
class TOMLTemplate(FileTemplate): class TOMLTemplate(FileTemplate):
def __init__( def __init__(
self, self,
toml_path : Path, toml_path: Path,
pattern : str = r'f{{(\S+)}}', key_pattern: str = r'f{{(\S+?)}}',
exe_pattern: str = r'x{{(.*)}}',
): ):
super().__init__( super().__init__(
toml_path, toml_path,
pattern=pattern key_pattern=key_pattern,
exe_pattern=exe_pattern,
) )
def fill( def fill(

View File

@ -14,7 +14,7 @@ def color_text(text, *colorama_args):
Note: we attempt to preserve expected nested behavior by only resetting the groups Note: we attempt to preserve expected nested behavior by only resetting the groups
(Fore, Back, Style) affected the styles passed in. This works when an outer call is (Fore, Back, Style) affected the styles passed in. This works when an outer call is
changing styles in one group, and an inner call is changing styles in another, but changing styles in one group, and an inner call is changing styles in another, but
_not_ when affected groups overlap. *not* when affected groups overlap.
For example, if an outer call is setting the foreground color (e.g., ``Fore.GREEN``), For example, if an outer call is setting the foreground color (e.g., ``Fore.GREEN``),
nested calls on the text being passed into the function can modify and reset the nested calls on the text being passed into the function can modify and reset the