Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
96fba06709 | |||
2553bc14af | |||
017f1c5b1c | |||
433af79028 | |||
b565d99882 | |||
c0d94b2de7 | |||
4ab6c4f100 | |||
e2f1fd30b6 |
148
README.md
148
README.md
@ -1,12 +1,142 @@
|
||||
# Overview
|
||||
`symconf` is a CLI tool for managing local application configuration. It uses a simple
|
||||
operational model that symlinks centralized config files to their expected locations across
|
||||
one's system. This central config directory can then be version controlled, and app
|
||||
config files can be updated in one place.
|
||||
# Symconf
|
||||
`symconf` is a CLI tool for managing local application configuration. It implements a
|
||||
general model that supports dynamically switching/reloading themes for any application,
|
||||
and provides a basic means of templatizing your config files.
|
||||
|
||||
`symconf` also facilitates dynamically setting system and application "themes," symlinking
|
||||
matching theme config files for registered apps and running config reloading scripts.
|
||||
## Simple example
|
||||
Below is a simple example demonstrating two system-wide theme switches:
|
||||
|
||||
For
|
||||
example, the following `symconf` call coordinates a light to dark mode switch
|
||||

|
||||
|
||||
This GIF shows two `symconf` calls, the first of which applies a `gruvbox` dark theme and
|
||||
the second a dark [`monobiome`][1] variant. Each call (of the form `symconf config -m dark -s
|
||||
style`) indicates a dark mode preference and a particular color palette that should be
|
||||
used when populating config file templates. Specifically, in this example, invoking
|
||||
`symconf` results in the following app-specific config updates:
|
||||
|
||||
- **GTK**: reacts to the mode setting and sets `prefer-dark` system-wide, changing general
|
||||
GTK-responsive applications like Nautilus and Firefox (and subsequently websites that
|
||||
are responsive to `prefers-color-scheme`)
|
||||
- **kitty**: theme template is re-generated using the specified palette, and `kitty`
|
||||
processes are sent a message to live-reload the new config file
|
||||
- **neovim**: a `vim` theme file (along with a statusline theme) is generated from the
|
||||
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
|
||||
- **sway**: the background color and window borders are dynamically set to base palette
|
||||
colors, and `swaymsg reload` is called
|
||||
- **fzf**: a palette-dependent theme is re-generated and re-exported
|
||||
- **rofi**: launcher text and highlight colors are set according to the mode and palette,
|
||||
applying on next invocation
|
||||
|
||||
This example highlights the generality of `symconf`, and so long as an app's config can be
|
||||
reloaded dynamically, you can use a single `symconf` call to apply themes for an arbitrary
|
||||
number of apps at once.
|
||||
|
||||
# Behavior
|
||||
`symconf` uses a simple operational model that symlinks centralized config files to their
|
||||
expected locations across the system. This central config directory can then be version
|
||||
controlled, and app config files can be updated in one place.
|
||||
|
||||
App config files can either be concrete (fully-specified) or templates (to be populated by
|
||||
values conditional on style, e.g., a palette). When `symconf` is executed with a
|
||||
particular mode preference (dark or light) and a style (any other indicator of thematic
|
||||
elements, often simply in the form of a palette like `solarized` or `gruvbox`), it
|
||||
searches for both concrete and template config files that match and symlinks them to
|
||||
registered locations. When necessary, `symconf` will also match and execute scripts to
|
||||
reload apps after updating their configuration.
|
||||
|
||||
You can find more details on how `symconf`'s matching scheme works in
|
||||
[Matching](docs/reference/matching.md).
|
||||
|
||||
# Configuring
|
||||
Before using, you must first set up your config directory to house your config files and
|
||||
give `symconf` something to act on. See [Configuring](docs/reference/configuring.md) for
|
||||
details.
|
||||
|
||||
# Installation
|
||||
The recommended way to install `symconf` is via `pipx`, which is particularly well-suited
|
||||
for managing Python packages meant to be used as CLI programs. With `pipx` on your system,
|
||||
you can install with
|
||||
|
||||
```sh
|
||||
pipx install symconf
|
||||
```
|
||||
|
||||
You can also install via `pip`, or clone and install locally.
|
||||
|
||||
# Usage
|
||||
- `-h --help`: print help message
|
||||
- `-c --config-dir`: set the location of the `symconf` config directory
|
||||
- `symconf config` is the subcommand used to match and set available config files for
|
||||
registered applications
|
||||
* `-a --apps`: comma-separate list of registered apps, or `"*"` (default) to consider
|
||||
all registered apps.
|
||||
* `-m --mode`: preferred lightness mode/scheme, either `light`, `dark`, `any`, or
|
||||
`none`.
|
||||
* `-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
|
||||
below).
|
||||
* `-T --template-vars`: additional groups to use when populating templates, in the form
|
||||
`<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
|
||||
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
|
||||
value in `--template-vars` (and we refer to each of these variables as _factors_ that help
|
||||
determine a config match):
|
||||
|
||||
- `any` will match config files with _any_ value for this factor, preferring config files
|
||||
with a value `none`, indicating no dependence on the factor. This is the default value
|
||||
when a factor is left unspecified.
|
||||
- `none` will match `"none"` directly for a given factor (so no special behavior), but
|
||||
used to indicate that a config file is independent of the factor. For instance,
|
||||
|
||||
```sh
|
||||
symconf config -m light -s none
|
||||
```
|
||||
|
||||
will match config files that capture the notion of a light mode, but do not depend on or
|
||||
provide further thematic components such as a color palette.
|
||||
|
||||
## Examples
|
||||
- Set a dark mode for all registered apps, matching any available style/palette component:
|
||||
|
||||
```sh
|
||||
symconf config -m dark
|
||||
```
|
||||
- Set `solarized` theme for `kitty` and match any available mode (light or dark):
|
||||
|
||||
```sh
|
||||
symconf config -s solarized -a kitty
|
||||
```
|
||||
- Set a dark `gruvbox` theme for multiple apps (but not all):
|
||||
|
||||
```sh
|
||||
symconf config -m dark -s gruvbox -apps="kitty,nvim"
|
||||
```
|
||||
- Set a dark `gruvbox` theme for all apps, and attempt to match other template elements:
|
||||
|
||||
```sh
|
||||
symconf config -m dark -s gruvbox -T font=mono window=sharp
|
||||
```
|
||||
|
||||
which would attempt to find and load key-value pairs in the files
|
||||
`$CONFIG_HOME/groups/font/mono.toml` and `$CONFIG_HOME/groups/window/sharp.toml` to be
|
||||
used as values when filling templatized config files.
|
||||
|
||||
|
||||
|
||||
[1]: https://github.com/ologio/monobiome
|
||||
|
BIN
docs/_static/example.gif
vendored
Normal file
BIN
docs/_static/example.gif
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 MiB |
@ -3,22 +3,29 @@
|
||||
{ref}`modindex`
|
||||
{ref}`search`
|
||||
|
||||
## Top-level module overview
|
||||
|
||||
```{eval-rst}
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
:recursive:
|
||||
|
||||
# list modules here for quick links
|
||||
symconf.config
|
||||
symconf.template
|
||||
symconf.matching
|
||||
symconf.reader
|
||||
symconf.runner
|
||||
```
|
||||
|
||||
## Auto-reference contents
|
||||
```{toctree}
|
||||
:maxdepth: 3
|
||||
:caption: Autoref
|
||||
|
||||
_autoref/symconf.rst
|
||||
```
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 3
|
||||
:maxdepth: 2
|
||||
:caption: Contents
|
||||
|
||||
reference/configuring
|
||||
@ -28,4 +35,7 @@ reference/documentation/index
|
||||
```
|
||||
|
||||
```{include} ../README.md
|
||||
:relative-docs: docs/
|
||||
:relative-images:
|
||||
```
|
||||
|
||||
|
0
example/README.md
Normal file
0
example/README.md
Normal file
@ -1 +0,0 @@
|
||||
/home/smgr/Documents/projects/olog/symconf/tests/test-config-dir/apps/test/user/none-light.aaa
|
@ -1 +0,0 @@
|
||||
/home/smgr/Documents/projects/olog/symconf/tests/test-config-dir/apps/test/user/test-light.ccc
|
@ -8,5 +8,9 @@ from symconf import config
|
||||
from symconf import matching
|
||||
from symconf import reader
|
||||
from symconf import template
|
||||
from symconf import theme
|
||||
from symconf import util
|
||||
|
||||
from importlib.metadata import version
|
||||
|
||||
|
||||
__version__ = version('symconf')
|
||||
|
@ -1,6 +1,7 @@
|
||||
import argparse
|
||||
from importlib.metadata import version
|
||||
|
||||
from symconf import util
|
||||
from symconf import util, __version__
|
||||
from symconf.config import ConfigManager
|
||||
|
||||
|
||||
@ -35,7 +36,7 @@ def add_update_subparser(subparsers):
|
||||
parser.add_argument(
|
||||
'-a', '--apps',
|
||||
required = False,
|
||||
default = "*",
|
||||
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'
|
||||
@ -47,8 +48,9 @@ def add_config_subparser(subparsers):
|
||||
cm = ConfigManager(args.config_dir)
|
||||
cm.configure_apps(
|
||||
apps=args.apps,
|
||||
scheme=args.scheme,
|
||||
style=args.palette,
|
||||
scheme=args.mode,
|
||||
style=args.style,
|
||||
**args.template_vars
|
||||
)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
@ -56,16 +58,17 @@ def add_config_subparser(subparsers):
|
||||
description='Set config files for registered applications.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-p', '--palette',
|
||||
'-s', '--style',
|
||||
required = False,
|
||||
default = "any",
|
||||
help = 'Palette name, must match a folder in themes/'
|
||||
default = 'any',
|
||||
help = 'Style indicator (often a color palette) capturing thematic details in '
|
||||
'a config file'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-s', '--scheme',
|
||||
'-m', '--mode',
|
||||
required = False,
|
||||
default = "any",
|
||||
help = 'Preferred lightness scheme, either "light" or "dark".'
|
||||
help = 'Preferred lightness mode/scheme, either "light," "dark," "any," or "none."'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-a', '--apps',
|
||||
@ -79,11 +82,64 @@ def add_config_subparser(subparsers):
|
||||
'-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=configure_apps)
|
||||
|
||||
def add_generate_subparser(subparsers):
|
||||
def generate_apps(args):
|
||||
cm = ConfigManager(args.config_dir)
|
||||
cm.generate_app_templates(
|
||||
gen_dir=args.output_dir,
|
||||
apps=args.apps,
|
||||
scheme=args.mode,
|
||||
style=args.style,
|
||||
**args.template_vars
|
||||
)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
'generate',
|
||||
description='Generate all template config files for specified apps'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-o', '--output-dir',
|
||||
required = True,
|
||||
type = util.absolute_path,
|
||||
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(
|
||||
'-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.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)
|
||||
|
||||
|
||||
# central argparse entry point
|
||||
parser = argparse.ArgumentParser(
|
||||
@ -96,12 +152,19 @@ parser.add_argument(
|
||||
type = util.absolute_path,
|
||||
help = 'Path to config directory'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-v', '--version',
|
||||
action='version',
|
||||
version=__version__,
|
||||
help = 'Print symconf version'
|
||||
)
|
||||
|
||||
# add subparsers
|
||||
subparsers = parser.add_subparsers(title='subcommand actions')
|
||||
add_config_subparser(subparsers)
|
||||
add_generate_subparser(subparsers)
|
||||
add_install_subparser(subparsers)
|
||||
add_update_subparser(subparsers)
|
||||
add_config_subparser(subparsers)
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -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
|
||||
path locations. That is,
|
||||
|
||||
```sh
|
||||
<config_path_name> -> <config_dir>/apps/<app_name>/<subdir>/<palette>-<scheme>.<config_path_name>
|
||||
```
|
||||
.. code-block:: sh
|
||||
|
||||
<config_path_name> -> <config_dir>/apps/<app_name>/<subdir>/<palette>-<scheme>.<config_path_name>
|
||||
|
||||
For example,
|
||||
|
||||
```
|
||||
palette1-light.conf.ini -> ~/.config/symconf/apps/user/palette1-light.conf.ini
|
||||
palette2-dark.app.conf -> ~/.config/symconf/apps/generated/palette2-dark.app.conf
|
||||
```
|
||||
.. 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
|
||||
|
||||
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``
|
||||
@ -220,16 +220,16 @@ class ConfigManager:
|
||||
files just the same as we do for non-template config files. That is, we will look
|
||||
for files of the format
|
||||
|
||||
```sh
|
||||
<style>-<scheme>.toml
|
||||
```
|
||||
.. code-block:: sh
|
||||
|
||||
<style>-<scheme>.toml
|
||||
|
||||
The only difference is that, while ``style`` can still include arbitrary style
|
||||
variants, it *must* have the form
|
||||
|
||||
```sh
|
||||
<variant-1>-...-<variant-N>-<palette>
|
||||
```
|
||||
.. code-block:: sh
|
||||
|
||||
<variant-1>-...-<variant-N>-<palette>
|
||||
|
||||
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
|
||||
@ -346,6 +346,56 @@ class ConfigManager:
|
||||
|
||||
return template_dict, relaxed_theme_matches
|
||||
|
||||
def _prepare_all_templates(
|
||||
self,
|
||||
scheme = 'any',
|
||||
style = 'any',
|
||||
) -> dict[str, dict]:
|
||||
palette_map = {}
|
||||
palette_group_dir = Path(self.group_dir, 'palette')
|
||||
if palette_group_dir.exists():
|
||||
for palette_path in palette_group_dir.iterdir():
|
||||
palette_map[palette_path.stem] = palette_path
|
||||
|
||||
palette_base = []
|
||||
if 'none' in palette_map:
|
||||
palette_base.append(palette_map['none'])
|
||||
|
||||
# then palette-scheme groups (require 2-combo logic)
|
||||
theme_matches = []
|
||||
theme_group_dir = Path(self.group_dir, 'theme')
|
||||
if theme_group_dir.exists():
|
||||
theme_matches = self.matcher.match_paths(
|
||||
theme_group_dir.iterdir(), # match files in groups/theme/
|
||||
self.matcher.prefix_order( # reg non-template order
|
||||
scheme, style, strict=True # set strict=True to ignore "nones"
|
||||
)
|
||||
)
|
||||
|
||||
theme_map = {}
|
||||
for fp in theme_matches:
|
||||
# still look through whole theme dir here (eg to match nones)
|
||||
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)
|
||||
|
||||
palette = fp.style.split('-')[-1]
|
||||
palette_paths = [*palette_base]
|
||||
if palette in palette_map:
|
||||
palette_paths.append(palette_map[palette])
|
||||
|
||||
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
|
||||
|
||||
def get_matching_configs(
|
||||
self,
|
||||
app_name,
|
||||
@ -367,7 +417,7 @@ class ConfigManager:
|
||||
``none-<scheme>`` and ``<style>-none`` are both available, in which case the latter
|
||||
will overwrite the former).
|
||||
|
||||
.. admonition: Edge cases
|
||||
.. admonition:: Edge cases
|
||||
|
||||
There are a few quirks to this matching scheme that yield potentially
|
||||
unintuitive results. As a recap:
|
||||
@ -476,9 +526,9 @@ class ConfigManager:
|
||||
|
||||
Scripts need to be placed in
|
||||
|
||||
```sh
|
||||
<config_dir>/apps/<app_name>/call/<style>-<scheme>.sh
|
||||
```
|
||||
.. code-block:: sh
|
||||
|
||||
<config_dir>/apps/<app_name>/call/<style>-<scheme>.sh
|
||||
|
||||
and are matched using the same heuristic employed by config file symlinking
|
||||
procedure (see ``get_matching_configs()``), albeit with a forced ``prefix_order``,
|
||||
@ -544,7 +594,7 @@ class ConfigManager:
|
||||
2. Get matching user config files via ``get_matching_configs()``
|
||||
3. Get matching template config files and the aggregate template dict via
|
||||
``get_matching_templates()``
|
||||
4. Interleave the two the result sets by pathname and match quality. Template
|
||||
4. Interleave the two result sets by pathname and match quality. Template
|
||||
matches are preferred in the case of tied scores. This resolves any
|
||||
pathname clashes across matching files.
|
||||
|
||||
@ -764,3 +814,66 @@ class ConfigManager:
|
||||
apps: str | list[str] = '*',
|
||||
):
|
||||
self._app_action('update.sh', apps)
|
||||
|
||||
def generate_app_templates(
|
||||
self,
|
||||
gen_dir : str | Path,
|
||||
apps : str | list[str] = '*',
|
||||
scheme : str = 'any',
|
||||
style : str = 'any',
|
||||
**kw_groups,
|
||||
):
|
||||
if apps == '*':
|
||||
app_list = list(self.app_registry.keys())
|
||||
else:
|
||||
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(f'> symconf parameters: ')
|
||||
print(f' > registered apps :: {color_text(app_list, Fore.YELLOW)}')
|
||||
print(f'> Writing templates...')
|
||||
|
||||
gen_dir = util.absolute_path(gen_dir)
|
||||
theme_map = self._prepare_all_templates(scheme, style)
|
||||
|
||||
for app_name in app_list:
|
||||
app_template_dir = Path(self.apps_dir, app_name, 'templates')
|
||||
if not app_template_dir.exists():
|
||||
continue
|
||||
|
||||
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_themes = len(theme_map)
|
||||
print(
|
||||
color_text("├─", Fore.BLUE),
|
||||
f'{app_name} :: generating ({num_temps}) templates from ({num_themes}) themes'
|
||||
)
|
||||
|
||||
for template_file in app_template_files:
|
||||
app_template = FileTemplate(template_file)
|
||||
|
||||
for theme_stem, theme_dict in theme_map.items():
|
||||
tgt_template_dir = Path(gen_dir, app_name)
|
||||
tgt_template_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
tgt_template_path = Path(
|
||||
tgt_template_dir,
|
||||
f'{theme_stem}.{template_file.name}'
|
||||
)
|
||||
filled_template = app_template.fill(theme_dict)
|
||||
tgt_template_path.write_text(filled_template)
|
||||
|
||||
print(
|
||||
color_text("│", Fore.BLUE),
|
||||
f'> generating "{tgt_template_path.name}"'
|
||||
)
|
||||
|
@ -1,9 +1,11 @@
|
||||
'''
|
||||
Top-level definitions
|
||||
Generic combinatorial name-matching subsystem
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
```sh
|
||||
soft-gruvbox-dark.kitty.conf
|
||||
```
|
||||
.. code-block:: sh
|
||||
|
||||
soft-gruvbox-dark.kitty.conf
|
||||
|
||||
gets mapped to
|
||||
|
||||
```sh
|
||||
style -> "soft-gruvbox"
|
||||
scheme -> "dark"
|
||||
pathname -> "kitty.conf"
|
||||
```
|
||||
.. code-block:: sh
|
||||
|
||||
style -> "soft-gruvbox"
|
||||
scheme -> "dark"
|
||||
pathname -> "kitty.conf"
|
||||
'''
|
||||
from pathlib import Path
|
||||
|
||||
@ -64,9 +66,9 @@ class Matcher:
|
||||
|
||||
Pathnames should be of the format
|
||||
|
||||
```sh
|
||||
<style>-<scheme>.<config_pathname>
|
||||
```
|
||||
.. code-block:: sh
|
||||
|
||||
<style>-<scheme>.<config_pathname>
|
||||
|
||||
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
|
||||
example, it could be
|
||||
|
||||
```py
|
||||
[
|
||||
('none', 'none')
|
||||
('none', 'dark')
|
||||
]
|
||||
```
|
||||
.. code-block:: python
|
||||
|
||||
[
|
||||
('none', 'none')
|
||||
('none', 'dark')
|
||||
]
|
||||
|
||||
indicating that either ``none-none.<config>`` or ``none-dark.<config>`` would be
|
||||
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
|
||||
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
|
||||
indexing factors of the FileParts would make the most sense, preventing the
|
||||
|
@ -1,3 +1,6 @@
|
||||
'''
|
||||
Simplified management for nested dictionaries
|
||||
'''
|
||||
import copy
|
||||
import pprint
|
||||
import tomllib
|
||||
|
@ -1,3 +1,6 @@
|
||||
'''
|
||||
Handle job/script execution
|
||||
'''
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
@ -1,3 +1,6 @@
|
||||
'''
|
||||
Support for basic config templates
|
||||
'''
|
||||
import re
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
@ -10,10 +13,12 @@ class Template:
|
||||
def __init__(
|
||||
self,
|
||||
template_str : str,
|
||||
pattern : str = r'f{{(\S+?)}}',
|
||||
key_pattern : str = r'f{{(\S+?)}}',
|
||||
exe_pattern : str = r'x{{((?:(?!x{{).)*)}}',
|
||||
):
|
||||
self.template_str = template_str
|
||||
self.pattern = pattern
|
||||
self.key_pattern = key_pattern
|
||||
self.exe_pattern = exe_pattern
|
||||
|
||||
def fill(
|
||||
self,
|
||||
@ -21,32 +26,66 @@ class Template:
|
||||
) -> str:
|
||||
dr = DictReader.from_dict(template_dict)
|
||||
|
||||
return re.sub(
|
||||
self.pattern,
|
||||
lambda m: str(dr.get(m.group(1))),
|
||||
exe_filled = re.sub(
|
||||
self.exe_pattern,
|
||||
lambda m: self._exe_fill(m, dr),
|
||||
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):
|
||||
def __init__(
|
||||
self,
|
||||
path : Path,
|
||||
pattern : str = r'f{{(\S+)}}',
|
||||
path: Path,
|
||||
key_pattern: str = r'f{{(\S+?)}}',
|
||||
exe_pattern : str = r'x{{((?:(?!x{{).)*)}}',
|
||||
):
|
||||
super().__init__(
|
||||
path.open('r').read(),
|
||||
pattern=pattern
|
||||
key_pattern=key_pattern,
|
||||
exe_pattern=exe_pattern,
|
||||
)
|
||||
|
||||
class TOMLTemplate(FileTemplate):
|
||||
def __init__(
|
||||
self,
|
||||
toml_path : Path,
|
||||
pattern : str = r'f{{(\S+)}}',
|
||||
toml_path: Path,
|
||||
key_pattern: str = r'f{{(\S+?)}}',
|
||||
exe_pattern : str = r'x{{((?:(?!x{{).)*)}}',
|
||||
):
|
||||
super().__init__(
|
||||
toml_path,
|
||||
pattern=pattern
|
||||
key_pattern=key_pattern,
|
||||
exe_pattern=exe_pattern,
|
||||
)
|
||||
|
||||
def fill(
|
||||
|
@ -1,74 +0,0 @@
|
||||
import argparse
|
||||
import inspect
|
||||
import json
|
||||
import tomllib as toml
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# separation sequences to use base on app
|
||||
app_sep_map = {
|
||||
'kitty': ' ',
|
||||
}
|
||||
|
||||
def generate_theme_files():
|
||||
basepath = get_running_path()
|
||||
|
||||
# set arg conditional variables
|
||||
palette_path = Path(basepath, 'themes', args.palette)
|
||||
colors_path = Path(palette_path, 'colors.json')
|
||||
theme_app = args.app
|
||||
|
||||
template_path = None
|
||||
output_path = None
|
||||
|
||||
if args.template is None:
|
||||
template_path = Path(palette_path, 'apps', theme_app, 'templates')
|
||||
else:
|
||||
template_path = Path(args.template).resolve()
|
||||
|
||||
if args.output is None:
|
||||
output_path = Path(palette_path, 'apps', theme_app, 'generated')
|
||||
else:
|
||||
output_path = Path(args.output).resolve()
|
||||
|
||||
# check paths
|
||||
if not colors_path.exists():
|
||||
print(f'Resolved colors path [{colors_path}] doesn\'t exist, exiting')
|
||||
return
|
||||
|
||||
if not template_path.exists():
|
||||
print(f'Template path [{template_path}] doesn\'t exist, exiting')
|
||||
return
|
||||
|
||||
if not output_path.exists() or not output_path.is_dir():
|
||||
print(f'Output path [{output_path}] doesn\'t exist or not a directory, exiting')
|
||||
return
|
||||
|
||||
print(f'Using palette colors [{colors_path}]')
|
||||
print(f'-> with templates in [{template_path}]')
|
||||
print(f'-> to output path [{output_path}]\n')
|
||||
|
||||
# load external files (JSON, TOML)
|
||||
colors_json = json.load(colors_path.open())
|
||||
|
||||
# get all matching TOML files
|
||||
template_list = [template_path]
|
||||
if template_path.is_dir():
|
||||
template_list = template_path.rglob('*.toml')
|
||||
|
||||
for template_path in template_list:
|
||||
template_toml = toml.load(template_path.open('rb'))
|
||||
|
||||
# lookup app-specific config separator
|
||||
config_sep = app_sep_map.get(theme_app, ' ')
|
||||
output_lines = []
|
||||
for config_key, color_key in template_toml.items():
|
||||
color_value = colors_json
|
||||
for _key in color_key.split('.'):
|
||||
color_value = color_value.get(_key, {})
|
||||
output_lines.append(f'{config_key}{config_sep}{color_value}')
|
||||
|
||||
output_file = Path(output_path, template_path.stem).with_suffix('.conf')
|
||||
output_file.write_text('\n'.join(output_lines))
|
||||
print(f'[{len(output_lines)}] lines written to [{output_file}] for app [{theme_app}]')
|
||||
|
@ -14,7 +14,7 @@ def color_text(text, *colorama_args):
|
||||
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
|
||||
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``),
|
||||
nested calls on the text being passed into the function can modify and reset the
|
||||
|
@ -7,5 +7,4 @@ def test_imports():
|
||||
|
||||
from symconf import config
|
||||
from symconf import reader
|
||||
from symconf import theme
|
||||
from symconf import util
|
||||
|
Loading…
Reference in New Issue
Block a user