4 Commits
0.5.1 ... 0.6.0

Author SHA1 Message Date
433af79028 add template generation functionality, version mapping 2024-08-12 00:23:39 -07:00
b565d99882 fix CLI arg clash 2024-08-11 16:01:18 -07:00
c0d94b2de7 fix README tag issue 2024-08-11 05:18:47 -07:00
4ab6c4f100 modify argument names, update README with examples and demo 2024-08-11 04:13:39 -07:00
10 changed files with 258 additions and 108 deletions

133
README.md
View File

@@ -1,34 +1,129 @@
# 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 makes it easy to templatize your config files.
and provides a basic means of templatizing your config files.
## Quick example
The single command `symconf config -m dark -s gruvbox` indicates a dark mode preference and
that the `gruvbox` palette should be used. In this example, invoking this command kicks
off several app-specific process to update the system state:
## Simple example
Below is a simple example demonstrating two system-wide theme switches:
![Simple example](docs/_static/example.gif)
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 Firefox (and subsequently websites that are responsive to
`prefers-color-scheme`)
- **kitty**: theme template is re-generated using the dark `gruvbox` palette, and `kitty`
processes are sent a message to live reload the new config
- **neovim**: a `vim` theme file is generated from the `gruvbox` palette, and running
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 is generated from the chosen palette, and running
instances of `neovim` are sent a message to re-source this theme
- **waybar**: bar styles are updated to match the mode setting
- **sway**: the background color and window borders are dynamically set to base `gruvbox`
- **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 for `gruvbox` colors and re-exported
- **rofi**: launcher text and highlight colors are set according to mode
- **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
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
`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.
`symconf` also facilitates dynamically setting system and application themes. You can
create themed variants of your config files, and `symconf` will "swap out" the matching
theme config files for registered apps and running config reloading scripts.
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).
# 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) 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
See more in [USAGE](/USAGE.md)
- `-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 indicate, 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`).
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 MiB

0
example/README.md Normal file
View File

View File

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

View File

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

View File

@@ -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')

View File

@@ -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,8 @@ 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,
)
parser = subparsers.add_parser(
@@ -56,16 +57,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',
@@ -84,6 +86,34 @@ def add_config_subparser(subparsers):
)
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.gen_dir,
apps=args.apps,
)
parser = subparsers.add_parser(
'generate',
description='Generate all template config files for specified apps'
)
parser.add_argument(
'-g', '--gen-dir',
required = True,
type = util.absolute_path,
help = 'Path to write generated template files'
)
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=generate_apps)
# central argparse entry point
parser = argparse.ArgumentParser(
@@ -96,12 +126,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():

View File

@@ -346,6 +346,45 @@ class ConfigManager:
return template_dict, relaxed_theme_matches
def _prepare_all_templates(self) -> 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_map = {}
theme_group_dir = Path(self.group_dir, 'theme')
if theme_group_dir.exists():
for theme_toml in theme_group_dir.iterdir():
fp = FilePart(theme_toml)
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,
@@ -544,7 +583,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 +803,55 @@ 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] = '*',
):
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()
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())
print(
color_text("├─", Fore.BLUE),
f'{app_name} :: generating ({len(app_template_files)}) template files'
)
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,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}]')

View File

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