Compare commits

..

No commits in common. "master" and "0.5.0" have entirely different histories.

16 changed files with 154 additions and 444 deletions

148
README.md
View File

@ -1,142 +1,12 @@
# Symconf # Overview
`symconf` is a CLI tool for managing local application configuration. It implements a `symconf` is a CLI tool for managing local application configuration. It uses a simple
general model that supports dynamically switching/reloading themes for any application, operational model that symlinks centralized config files to their expected locations across
and provides a basic means of templatizing your config files. one's system. This central config directory can then be version controlled, and app
config files can be updated in one place.
## Simple example `symconf` also facilitates dynamically setting system and application "themes," symlinking
Below is a simple example demonstrating two system-wide theme switches: matching theme config files for registered apps and running config reloading scripts.
![Simple example](docs/_static/example.gif) 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 MiB

View File

@ -3,29 +3,22 @@
{ref}`modindex` {ref}`modindex`
{ref}`search` {ref}`search`
## Top-level module overview
```{eval-rst} ```{eval-rst}
.. autosummary:: .. autosummary::
:nosignatures: :nosignatures:
:recursive:
symconf.config # list modules here for quick links
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: 2 :maxdepth: 3
:caption: Contents :caption: Contents
reference/configuring reference/configuring
@ -35,7 +28,4 @@ reference/documentation/index
``` ```
```{include} ../README.md ```{include} ../README.md
:relative-docs: docs/
:relative-images:
``` ```

View File

1
sym_tgt/test/aaa Symbolic link
View File

@ -0,0 +1 @@
/home/smgr/Documents/projects/olog/symconf/tests/test-config-dir/apps/test/user/none-light.aaa

1
sym_tgt/test/ccc Symbolic link
View File

@ -0,0 +1 @@
/home/smgr/Documents/projects/olog/symconf/tests/test-config-dir/apps/test/user/test-light.ccc

View File

@ -8,9 +8,5 @@ from symconf import config
from symconf import matching from symconf import matching
from symconf import reader from symconf import reader
from symconf import template from symconf import template
from symconf import theme
from symconf import util from symconf import util
from importlib.metadata import version
__version__ = version('symconf')

View File

@ -1,7 +1,6 @@
import argparse import argparse
from importlib.metadata import version
from symconf import util, __version__ from symconf import util
from symconf.config import ConfigManager from symconf.config import ConfigManager
@ -36,7 +35,7 @@ def add_update_subparser(subparsers):
parser.add_argument( parser.add_argument(
'-a', '--apps', '-a', '--apps',
required = False, required = False,
default = '*', default = "*",
type = lambda s: s.split(',') if s != '*' else s, type = lambda s: s.split(',') if s != '*' else s,
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'
@ -48,9 +47,8 @@ def add_config_subparser(subparsers):
cm = ConfigManager(args.config_dir) cm = ConfigManager(args.config_dir)
cm.configure_apps( cm.configure_apps(
apps=args.apps, apps=args.apps,
scheme=args.mode, scheme=args.scheme,
style=args.style, style=args.palette,
**args.template_vars
) )
parser = subparsers.add_parser( parser = subparsers.add_parser(
@ -58,17 +56,16 @@ def add_config_subparser(subparsers):
description='Set config files for registered applications.' description='Set config files for registered applications.'
) )
parser.add_argument( parser.add_argument(
'-s', '--style', '-p', '--palette',
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, required = False,
default = "any", default = "any",
help = 'Preferred lightness mode/scheme, either "light," "dark," "any," or "none."' help = 'Palette name, must match a folder in themes/'
)
parser.add_argument(
'-s', '--scheme',
required = False,
default = "any",
help = 'Preferred lightness scheme, either "light" or "dark".'
) )
parser.add_argument( parser.add_argument(
'-a', '--apps', '-a', '--apps',
@ -82,64 +79,11 @@ 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'
) )
parser.set_defaults(func=configure_apps) 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 # central argparse entry point
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
@ -152,19 +96,12 @@ parser.add_argument(
type = util.absolute_path, type = util.absolute_path,
help = 'Path to config directory' help = 'Path to config directory'
) )
parser.add_argument(
'-v', '--version',
action='version',
version=__version__,
help = 'Print symconf version'
)
# add subparsers # add subparsers
subparsers = parser.add_subparsers(title='subcommand actions') subparsers = parser.add_subparsers(title='subcommand actions')
add_config_subparser(subparsers)
add_generate_subparser(subparsers)
add_install_subparser(subparsers) add_install_subparser(subparsers)
add_update_subparser(subparsers) add_update_subparser(subparsers)
add_config_subparser(subparsers)
def main(): def main():

View File

@ -1,19 +1,19 @@
''' '''
Primary config management abstractions Get the config map for a provided app.
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,
.. code-block:: sh ```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
palette1-light.conf.ini -> ~/.config/symconf/apps/user/palette1-light.conf.ini palette2-dark.app.conf -> ~/.config/symconf/apps/generated/palette2-dark.app.conf
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
.. code-block:: sh ```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
.. code-block:: sh ```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,56 +346,6 @@ class ConfigManager:
return template_dict, relaxed_theme_matches 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( def get_matching_configs(
self, self,
app_name, app_name,
@ -417,7 +367,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:
@ -526,9 +476,9 @@ class ConfigManager:
Scripts need to be placed in Scripts need to be placed in
.. code-block:: sh ```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``,
@ -594,7 +544,7 @@ class ConfigManager:
2. Get matching user config files via ``get_matching_configs()`` 2. Get matching user config files via ``get_matching_configs()``
3. Get matching template config files and the aggregate template dict via 3. Get matching template config files and the aggregate template dict via
``get_matching_templates()`` ``get_matching_templates()``
4. Interleave the two result sets by pathname and match quality. Template 4. Interleave the two the result sets by pathname and match quality. Template
matches are preferred in the case of tied scores. This resolves any matches are preferred in the case of tied scores. This resolves any
pathname clashes across matching files. pathname clashes across matching files.
@ -814,66 +764,3 @@ class ConfigManager:
apps: str | list[str] = '*', apps: str | list[str] = '*',
): ):
self._app_action('update.sh', apps) 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}"'
)

View File

@ -1,11 +1,9 @@
''' '''
Generic combinatorial name-matching subsystem Top-level definitions
Config files are expected to have names matching the following spec: Config files are expected to have names matching the following spec:
.. code-block:: sh <style>-<scheme>.<config_pathname>
<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,
@ -17,17 +15,17 @@ Config files are expected to have names matching the following spec:
For example For example
.. code-block:: sh ```sh
soft-gruvbox-dark.kitty.conf
soft-gruvbox-dark.kitty.conf ```
gets mapped to gets mapped to
.. code-block:: sh ```sh
style -> "soft-gruvbox"
style -> "soft-gruvbox" scheme -> "dark"
scheme -> "dark" pathname -> "kitty.conf"
pathname -> "kitty.conf" ```
''' '''
from pathlib import Path from pathlib import Path
@ -66,9 +64,9 @@ class Matcher:
Pathnames should be of the format Pathnames should be of the format
.. code-block:: sh ```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>``.
''' '''
@ -153,12 +151,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
.. code-block:: python ```py
[ [
('none', 'none') ('none', 'none')
('none', 'dark') ('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.
@ -168,7 +166,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,6 +1,3 @@
'''
Simplified management for nested dictionaries
'''
import copy import copy
import pprint import pprint
import tomllib import tomllib

View File

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

View File

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

74
symconf/theme.py Normal file
View File

@ -0,0 +1,74 @@
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}]')

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

View File

@ -7,4 +7,5 @@ def test_imports():
from symconf import config from symconf import config
from symconf import reader from symconf import reader
from symconf import theme
from symconf import util from symconf import util