Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8039fb3d0b | |||
| a12b952343 | |||
| df5262ec05 | |||
| 13e366d1fe | |||
| 4d707b97e4 | |||
| 2b0702fe36 | |||
| 96fba06709 | |||
| 2553bc14af | |||
| 017f1c5b1c | |||
| 433af79028 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,6 +5,8 @@ __pycache__/
|
|||||||
.ipynb_checkpoints/
|
.ipynb_checkpoints/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.python-version
|
.python-version
|
||||||
|
.venv/
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
# vendor and build files
|
# vendor and build files
|
||||||
dist/
|
dist/
|
||||||
|
|||||||
164
README.md
164
README.md
@@ -1,62 +1,73 @@
|
|||||||
# Symconf
|
# Symconf
|
||||||
`symconf` is a CLI tool for managing local application configuration. It implements a
|
`symconf` is a CLI tool for managing local application configuration. It
|
||||||
general model that supports dynamically switching/reloading themes for any application,
|
implements a general model that supports dynamically switching/reloading themes
|
||||||
and provides a basic means of templatizing your config files.
|
for any application, and provides a basic means of templatizing your config
|
||||||
|
files.
|
||||||
|
|
||||||
## Simple example
|
## Simple example
|
||||||
Below is a simple example demonstrating two system-wide theme switches:
|
Below is a simple example demonstrating two system-wide theme switches:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
This GIF shows two `symconf` calls, the first of which applies a `gruvbox` dark theme and
|
This GIF shows two `symconf` calls, the first of which applies a `gruvbox` dark
|
||||||
the second a dark [`monobiome`][1] variant. Each call (of the form `symconf config -m dark -s
|
theme and the second a dark [`monobiome`][1] variant. Each call (of the form
|
||||||
style`) indicates a dark mode preference and a particular color palette that should be
|
`symconf config -m dark -s style`) indicates a dark mode preference and a
|
||||||
used when populating config file templates. Specifically, in this example, invoking
|
particular color palette that should be used when populating config file
|
||||||
`symconf` results in the following app-specific config updates:
|
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**: reacts to the mode setting and sets `prefer-dark` system-wide,
|
||||||
GTK-responsive applications like Nautilus and Firefox (and subsequently websites that
|
changing general GTK-responsive applications like Nautilus and Firefox (and
|
||||||
are responsive to `prefers-color-scheme`)
|
subsequently websites that 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
|
||||||
processes are sent a message to live-reload the new config file
|
`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
|
- **neovim**: a `vim` theme file (along with a statusline theme) is generated
|
||||||
instances of `neovim` are sent a message to re-source this theme
|
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
|
- **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
|
||||||
colors, and `swaymsg reload` is called
|
palette colors, and `swaymsg reload` is called
|
||||||
- **fzf**: a palette-dependent theme is re-generated and re-exported
|
- **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,
|
- **rofi**: launcher text and highlight colors are set according to the mode
|
||||||
applying on next invocation
|
and palette, applying on next invocation
|
||||||
|
|
||||||
This example highlights the generality of `symconf`, and so long as an app's config can be
|
This example highlights the generality of `symconf`, and so long as an app's
|
||||||
reloaded dynamically, you can use a single `symconf` call to apply themes for an arbitrary
|
config can be reloaded dynamically, you can use a single `symconf` call to
|
||||||
number of apps at once.
|
apply themes for an arbitrary number of apps at once.
|
||||||
|
|
||||||
# Behavior
|
# Behavior
|
||||||
`symconf` uses a simple operational model that symlinks centralized config files to their
|
`symconf` uses a simple operational model that symlinks centralized config
|
||||||
expected locations across the system. This central config directory can then be version
|
files to their expected locations across the system. This central config
|
||||||
controlled, and app config files can be updated in one place.
|
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
|
App config files can either be concrete (fully-specified) or templates (to be
|
||||||
values conditional on style, e.g., a palette). When `symconf` is executed with a
|
populated by values conditional on style, e.g., a palette). When `symconf` is
|
||||||
particular mode preference (dark or light) and a style (any other indicator of thematic
|
executed with a particular mode preference (dark or light) and a style (any
|
||||||
elements, often simply in the form of a palette like `solarized` or `gruvbox`), it
|
other indicator of thematic elements, often simply in the form of a palette
|
||||||
searches for both concrete and template config files that match and symlinks them to
|
like `solarized` or `gruvbox`), it searches for both concrete and template
|
||||||
registered locations. When necessary, `symconf` will also match and execute scripts to
|
config files that match and symlinks them to registered locations. When
|
||||||
reload apps after updating their configuration.
|
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
|
You can find more details on how `symconf`'s matching scheme works in
|
||||||
[Matching](docs/reference/matching).
|
[Matching](docs/reference/matching.md).
|
||||||
|
|
||||||
# Configuring
|
# Configuring
|
||||||
Before using, you must first set up your config directory to house your config files and
|
Before using, you must first set up your config directory to house your config
|
||||||
give `symconf` something to act on. See [Configuring](docs/reference/configuring) for
|
files and give `symconf` something to act on. See
|
||||||
details.
|
[Configuring](docs/reference/configuring.md) for details.
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
The recommended way to install `symconf` is via `pipx`, which is particularly well-suited
|
The recommended way to install `symconf` is via `uv`'s "tool" subsystem, which
|
||||||
for managing Python packages meant to be used as CLI programs. With `pipx` on your system,
|
is well-suited for managing Python packages meant to be used as CLI programs.
|
||||||
you can install with
|
With `uv` on your system, you can install with
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv tool install symconf
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can use `pipx` to similar effect:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pipx install symconf
|
pipx install symconf
|
||||||
@@ -67,44 +78,60 @@ You can also install via `pip`, or clone and install locally.
|
|||||||
# Usage
|
# Usage
|
||||||
- `-h --help`: print help message
|
- `-h --help`: print help message
|
||||||
- `-c --config-dir`: set the location of the `symconf` config directory
|
- `-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
|
- `symconf config` is the subcommand used to match and set available config
|
||||||
registered applications
|
files for registered applications
|
||||||
* `-a --apps`: comma-separate list of registered apps, or `"*"` (default) to consider
|
* `-a --apps`: comma-separate list of registered apps, or `"*"` (default) to
|
||||||
all registered apps.
|
consider all registered apps.
|
||||||
* `-m --mode`: preferred lightness mode/scheme, either `light`, `dark`, `any`, or
|
* `-m --mode`: preferred lightness mode/scheme, either `light`, `dark`,
|
||||||
`none`.
|
`any`, or `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
|
||||||
details in a config file to be matched. `any` or `none` are reserved keywords (see
|
thematic details in a config file to be matched. `any` or `none` are
|
||||||
below).
|
reserved keywords (see below).
|
||||||
* `-T --template-vars`: additional groups to use when populating templates, in the form
|
* `-T --template-vars`: additional groups to use when populating templates,
|
||||||
`<group>=<value>`, where `<group>` is a template group with a folder
|
in the form `<group>=<value>`, where `<group>` is a template group with a
|
||||||
`$CONFIG_HOME/groups/<group>/` and `<value>` should correspond to a TOML file in this
|
folder `$CONFIG_HOME/groups/<group>/` and `<value>` should correspond to a
|
||||||
folder (i.e., `<value>.toml`).
|
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
|
The keywords `any` and `none` can be used when specifying `--mode`, `--style`,
|
||||||
value in `--template-vars` (and we refer to each of these variables as _factors_ that help
|
or as a value in `--template-vars` (and we refer to each of these variables as
|
||||||
determine a config match):
|
_factors_ that help determine a config match):
|
||||||
|
|
||||||
- `any` will match config files with _any_ value for this factor, preferring config files
|
- `any` will match config files with _any_ value for this factor, preferring
|
||||||
with a value `none`, indicating no dependence on the factor. This is the default value
|
config files with a value `none`, indicating no dependence on the factor.
|
||||||
when a factor is left unspecified.
|
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
|
- `none` will match `"none"` directly for a given factor (so no special
|
||||||
used to indicate that a config file is independent of the factor. For instance,
|
behavior), but used to indicate that a config file is independent of the
|
||||||
|
factor. For instance,
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
symconf config -m light -s none
|
symconf config -m light -s none
|
||||||
```
|
```
|
||||||
|
|
||||||
will match config files that capture the notion of a light mode, but do not depend on or
|
will match config files that capture the notion of a light mode, but do not
|
||||||
provide further thematic components such as a color palette.
|
depend on or provide further thematic components such as a color palette.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
- Set a dark mode for all registered apps, matching any available style/palette component:
|
- Set a dark mode for all registered apps, matching any available style/palette
|
||||||
|
component:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
symconf config -m dark
|
symconf config -m dark
|
||||||
```
|
```
|
||||||
- Set `solarized` theme for `kitty` and match any available mode (light or dark):
|
- Set `solarized` theme for `kitty` and match any available mode (light or
|
||||||
|
dark):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
symconf config -s solarized -a kitty
|
symconf config -s solarized -a kitty
|
||||||
@@ -114,16 +141,17 @@ determine a config match):
|
|||||||
```sh
|
```sh
|
||||||
symconf config -m dark -s gruvbox -apps="kitty,nvim"
|
symconf config -m dark -s gruvbox -apps="kitty,nvim"
|
||||||
```
|
```
|
||||||
- Set a dark `gruvbox` theme for all apps, and attempt to match other template elements:
|
- Set a dark `gruvbox` theme for all apps, and attempt to match other template
|
||||||
|
elements:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
symconf config -m dark -s gruvbox -T font=mono window=sharp
|
symconf config -m dark -s gruvbox -T font=mono window=sharp
|
||||||
```
|
```
|
||||||
|
|
||||||
which would attempt to find and load key-value pairs in the files
|
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
|
`$CONFIG_HOME/groups/font/mono.toml` and
|
||||||
used as values when filling templatized config files.
|
`$CONFIG_HOME/groups/window/sharp.toml` to be used as values when filling
|
||||||
|
templatized config files.
|
||||||
|
|
||||||
|
|
||||||
[1]: https://github.com/ologio/monobiome
|
[1]: https://github.com/ologio/monobiome
|
||||||
|
|||||||
44
docs/conf.py
44
docs/conf.py
@@ -3,43 +3,47 @@
|
|||||||
# For the full list of built-in configuration values, see the documentation:
|
# For the full list of built-in configuration values, see the documentation:
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information ------------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
project = 'symconf'
|
project = "symconf"
|
||||||
copyright = '2024, Sam Griesemer'
|
copyright = "2025, Sam Griesemer"
|
||||||
author = 'Sam Griesemer'
|
author = "Sam Griesemer"
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ----------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
||||||
extensions = [
|
extensions = [
|
||||||
"sphinx.ext.autodoc",
|
"sphinx.ext.autodoc",
|
||||||
"sphinx.ext.autosummary", # enables a directive to be specified manually that gathers
|
# enables a directive to be specified manually that gathers module/object
|
||||||
# module/object summary details in a table
|
# summary details in a table
|
||||||
"sphinx.ext.viewcode", # allow viewing source in the HTML pages
|
"sphinx.ext.autosummary",
|
||||||
"myst_parser", # only really applies to manual docs; docstrings still need RST-like
|
# allow viewing source in the HTML pages
|
||||||
"sphinx.ext.napoleon", # enables Google-style docstring formats
|
"sphinx.ext.viewcode",
|
||||||
"sphinx_autodoc_typehints", # external extension that allows arg types to be inferred by type hints
|
# only really applies to manual docs; docstrings still need RST-like
|
||||||
|
"myst_parser",
|
||||||
|
# enables Google-style docstring formats
|
||||||
|
"sphinx.ext.napoleon",
|
||||||
|
# external extension that allows arg types to be inferred by type hints
|
||||||
|
"sphinx_autodoc_typehints",
|
||||||
]
|
]
|
||||||
autosummary_generate = True
|
autosummary_generate = True
|
||||||
autosummary_imported_members = True
|
autosummary_imported_members = True
|
||||||
|
|
||||||
# include __init__ definitions in autodoc
|
# include __init__ definitions in autodoc
|
||||||
autodoc_default_options = {
|
autodoc_default_options = {
|
||||||
'special-members': '__init__',
|
"special-members": "__init__",
|
||||||
}
|
}
|
||||||
|
|
||||||
templates_path = ['_templates']
|
templates_path = ["_templates"]
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output -------------------------------------------------
|
# -- Options for HTML output --------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
|
|
||||||
html_theme = 'furo'
|
html_theme = "furo"
|
||||||
html_static_path = ['_static']
|
html_static_path = ["_static"]
|
||||||
#html_sidebars = {
|
# html_sidebars = {
|
||||||
# '**': ['/modules.html'],
|
# '**': ['/modules.html'],
|
||||||
#}
|
# }
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
# `symconf` package docs
|
# `symconf` package
|
||||||
|
*General-purpose local application configuration manager*
|
||||||
|
|
||||||
{ref}`genindex`
|
{ref}`genindex`
|
||||||
{ref}`modindex`
|
{ref}`modindex`
|
||||||
{ref}`search`
|
|
||||||
|
|
||||||
```{eval-rst}
|
```{eval-rst}
|
||||||
.. autosummary::
|
.. autosummary::
|
||||||
:nosignatures:
|
:nosignatures:
|
||||||
|
:recursive:
|
||||||
|
:caption: Modules
|
||||||
|
|
||||||
# list modules here for quick links
|
symconf.config
|
||||||
|
symconf.reader
|
||||||
|
symconf.runner
|
||||||
|
symconf.matching
|
||||||
|
symconf.template
|
||||||
```
|
```
|
||||||
|
|
||||||
```{toctree}
|
```{toctree}
|
||||||
:maxdepth: 3
|
:maxdepth: 1
|
||||||
:caption: Autoref
|
|
||||||
|
|
||||||
_autoref/symconf.rst
|
|
||||||
```
|
|
||||||
|
|
||||||
```{toctree}
|
|
||||||
:maxdepth: 3
|
|
||||||
:caption: Contents
|
:caption: Contents
|
||||||
|
:hidden:
|
||||||
|
|
||||||
reference/configuring
|
|
||||||
reference/usage
|
|
||||||
reference/archive
|
reference/archive
|
||||||
reference/documentation/index
|
reference/configuring
|
||||||
|
reference/matching
|
||||||
|
reference/usage
|
||||||
```
|
```
|
||||||
|
|
||||||
```{include} ../README.md
|
```{include} ../README.md
|
||||||
|
:relative-docs: docs/
|
||||||
|
:relative-images:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +1,89 @@
|
|||||||
# Archive
|
# Archive
|
||||||
The `autoconf` project is an attempt at wrangling the complexity of configuring many
|
The `autoconf` project is an attempt at wrangling the complexity of configuring
|
||||||
applications across one's Linux system. It provides a simple operational model for pulling
|
many applications across one's Linux system. It provides a simple operational
|
||||||
many application config files into one place, as well as generating/setting color schemes
|
model for pulling many application config files into one place, as well as
|
||||||
across apps.
|
generating/setting color schemes across apps.
|
||||||
|
|
||||||
Quick terminology rundown for theme-related items:
|
Quick terminology rundown for theme-related items:
|
||||||
|
|
||||||
- **Theme**: loose term referring generally to the overall aesthetic of a visual setting.
|
- **Theme**: loose term referring generally to the overall aesthetic of a
|
||||||
Ignoring stylistic changes (only applicable to some apps; example here might be a
|
visual setting. Ignoring stylistic changes (only applicable to some apps;
|
||||||
a particular setting of the `waybar` layout), a theme is often just the wrapper term for
|
example here might be a a particular setting of the `waybar` layout), a theme
|
||||||
a choice of color _palette_ and _scheme_. For example, "tone4-light" could be a _theme_
|
is often just the wrapper term for a choice of color _palette_ and _scheme_.
|
||||||
setting for an app like `kitty`, referring to both a palette and scheme.
|
For example, "tone4-light" could be a _theme_ setting for an app like
|
||||||
- **Palette**: a set of base colors used to style text or other aspects of an app's
|
`kitty`, referring to both a palette and scheme.
|
||||||
displayed assets
|
- **Palette**: a set of base colors used to style text or other aspects of an
|
||||||
|
app's displayed assets
|
||||||
- **Scheme**: an indication of lightness, typically either "light" or "dark.
|
- **Scheme**: an indication of lightness, typically either "light" or "dark.
|
||||||
|
|
||||||
As far as managing settings across apps, there are current two useful classifications
|
As far as managing settings across apps, there are current two useful
|
||||||
here:
|
classifications here:
|
||||||
|
|
||||||
1. **Inseparable from theme**: some apps (e.g., `sway`, `waybar`) have color scheme
|
1. **Inseparable from theme**: some apps (e.g., `sway`, `waybar`) have color
|
||||||
components effectively built in to their canonical configuration file. This can make it
|
scheme components effectively built in to their canonical configuration
|
||||||
hard to set themes dynamically, as it would likely require some involved
|
file. This can make it hard to set themes dynamically, as it would likely
|
||||||
matching/substitution rules. This is not a level of complexity I'm willing to embrace,
|
require some involved matching/substitution rules. This is not a level of
|
||||||
so we simply split the config files according to theme and/or scheme.
|
complexity I'm willing to embrace, so we simply split the config files
|
||||||
2. **Can load an external theme file**: some apps (e.g., `kitty`) have a clear mechanism
|
according to theme and/or scheme.
|
||||||
for loading themes. This typically implies some distinct color format, although usually
|
2. **Can load an external theme file**: some apps (e.g., `kitty`) have a clear
|
||||||
somewhat easy to generate (don't have to navigate non-color settings, for instance).
|
mechanism for loading themes. This typically implies some distinct color
|
||||||
Such apps allow for an even less "invasive" config swapping process when setting a new
|
format, although usually somewhat easy to generate (don't have to navigate
|
||||||
theme, as one can just swap out the external theme file.
|
non-color settings, for instance). Such apps allow for an even less
|
||||||
|
"invasive" config swapping process when setting a new theme, as one can just
|
||||||
|
swap out the external theme file.
|
||||||
|
|
||||||
To be clear on operation implications here: apps of type (1) must have _manually
|
To be clear on operation implications here: apps of type (1) must have
|
||||||
maintained_ config variations according the desired themes. General theme settings must
|
_manually maintained_ config variations according the desired themes. General
|
||||||
follow the naming scheme `<app-name>-<palette>-<scheme>.<ext>`. For example, if I wanted to set
|
theme settings must follow the naming scheme
|
||||||
`sway` to a light variation (which, at the time of writing, would just entail changing a
|
`<app-name>-<palette>-<scheme>.<ext>`. For example, if I wanted to set `sway`
|
||||||
single background color), I must have explicitly created a `sway-tone4-light.conf` file
|
to a light variation (which, at the time of writing, would just entail changing
|
||||||
that captures this setting. The canonical config file will then be symlinked to the
|
a single background color), I must have explicitly created a
|
||||||
theme-specific file when the theme is set. (Note that the palette in this example is pretty much
|
`sway-tone4-light.conf` file that captures this setting. The canonical config
|
||||||
irrelevant, but it needs to be present in order to match the overarching setting; here you
|
file will then be symlinked to the theme-specific file when the theme is set.
|
||||||
can just think of the format being `<app-name>-<theme>.<ext>`, where `tone4-light` is the
|
(Note that the palette in this example is pretty much irrelevant, but it needs
|
||||||
provided theme name.)
|
to be present in order to match the overarching setting; here you can just
|
||||||
|
think of the format being `<app-name>-<theme>.<ext>`, where `tone4-light` is
|
||||||
|
the provided theme name.)
|
||||||
|
|
||||||
For apps of type (2), the canonical config file can remain untouched so long as it refers
|
For apps of type (2), the canonical config file can remain untouched so long as
|
||||||
to a fixed, generic theme file. For example, with `kitty`, my config file can point to a
|
it refers to a fixed, generic theme file. For example, with `kitty`, my config
|
||||||
`current-theme.conf` file, which will be symlinked to a specific theme file here in
|
file can point to a `current-theme.conf` file, which will be symlinked to a
|
||||||
`autoconf` when a change is requested. This enables a couple of conveniences:
|
specific theme file here in `autoconf` when a change is requested. This enables
|
||||||
|
a couple of conveniences:
|
||||||
|
|
||||||
- The true config directory on disk remains unpolluted with theme variants.
|
- The true config directory on disk remains unpolluted with theme variants.
|
||||||
- If the set theme is regenerated, there is no intervention necessary to propagate its
|
- If the set theme is regenerated, there is no intervention necessary to
|
||||||
changes to the target app. The symlinked file itself will be updated when the theme
|
propagate its changes to the target app. The symlinked file itself will be
|
||||||
does, ensuring the latest theme version is always immediately available and pointed to
|
updated when the theme does, ensuring the latest theme version is always
|
||||||
by the app.
|
immediately available and pointed to by the app.
|
||||||
|
|
||||||
Keep in mind that some apps may fall into some grey area here, allowing some external
|
Keep in mind that some apps may fall into some grey area here, allowing some
|
||||||
customization but locking down other settings internally. In such instances, there's no
|
external customization but locking down other settings internally. In such
|
||||||
need to overcomplicate things; just stick to explicit config variants under the type (1)
|
instances, there's no need to overcomplicate things; just stick to explicit
|
||||||
umbrella. Type (2) only works for generated themes anyhow; even if the target app can load
|
config variants under the type (1) umbrella. Type (2) only works for generated
|
||||||
an external theme, type (1) should be used if preset themes are fixed.
|
themes anyhow; even if the target app can load an external theme, type (1)
|
||||||
|
should be used if preset themes are fixed.
|
||||||
|
|
||||||
## Naming standards
|
## Naming standards
|
||||||
To keep things simple, we use a few fixed naming standards for setting app config files
|
To keep things simple, we use a few fixed naming standards for setting app
|
||||||
and their themed counterparts. The app registry requires each theme-eligible app to
|
config files and their themed counterparts. The app registry requires each
|
||||||
provide a config directory (`config_dir`), containing some canonical config file
|
theme-eligible app to provide a config directory (`config_dir`), containing
|
||||||
(`config_file`) and to serve as a place for theme-specific config variations. The
|
some canonical config file (`config_file`) and to serve as a place for
|
||||||
following naming schemes must be used in order for theme switching to behave
|
theme-specific config variations. The following naming schemes must be used in
|
||||||
appropriately:
|
order for theme switching to behave appropriately:
|
||||||
|
|
||||||
- When setting a theme for a particular app, the following variables will be available:
|
- When setting a theme for a particular app, the following variables will be
|
||||||
|
available:
|
||||||
* `<app-name>`
|
* `<app-name>`
|
||||||
* `<palette>`
|
* `<palette>`
|
||||||
* `<scheme>`
|
* `<scheme>`
|
||||||
- For apps with `external_theme = False`, config variants must named as
|
- For apps with `external_theme = False`, config variants must named as
|
||||||
`<app-name>-<palette>-<scheme>.<ext>`, where `<ext>` is the app's default config file
|
`<app-name>-<palette>-<scheme>.<ext>`, where `<ext>` is the app's default
|
||||||
extension.
|
config file extension.
|
||||||
- For apps with `external_theme = True`, the file `<config-dir>/current-theme.conf` will
|
- For apps with `external_theme = True`, the file
|
||||||
be used when symlinking the requested theme. The config file thus must point to this
|
`<config-dir>/current-theme.conf` will be used when symlinking the requested
|
||||||
file in order to change with the set theme.
|
theme. The config file thus must point to this file in order to change with
|
||||||
|
the set theme.
|
||||||
|
|
||||||
Additionally, the theme symlink will be created from the file
|
Additionally, the theme symlink will be created from the file
|
||||||
|
|
||||||
@@ -87,147 +96,162 @@ appropriately:
|
|||||||
## Directory structure
|
## Directory structure
|
||||||
|
|
||||||
- `autoconf/`: main repo directory
|
- `autoconf/`: main repo directory
|
||||||
* `config/`: app-specific configuration files. Each folder inside this directory is
|
* `config/`: app-specific configuration files. Each folder inside this
|
||||||
app-specific, and the target of associated copy operations when a config sync is
|
directory is app-specific, and the target of associated copy operations
|
||||||
performed. Nothing in this directory should pertain to any repo functionality; it
|
when a config sync is performed. Nothing in this directory should pertain
|
||||||
should only contain config files that originated elsewhere on the system.
|
to any repo functionality; it should only contain config files that
|
||||||
* `themes/`: app-independent theme data files. Each folder in this directory should
|
originated elsewhere on the system.
|
||||||
correspond to a specific color palette and house any relevant color spec files
|
* `themes/`: app-independent theme data files. Each folder in this
|
||||||
(currently likely be a `colors.json`). Also servers the output location for
|
directory should correspond to a specific color palette and house any
|
||||||
generated theme files
|
relevant color spec files (currently likely be a `colors.json`). Also
|
||||||
* `<palette>/colors.json`: JSON formatted color key-value pairings for palette
|
servers the output location for generated theme files
|
||||||
colors. There's no standard here aside from the filename and format; downstream
|
* `<palette>/colors.json`: JSON formatted color key-value pairings for
|
||||||
app-specific TOML templates can be dependent on any key naming scheme within the
|
palette colors. There's no standard here aside from the filename and
|
||||||
JSON.
|
format; downstream app-specific TOML templates can be dependent on
|
||||||
+ `<palette>/apps/<app-name>/templates/`: houses the TOML maps for the color
|
any key naming scheme within the JSON.
|
||||||
palette `<palette>` under app `<app-name>`. Files `<fname>.toml` will be mapped to
|
+ `<palette>/apps/<app-name>/templates/`: houses the TOML maps for the
|
||||||
`<fname>.conf` in the theme output folder (below), so ensure the naming
|
color palette `<palette>` under app `<app-name>`. Files
|
||||||
standards align with those outlined above.
|
`<fname>.toml` will be mapped to `<fname>.conf` in the theme output
|
||||||
+ `<palette>/apps/<app-name>/generated/`: output directory for generated scheme
|
folder (below), so ensure the naming standards align with those
|
||||||
variants. These are the symlink targets for dynamically set external themes.
|
outlined above.
|
||||||
* `app_registry.toml`: global application "registry" used by sync and theme-setting
|
+ `<palette>/apps/<app-name>/generated/`: output directory for
|
||||||
scripts. This lets apps be dynamically added or removed from being eligible for
|
generated scheme variants. These are the symlink targets for
|
||||||
config-related operations.
|
dynamically set external themes.
|
||||||
|
* `app_registry.toml`: global application "registry" used by sync and
|
||||||
|
theme-setting scripts. This lets apps be dynamically added or removed
|
||||||
|
from being eligible for config-related operations.
|
||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
`set_theme.py`: sets a theme across select apps.
|
`set_theme.py`: sets a theme across select apps.
|
||||||
|
|
||||||
- Applies to specific app with `-a <app>` , or to all apps in the `app_registry.toml` with
|
- Applies to specific app with `-a <app>` , or to all apps in the
|
||||||
`-a "*"`.
|
`app_registry.toml` with `-a "*"`.
|
||||||
- Uses symlinks to set canonical config files to theme-based variations. Which files get
|
- Uses symlinks to set canonical config files to theme-based variations. Which
|
||||||
set depends on the _app type_ (see above), which really just boils down to whether
|
files get set depends on the _app type_ (see above), which really just boils
|
||||||
theming (1) can be specified with an external format, and (2) if it depends on
|
down to whether theming (1) can be specified with an external format, and (2)
|
||||||
auto-generated theme files from within `autoconf`.
|
if it depends on auto-generated theme files from within `autoconf`.
|
||||||
- Palette and scheme are specified as expected. They are used to infer proper paths
|
- Palette and scheme are specified as expected. They are used to infer proper
|
||||||
according to naming and structure standards.
|
paths according to naming and structure standards.
|
||||||
- Real config files will never be overwritten. To begin setting themes with the script,
|
- Real config files will never be overwritten. To begin setting themes with the
|
||||||
you must delete the canonical config file expected by the app (and specified in the app
|
script, you must delete the canonical config file expected by the app (and
|
||||||
registry) to allow the first symlink to be set. From there on out, symlinks will be
|
specified in the app registry) to allow the first symlink to be set. From
|
||||||
automatically flushed.
|
there on out, symlinks will be automatically flushed.
|
||||||
- A report will be provided on which apps were successfully set to the requested theme,
|
- A report will be provided on which apps were successfully set to the
|
||||||
along with the file stems. A number of checks are in place for the existence of involved
|
requested theme, along with the file stems. A number of checks are in place
|
||||||
files and directories. Overall, the risk of overwritting a real config file is low; we
|
for the existence of involved files and directories. Overall, the risk of
|
||||||
only flush existing symlinks, and if the would-be target for the requested theme (be it
|
overwritting a real config file is low; we only flush existing symlinks, and
|
||||||
from an auto-generated theme file, or from a manually manage config variant) doesn't
|
if the would-be target for the requested theme (be it from an auto-generated
|
||||||
exist, that app's config will be completed skipped. Essentially, everything must be in
|
theme file, or from a manually manage config variant) doesn't exist, that
|
||||||
|
app's config will be completed skipped. Essentially, everything must be in
|
||||||
perfect shape before the symlink trigger is officially pulled.
|
perfect shape before the symlink trigger is officially pulled.
|
||||||
|
|
||||||
|
`gen_theme.py`: generates theme files for palettes by mapping their color
|
||||||
|
definitions through app-specific templates. These templates specific how to
|
||||||
|
relate an app's theme variables to the color names provided by the template.
|
||||||
|
|
||||||
`gen_theme.py`: generates theme files for palettes by mapping their color definitions
|
- An app and palette are the two required parameters. If no template or output
|
||||||
through app-specific templates. These templates specific how to relate an app's theme
|
paths are provided, they will be inferred according to the theme path
|
||||||
variables to the color names provided by the template.
|
standards seen above.
|
||||||
|
- The `--template` argument can be a directory or a file, depending on what
|
||||||
|
theme files you'd like to render.
|
||||||
|
- The `--output` path, if specified, must be a directory. Generated theme files
|
||||||
|
take on a name with the same stem as their source template, but using the
|
||||||
|
`.conf` extension.
|
||||||
|
- The TOML templates should make config variable names to JSON dot-notation
|
||||||
|
accessors. If color definitions are nested, the dot notation should be
|
||||||
|
properly expanded by the script when mapping the colors to keyword values.
|
||||||
|
- There are a number of checks for existing paths, even those inferred (e.g.,
|
||||||
|
template and output) from the palette and app. If the appropriate setup
|
||||||
|
hasn't been followed, the script will fail. Make sure the `theme` folder in
|
||||||
|
question and it's nested `app` directory are correctly setup before running
|
||||||
|
the script. (Perhaps down the line there are some easy auto-setup steps to
|
||||||
|
take here, but I'm not making that jump now.)
|
||||||
|
- TODO: open up different app "writers," or make it easy to extend output
|
||||||
|
syntax based on the app in question. This would like be as simple as mapping
|
||||||
|
app names to line-generating functions, which accept the keyword and color
|
||||||
|
(among other items). This can be fleshed out as needed.
|
||||||
|
|
||||||
- An app and palette are the two required parameters. If no template or output paths are
|
`sync.sh`: copies relevant configuration files from local paths into the
|
||||||
provided, they will be inferred according to the theme path standards seen above.
|
`autoconf` subpath. Markdown files in the docs directory then reference the
|
||||||
- The `--template` argument can be a directory or a file, depending on what theme files
|
local copies of these files, meaning the documentation updates dynamically when
|
||||||
you'd like to render.
|
the configuration files do. That is, the (possibly extracted) config snippets
|
||||||
- The `--output` path, if specified, must be a directory. Generated theme files take on
|
will change with the current state of my system config without any manual
|
||||||
a name with the same stem as their source template, but using the `.conf` extension.
|
intervention of the documentation files.
|
||||||
- The TOML templates should make config variable names to JSON dot-notation accessors. If
|
|
||||||
color definitions are nested, the dot notation should be properly expanded by the script
|
|
||||||
when mapping the colors to keyword values.
|
|
||||||
- There are a number of checks for existing paths, even those inferred (e.g., template and
|
|
||||||
output) from the palette and app. If the appropriate setup hasn't been followed, the
|
|
||||||
script will fail. Make sure the `theme` folder in question and it's nested `app`
|
|
||||||
directory are correctly setup before running the script. (Perhaps down the line there
|
|
||||||
are some easy auto-setup steps to take here, but I'm not making that jump now.)
|
|
||||||
- TODO: open up different app "writers," or make it easy to extend output syntax based on
|
|
||||||
the app in question. This would like be as simple as mapping app names to
|
|
||||||
line-generating functions, which accept the keyword and color (among other items). This
|
|
||||||
can be fleshed out as needed.
|
|
||||||
|
|
||||||
`sync.sh`: copies relevant configuration files from local paths into the `autoconf`
|
|
||||||
subpath. Markdown files in the docs directory then reference the local copies of these
|
|
||||||
files, meaning the documentation updates dynamically when the configuration files do. That
|
|
||||||
is, the (possibly extracted) config snippets will change with the current state of my
|
|
||||||
system config without any manual intervention of the documentation files.
|
|
||||||
|
|
||||||
### Specific theme-setting example
|
### Specific theme-setting example
|
||||||
To make clear how the theme setting script works on my system, the following breaks down
|
To make clear how the theme setting script works on my system, the following
|
||||||
exactly what steps are taken to exert as much scheme control as possible. Everything at this
|
breaks down exactly what steps are taken to exert as much scheme control as
|
||||||
point is wrapped up in a single `make set-<palette>-<scheme>` call; suppose we're
|
possible. Everything at this point is wrapped up in a single `make
|
||||||
currently running the dark scheme (see first image) and I run `make set-tone4-light`:
|
set-<palette>-<scheme>` call; suppose we're currently running the dark scheme
|
||||||
|
(see first image) and I run `make set-tone4-light`:
|
||||||
|
|
||||||

|
](_static/set-theme-1.png)
|
||||||
|
|
||||||
_(Starting point; have a GTK app (GNOME files), `kitty`, and Firefox (with the
|
_(Starting point; have a GTK app (GNOME files), `kitty`, and Firefox (with the
|
||||||
system-dependent default theme set). In Firefox, I have open `localsys` with its
|
system-dependent default theme set). In Firefox, I have open `localsys` with
|
||||||
scheme-mode to set to "auto," which should reflect the theme setting picked up by the
|
its scheme-mode to set to "auto," which should reflect the theme setting picked
|
||||||
browser (and note the white tab icon).)_
|
up by the browser (and note the white tab icon).)_
|
||||||
|
|
||||||
1. `set_theme.py` is invoked. Global settings are applied first, based on my OS (`Linux`),
|
1. `set_theme.py` is invoked. Global settings are applied first, based on my OS
|
||||||
which calls
|
(`Linux`), which calls
|
||||||
|
|
||||||
```
|
```
|
||||||
gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'
|
gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'
|
||||||
```
|
```
|
||||||
|
|
||||||
controlling settings for GTK apps and other `desktop-portal`-aware programs. This
|
controlling settings for GTK apps and other `desktop-portal`-aware programs.
|
||||||
yields the following:
|
This yields the following:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
_(Portal-aware apps changed, config apps not yet set. Scheme-aware sites are updated
|
_(Portal-aware apps changed, config apps not yet set. Scheme-aware sites are
|
||||||
without page refresh.)_
|
updated without page refresh.)_
|
||||||
2. Specific application styles are set. For now the list is small, including `kitty`,
|
2. Specific application styles are set. For now the list is small, including
|
||||||
`waybar`, and `sway`. `kitty` is the only type (2) application here, whereas the other
|
`kitty`, `waybar`, and `sway`. `kitty` is the only type (2) application
|
||||||
two are type (1).
|
here, whereas the other two are type (1).
|
||||||
|
|
||||||
a. For the type (1) apps, the canonical config files as specified in the app registry
|
a. For the type (1) apps, the canonical config files as specified in the app
|
||||||
are symlinked to their light variants. For `sway`, this is `~/.config/sway/config`,
|
registry are symlinked to their light variants. For `sway`, this is
|
||||||
and if we look at the `file`:
|
`~/.config/sway/config`, and if we look at the `file`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
.config/sway/config: symbolic link to ~/.config/sway/sway-tone4-light
|
.config/sway/config: symbolic link to ~/.config/sway/sway-tone4-light
|
||||||
```
|
```
|
||||||
b. For the type (2) apps, just the `current-theme.conf` file is symlinked to the
|
b. For the type (2) apps, just the `current-theme.conf` file is symlinked to
|
||||||
relevant palette-scheme file. `kitty` is such an app, with a supported theme file
|
the relevant palette-scheme file. `kitty` is such an app, with a
|
||||||
for `tone4`, and those files have been auto-generated via `gen_theme.py`. Looking at
|
supported theme file for `tone4`, and those files have been
|
||||||
this file under the `kitty` config directory:
|
auto-generated via `gen_theme.py`. Looking at this file under the `kitty`
|
||||||
|
config directory:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
.config/kitty/current-theme.conf: symbolic link to ~/Documents/projects/autoconf/autoconf/themes/tone4/apps/kitty/generated/light.conf
|
.config/kitty/current-theme.conf: symbolic link to
|
||||||
|
~/Documents/projects/autoconf/autoconf/themes/tone4/apps/kitty/generated/light.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
The `kitty.conf` file isn't changed, as all palette-related items are specified in
|
The `kitty.conf` file isn't changed, as all palette-related items are
|
||||||
the theme file. (Note that the general notion of a "theme" could include changes to
|
specified in the theme file. (Note that the general notion of a "theme"
|
||||||
other stylistic aspects, like the font family; this would likely require some hybrid
|
could include changes to other stylistic aspects, like the font family;
|
||||||
type 1-2 approach not yet implemented).
|
this would likely require some hybrid type 1-2 approach not yet
|
||||||
3. Live application instances are reloaded, according to the registered `refresh_cmd`s.
|
implemented).
|
||||||
Here the apps with style/config files set in step (2) are reloaded to reflect those
|
3. Live application instances are reloaded, according to the registered
|
||||||
changes. Again, in this example, this is `kitty`, `sway`, and the `waybar`.
|
`refresh_cmd`s. Here the apps with style/config files set in step (2) are
|
||||||
|
reloaded to reflect those changes. Again, in this example, this is `kitty`,
|
||||||
|
`sway`, and the `waybar`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
_(Final light setting: portal-dependent apps _and_ config-based apps changed)_
|
_(Final light setting: portal-dependent apps _and_ config-based apps
|
||||||
4. `set_theme.py` provides a report for the actions taken; in this case, the following was
|
changed)_
|
||||||
printed:
|
4. `set_theme.py` provides a report for the actions taken; in this case, the
|
||||||
|
following was printed:
|
||||||
|
|
||||||

|

|
||||||
_(`set_theme.py` output)_
|
_(`set_theme.py` output)_
|
||||||
|
|||||||
@@ -1,61 +1,64 @@
|
|||||||
# Configuring
|
# Configuring
|
||||||
`symconf` operates on a central directory that houses all of the config files you may wish
|
`symconf` operates on a central directory that houses all of the config files
|
||||||
to apply. The default location for this directory is your `$XDG_CONFIG_HOME` (e.g.,
|
you may wish to apply. The default location for this directory is your
|
||||||
`~/.config/symconf/`), but it can be any location on your system so long as it's specified
|
`$XDG_CONFIG_HOME` (e.g., `~/.config/symconf/`), but it can be any location on
|
||||||
(see more in Usage).
|
your system so long as it's specified (see more in Usage).
|
||||||
|
|
||||||
`symconf` expects you to create two top-level components in your config directory: an
|
`symconf` expects you to create two top-level components in your config
|
||||||
`apps/` directory and an `app_registry.toml` file.
|
directory: an `apps/` directory and an `app_registry.toml` file.
|
||||||
|
|
||||||
**High-level view**:
|
**High-level view**:
|
||||||
|
|
||||||
- `symconf` operates on a single directory that houses all your config files
|
- `symconf` operates on a single directory that houses all your config files
|
||||||
- Config files in this directory must be placed under an `apps/<app-name>/` folder to be
|
- Config files in this directory must be placed under an `apps/<app-name>/`
|
||||||
associated with the app `<app-name>`
|
folder to be associated with the app `<app-name>`
|
||||||
- For apps to be visible, you need an `app_registry.toml` file that tells `symconf` where
|
- For apps to be visible, you need an `app_registry.toml` file that tells
|
||||||
to symlink your files in `apps/`
|
`symconf` where to symlink your files in `apps/`
|
||||||
|
|
||||||
## Apps directory
|
## Apps directory
|
||||||
An `apps/` directory should be created in your config home, with a subdirectory
|
An `apps/` directory should be created in your config home, with a subdirectory
|
||||||
`apps/<app-name>/` for each app with config files that you'd like to be visible to
|
`apps/<app-name>/` for each app with config files that you'd like to be visible
|
||||||
`symconf`. Note that simply populating an app's config folder here will do nothing on its
|
to `symconf`. Note that simply populating an app's config folder here will do
|
||||||
own; the app must also have been registered (discussed in item #2) in order for these
|
nothing on its own; the app must also have been registered (discussed in item
|
||||||
files to be used when `symconf` is invoked. (This just means you can populate your `apps/`
|
#2) in order for these files to be used when `symconf` is invoked. (This just
|
||||||
folder safely without expecting any default behavior. More often than not you'll be
|
means you can populate your `apps/` folder safely without expecting any default
|
||||||
expected to tell `symconf` exactly where your config files should end up, meaning you know
|
behavior. More often than not you'll be expected to tell `symconf` exactly
|
||||||
exactly what it's doing.)
|
where your config files should end up, meaning you know exactly what it's
|
||||||
|
doing.)
|
||||||
|
|
||||||
### User config
|
### User config
|
||||||
Inside your app-specific subdirectory, your managed config files should be placed in a
|
Inside your app-specific subdirectory, your managed config files should be
|
||||||
`user/` subdirectory (distinguishing them from those generated by templates; see more
|
placed in a `user/` subdirectory (distinguishing them from those generated by
|
||||||
in Themes). Your config files themselves are then expected to follow a specific naming
|
templates; see more in Themes). Your config files themselves are then expected
|
||||||
scheme:
|
to follow a specific naming scheme:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
<palette>-<scheme>.<config-name>
|
<palette>-<scheme>.<config-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
This ties your config file to a particular theme setting as needed, and `symconf` will
|
This ties your config file to a particular theme setting as needed, and
|
||||||
apply it if it matches the theme setting you provide when invoked. The specific values are
|
`symconf` will apply it if it matches the theme setting you provide when
|
||||||
as follows:
|
invoked. The specific values are as follows:
|
||||||
|
|
||||||
- `scheme`: can be `light`, `dark`, or `none`. Indicates whether the config file should
|
- `scheme`: can be `light`, `dark`, or `none`. Indicates whether the config
|
||||||
be applied specifically when requesting a light or dark mode. Use `none` to indicate
|
file should be applied specifically when requesting a light or dark mode. Use
|
||||||
that the config file does not have settings specific to a light/dark mode.
|
`none` to indicate that the config file does not have settings specific to a
|
||||||
- `palette`: a "palette name" of your choosing, or `none`. The palette name you use
|
light/dark mode.
|
||||||
here may refer specifically to a color palette used by the config file, but can be
|
- `palette`: a "palette name" of your choosing, or `none`. The palette name you
|
||||||
used generally to indicate any particular group of config settings (e.g., fonts,
|
use here may refer specifically to a color palette used by the config file,
|
||||||
transparency, etc). Use `none` to indicate that the file does not correspond to any
|
but can be used generally to indicate any particular group of config settings
|
||||||
particular style group.
|
(e.g., fonts, transparency, etc). Use `none` to indicate that the file does
|
||||||
- `config-name`: the _name_ of the config file. This should correspond to _same path
|
not correspond to any particular style group.
|
||||||
name_ that is expected by the app being configured. For example, if your app expects a
|
- `config-name`: the _name_ of the config file. This should correspond to _same
|
||||||
config file at `a/b/c/d.conf`, "`d.conf`" is the path name.
|
path name_ that is expected by the app being configured. For example, if your
|
||||||
|
app expects a config file at `a/b/c/d.conf`, "`d.conf`" is the path name.
|
||||||
|
|
||||||
When invoking `symconf` with specific scheme and palette settings (see more in Usage),
|
When invoking `symconf` with specific scheme and palette settings (see more in
|
||||||
appropriate config files can be matched based on how you've named your files.
|
Usage), appropriate config files can be matched based on how you've named your
|
||||||
|
files.
|
||||||
|
|
||||||
For example, suppose I want to set up a simple light/dark mode switch for the `kitty`
|
For example, suppose I want to set up a simple light/dark mode switch for the
|
||||||
terminal emulator. The following tree demonstrates a valid setup:
|
`kitty` terminal emulator. The following tree demonstrates a valid setup:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
<config-home>
|
<config-home>
|
||||||
@@ -66,30 +69,31 @@ terminal emulator. The following tree demonstrates a valid setup:
|
|||||||
└── none-dark.kitty.conf
|
└── none-dark.kitty.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
where `none-light.kitty.conf` may set a light background and `none-dark.kitty.conf` a dark
|
where `none-light.kitty.conf` may set a light background and
|
||||||
one. `none` is used for the `<palette>` part of the name to indicate the configuration does
|
`none-dark.kitty.conf` a dark one. `none` is used for the `<palette>` part of
|
||||||
not pertain to any specific palette and can be matched even if one is not provided. With
|
the name to indicate the configuration does not pertain to any specific palette
|
||||||
an appropriate `app_regsitry.toml` file (see below), invoking
|
and can be matched even if one is not provided. With an appropriate
|
||||||
|
`app_regsitry.toml` file (see below), invoking
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
symconf --theme=light --apps=kitty
|
symconf --theme=light --apps=kitty
|
||||||
```
|
```
|
||||||
|
|
||||||
would symlink `$XDG_CONFIG_HOME/symconf/apps/kitty/user/none-light.kitty.conf` to
|
would symlink `$XDG_CONFIG_HOME/symconf/apps/kitty/user/none-light.kitty.conf`
|
||||||
`~/.config/kitty/kitty.conf`.
|
to `~/.config/kitty/kitty.conf`.
|
||||||
|
|
||||||
### Templatized config
|
### Templatized config
|
||||||
Note the potential inconvenience in needing to manage two separate config files in the
|
Note the potential inconvenience in needing to manage two separate config files
|
||||||
above example, very likely with all but one line of difference. Templating enables
|
in the above example, very likely with all but one line of difference.
|
||||||
populating config template files dynamically with theme-specific variables of your
|
Templating enables populating config template files dynamically with
|
||||||
choosing.
|
theme-specific variables of your choosing.
|
||||||
|
|
||||||
|
|
||||||
### Reload scripts
|
### Reload scripts
|
||||||
After symlinking a new set of config files, it is often necessary to reload the system or
|
After symlinking a new set of config files, it is often necessary to reload the
|
||||||
relevant apps in order for the new config settings to apply. Within an app's subdirectory,
|
system or relevant apps in order for the new config settings to apply. Within
|
||||||
a `call/` folder can be created to hold scripts that should apply based on certain schemes
|
an app's subdirectory, a `call/` folder can be created to hold scripts that
|
||||||
or palettes (matching them in the same way as config files). For example,
|
should apply based on certain schemes or palettes (matching them in the same
|
||||||
|
way as config files). For example,
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
<config-home>
|
<config-home>
|
||||||
@@ -99,22 +103,22 @@ or palettes (matching them in the same way as config files). For example,
|
|||||||
└── none-none.sh
|
└── none-none.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
`none-none.sh` might simply contain `kill -s USR1 $(pgrep kitty)`, which is a way to tell
|
`none-none.sh` might simply contain `kill -s USR1 $(pgrep kitty)`, which is a
|
||||||
all running `kitty` instances to reload their config settings. Again, following the naming
|
way to tell all running `kitty` instances to reload their config settings.
|
||||||
scheme for config files, a script named `none-none.sh` will apply under any scheme or
|
Again, following the naming scheme for config files, a script named
|
||||||
palette specification. Thus, in our light/dark mode switch example, invoking `symconf
|
`none-none.sh` will apply under any scheme or palette specification. Thus, in
|
||||||
--theme=light --apps=kitty` would:
|
our light/dark mode switch example, invoking `symconf --theme=light
|
||||||
|
--apps=kitty` would:
|
||||||
|
|
||||||
1. Search and match the config name `none-light.kitty.conf` and symlink it to
|
1. Search and match the config name `none-light.kitty.conf` and symlink it to
|
||||||
`~/.config/kitty/kitty.conf`.
|
`~/.config/kitty/kitty.conf`.
|
||||||
2. Search and match `none-none.sh` and execute it, applying the new light mode settings to
|
2. Search and match `none-none.sh` and execute it, applying the new light mode
|
||||||
all running `kitty` instances.
|
settings to all running `kitty` instances.
|
||||||
|
|
||||||
|
|
||||||
## App registry
|
## App registry
|
||||||
An `app_registry.toml` file, used to specify the target locations for app-specific
|
An `app_registry.toml` file, used to specify the target locations for
|
||||||
config files. To "register" an app, you simply need to add the following text block to
|
app-specific config files. To "register" an app, you simply need to add the
|
||||||
the `app_registry.toml` file:
|
following text block to the `app_registry.toml` file:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[app.<app-name>]
|
[app.<app-name>]
|
||||||
@@ -128,22 +132,22 @@ config_map = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
(Note that text in angle brackets refers to values that should be replaced.) This tells
|
(Note that text in angle brackets refers to values that should be replaced.)
|
||||||
`symconf` how it should handle each app's config files. The `<app-name>` (e.g.,
|
This tells `symconf` how it should handle each app's config files. The
|
||||||
`kitty`) should correspond to a subdirectory under `apps/` (e.g., `apps/kitty/`) that
|
`<app-name>` (e.g., `kitty`) should correspond to a subdirectory under `apps/`
|
||||||
holds your config files for that app. As shown, you then need to supply either of the
|
(e.g., `apps/kitty/`) that holds your config files for that app. As shown, you
|
||||||
following options:
|
then need to supply either of the following options:
|
||||||
|
|
||||||
- `config_dir`: specifies a single directory where all of the app's matching config
|
- `config_dir`: specifies a single directory where all of the app's matching
|
||||||
files should be symlinked. In the `kitty` example, this might be `~/.config/kitty`.
|
config files should be symlinked. In the `kitty` example, this might be
|
||||||
This is the simplest and most common option provided most apps expect all of their
|
`~/.config/kitty`. This is the simplest and most common option provided most
|
||||||
config files to be in a single directory.
|
apps expect all of their config files to be in a single directory.
|
||||||
- `config_map`: a dictionary mapping config file _path names_ to the _exact paths_ that
|
- `config_map`: a dictionary mapping config file _path names_ to the _exact
|
||||||
should be created during the symlink process. This is typically needed when an app
|
paths_ that should be created during the symlink process. This is typically
|
||||||
has many config files that need to be set in several disparate locations across your
|
needed when an app has many config files that need to be set in several
|
||||||
system. In the `kitty` example, although not necessary (and in general you should
|
disparate locations across your system. In the `kitty` example, although not
|
||||||
prefer to set `config_dir` when applicable), we could have the following
|
necessary (and in general you should prefer to set `config_dir` when
|
||||||
`config_map`:
|
applicable), we could have the following `config_map`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[app.kitty]
|
[app.kitty]
|
||||||
@@ -152,8 +156,9 @@ following options:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This tells `symconf` to symlink the exact location `~/.config/kitty/kitty.conf` to
|
This tells `symconf` to symlink the exact location
|
||||||
the matching `kitty.conf` under the `apps/kitty` directory.
|
`~/.config/kitty/kitty.conf` to the matching `kitty.conf` under the
|
||||||
|
`apps/kitty` directory.
|
||||||
|
|
||||||
## Directory structure
|
## Directory structure
|
||||||
In total, the structure of your base config directory can look as follows:
|
In total, the structure of your base config directory can look as follows:
|
||||||
|
|||||||
@@ -1,6 +1,170 @@
|
|||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
```{toctree}
|
```sh
|
||||||
:hidden:
|
usage: symconf [-h] [-c CONFIG_DIR] [-v] {config,generate,install,update} ...
|
||||||
|
|
||||||
|
Manage application configuration with symlinks.
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-c CONFIG_DIR, --config-dir CONFIG_DIR
|
||||||
|
Path to config directory
|
||||||
|
-v, --version Print symconf version
|
||||||
|
|
||||||
|
subcommand actions:
|
||||||
|
{config,generate,install,update}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Additional argument details:
|
||||||
|
|
||||||
|
- `-h --help`: print help message
|
||||||
|
- `-c --config-dir`: the location of the `symconf` config directory. Assumes
|
||||||
|
`$XDG_CONFIG_HOME` (e.g., `~/.config/symconf/`) by default.
|
||||||
|
|
||||||
|
## `config` subcommand
|
||||||
|
The config subcommand applies symlinks for registered application routes that
|
||||||
|
meet specified constraints.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
usage: symconf config [-h] [-s STYLE] [-m MODE] [-a APPS] [-T TEMPLATE_VARS [TEMPLATE_VARS ...]]
|
||||||
|
|
||||||
|
Set config files for registered applications.
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-s STYLE, --style STYLE
|
||||||
|
Style indicator (often a color palette) capturing thematic details in a config file
|
||||||
|
-m MODE, --mode MODE Preferred lightness mode/scheme, either "light," "dark," "any," or "none."
|
||||||
|
-a APPS, --apps APPS Application target for theme. App must be present in the registry. Use "*" to apply to all registered apps
|
||||||
|
-T TEMPLATE_VARS [TEMPLATE_VARS ...], --template-vars TEMPLATE_VARS [TEMPLATE_VARS ...]
|
||||||
|
Groups to use when populating templates, in the form group=value
|
||||||
|
```
|
||||||
|
|
||||||
|
- `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`).
|
||||||
|
|
||||||
|
|
||||||
|
## `generate` subcommand
|
||||||
|
The generate subcommand fills in templates with theme values matched under
|
||||||
|
provided constraints,
|
||||||
|
|
||||||
|
```sh
|
||||||
|
usage: symconf generate [-h] -o OUTPUT_DIR [-s STYLE] [-m MODE] [-a APPS] [-T TEMPLATE_VARS [TEMPLATE_VARS ...]]
|
||||||
|
|
||||||
|
Generate all template config files for specified apps
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-o OUTPUT_DIR, --output-dir OUTPUT_DIR
|
||||||
|
Path to write generated template files
|
||||||
|
-s STYLE, --style STYLE
|
||||||
|
Style indicator (often a color palette) capturing thematic details in a config file
|
||||||
|
-m MODE, --mode MODE Preferred lightness mode/scheme, either "light," "dark," "any," or "none."
|
||||||
|
-a APPS, --apps APPS Application target for theme. App must be present in the registry. Use "*" to apply to all registered apps
|
||||||
|
-T TEMPLATE_VARS [TEMPLATE_VARS ...], --template-vars TEMPLATE_VARS [TEMPLATE_VARS ...]
|
||||||
|
Groups to use when populating templates, in the form group=value
|
||||||
|
```
|
||||||
|
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
## `install` subcommand
|
||||||
|
The install subcommand runs the `install.sh` scripts for registered apps.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
usage: symconf install [-h] [-a APPS]
|
||||||
|
|
||||||
|
Run install scripts for registered applications.
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-a APPS, --apps APPS Application target for theme. App must be present in the registry. Use "*" to apply to all registered apps
|
||||||
|
```
|
||||||
|
|
||||||
|
- `symconf install`: runs install scripts for matching apps
|
||||||
|
* `-a --apps`: comma-separate list of registered apps, or `"*"` (default) to
|
||||||
|
consider all registered apps.
|
||||||
|
|
||||||
|
## `update` subcommand
|
||||||
|
The update subcommand runs the `update.sh` scripts for registered apps. This
|
||||||
|
action expects the install process for each matched app to have been run
|
||||||
|
beforehand.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
usage: symconf update [-h] [-a APPS]
|
||||||
|
|
||||||
|
Run update scripts for registered applications.
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-a APPS, --apps APPS Application target for theme. App must be present in the registry. Use "*" to apply to 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.
|
||||||
|
|
||||||
|
## Matching factors
|
||||||
|
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.
|
||||||
|
|||||||
0
example/README.md
Normal file
0
example/README.md
Normal file
@@ -1,27 +1,29 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools", "wheel", "setuptools-git-versioning>=2.0,<3"]
|
requires = ["setuptools", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[tool.setuptools-git-versioning]
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "symconf"
|
name = "symconf"
|
||||||
|
version = "0.8.4"
|
||||||
description = "Local app configuration manager"
|
description = "Local app configuration manager"
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dynamic = ["version"]
|
|
||||||
#license = {file = "LICENSE"}
|
|
||||||
authors = [
|
authors = [
|
||||||
{ name="Sam Griesemer", email="samgriesemer+git@gmail.com" },
|
{ name="Sam Griesemer", email="git@olog.io" },
|
||||||
|
]
|
||||||
|
readme = "README.md"
|
||||||
|
license = "MIT"
|
||||||
|
keywords = [
|
||||||
|
"tempate-engine",
|
||||||
|
"theme-switcher",
|
||||||
|
"configuration-files",
|
||||||
]
|
]
|
||||||
keywords = ["config"]
|
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python",
|
||||||
"License :: OSI Approved :: MIT License",
|
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
|
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
|
"Intended Audience :: End Users/Desktop",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyxdg",
|
"pyxdg",
|
||||||
@@ -32,15 +34,19 @@ dependencies = [
|
|||||||
symconf = "symconf.__main__:main"
|
symconf = "symconf.__main__:main"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
tests = ["pytest"]
|
dev = [
|
||||||
docs = [
|
"ipykernel",
|
||||||
|
]
|
||||||
|
doc = [
|
||||||
"sphinx",
|
"sphinx",
|
||||||
"sphinx-togglebutton",
|
"sphinx-togglebutton",
|
||||||
"sphinx-autodoc-typehints",
|
"sphinx-autodoc-typehints",
|
||||||
"furo",
|
"furo",
|
||||||
"myst-parser",
|
"myst-parser",
|
||||||
]
|
]
|
||||||
build = ["build", "twine"]
|
test = [
|
||||||
|
"pytest"
|
||||||
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://doc.olog.io/symconf"
|
Homepage = "https://doc.olog.io/symconf"
|
||||||
@@ -48,6 +54,21 @@ Documentation = "https://doc.olog.io/symconf"
|
|||||||
Repository = "https://git.olog.io/olog/symconf"
|
Repository = "https://git.olog.io/olog/symconf"
|
||||||
Issues = "https://git.olog.io/olog/symconf/issues"
|
Issues = "https://git.olog.io/olog/symconf/issues"
|
||||||
|
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
include = ["symconf*"] # pattern to match package names
|
include = ["symconf*"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 79
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["ANN", "E", "F", "UP", "B", "SIM", "I", "C4", "PERF"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
length-sort = true
|
||||||
|
order-by-type = false
|
||||||
|
force-sort-within-sections = false
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "space"
|
||||||
|
docstring-code-format = true
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
from symconf.runner import Runner
|
from importlib.metadata import version
|
||||||
from symconf.reader import DictReader
|
|
||||||
|
from symconf import util, config, reader, matching, template
|
||||||
from symconf.config import ConfigManager
|
from symconf.config import ConfigManager
|
||||||
|
from symconf.reader import DictReader
|
||||||
|
from symconf.runner import Runner
|
||||||
from symconf.matching import Matcher, FilePart
|
from symconf.matching import Matcher, FilePart
|
||||||
from symconf.template import Template, FileTemplate, TOMLTemplate
|
from symconf.template import Template, FileTemplate, TOMLTemplate
|
||||||
|
|
||||||
from symconf import config
|
__version__ = version("symconf")
|
||||||
from symconf import matching
|
|
||||||
from symconf import reader
|
|
||||||
from symconf import template
|
|
||||||
from symconf import theme
|
|
||||||
from symconf import util
|
|
||||||
|
|||||||
@@ -1,117 +1,214 @@
|
|||||||
import argparse
|
from argparse import Namespace, ArgumentParser
|
||||||
|
|
||||||
from symconf import util
|
from symconf import util, __version__
|
||||||
from symconf.config import ConfigManager
|
from symconf.config import ConfigManager
|
||||||
|
|
||||||
|
|
||||||
def add_install_subparser(subparsers):
|
def add_install_subparser(subparsers: ArgumentParser) -> None:
|
||||||
def install_apps(args):
|
def install_apps(args: Namespace) -> None:
|
||||||
cm = ConfigManager(args.config_dir)
|
cm = ConfigManager(args.config_dir)
|
||||||
cm.install_apps(apps=args.apps)
|
cm.install_apps(apps=args.apps)
|
||||||
|
|
||||||
parser = subparsers.add_parser(
|
parser = subparsers.add_parser(
|
||||||
'install',
|
"install",
|
||||||
description='Run install scripts for registered applications.'
|
description="Run install scripts for registered applications.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-a', '--apps',
|
"-a",
|
||||||
required = False,
|
"--apps",
|
||||||
default = "*",
|
required=False,
|
||||||
type = lambda s: s.split(',') if s != '*' else s,
|
default="*",
|
||||||
help = 'Application target for theme. App must be present in the registry. ' \
|
type=lambda s: s.split(",") if s != "*" else s,
|
||||||
+ 'Use "*" to apply to all registered apps'
|
help=(
|
||||||
|
"Application target for theme. App must be present in the "
|
||||||
|
'registry. Use "*" to apply to all registered apps'
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.set_defaults(func=install_apps)
|
parser.set_defaults(func=install_apps)
|
||||||
|
|
||||||
def add_update_subparser(subparsers):
|
|
||||||
def update_apps(args):
|
def add_update_subparser(subparsers: ArgumentParser) -> None:
|
||||||
|
def update_apps(args: Namespace) -> None:
|
||||||
cm = ConfigManager(args.config_dir)
|
cm = ConfigManager(args.config_dir)
|
||||||
cm.update_apps(apps=args.apps)
|
cm.update_apps(apps=args.apps)
|
||||||
|
|
||||||
parser = subparsers.add_parser(
|
parser = subparsers.add_parser(
|
||||||
'update',
|
"update", description="Run update scripts for registered applications."
|
||||||
description='Run update scripts for registered applications.'
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-a', '--apps',
|
"-a",
|
||||||
required = False,
|
"--apps",
|
||||||
default = '*',
|
required=False,
|
||||||
type = lambda s: s.split(',') if s != '*' else s,
|
default="*",
|
||||||
help = 'Application target for theme. App must be present in the registry. ' \
|
type=lambda s: s.split(",") if s != "*" else s,
|
||||||
+ 'Use "*" to apply to all registered apps'
|
help=(
|
||||||
|
"Application target for theme. App must be present in the "
|
||||||
|
'registry. Use "*" to apply to all registered apps'
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.set_defaults(func=update_apps)
|
parser.set_defaults(func=update_apps)
|
||||||
|
|
||||||
def add_config_subparser(subparsers):
|
|
||||||
def configure_apps(args):
|
def add_config_subparser(subparsers: ArgumentParser) -> None:
|
||||||
|
def configure_apps(args: Namespace) -> None:
|
||||||
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.mode,
|
||||||
style=args.style,
|
style=args.style,
|
||||||
|
**args.template_vars,
|
||||||
)
|
)
|
||||||
|
|
||||||
parser = subparsers.add_parser(
|
parser = subparsers.add_parser(
|
||||||
'config',
|
"config", description="Set config files for registered applications."
|
||||||
description='Set config files for registered applications.'
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-s', '--style',
|
"-s",
|
||||||
required = False,
|
"--style",
|
||||||
default = 'any',
|
required=False,
|
||||||
help = 'Style indicator (often a color palette) capturing thematic details in '
|
default="any",
|
||||||
'a config file'
|
help=(
|
||||||
|
"Style indicator (often a color palette) capturing "
|
||||||
|
"thematic details in a config file"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-m', '--mode',
|
"-m",
|
||||||
required = False,
|
"--mode",
|
||||||
default = "any",
|
required=False,
|
||||||
help = 'Preferred lightness mode/scheme, either "light," "dark," "any," or "none."'
|
default="any",
|
||||||
|
help=(
|
||||||
|
'Preferred lightness mode/scheme, either "light," "dark," '
|
||||||
|
'"any," or "none."'
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-a', '--apps',
|
"-a",
|
||||||
required = False,
|
"--apps",
|
||||||
default = "*",
|
required=False,
|
||||||
type = lambda s: s.split(',') if s != '*' else s,
|
default="*",
|
||||||
help = 'Application target for theme. App must be present in the registry. ' \
|
type=lambda s: s.split(",") if s != "*" else s,
|
||||||
+ 'Use "*" to apply to all registered apps'
|
help=(
|
||||||
|
"Application target for theme. App must be present in the "
|
||||||
|
'registry. Use "*" to apply to all registered apps'
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-T', '--template-vars',
|
"-T",
|
||||||
required = False,
|
"--template-vars",
|
||||||
nargs='+',
|
required=False,
|
||||||
|
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: ArgumentParser) -> None:
|
||||||
|
def generate_apps(args: Namespace) -> None:
|
||||||
|
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 = ArgumentParser(
|
||||||
'symconf',
|
"symconf", description="Manage application configuration with symlinks."
|
||||||
description='Manage application configuration with symlinks.'
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-c', '--config-dir',
|
"-c",
|
||||||
default = util.xdg_config_path(),
|
"--config-dir",
|
||||||
type = util.absolute_path,
|
default=util.xdg_config_path(),
|
||||||
help = 'Path to config directory'
|
type=util.absolute_path,
|
||||||
|
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() -> None:
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if 'func' in args:
|
if "func" in args:
|
||||||
args.func(args)
|
args.func(args)
|
||||||
else:
|
else:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
1027
symconf/config.py
1027
symconf/config.py
File diff suppressed because it is too large
Load Diff
@@ -1,188 +1,201 @@
|
|||||||
'''
|
"""
|
||||||
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
|
||||||
|
|
||||||
- ``config_pathname``: refers to a concrete filename, typically that which is expected by
|
<style>-<scheme>.<config_pathname>
|
||||||
the target app (e.g., ``kitty.conf``). In the context of ``config_map`` in the registry,
|
|
||||||
however, it merely serves as an identifier, as it can be mapped onto any path.
|
- ``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, however, it merely serves as an identifier,
|
||||||
|
as it can be mapped onto any path.
|
||||||
- ``scheme``: indicates the lightness mode ("light" or "dark")
|
- ``scheme``: indicates the lightness mode ("light" or "dark")
|
||||||
- ``style``: general identifier capturing the stylizations applied to the config file.
|
- ``style``: general identifier capturing the stylizations applied to the
|
||||||
This is typically of the form ``<variant>-<palette>``, i.e., including a reference to a
|
config file. This is typically of the form ``<variant>-<palette>``, i.e.,
|
||||||
particular color palette.
|
including a reference to a particular color palette.
|
||||||
|
|
||||||
For example
|
For example
|
||||||
|
|
||||||
```sh
|
.. code-block:: sh
|
||||||
soft-gruvbox-dark.kitty.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
gets mapped to
|
soft-gruvbox-dark.kitty.conf
|
||||||
|
|
||||||
|
gets mapped to
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
style -> "soft-gruvbox"
|
||||||
|
scheme -> "dark"
|
||||||
|
pathname -> "kitty.conf"
|
||||||
|
"""
|
||||||
|
|
||||||
```sh
|
|
||||||
style -> "soft-gruvbox"
|
|
||||||
scheme -> "dark"
|
|
||||||
pathname -> "kitty.conf"
|
|
||||||
```
|
|
||||||
'''
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from symconf import util
|
from symconf import util
|
||||||
|
|
||||||
|
|
||||||
class FilePart:
|
class FilePart:
|
||||||
def __init__(self, path: str | Path):
|
def __init__(self, path: str | Path) -> None:
|
||||||
self.path = util.absolute_path(path)
|
self.path = util.absolute_path(path)
|
||||||
self.pathname = self.path.name
|
self.pathname = self.path.name
|
||||||
|
|
||||||
parts = str(self.pathname).split('.')
|
parts = str(self.pathname).split(".")
|
||||||
if len(parts) < 2:
|
if len(parts) < 2:
|
||||||
raise ValueError(f'Filename "{pathname}" incorrectly formatted, ignoring')
|
raise ValueError(
|
||||||
|
f'Filename "{self.pathname}" incorrectly formatted, ignoring'
|
||||||
|
)
|
||||||
|
|
||||||
self.theme = parts[0]
|
self.theme = parts[0]
|
||||||
self.conf = '.'.join(parts[1:])
|
self.conf = ".".join(parts[1:])
|
||||||
|
|
||||||
theme_split = self.theme.split('-')
|
theme_split = self.theme.split("-")
|
||||||
self.scheme = theme_split[-1]
|
self.scheme = theme_split[-1]
|
||||||
self.style = '-'.join(theme_split[:-1])
|
self.style = "-".join(theme_split[:-1])
|
||||||
|
|
||||||
self.index = -1
|
self.index = -1
|
||||||
|
|
||||||
def set_index(self, idx: int):
|
def set_index(self, idx: int) -> None:
|
||||||
self.index = idx
|
self.index = idx
|
||||||
|
|
||||||
|
|
||||||
class Matcher:
|
class Matcher:
|
||||||
def get_file_parts(
|
def get_file_parts(
|
||||||
self,
|
self,
|
||||||
paths: list[str | Path],
|
paths: list[str | Path],
|
||||||
) -> list[FilePart]:
|
) -> list[FilePart]:
|
||||||
'''
|
"""
|
||||||
Split pathnames into parts for matching.
|
Split pathnames into parts for matching.
|
||||||
|
|
||||||
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>``.
|
|
||||||
'''
|
|
||||||
file_parts = []
|
file_parts = []
|
||||||
for path in paths:
|
for path in paths:
|
||||||
try:
|
try:
|
||||||
config_file = FilePart(path)
|
config_file = FilePart(path)
|
||||||
file_parts.append(config_file)
|
file_parts.append(config_file)
|
||||||
except ValueError as e:
|
except ValueError:
|
||||||
print(f'Filename "{pathname}" incorrectly formatted, ignoring')
|
print(f'Filename "{path}" incorrectly formatted, ignoring')
|
||||||
|
|
||||||
return file_parts
|
return file_parts
|
||||||
|
|
||||||
def prefix_order(
|
def prefix_order(
|
||||||
self,
|
self,
|
||||||
scheme,
|
scheme: str,
|
||||||
style,
|
style: str,
|
||||||
strict=False,
|
strict: bool = False,
|
||||||
) -> list[tuple[str, str]]:
|
) -> list[tuple[str, str]]:
|
||||||
'''
|
"""
|
||||||
Determine the order of concrete config pathname parts to match, given the
|
Determine the order of concrete config pathname parts to match, given
|
||||||
``scheme`` and ``style`` inputs.
|
the ``scheme`` and ``style`` inputs.
|
||||||
|
|
||||||
There is a unique preferred match order when ``style``, ``scheme``, both, or none
|
There is a unique preferred match order when ``style``, ``scheme``,
|
||||||
are ``any``. In general, when ``any`` is provided for a given factor, it is
|
both, or none are ``any``. In general, when ``any`` is provided for a
|
||||||
best matched by a config file that expresses indifference under that factor.
|
given factor, it is best matched by a config file that expresses
|
||||||
'''
|
indifference under that factor.
|
||||||
# explicit cases are the most easily managed here, even if a little redundant
|
"""
|
||||||
|
|
||||||
|
# explicit cases are the most easily managed here, even if a little
|
||||||
|
# redundant
|
||||||
if strict:
|
if strict:
|
||||||
theme_order = [
|
theme_order = [
|
||||||
(style, scheme),
|
(style, scheme),
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
# inverse order of match relaxation; intention being to overwrite with
|
# inverse order of match relaxation; intention being to overwrite
|
||||||
# results from increasingly relevant groups given the conditions
|
# with results from increasingly relevant groups given the
|
||||||
if style == 'any' and scheme == 'any':
|
# conditions
|
||||||
|
if style == "any" and scheme == "any":
|
||||||
# prefer both be "none", with preference for specific scheme
|
# prefer both be "none", with preference for specific scheme
|
||||||
theme_order = [
|
theme_order = [
|
||||||
(style , scheme),
|
(style, scheme),
|
||||||
(style , 'none'),
|
(style, "none"),
|
||||||
('none' , scheme),
|
("none", scheme),
|
||||||
('none' , 'none'),
|
("none", "none"),
|
||||||
]
|
]
|
||||||
elif style == 'any':
|
elif style == "any":
|
||||||
# prefer style to be "none", then specific, then relax specific scheme
|
# prefer style to be "none", then specific, then relax specific
|
||||||
# to "none"
|
# scheme to "none"
|
||||||
theme_order = [
|
theme_order = [
|
||||||
(style , 'none'),
|
(style, "none"),
|
||||||
('none' , 'none'),
|
("none", "none"),
|
||||||
(style , scheme),
|
(style, scheme),
|
||||||
('none' , scheme),
|
("none", scheme),
|
||||||
]
|
]
|
||||||
elif scheme == 'any':
|
elif scheme == "any":
|
||||||
# prefer scheme to be "none", then specific, then relax specific style
|
# prefer scheme to be "none", then specific, then relax
|
||||||
# to "none"
|
# specific style to "none"
|
||||||
theme_order = [
|
theme_order = [
|
||||||
('none' , scheme),
|
("none", scheme),
|
||||||
('none' , 'none'),
|
("none", "none"),
|
||||||
(style , scheme),
|
(style, scheme),
|
||||||
(style , 'none'),
|
(style, "none"),
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
# neither component is any; prefer most specific
|
# neither component is any; prefer most specific
|
||||||
theme_order = [
|
theme_order = [
|
||||||
('none' , 'none'),
|
("none", "none"),
|
||||||
('none' , scheme),
|
("none", scheme),
|
||||||
(style , 'none'),
|
(style, "none"),
|
||||||
(style , scheme),
|
(style, scheme),
|
||||||
]
|
]
|
||||||
|
|
||||||
return theme_order
|
return theme_order
|
||||||
|
|
||||||
def match_paths(
|
def match_paths(
|
||||||
self,
|
self,
|
||||||
paths: list[str | Path],
|
paths: list[str | Path],
|
||||||
prefix_order: list[tuple[str, str]],
|
prefix_order: list[tuple[str, str]],
|
||||||
) -> list[FilePart]:
|
) -> list[FilePart]:
|
||||||
'''
|
"""
|
||||||
Find and return FilePart matches according to the provided prefix order.
|
Find and return FilePart matches according to the provided prefix
|
||||||
|
order.
|
||||||
|
|
||||||
The prefix order specifies all valid style-scheme combos that can be considered as
|
The prefix order specifies all valid style-scheme combos that can be
|
||||||
"consistent" with some user input (and is computed external to this method). For
|
considered as "consistent" with some user input (and is computed
|
||||||
example, it could be
|
external to this method). For example, it could be
|
||||||
|
|
||||||
```py
|
.. code-block:: python
|
||||||
[
|
|
||||||
('none', 'none')
|
|
||||||
('none', 'dark')
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
indicating that either ``none-none.<config>`` or ``none-dark.<config>`` would be
|
[("none", "none")("none", "dark")]
|
||||||
considered matching pathnames, with the latter being preferred.
|
|
||||||
|
|
||||||
This method exists because we need a way to allow any of the combos in the prefix
|
indicating that either ``none-none.<config>`` or ``none-dark.<config>``
|
||||||
order to match the candidate files. We don't know a priori how good of a match
|
would be considered matching pathnames, with the latter being
|
||||||
will be available, so we consider each file for each of the prefixes, and take the
|
preferred.
|
||||||
latest/best match for each unique config pathname (allowing for a "soft" match).
|
|
||||||
|
|
||||||
.. admonition: Checking for matches
|
|
||||||
|
|
||||||
When thinking about how best to structure this method, it initially felt like
|
This method exists because we need a way to allow any of the combos in
|
||||||
indexing factors of the FileParts would make the most sense, preventing the
|
the prefix order to match the candidate files. We don't know a priori
|
||||||
inner loop that needs to inspect each FilePart for each element of the prefix
|
how good of a match will be available, so we consider each file for
|
||||||
order. But indexing the file parts and checking against prefixes isn't so
|
each of the prefixes, and take the latest/best match for each unique
|
||||||
straightforward, as we'd still need to check matches by factor. For instance,
|
config pathname (allowing for a "soft" match).
|
||||||
if we index by style-scheme, either are allowed to be "any," so we'd need to
|
|
||||||
check for the 4 valid combos and join the matching lists. If we index by both
|
|
||||||
factors individually, we may have several files associated with a given key,
|
|
||||||
and then need to coordinate the checks across both to ensure they belong to
|
|
||||||
the same file.
|
|
||||||
|
|
||||||
In any case, you should be able to do this in a way that's a bit more
|
.. admonition:: Checking for matches
|
||||||
efficient, but the loop and the simple conditionals is just much simpler to
|
|
||||||
follow. We're also talking about at most 10s of files, so it really doesn't
|
When thinking about how best to structure this method, it initially
|
||||||
matter.
|
felt like indexing factors of the FileParts would make the most
|
||||||
|
sense, preventing the inner loop that needs to inspect each
|
||||||
|
FilePart for each element of the prefix order. But indexing the
|
||||||
|
file parts and checking against prefixes isn't so straightforward,
|
||||||
|
as we'd still need to check matches by factor. For instance, if we
|
||||||
|
index by style-scheme, either are allowed to be "any," so we'd need
|
||||||
|
to check for the 4 valid combos and join the matching lists. If we
|
||||||
|
index by both factors individually, we may have several files
|
||||||
|
associated with a given key, and then need to coordinate the checks
|
||||||
|
across both to ensure they belong to the same file.
|
||||||
|
|
||||||
|
In any case, you should be able to do this in a way that's a bit
|
||||||
|
more efficient, but the loop and the simple conditionals is just
|
||||||
|
much simpler to follow. We're also talking about at most 10s of
|
||||||
|
files, so it really doesn't matter.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
pathnames:
|
pathnames:
|
||||||
@@ -190,50 +203,54 @@ class Matcher:
|
|||||||
style:
|
style:
|
||||||
prefix_order:
|
prefix_order:
|
||||||
strict:
|
strict:
|
||||||
'''
|
"""
|
||||||
|
|
||||||
file_parts = self.get_file_parts(paths)
|
file_parts = self.get_file_parts(paths)
|
||||||
|
|
||||||
ordered_matches = []
|
ordered_matches = []
|
||||||
for i, (style_prefix, scheme_prefix) in enumerate(prefix_order):
|
for i, (style_prefix, scheme_prefix) in enumerate(prefix_order):
|
||||||
for fp in file_parts:
|
for fp in file_parts:
|
||||||
style_match = style_prefix == fp.style or style_prefix == 'any'
|
style_match = style_prefix == fp.style or style_prefix == "any"
|
||||||
scheme_match = scheme_prefix == fp.scheme or scheme_prefix == 'any'
|
scheme_match = (
|
||||||
|
scheme_prefix == fp.scheme or scheme_prefix == "any"
|
||||||
|
)
|
||||||
|
|
||||||
if style_match and scheme_match:
|
if style_match and scheme_match:
|
||||||
fp.set_index(i+1)
|
fp.set_index(i + 1)
|
||||||
ordered_matches.append(fp)
|
ordered_matches.append(fp)
|
||||||
|
|
||||||
return ordered_matches
|
return ordered_matches
|
||||||
|
|
||||||
def relaxed_match(
|
def relaxed_match(self, match_list: list[FilePart]) -> list[FilePart]:
|
||||||
self,
|
"""
|
||||||
match_list: list[FilePart]
|
|
||||||
) -> list[FilePart]:
|
|
||||||
'''
|
|
||||||
Isolate the best match in a match list and find its relaxed variants.
|
Isolate the best match in a match list and find its relaxed variants.
|
||||||
|
|
||||||
This method allows us to use the ``match_paths()`` method for matching templates
|
This method allows us to use the ``match_paths()`` method for matching
|
||||||
rather than direct user config files. In the latter case, we want to symlink the
|
templates rather than direct user config files. In the latter case, we
|
||||||
single best config file match for each stem, across all stems with matching
|
want to symlink the single best config file match for each stem, across
|
||||||
prefixes (e.g., ``none-dark.config.a`` and ``solarized-dark.config.b`` have two
|
all stems with matching prefixes (e.g., ``none-dark.config.a`` and
|
||||||
separate stems with prefixes that could match ``scheme=dark, style=any`` query).
|
``solarized-dark.config.b`` have two separate stems with prefixes that
|
||||||
We can find these files by just indexing the ``match_path`` outputs (i.e., all
|
could match ``scheme=dark, style=any`` query). We can find these files
|
||||||
matches) by config pathname and taking the one that appears latest (under the
|
by just indexing the ``match_path`` outputs (i.e., all matches) by
|
||||||
|
config pathname and taking the one that appears latest (under the
|
||||||
prefix order) for each unique value.
|
prefix order) for each unique value.
|
||||||
|
|
||||||
In the template matching case, we want only a single best file match, period
|
In the template matching case, we want only a single best file match,
|
||||||
(there's really no notion of "config stems," it's just the prefixes). Once that
|
period (there's really no notion of "config stems," it's just the
|
||||||
match has been found, we can then "relax" either the scheme or style (or both) to
|
prefixes). Once that match has been found, we can then "relax" either
|
||||||
``none``, and if the corresponding files exist, we use those as parts of the
|
the scheme or style (or both) to ``none``, and if the corresponding
|
||||||
template keys. For example, if we match ``solarized-dark.toml``, we would also
|
files exist, we use those as parts of the template keys. For example,
|
||||||
consider the values in ``none-dark.toml`` if available. The TOML values that are
|
if we match ``solarized-dark.toml``, we would also consider the values
|
||||||
defined in the most specific (i.e., better under the prefix order) match are
|
in ``none-dark.toml`` if available. The TOML values that are defined in
|
||||||
loaded "on top of" those less specific matches, overwriting keys when there's a
|
the most specific (i.e., better under the prefix order) match are
|
||||||
conflict. ``none-dark.toml``, for instance, might define a general dark scheme
|
loaded "on top of" those less specific matches, overwriting keys when
|
||||||
background color, but a more specific definition in ``solarized-dark.toml`` would
|
there's a conflict. ``none-dark.toml``, for instance, might define a
|
||||||
take precedent. These TOML files would be stacked before using the resulting
|
general dark scheme background color, but a more specific definition in
|
||||||
dictionary to populate config templates.
|
``solarized-dark.toml`` would take precedent. These TOML files would be
|
||||||
'''
|
stacked before using the resulting dictionary to populate config
|
||||||
|
templates.
|
||||||
|
"""
|
||||||
|
|
||||||
if not match_list:
|
if not match_list:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -241,11 +258,10 @@ class Matcher:
|
|||||||
match = match_list[-1]
|
match = match_list[-1]
|
||||||
|
|
||||||
for fp in match_list:
|
for fp in match_list:
|
||||||
style_match = fp.style == match.style or fp.style == 'none'
|
style_match = fp.style == match.style or fp.style == "none"
|
||||||
scheme_match = fp.scheme == match.scheme or fp.scheme == 'none'
|
scheme_match = fp.scheme == match.scheme or fp.scheme == "none"
|
||||||
|
|
||||||
if style_match and scheme_match:
|
if style_match and scheme_match:
|
||||||
relaxed_map[fp.pathname] = fp
|
relaxed_map[fp.pathname] = fp
|
||||||
|
|
||||||
return list(relaxed_map.values())
|
return list(relaxed_map.values())
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,44 @@
|
|||||||
|
"""
|
||||||
|
Simplified management for nested dictionaries
|
||||||
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import pprint
|
import pprint
|
||||||
import tomllib
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import tomllib
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from symconf.util import deep_update
|
from symconf.util import deep_update
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DictReader:
|
class DictReader:
|
||||||
def __init__(self, toml_path=None):
|
def __init__(self, toml_path: str | None = None) -> None:
|
||||||
self._config = {}
|
self._config = {}
|
||||||
self.toml_path = toml_path
|
self.toml_path = toml_path
|
||||||
|
|
||||||
if toml_path is not None:
|
if toml_path is not None:
|
||||||
self._config = self._load_toml(toml_path)
|
self._config = self._load_toml(toml_path)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return pprint.pformat(self._config, indent=4)
|
return pprint.pformat(self._config, indent=4)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _load_toml(toml_path) -> dict[str, Any]:
|
def _load_toml(toml_path: str) -> dict[str, Any]:
|
||||||
return tomllib.loads(Path(toml_path).read_text())
|
return tomllib.loads(Path(toml_path).read_text())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, config_dict):
|
def from_dict(cls, config_dict: dict) -> "DictReader":
|
||||||
new_instance = cls()
|
new_instance = cls()
|
||||||
new_instance._config = copy.deepcopy(config_dict)
|
new_instance._config = copy.deepcopy(config_dict)
|
||||||
return new_instance
|
return new_instance
|
||||||
|
|
||||||
def update(self, config, in_place=False):
|
def update(
|
||||||
|
self, config: "DictReader", in_place: bool = False
|
||||||
|
) -> "DictReader":
|
||||||
new_config = deep_update(self._config, config._config)
|
new_config = deep_update(self._config, config._config)
|
||||||
|
|
||||||
if in_place:
|
if in_place:
|
||||||
@@ -38,13 +47,14 @@ class DictReader:
|
|||||||
|
|
||||||
return self.from_dict(new_config)
|
return self.from_dict(new_config)
|
||||||
|
|
||||||
def copy(self):
|
def copy(self) -> "DictReader":
|
||||||
return self.from_dict(copy.deepcopy(self._config))
|
return self.from_dict(copy.deepcopy(self._config))
|
||||||
|
|
||||||
def get_subconfig(self, key): pass
|
def get_subconfig(self, key: str) -> None: # "DictReader":
|
||||||
|
pass
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key: str, default: str | None = None) -> str | None:
|
||||||
keys = key.split('.')
|
keys = key.split(".")
|
||||||
|
|
||||||
subconfig = self._config
|
subconfig = self._config
|
||||||
for subkey in keys[:-1]:
|
for subkey in keys[:-1]:
|
||||||
@@ -55,8 +65,8 @@ class DictReader:
|
|||||||
|
|
||||||
return subconfig.get(keys[-1], default)
|
return subconfig.get(keys[-1], default)
|
||||||
|
|
||||||
def set(self, key, value):
|
def set(self, key: str, value: str | None) -> bool:
|
||||||
keys = key.split('.')
|
keys = key.split(".")
|
||||||
|
|
||||||
subconfig = self._config
|
subconfig = self._config
|
||||||
for subkey in keys[:-1]:
|
for subkey in keys[:-1]:
|
||||||
@@ -65,7 +75,8 @@ class DictReader:
|
|||||||
|
|
||||||
if type(subconfig) is not dict:
|
if type(subconfig) is not dict:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Attempting to set nested key with an existing non-dict parent'
|
"Attempting to set nested key with an "
|
||||||
|
"existing non-dict parent"
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -75,20 +86,20 @@ class DictReader:
|
|||||||
subconfig[subkey] = subdict
|
subconfig[subkey] = subdict
|
||||||
subconfig = subdict
|
subconfig = subdict
|
||||||
|
|
||||||
subconfig.update({ keys[-1]: value })
|
subconfig.update({keys[-1]: value})
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def generate_hash(self, exclude_keys=None):
|
def generate_hash(self, exclude_keys: list[str] | None = None) -> str:
|
||||||
inst_copy = self.copy()
|
inst_copy = self.copy()
|
||||||
|
|
||||||
if exclude_keys is not None:
|
if exclude_keys is not None:
|
||||||
for key in exclude_keys:
|
for key in exclude_keys:
|
||||||
inst_copy.set(key, None)
|
inst_copy.set(key, None)
|
||||||
|
|
||||||
items = inst_copy._config.items()
|
items = inst_copy._config.items()
|
||||||
|
|
||||||
# create hash from config options
|
# create hash from config options
|
||||||
config_str = str(sorted(items))
|
config_str = str(sorted(items))
|
||||||
|
|
||||||
return hashlib.md5(config_str.encode()).hexdigest()
|
return hashlib.md5(config_str.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,53 @@
|
|||||||
|
"""
|
||||||
|
Handle job/script execution
|
||||||
|
"""
|
||||||
|
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from colorama import Fore, Back, Style
|
from colorama import Fore, Style
|
||||||
|
|
||||||
from symconf.util import printc, color_text
|
from symconf.util import color_text
|
||||||
|
|
||||||
|
|
||||||
class Runner:
|
class Runner:
|
||||||
def run_script(
|
def run_script(
|
||||||
self,
|
self,
|
||||||
script: str | Path,
|
script: str | Path,
|
||||||
):
|
) -> str | None:
|
||||||
script_path = Path(script)
|
script_path = Path(script)
|
||||||
|
|
||||||
if script_path.stat().st_mode & stat.S_IXUSR == 0:
|
if script_path.stat().st_mode & stat.S_IXUSR == 0:
|
||||||
print(
|
print(
|
||||||
color_text("│", Fore.BLUE),
|
color_text("│", Fore.BLUE),
|
||||||
color_text(
|
color_text(
|
||||||
f' > script "{script_path.name}" missing execute permissions, skipping',
|
f' > script "{script_path.name}" missing '
|
||||||
Fore.RED + Style.DIM
|
"execute permissions, skipping",
|
||||||
)
|
Fore.RED + Style.DIM,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
print(
|
print(
|
||||||
color_text("│", Fore.BLUE),
|
color_text("│", Fore.BLUE),
|
||||||
color_text(
|
color_text(f' > running script "{script_path.name}"', Fore.BLUE),
|
||||||
f' > running script "{script_path.name}"',
|
|
||||||
Fore.BLUE
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
output = subprocess.check_output(str(script_path), shell=True)
|
output = subprocess.check_output(str(script_path), shell=True)
|
||||||
|
|
||||||
if output:
|
if output:
|
||||||
fmt_output = output.decode().strip().replace(
|
fmt_output = (
|
||||||
'\n',
|
output.decode()
|
||||||
f'\n{Fore.BLUE}{Style.NORMAL}│{Style.DIM} '
|
.strip()
|
||||||
|
.replace("\n", f"\n{Fore.BLUE}{Style.NORMAL}│{Style.DIM} ")
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
color_text("│", Fore.BLUE),
|
color_text("│", Fore.BLUE),
|
||||||
color_text(
|
color_text(
|
||||||
f' captured script output "{fmt_output}"',
|
f' captured script output "{fmt_output}"',
|
||||||
Fore.BLUE + Style.DIM
|
Fore.BLUE + Style.DIM,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return output
|
return output
|
||||||
@@ -52,7 +55,7 @@ class Runner:
|
|||||||
def run_many(
|
def run_many(
|
||||||
self,
|
self,
|
||||||
script_list: list[str | Path],
|
script_list: list[str | Path],
|
||||||
):
|
) -> list[str | None]:
|
||||||
outputs = []
|
outputs = []
|
||||||
for script in script_list:
|
for script in script_list:
|
||||||
output = self.run_script(script)
|
output = self.run_script(script)
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
"""
|
||||||
|
Support for basic config templates
|
||||||
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import tomllib
|
import tomllib
|
||||||
|
from typing import Any
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from symconf import util
|
from symconf import util
|
||||||
@@ -9,62 +14,96 @@ from symconf.reader import DictReader
|
|||||||
class Template:
|
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{{((?:(?!x{{).)*)}}",
|
||||||
|
) -> None:
|
||||||
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,
|
||||||
template_dict : dict,
|
template_dict: dict,
|
||||||
) -> 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: re.Match,
|
||||||
|
dict_reader: DictReader,
|
||||||
|
) -> str:
|
||||||
|
key = match.group(1)
|
||||||
|
|
||||||
|
return str(dict_reader.get(key))
|
||||||
|
|
||||||
|
def _exe_fill(
|
||||||
|
self,
|
||||||
|
match: re.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{{((?:(?!x{{).)*)}}",
|
||||||
|
) -> None:
|
||||||
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{{((?:(?!x{{).)*)}}",
|
||||||
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
toml_path,
|
toml_path,
|
||||||
pattern=pattern
|
key_pattern=key_pattern,
|
||||||
|
exe_pattern=exe_pattern,
|
||||||
)
|
)
|
||||||
|
|
||||||
def fill(
|
def fill_dict(
|
||||||
self,
|
self,
|
||||||
template_dict : dict,
|
template_dict: dict,
|
||||||
) -> str:
|
) -> dict[str, Any]:
|
||||||
filled_template = super().fill(template_dict)
|
filled_template = super().fill(template_dict)
|
||||||
toml_dict = tomllib.loads(filled_template)
|
toml_dict = tomllib.loads(filled_template)
|
||||||
|
|
||||||
return toml_dict
|
return toml_dict
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def stack_toml(
|
def stack_toml(path_list: list[Path]) -> dict:
|
||||||
path_list: list[Path]
|
|
||||||
) -> dict:
|
|
||||||
stacked_dict = {}
|
stacked_dict = {}
|
||||||
for toml_path in path_list:
|
for toml_path in path_list:
|
||||||
updated_map = tomllib.load(toml_path.open('rb'))
|
updated_map = tomllib.load(toml_path.open("rb"))
|
||||||
stacked_dict = util.deep_update(stacked_dict, updated_map)
|
stacked_dict = util.deep_update(stacked_dict, updated_map)
|
||||||
|
|
||||||
return stacked_dict
|
return stacked_dict
|
||||||
|
|||||||
@@ -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}]')
|
|
||||||
|
|
||||||
@@ -1,33 +1,35 @@
|
|||||||
import re
|
import re
|
||||||
import argparse
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from argparse import Action, Namespace, ArgumentParser
|
||||||
|
|
||||||
from xdg import BaseDirectory
|
from xdg import BaseDirectory
|
||||||
from colorama import Fore, Back, Style
|
from colorama import Back, Fore, Style
|
||||||
from colorama.ansi import AnsiFore, AnsiBack, AnsiStyle
|
from colorama.ansi import AnsiCodes
|
||||||
|
|
||||||
|
|
||||||
def color_text(text, *colorama_args):
|
def color_text(text: str, *colorama_args: AnsiCodes) -> str:
|
||||||
'''
|
"""
|
||||||
Colorama text helper function
|
Colorama text helper function
|
||||||
|
|
||||||
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
|
||||||
(Fore, Back, Style) affected the styles passed in. This works when an outer call is
|
groups (Fore, Back, Style) affected the styles passed in. This works when
|
||||||
changing styles in one group, and an inner call is changing styles in another, but
|
an outer call is changing styles in one group, and an inner call is
|
||||||
_not_ when affected groups overlap.
|
changing styles in another, but *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 background or style with affecting the foreground.
|
||||||
|
The primary use case here is styling a group of text a single color, but
|
||||||
|
applying ``BRIGHT`` or ``DIM`` styles only to some text elements within. If
|
||||||
|
we didn't reset by group, the outer coloration request will be "canceled
|
||||||
|
out" as soon as the first inner call is made (since the unconditional
|
||||||
|
behavior just employs ``Style.RESET_ALL``).
|
||||||
|
"""
|
||||||
|
|
||||||
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
|
|
||||||
background or style with affecting the foreground. The primary use case here is
|
|
||||||
styling a group of text a single color, but applying ``BRIGHT`` or ``DIM`` styles only
|
|
||||||
to some text elements within. If we didn't reset by group, the outer coloration
|
|
||||||
request will be "canceled out" as soon as the first inner call is made (since the
|
|
||||||
unconditional behavior just employs ``Style.RESET_ALL``).
|
|
||||||
'''
|
|
||||||
# reverse map colorama Ansi codes
|
# reverse map colorama Ansi codes
|
||||||
resets = []
|
resets = []
|
||||||
for carg in colorama_args:
|
for carg in colorama_args:
|
||||||
match = re.match(r'.*\[(\d+)m', carg)
|
match = re.match(r".*\[(\d+)m", carg)
|
||||||
if match:
|
if match:
|
||||||
intv = int(match.group(1))
|
intv = int(match.group(1))
|
||||||
if (intv >= 30 and intv <= 39) or (intv >= 90 and intv <= 97):
|
if (intv >= 30 and intv <= 39) or (intv >= 90 and intv <= 97):
|
||||||
@@ -39,42 +41,62 @@ def color_text(text, *colorama_args):
|
|||||||
|
|
||||||
return f"{''.join(colorama_args)}{text}{''.join(resets)}"
|
return f"{''.join(colorama_args)}{text}{''.join(resets)}"
|
||||||
|
|
||||||
def printc(text, *colorama_args):
|
|
||||||
|
def printc(text: str, *colorama_args: AnsiCodes) -> None:
|
||||||
print(color_text(text, *colorama_args))
|
print(color_text(text, *colorama_args))
|
||||||
|
|
||||||
|
|
||||||
def absolute_path(path: str | Path) -> Path:
|
def absolute_path(path: str | Path) -> Path:
|
||||||
return Path(path).expanduser().absolute()
|
return Path(path).expanduser().absolute()
|
||||||
|
|
||||||
def xdg_config_path():
|
|
||||||
return Path(BaseDirectory.save_config_path('symconf'))
|
def xdg_config_path() -> Path:
|
||||||
|
return Path(BaseDirectory.save_config_path("symconf"))
|
||||||
|
|
||||||
|
|
||||||
def to_tilde_path(path: Path) -> Path:
|
def to_tilde_path(path: Path) -> Path:
|
||||||
'''
|
"""
|
||||||
Abbreviate an absolute path by replacing HOME with "~", if applicable.
|
Abbreviate an absolute path by replacing HOME with "~", if applicable.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return Path(f"~/{path.relative_to(Path.home())}")
|
return Path(f"~/{path.relative_to(Path.home())}")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def deep_update(mapping: dict, *updating_mappings: dict) -> dict:
|
def deep_update(mapping: dict, *updating_mappings: dict) -> dict:
|
||||||
'''Code adapted from pydantic'''
|
"""Code adapted from pydantic"""
|
||||||
|
|
||||||
updated_mapping = mapping.copy()
|
updated_mapping = mapping.copy()
|
||||||
for updating_mapping in updating_mappings:
|
for updating_mapping in updating_mappings:
|
||||||
for k, v in updating_mapping.items():
|
for k, v in updating_mapping.items():
|
||||||
if k in updated_mapping and isinstance(updated_mapping[k], dict) and isinstance(v, dict):
|
if (
|
||||||
|
k in updated_mapping
|
||||||
|
and isinstance(updated_mapping[k], dict)
|
||||||
|
and isinstance(v, dict)
|
||||||
|
):
|
||||||
updated_mapping[k] = deep_update(updated_mapping[k], v)
|
updated_mapping[k] = deep_update(updated_mapping[k], v)
|
||||||
else:
|
else:
|
||||||
updated_mapping[k] = v
|
updated_mapping[k] = v
|
||||||
return updated_mapping
|
return updated_mapping
|
||||||
|
|
||||||
|
|
||||||
class KVPair(argparse.Action):
|
class KVPair(Action):
|
||||||
def __call__(self, parser, namespace, values, option_string=None):
|
def __call__(
|
||||||
|
self,
|
||||||
|
parser: ArgumentParser,
|
||||||
|
namespace: Namespace,
|
||||||
|
values: list[str],
|
||||||
|
option_string: str | None = None,
|
||||||
|
) -> None:
|
||||||
kv_dict = getattr(namespace, self.dest, {})
|
kv_dict = getattr(namespace, self.dest, {})
|
||||||
|
|
||||||
if kv_dict is None:
|
if kv_dict is None:
|
||||||
kv_dict = {}
|
kv_dict = {}
|
||||||
|
|
||||||
for value in values:
|
for value in values:
|
||||||
key, val = value.split('=', 1)
|
key, val = value.split("=", 1)
|
||||||
kv_dict[key] = val
|
kv_dict[key] = val
|
||||||
|
|
||||||
setattr(namespace, self.dest, kv_dict)
|
setattr(namespace, self.dest, kv_dict)
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
def test_imports():
|
|
||||||
from symconf.runner import Runner
|
|
||||||
from symconf.reader import DictReader
|
|
||||||
from symconf.config import ConfigManager
|
|
||||||
from symconf.matching import Matcher, FilePart
|
|
||||||
from symconf.template import Template, FileTemplate, TOMLTemplate
|
|
||||||
|
|
||||||
from symconf import config
|
|
||||||
from symconf import reader
|
|
||||||
from symconf import theme
|
|
||||||
from symconf import util
|
|
||||||
@@ -2,64 +2,64 @@ from pathlib import Path
|
|||||||
|
|
||||||
from symconf import ConfigManager
|
from symconf import ConfigManager
|
||||||
|
|
||||||
|
config_dir = Path(__file__, "..", "test-config-dir/").resolve()
|
||||||
config_dir = Path(
|
|
||||||
__file__, '..', 'test-config-dir/'
|
|
||||||
).resolve()
|
|
||||||
cm = ConfigManager(config_dir)
|
cm = ConfigManager(config_dir)
|
||||||
|
|
||||||
|
|
||||||
def test_matching_configs_exact():
|
def test_matching_configs_exact() -> None:
|
||||||
'''
|
"""
|
||||||
Test matching exact style and scheme. Given strict mode not set (allowing relaxation
|
Test matching exact style and scheme. Given strict mode not set (allowing
|
||||||
to "none"), the order of matching should be
|
relaxation to "none"), the order of matching should be
|
||||||
|
|
||||||
1. (none, none) :: none-none.aaa
|
1. (none, none) :: none-none.aaa
|
||||||
2. (none, scheme) :: none-light.aaa
|
2. (none, scheme) :: none-light.aaa
|
||||||
3. (style, none) :: test-none.aaa
|
3. (style, none) :: test-none.aaa
|
||||||
4. (style, scheme) :: test-light.ccc
|
4. (style, scheme) :: test-light.ccc
|
||||||
|
|
||||||
Yielding "test-light.aaa", "test-light.ccc" (unique only on config pathname).
|
Yielding "test-light.aaa", "test-light.ccc" (unique only on config
|
||||||
'''
|
pathname).
|
||||||
|
"""
|
||||||
any_light = cm.get_matching_configs(
|
any_light = cm.get_matching_configs(
|
||||||
'test',
|
"test",
|
||||||
style='test',
|
style="test",
|
||||||
scheme='light',
|
scheme="light",
|
||||||
)
|
)
|
||||||
print(any_light)
|
print(any_light)
|
||||||
|
|
||||||
assert len(any_light) == 2
|
assert len(any_light) == 2
|
||||||
assert any_light['aaa'].pathname == 'test-none.aaa'
|
assert any_light["aaa"].pathname == "test-none.aaa"
|
||||||
assert any_light['ccc'].pathname == 'test-light.ccc'
|
assert any_light["ccc"].pathname == "test-light.ccc"
|
||||||
|
|
||||||
def test_matching_configs_any_style():
|
|
||||||
'''
|
def test_matching_configs_any_style() -> None:
|
||||||
Test matching "any" style and exact scheme. Given strict mode not set (allowing
|
"""
|
||||||
relaxation to "none"), the order of matching should be
|
Test matching "any" style and exact scheme. Given strict mode not set
|
||||||
|
(allowing relaxation to "none"), the order of matching should be
|
||||||
|
|
||||||
1. (style, none) :: none-none.aaa, test-none.aaa
|
1. (style, none) :: none-none.aaa, test-none.aaa
|
||||||
2. (none, none) :: none-none.aaa
|
2. (none, none) :: none-none.aaa
|
||||||
3. (style, scheme) :: test-dark.bbb
|
3. (style, scheme) :: test-dark.bbb
|
||||||
4. (none, scheme) :: (nothing)
|
4. (none, scheme) :: (nothing)
|
||||||
|
|
||||||
Yielding "none-none.aaa" (should always overwrite "test-none.aaa" due to "any"'s
|
Yielding "none-none.aaa" (should always overwrite "test-none.aaa" due to
|
||||||
preference for non-specific matches, i.e., "none"s), "test-none.ddd", "test-dark.bbb"
|
"any"'s preference for non-specific matches, i.e., "none"s),
|
||||||
(unique only on config pathname).
|
"test-none.ddd", "test-dark.bbb" (unique only on config pathname).
|
||||||
'''
|
"""
|
||||||
any_dark = cm.get_matching_configs(
|
any_dark = cm.get_matching_configs(
|
||||||
'test',
|
"test",
|
||||||
style='any',
|
style="any",
|
||||||
scheme='dark',
|
scheme="dark",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(any_dark) == 2
|
assert len(any_dark) == 2
|
||||||
assert any_dark['aaa'].pathname == 'none-none.aaa'
|
assert any_dark["aaa"].pathname == "none-none.aaa"
|
||||||
assert any_dark['bbb'].pathname == 'test-dark.bbb'
|
assert any_dark["bbb"].pathname == "test-dark.bbb"
|
||||||
|
|
||||||
def test_matching_configs_any_scheme():
|
|
||||||
'''
|
def test_matching_configs_any_scheme() -> None:
|
||||||
Test matching exact style and "any" scheme. Given strict mode not set (allowing
|
"""
|
||||||
relaxation to "none"), the order of matching should be
|
Test matching exact style and "any" scheme. Given strict mode not set
|
||||||
|
(allowing relaxation to "none"), the order of matching should be
|
||||||
|
|
||||||
1. (none, scheme) :: none-light.aaa & none-none.aaa
|
1. (none, scheme) :: none-light.aaa & none-none.aaa
|
||||||
2. (none, none) :: none-none.aaa
|
2. (none, none) :: none-none.aaa
|
||||||
@@ -67,54 +67,64 @@ def test_matching_configs_any_scheme():
|
|||||||
4. (style, none) :: test-none.aaa
|
4. (style, none) :: test-none.aaa
|
||||||
|
|
||||||
Yielding "test-none.aaa", "test-light.ccc", "test-dark.bbb"
|
Yielding "test-none.aaa", "test-light.ccc", "test-dark.bbb"
|
||||||
'''
|
"""
|
||||||
test_any = cm.get_matching_configs(
|
test_any = cm.get_matching_configs(
|
||||||
'test',
|
"test",
|
||||||
style='test',
|
style="test",
|
||||||
scheme='any',
|
scheme="any",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(test_any) == 3
|
assert len(test_any) == 3
|
||||||
assert test_any['aaa'].pathname == 'test-none.aaa'
|
assert test_any["aaa"].pathname == "test-none.aaa"
|
||||||
assert test_any['bbb'].pathname == 'test-dark.bbb'
|
assert test_any["bbb"].pathname == "test-dark.bbb"
|
||||||
assert test_any['ccc'].pathname == 'test-light.ccc'
|
assert test_any["ccc"].pathname == "test-light.ccc"
|
||||||
|
|
||||||
def test_matching_scripts():
|
|
||||||
'''
|
def test_matching_scripts() -> None:
|
||||||
Test matching exact style and scheme. Given strict mode not set (allowing relaxation
|
"""
|
||||||
to "none"), the order of matching should be
|
Test matching exact style and scheme. Given strict mode not set (allowing
|
||||||
|
relaxation to "none"), the order of matching should be
|
||||||
|
|
||||||
1. (none, none) :: none-none.sh
|
1. (none, none) :: none-none.sh
|
||||||
2. (none, scheme) :: none-light.sh
|
2. (none, scheme) :: none-light.sh
|
||||||
3. (style, none) :: test-none.sh
|
3. (style, none) :: test-none.sh
|
||||||
4. (style, scheme) :: (nothing)
|
4. (style, scheme) :: (nothing)
|
||||||
|
|
||||||
Yielding (ordered by dec specificity) "test-none.sh" as primary match, then relaxation
|
Yielding (ordered by dec specificity) "test-none.sh" as primary match, then
|
||||||
match "none-none.sh".
|
relaxation match "none-none.sh".
|
||||||
'''
|
"""
|
||||||
test_any = cm.get_matching_scripts(
|
test_any = cm.get_matching_scripts(
|
||||||
'test',
|
"test",
|
||||||
style='test',
|
style="test",
|
||||||
scheme='any',
|
scheme="any",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(test_any) == 2
|
assert len(test_any) == 2
|
||||||
assert list(map(lambda p:p.pathname, test_any)) == ['test-none.sh', 'none-none.sh']
|
assert [p.pathname for p in test_any] == [
|
||||||
|
"test-none.sh",
|
||||||
|
"none-none.sh",
|
||||||
|
]
|
||||||
|
|
||||||
any_light = cm.get_matching_scripts(
|
any_light = cm.get_matching_scripts(
|
||||||
'test',
|
"test",
|
||||||
style='any',
|
style="any",
|
||||||
scheme='light',
|
scheme="light",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(any_light) == 2
|
assert len(any_light) == 2
|
||||||
assert list(map(lambda p:p.pathname, any_light)) == ['none-light.sh', 'none-none.sh']
|
assert [p.pathname for p in any_light] == [
|
||||||
|
"none-light.sh",
|
||||||
|
"none-none.sh",
|
||||||
|
]
|
||||||
|
|
||||||
any_dark = cm.get_matching_scripts(
|
any_dark = cm.get_matching_scripts(
|
||||||
'test',
|
"test",
|
||||||
style='any',
|
style="any",
|
||||||
scheme='dark',
|
scheme="dark",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(any_dark) == 2
|
assert len(any_dark) == 2
|
||||||
assert list(map(lambda p:p.pathname, any_dark)) == ['test-none.sh', 'none-none.sh']
|
assert [p.pathname for p in any_dark] == [
|
||||||
|
"test-none.sh",
|
||||||
|
"none-none.sh",
|
||||||
|
]
|
||||||
|
|||||||
@@ -2,30 +2,47 @@ from pathlib import Path
|
|||||||
|
|
||||||
from symconf import Template, TOMLTemplate
|
from symconf import Template, TOMLTemplate
|
||||||
|
|
||||||
def test_template_fill():
|
|
||||||
|
def test_template_fill() -> None:
|
||||||
# test simple replacment
|
# test simple replacment
|
||||||
assert Template('f{{a}} - f{{b}}').fill({
|
assert (
|
||||||
'a': 1,
|
Template("f{{a}} - f{{b}}").fill(
|
||||||
'b': 2,
|
{
|
||||||
}) == '1 - 2'
|
"a": 1,
|
||||||
|
"b": 2,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
== "1 - 2"
|
||||||
|
)
|
||||||
|
|
||||||
# test nested brackets (using default pattern)
|
# test nested brackets (using default pattern)
|
||||||
assert Template('{{ f{{a}} - f{{b}} }}').fill({
|
assert (
|
||||||
'a': 1,
|
Template("{{ f{{a}} - f{{b}} }}").fill(
|
||||||
'b': 2,
|
{
|
||||||
}) == '{{ 1 - 2 }}'
|
"a": 1,
|
||||||
|
"b": 2,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
== "{{ 1 - 2 }}"
|
||||||
|
)
|
||||||
|
|
||||||
# test tight nested brackets (requires greedy quantifier)
|
# test tight nested brackets (requires greedy quantifier)
|
||||||
assert Template('{{f{{a}} - f{{b}}}}').fill({
|
assert (
|
||||||
'a': 1,
|
Template("{{f{{a}} - f{{b}}}}").fill(
|
||||||
'b': 2,
|
{
|
||||||
}) == '{{1 - 2}}'
|
"a": 1,
|
||||||
|
"b": 2,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
== "{{1 - 2}}"
|
||||||
|
)
|
||||||
|
|
||||||
def test_toml_template_fill():
|
|
||||||
|
def test_toml_template_fill() -> None:
|
||||||
test_group_dir = Path(
|
test_group_dir = Path(
|
||||||
__file__, '..', 'test-config-dir/groups/test/'
|
__file__, "..", "test-config-dir/groups/test/"
|
||||||
).resolve()
|
).resolve()
|
||||||
|
|
||||||
stacked_dict = TOMLTemplate.stack_toml(test_group_dir.iterdir())
|
stacked_dict = TOMLTemplate.stack_toml(test_group_dir.iterdir())
|
||||||
|
|
||||||
assert stacked_dict == {'base':'aaa','concrete':'zzz'}
|
assert stacked_dict == {"base": "aaa", "concrete": "zzz"}
|
||||||
|
|||||||
Reference in New Issue
Block a user