overhaul ConfigManager, add basic tests, add basic docs

This commit is contained in:
Sam G. 2024-07-05 02:06:05 -07:00
parent dd724ee1ff
commit fd32cfd5ba
33 changed files with 791 additions and 319 deletions

238
README.md
View File

@ -1,233 +1,11 @@
# autoconf
The `autoconf` project is an attempt at wrangling the complexity of configuring many
applications across one's Linux system. It provides a simple operational model for pulling
many application config files into one place, as well as generating/setting color schemes
across apps.
# Overview
`symconf` is a CLI tool for managing local application configuration. It uses a simple
operational model that symlinks centralized config files to their expected locations across
the system. This central config directory can then be version controlled.
Quick terminology rundown for theme-related items:
`symconf` also facilitates dynamically setting system and application "themes," symlinking
matching theme config files for registered apps and running config reloading scripts.
- **Theme**: loose term referring generally to the overall aesthetic of a visual setting.
Ignoring stylistic changes (only applicable to some apps; example here might be a
a particular setting of the `waybar` layout), a theme is often just the wrapper term for
a choice of color _palette_ and _scheme_. For example, "tone4-light" could be a _theme_
setting for an app like `kitty`, referring to both a palette and scheme.
- **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.
For
example, the following `symconf` call coordinates a light to dark mode switch
As far as managing settings across apps, there are current two useful classifications
here:
1. **Inseparable from theme**: some apps (e.g., `sway`, `waybar`) have color scheme
components effectively built in to their canonical configuration file. This can make it
hard to set themes dynamically, as it would likely require some involved
matching/substitution rules. This is not a level of complexity I'm willing to embrace,
so we simply split the config files according to theme and/or scheme.
2. **Can load an external theme file**: some apps (e.g., `kitty`) have a clear mechanism
for loading themes. This typically implies some distinct color format, although usually
somewhat easy to generate (don't have to navigate 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
maintained_ config variations according the desired themes. General theme settings must
follow the naming scheme `<app-name>-<palette>-<scheme>.<ext>`. For example, if I wanted to set
`sway` to a light variation (which, at the time of writing, would just entail changing a
single background color), I must have explicitly created a `sway-tone4-light.conf` file
that captures this setting. The canonical config file will then be symlinked to the
theme-specific file when the theme is set. (Note that the palette in this example is pretty much
irrelevant, but it needs 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
to a fixed, generic theme file. For example, with `kitty`, my config file can point to a
`current-theme.conf` file, which will be symlinked to a 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.
- If the set theme is regenerated, there is no intervention necessary to propagate its
changes to the target app. The symlinked file itself will be updated when the theme
does, ensuring the latest theme version is always immediately available and pointed to
by the app.
Keep in mind that some apps may fall into some grey area here, allowing some external
customization but locking down other settings internally. In such instances, there's no
need to overcomplicate things; just stick to explicit config variants under the type (1)
umbrella. Type (2) only works for generated themes anyhow; even if the target app can load
an external theme, type (1) should be used if preset themes are fixed.
## Naming standards
To keep things simple, we use a few fixed naming standards for setting app config files
and their themed counterparts. The app registry requires each theme-eligible app to
provide a config directory (`config_dir`), containing some canonical config file
(`config_file`) and to serve as a place for theme-specific config variations. The
following naming schemes must be used in order for theme switching to behave
appropriately:
- When setting a theme for a particular app, the following variables will be available:
* `<app-name>`
* `<palette>`
* `<scheme>`
- 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
extension.
- For apps with `external_theme = True`, the file `<config-dir>/current-theme.conf` will
be used when symlinking the requested 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
```
<autoconf-root>/autoconf/themes/<palette>/apps/<app-name>/generated/<scheme>.conf
```
to `<config-dir>/current-theme.conf`.
## Directory structure
- `autoconf/`: main repo directory
* `config/`: app-specific configuration files. Each folder inside this directory is
app-specific, and the target of associated copy operations when a config sync is
performed. Nothing in this directory should pertain to any repo functionality; it
should only contain config files that originated elsewhere on the system.
* `themes/`: app-independent theme data files. Each folder in this directory should
correspond to a specific color palette and house any relevant color spec files
(currently likely be a `colors.json`). Also servers the output location for
generated theme files
* `<palette>/colors.json`: JSON formatted color key-value pairings for palette
colors. There's no standard here aside from the filename and format; downstream
app-specific TOML templates can be dependent on any key naming scheme within the
JSON.
+ `<palette>/apps/<app-name>/templates/`: houses the TOML maps for the color
palette `<palette>` under app `<app-name>`. Files `<fname>.toml` will be mapped to
`<fname>.conf` in the theme output folder (below), so ensure the naming
standards align with those outlined above.
+ `<palette>/apps/<app-name>/generated/`: output directory for generated scheme
variants. These are the symlink targets for 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
`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
`-a "*"`.
- Uses symlinks to set canonical config files to theme-based variations. Which files get
set depends on the _app type_ (see above), which really just boils down to whether
theming (1) can be specified with an external format, and (2) 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
according to naming and structure standards.
- Real config files will never be overwritten. To begin setting themes with the script,
you must delete the canonical config file expected by the app (and specified in the app
registry) to allow the first symlink to be set. From there on out, symlinks will be
automatically flushed.
- A report will be provided on which apps were successfully set to the requested theme,
along with the file stems. A number of checks are in place for the existence of involved
files and directories. Overall, the risk of overwritting a real config file is low; we
only flush existing symlinks, and if the would-be target for the requested theme (be it
from an auto-generated 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.
`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.
- An app and palette are the two required parameters. If no template or output paths are
provided, they will be inferred according to the theme path 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.
`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
To make clear how the theme setting script works on my system, the following breaks down
exactly what steps are taken to exert as much scheme control as possible. Everything at this
point is wrapped up in a single `make set-<palette>-<scheme>` call; suppose we're
currently running the dark scheme (see first image) and I run `make set-tone4-light`:
![
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
scheme-mode to set to "auto," which should reflect the theme setting picked up by the
browser (and note the white tab icon).
](_static/set-theme-1.png)
_(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
scheme-mode to set to "auto," which should reflect the theme setting picked 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`),
which calls
```
gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'
```
controlling settings for GTK apps and other `desktop-portal`-aware programs. This
yields the following:
![Portal-aware apps changed, config apps not yet set](_static/set-theme-2.png)
_(Portal-aware apps changed, config apps not yet set. Scheme-aware sites are updated
without page refresh.)_
2. Specific application styles are set. For now the list is small, including `kitty`,
`waybar`, and `sway`. `kitty` is the only type (2) application here, whereas the other
two are type (1).
a. For the type (1) apps, the canonical config files as specified in the app registry
are symlinked to their light variants. For `sway`, this is `~/.config/sway/config`,
and if we look at the `file`:
```sh
.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
relevant palette-scheme file. `kitty` is such an app, with a supported theme file
for `tone4`, and those files have been auto-generated via `gen_theme.py`. Looking at
this file under the `kitty` config directory:
```sh
.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 theme file. (Note that the general notion of a "theme" could include changes to
other stylistic aspects, like the font family; this would likely require some hybrid
type 1-2 approach not yet implemented).
3. Live application instances are reloaded, according to the registered `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](_static/set-theme-3.png)
_(Final light setting: portal-dependent apps _and_ config-based apps changed)_
4. `set_theme.py` provides a report for the actions taken; in this case, the following was
printed:
![`set_theme.py` output](_static/set-theme-4.png)
_(`set_theme.py` output)_

View File

@ -52,3 +52,4 @@
- Refresh scripts should likely specify a shell shabang at the top of the file
- `apps` can serve as a dotfiles folder
- Support symlinking whole folders?
- `any` might prefer to match configs with none over specific options, but will match any

View File

@ -1 +0,0 @@
from autoconf.config import ConfigManager

View File

@ -6,7 +6,7 @@
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = '<project-name>'
project = 'symconf'
copyright = '2024, Sam Griesemer'
author = 'Sam Griesemer'
@ -15,18 +15,25 @@ author = 'Sam Griesemer'
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.viewcode",
"myst_parser",
"sphinx.ext.autosummary", # enables a directive to be specified manually that gathers
# module/object summary details in a table
"sphinx.ext.viewcode", # allow viewing source in the HTML pages
"myst_parser", # only really applies to manual docs; docstrings still need RST-like
"sphinx.ext.napoleon", # enables Google-style docstring formats
"sphinx_autodoc_typehints", # external extension that allows arg types to be inferred by type hints
]
autosummary_generate = True
autosummary_imported_members = True
# include __init__ definitions in autodoc
autodoc_default_options = {
'special-members': '__init__',
}
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

View File

@ -1,4 +1,4 @@
# `autoconf` package docs
# `symconf` package docs
{ref}`genindex`
{ref}`modindex`
{ref}`search`
@ -14,15 +14,17 @@
:maxdepth: 3
:caption: Autoref
_autoref/autoconf.rst
_autoref/symconf.rst
```
```{toctree}
:maxdepth: 3
:caption: Contents
reference/configuring
reference/usage
reference/archive
reference/documentation/index
reference/site/index
```
```{include} ../README.md

233
docs/reference/archive.md Normal file
View File

@ -0,0 +1,233 @@
# Archive
The `autoconf` project is an attempt at wrangling the complexity of configuring many
applications across one's Linux system. It provides a simple operational model for pulling
many application config files into one place, as well as generating/setting color schemes
across apps.
Quick terminology rundown for theme-related items:
- **Theme**: loose term referring generally to the overall aesthetic of a visual setting.
Ignoring stylistic changes (only applicable to some apps; example here might be a
a particular setting of the `waybar` layout), a theme is often just the wrapper term for
a choice of color _palette_ and _scheme_. For example, "tone4-light" could be a _theme_
setting for an app like `kitty`, referring to both a palette and scheme.
- **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.
As far as managing settings across apps, there are current two useful classifications
here:
1. **Inseparable from theme**: some apps (e.g., `sway`, `waybar`) have color scheme
components effectively built in to their canonical configuration file. This can make it
hard to set themes dynamically, as it would likely require some involved
matching/substitution rules. This is not a level of complexity I'm willing to embrace,
so we simply split the config files according to theme and/or scheme.
2. **Can load an external theme file**: some apps (e.g., `kitty`) have a clear mechanism
for loading themes. This typically implies some distinct color format, although usually
somewhat easy to generate (don't have to navigate 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
maintained_ config variations according the desired themes. General theme settings must
follow the naming scheme `<app-name>-<palette>-<scheme>.<ext>`. For example, if I wanted to set
`sway` to a light variation (which, at the time of writing, would just entail changing a
single background color), I must have explicitly created a `sway-tone4-light.conf` file
that captures this setting. The canonical config file will then be symlinked to the
theme-specific file when the theme is set. (Note that the palette in this example is pretty much
irrelevant, but it needs 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
to a fixed, generic theme file. For example, with `kitty`, my config file can point to a
`current-theme.conf` file, which will be symlinked to a 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.
- If the set theme is regenerated, there is no intervention necessary to propagate its
changes to the target app. The symlinked file itself will be updated when the theme
does, ensuring the latest theme version is always immediately available and pointed to
by the app.
Keep in mind that some apps may fall into some grey area here, allowing some external
customization but locking down other settings internally. In such instances, there's no
need to overcomplicate things; just stick to explicit config variants under the type (1)
umbrella. Type (2) only works for generated themes anyhow; even if the target app can load
an external theme, type (1) should be used if preset themes are fixed.
## Naming standards
To keep things simple, we use a few fixed naming standards for setting app config files
and their themed counterparts. The app registry requires each theme-eligible app to
provide a config directory (`config_dir`), containing some canonical config file
(`config_file`) and to serve as a place for theme-specific config variations. The
following naming schemes must be used in order for theme switching to behave
appropriately:
- When setting a theme for a particular app, the following variables will be available:
* `<app-name>`
* `<palette>`
* `<scheme>`
- 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
extension.
- For apps with `external_theme = True`, the file `<config-dir>/current-theme.conf` will
be used when symlinking the requested 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
```
<autoconf-root>/autoconf/themes/<palette>/apps/<app-name>/generated/<scheme>.conf
```
to `<config-dir>/current-theme.conf`.
## Directory structure
- `autoconf/`: main repo directory
* `config/`: app-specific configuration files. Each folder inside this directory is
app-specific, and the target of associated copy operations when a config sync is
performed. Nothing in this directory should pertain to any repo functionality; it
should only contain config files that originated elsewhere on the system.
* `themes/`: app-independent theme data files. Each folder in this directory should
correspond to a specific color palette and house any relevant color spec files
(currently likely be a `colors.json`). Also servers the output location for
generated theme files
* `<palette>/colors.json`: JSON formatted color key-value pairings for palette
colors. There's no standard here aside from the filename and format; downstream
app-specific TOML templates can be dependent on any key naming scheme within the
JSON.
+ `<palette>/apps/<app-name>/templates/`: houses the TOML maps for the color
palette `<palette>` under app `<app-name>`. Files `<fname>.toml` will be mapped to
`<fname>.conf` in the theme output folder (below), so ensure the naming
standards align with those outlined above.
+ `<palette>/apps/<app-name>/generated/`: output directory for generated scheme
variants. These are the symlink targets for 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
`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
`-a "*"`.
- Uses symlinks to set canonical config files to theme-based variations. Which files get
set depends on the _app type_ (see above), which really just boils down to whether
theming (1) can be specified with an external format, and (2) 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
according to naming and structure standards.
- Real config files will never be overwritten. To begin setting themes with the script,
you must delete the canonical config file expected by the app (and specified in the app
registry) to allow the first symlink to be set. From there on out, symlinks will be
automatically flushed.
- A report will be provided on which apps were successfully set to the requested theme,
along with the file stems. A number of checks are in place for the existence of involved
files and directories. Overall, the risk of overwritting a real config file is low; we
only flush existing symlinks, and if the would-be target for the requested theme (be it
from an auto-generated 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.
`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.
- An app and palette are the two required parameters. If no template or output paths are
provided, they will be inferred according to the theme path 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.
`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
To make clear how the theme setting script works on my system, the following breaks down
exactly what steps are taken to exert as much scheme control as possible. Everything at this
point is wrapped up in a single `make set-<palette>-<scheme>` call; suppose we're
currently running the dark scheme (see first image) and I run `make set-tone4-light`:
![
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
scheme-mode to set to "auto," which should reflect the theme setting picked up by the
browser (and note the white tab icon).
](_static/set-theme-1.png)
_(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
scheme-mode to set to "auto," which should reflect the theme setting picked 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`),
which calls
```
gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'
```
controlling settings for GTK apps and other `desktop-portal`-aware programs. This
yields the following:
![Portal-aware apps changed, config apps not yet set](_static/set-theme-2.png)
_(Portal-aware apps changed, config apps not yet set. Scheme-aware sites are updated
without page refresh.)_
2. Specific application styles are set. For now the list is small, including `kitty`,
`waybar`, and `sway`. `kitty` is the only type (2) application here, whereas the other
two are type (1).
a. For the type (1) apps, the canonical config files as specified in the app registry
are symlinked to their light variants. For `sway`, this is `~/.config/sway/config`,
and if we look at the `file`:
```sh
.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
relevant palette-scheme file. `kitty` is such an app, with a supported theme file
for `tone4`, and those files have been auto-generated via `gen_theme.py`. Looking at
this file under the `kitty` config directory:
```sh
.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 theme file. (Note that the general notion of a "theme" could include changes to
other stylistic aspects, like the font family; this would likely require some hybrid
type 1-2 approach not yet implemented).
3. Live application instances are reloaded, according to the registered `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](_static/set-theme-3.png)
_(Final light setting: portal-dependent apps _and_ config-based apps changed)_
4. `set_theme.py` provides a report for the actions taken; in this case, the following was
printed:
![`set_theme.py` output](_static/set-theme-4.png)
_(`set_theme.py` output)_

View File

@ -0,0 +1,170 @@
# Configuring
`symconf` operates on a central directory that houses all of the config files you may wish
to apply. The default location for this directory is your `$XDG_CONFIG_HOME` (e.g.,
`~/.config/symconf/`), but it can be any location on 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
`apps/` directory and an `app_registry.toml` file.
## Apps directory
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
`symconf`. Note that simply populating an app's config folder here will do nothing on its
own; the app must also have been registered (discussed in item #2) in order for these
files to be used when `symconf` is invoked. (This just means you can populate your `apps/`
folder safely without expecting any default behavior. More often than not you'll be
expected to tell `symconf` exactly where your config files should end up, meaning you know
exactly what it's doing.)
### User config
Inside your app-specific subdirectory, your managed config files should be placed in a
`user/` subdirectory (distinguishing them from those generated by templates; see more
in Themes). Your config files themselves are then expected to follow a specific naming
scheme:
```sh
<palette>-<scheme>.<config-name>
```
This ties your config file to a particular theme setting as needed, and `symconf` will
apply it if it matches the theme setting you provide when invoked. The specific values are
as follows:
- `scheme`: can be `light`, `dark`, or `none`. Indicates whether the config file should
be applied specifically when requesting a light or dark mode. Use `none` to indicate
that the config file does not have settings specific to a light/dark mode.
- `palette`: a "palette name" of your choosing, or `none`. The palette name you use
here may refer specifically to a color palette used by the config file, but can be
used generally to indicate any particular group of config settings (e.g., fonts,
transparency, etc). Use `none` to indicate that the file does not correspond to any
particular style group.
- `config-name`: the _name_ of the config file. This should correspond to _same path
name_ that is expected by the app. 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),
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`
terminal emulator. The following tree demonstrates a valid setup:
```sh
<config-home>
└── apps/
└── kitty/
└── user/
├── none-light.kitty.conf
└── none-dark.kitty.conf
```
where `none-light.kitty.conf` may set a light background and `none-dark.kitty.conf` a dark
one. `none` is used for the `<palette>` part of the name to indicate the configuration does
not pertain to any specific palette and can be matched even if one is not provided. With
an appropriate `app_regsitry.toml` file (see below), invoking
```sh
symconf --theme=light --apps=kitty
```
would symlink `$XDG_CONFIG_HOME/symconf/apps/kitty/user/none-light.kitty.conf` to
`~/.config/kitty/kitty.conf`.
### Templatized config
Note the potential inconvenience in needing to manage two separate config files in the
above example, very likely with all but one line of difference. Templating enables
populating config template files dynamically with theme-specific variables of your
choosing.
### Reload scripts
After symlinking a new set of config files, it is often necessary to reload the system or
relevant apps in order for the new config settings to apply. Within an app's subdirectory,
a `call/` folder can be created to hold scripts that should apply based on certain schemes
or palettes (matching them in the same way as config files). For example,
```sh
<config-home>
└── apps/
└── kitty/
└── call/
└── none-none.sh
```
`none-none.sh` might simply contain `kill -s USR1 $(pgrep kitty)`, which is a way to tell
all running `kitty` instances to reload their config settings. Again, following the naming
scheme for config files, a script named `none-none.sh` will apply under any scheme or
palette specification. Thus, in 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
`~/.config/kitty/kitty.conf`.
2. Search and match `none-none.sh` and execute it, applying the new light mode settings to
all running `kitty` instances.
## App registry
An `app_registry.toml` file, used to specify the target locations for app-specific
config files. To "register" an app, you simply need to add the following text block to
the `app_registry.toml` file:
```toml
[app.<app-name>]
# DEFINE *ONE* OF THE FOLLOWING
config_dir = "<path-to-app-config-folder>"
# OR
config_map = {
"<conf-pathname-1>" = "<path-to-exact-config-file>"
"<conf-pathname-2>" = "<path-to-exact-config-file>"
# ...
}
```
(Note that text in angle brackets refers to values that should be replaced.) This tells
`symconf` how it should handle each app's config files. The `<app-name>` (e.g.,
`kitty`) should correspond to a subdirectory under `apps/` (e.g., `apps/kitty/`) that
holds your config files for that app. As shown, you then need to supply either of the
following options:
- `config_dir`: specifies a single directory where all of the app's matching config
files should be symlinked. In the `kitty` example, this might be `~/.config/kitty`.
This is the simplest and most common option provided most 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
should be created during the symlink process. This is typically needed when an app
has many config files that need to be set in several disparate locations across your
system. In the `kitty` example, although not necessary (and in general you should
prefer to set `config_dir` when applicable), we could have the following
`config_map`:
```toml
[app.kitty]
config_map = {
"kitty.conf": "~/.config/kitty/kitty.conf"
}
```
This tells `symconf` to symlink the exact location `~/.config/kitty/kitty.conf` to
the matching `kitty.conf` under the `apps/kitty` directory.
## Directory structure
In total, the structure of your base config directory can look as follows:
```sh
~/.config/symconf/
├── app_registry.toml
└── apps/
└── <app>/
   ├── user/ # user managed
│   └── none-none.<config-name>
   ├── generated/ # automatically populated
│   └── none-none.<config-name>
   ├── templates/ # config templates
│   └── none-none.template
   └── call/ # reload scripts
   └── none-none.sh
```
## Misc remarks
### Multiple config files with same path name

View File

@ -1,8 +1,5 @@
# Documentation
```{toctree}
:hidden:
sphinx
```

6
docs/reference/usage.md Normal file
View File

@ -0,0 +1,6 @@
# Usage
```{toctree}
:hidden:
```

View File

@ -25,6 +25,7 @@ classifiers = [
]
dependencies = [
"pyxdg",
"colorama",
]
[project.optional-dependencies]
@ -36,6 +37,7 @@ docs = [
"furo",
"myst-parser",
]
build = ["build", "twine"]
[project.urls]
Homepage = "https://doc.olog.io/symconf"

5
symconf/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from symconf.config import ConfigManager
from symconf import config
from symconf import theme
from symconf import util

View File

@ -1,13 +1,12 @@
import argparse
import util
#from gen_theme import generate_theme_files
from autoconf.config import ConfigManager
from symconf import util
from symconf.config import ConfigManager
def add_set_subparser(subparsers):
def update_app_settings(args):
cm = ConfigManager(args.config_dif)
cm = ConfigManager(args.config_dir)
cm.update_apps(
apps=args.apps,
scheme=args.scheme,
@ -35,8 +34,8 @@ def add_set_subparser(subparsers):
parser.add_argument(
'-a', '--apps',
required = False,
default = "any",
type = lambda s: s.split(',') if s != '*' else s
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'
)
@ -75,7 +74,7 @@ def add_gen_subparser(subparsers):
# central argparse entry point
parser = argparse.ArgumentParser(
'autoconf',
'symconf',
description='Generate theme files for various applications. Uses a template (in TOML ' \
+ 'format) to map application-specific config keywords to colors (in JSON ' \
+ 'format).'
@ -88,13 +87,14 @@ parser.add_argument(
)
# add subparsers
subparsers = parser.get_subparsers()
subparsers = parser.add_subparsers(title='subcommand actions')
#add_gen_subparser(subparsers)
add_set_subparser(subparsers)
if __name__ == '__main__':
args = parser.parse_args()
print(args)
if 'func' in args:
args.func(args)

View File

@ -8,7 +8,7 @@ from pathlib import Path
from colorama import Fore, Back, Style
from autoconf import util
from symconf import util
class ConfigManager:
@ -23,7 +23,7 @@ class ConfigManager:
Parameters:
config_dir: config parent directory housing expected files (registry,
app-specific conf files, etc). Defaults to
``"$XDG_CONFIG_HOME/autoconf/"``.
``"$XDG_CONFIG_HOME/symconf/"``.
disable_registry: disable checks for a registry file in the ``config_dir``.
Should really only be set when using this programmatically
and manually supplying app settings.
@ -64,7 +64,6 @@ class ConfigManager:
def _check_registry(self):
registry_path = Path(self.config_dir, 'app_registry.toml')
self.app_registry = {}
if not registry_path.exists():
print(
Fore.YELLOW \
@ -82,7 +81,7 @@ class ConfigManager:
self.app_registry = app_registry.get('app', {})
def resolve_scheme(self, scheme):
def _resolve_scheme(self, scheme):
# if scheme == 'auto':
# os_cmd_groups = {
# 'Linux': (
@ -105,13 +104,13 @@ class ConfigManager:
return scheme
def resolve_palette(self, palette):
def _resolve_palette(self, palette):
if palette == 'auto':
return 'any'
return palette
def app_config_map(self, app_name):
def app_config_map(self, app_name) -> dict[str, Path]:
'''
Get the config map for a provided app.
@ -125,8 +124,8 @@ class ConfigManager:
For example,
```
palette1-light.conf.ini -> ~/.config/autoconf/apps/user/palette1-light.conf.ini
palette2-dark.app.conf -> ~/.config/autoconf/apps/generated/palette2-dark.app.conf
palette1-light.conf.ini -> ~/.config/symconf/apps/user/palette1-light.conf.ini
palette2-dark.app.conf -> ~/.config/symconf/apps/generated/palette2-dark.app.conf
```
This ensures we have unique config names pointing to appropriate locations (which
@ -147,12 +146,109 @@ class ConfigManager:
return file_map
def _get_file_parts(self, pathnames):
# now match theme files in order of inc. specificity; for each unique config file
# tail, only the most specific matching file sticks
file_parts = []
for pathname in pathnames:
parts = str(pathname).split('.')
if len(parts) < 2:
print(f'Filename "{pathname}" incorrectly formatted, ignoring')
continue
theme_part, conf_part = parts[0], '.'.join(parts[1:])
file_parts.append((theme_part, conf_part, pathname))
return file_parts
def _get_prefix_order(
self,
scheme,
palette,
strict=False,
):
if strict:
theme_order = [
(palette, scheme),
]
else:
# inverse order of match relaxation; intention being to overwrite with
# results from increasingly relevant groups given the conditions
if palette == 'any' and scheme == 'any':
# prefer both be "none", with preference for specific scheme
theme_order = [
(palette , scheme),
(palette , 'none'),
('none' , scheme),
('none' , 'none'),
]
elif palette == 'any':
# prefer palette to be "none", then specific, then relax specific scheme
# to "none"
theme_order = [
(palette , 'none'),
('none' , 'none'),
(palette , scheme),
('none' , scheme),
]
elif scheme == 'any':
# prefer scheme to be "none", then specific, then relax specific palette
# to "none"
theme_order = [
('none' , scheme),
('none' , 'none'),
(palette , scheme),
(palette , 'none'),
]
else:
# neither component is any; prefer most specific
theme_order = [
('none' , 'none'),
('none' , scheme),
(palette , 'none'),
(palette , scheme),
]
return theme_order
def match_pathnames(
self,
pathnames,
scheme,
palette,
prefix_order=None,
strict=False,
):
file_parts = self._get_file_parts(pathnames)
if prefix_order is None:
prefix_order = self._get_prefix_order(
scheme,
palette,
strict=strict,
)
ordered_matches = []
for palette_prefix, scheme_prefix in prefix_order:
for theme_part, conf_part, pathname in file_parts:
theme_split = theme_part.split('-')
palette_part, scheme_part = '-'.join(theme_split[:-1]), theme_split[-1]
palette_match = palette_prefix == palette_part or palette_prefix == 'any'
scheme_match = scheme_prefix == scheme_part or scheme_prefix == 'any'
if palette_match and scheme_match:
ordered_matches.append((conf_part, theme_part, pathname))
return ordered_matches
def get_matching_configs(
self,
app_name,
scheme='auto',
palette='auto',
) -> dict[str, str]:
strict=False,
) -> dict[str, Path]:
'''
Get app config files that match the provided scheme and palette.
@ -161,42 +257,74 @@ class ConfigManager:
where ``<palette>-<scheme>`` is the "theme part" and ``<path-name>`` is the "conf
part." For those config files with the same "conf part," only the entry with the
most specific "theme part" will be stored. By "most specific," we mean those
entries with the fewest possible components named ``any``, with ties broken in
entries with the fewest possible components named ``none``, with ties broken in
favor of a more specific ``palette`` (the only "tie" really possible here is when
`any-<scheme>` and `<palette>-any` are both available, in which case the latter
``none-<scheme>`` and ``<palette>-none`` are both available, in which case the latter
will overwrite the former).
.. admonition: Edge cases
There are a few quirks to this matching scheme that yield potentially
unintuitive results. As a recap:
- The "theme part" of a config file name includes both a palette and a scheme
component. Either of those parts may be "none," which simply indicates that
that particular file does not attempt to change that factor. "none-light,"
for instance, might simply set a light background, having no effect on other
theme settings.
- Non-keyword queries for scheme and palette will always be matched exactly.
However, if an exact match is not available, we also look for "none" in each
component's place. For example, if we wanted to set "solarized-light" but
only "none-light" was available, it would still be set because we can still
satisfy the desire scheme (light). The same goes for the palette
specification, and if neither match, "none-none" will always be matched if
available. Note that if "none" is specified exactly, it will be matched
exactly, just like any other value.
- During a query, "any" may also be specified for either component, indicating
we're okay to match any file's text for that part. For example, if I have
two config files ``"p1-dark"`` and ``"p2-dark"``, the query for ``("any",
"dark")`` would suggest I'd like the dark scheme but am okay with either
palette.
It's under the "any" keyword where possibly counter-intuitive results may come
about. Specifying "any" does not change the mechanism that seeks to optionally
match "none" if no specific match is available. For example, suppose we have
the config file ``red-none`` (setting red colors regardless of a light/dark
mode). If I query for ``("any", "dark")``, ``red-none`` will be matched
(supposing there are no more direct matches available). Because we don't a
match specifically for the scheme "dark," it gets relaxed to "none." But we
indicated we're okay to match any palette. So despite asking for a config that
sets a dark scheme and not caring about the palette, we end up with a config
that explicitly does nothing about the scheme but sets a particular palette.
This matching process is still consistent with what we expect the keywords to
do, it just slightly muddies the waters with regard to what can be matched
(mostly due to the amount that's happening under the hood here).
This example is the primary driver behind the optional ``strict`` setting,
which in this case would force the dark scheme to be matched (and ultimately
find no matches).
Also: when "any" is used for a component, options with "none" are prioritized,
allowing "any" to be as flexible and unassuming as possible (only matching a
random specific config among the options if there is no "none" available).
'''
app_dir = Path(self.apps_dir, app_name)
scheme = self.resolve_scheme(scheme)
palette = self.resolve_palette(palette)
scheme = self._resolve_scheme(scheme)
palette = self._resolve_palette(palette)
# now match theme files in order of inc. specificity; for each unique config file
# tail, only the most specific matching file sticks
file_parts = []
app_config_map = self.app_config_map(app_name)
for pathname in app_config_map:
parts = pathname.split('.')
if len(parts) < 2:
print(f'Filename "{filename}" incorrectly formatted, ignoring')
continue
theme_part, conf_part = parts[0], '.'.join(parts[1:])
file_parts.append((theme_part, conf_part, pathname))
theme_prefixes = [
'any-any',
f'any-{scheme}',
f'{palette}-any',
f'{palette}-{scheme}'
]
ordered_matches = self.match_pathnames(
app_config_map,
scheme,
palette,
strict=strict,
)
matching_file_map = {}
for theme_prefix in theme_prefixes:
for theme_part, conf_part, pathname in file_parts:
if theme_part == theme_prefix:
matching_file_map[conf_part] = app_config_map[pathname]
for conf_part, theme_part, pathname in ordered_matches:
matching_file_map[conf_part] = app_config_map[pathname]
return matching_file_map
@ -216,7 +344,13 @@ class ConfigManager:
```
and are matched using the same heuristic employed by config file symlinking
procedure (see ``get_matching_configs()``).
procedure (see ``get_matching_configs()``), albeit with a forced ``prefix_order``,
ordered by increasing specificity. The order is then reversed, and the final list
orders the scripts by the first time they appear (intention being to reload
specific settings first).
TODO: consider running just the most specific script? Users might want to design
their scripts to be stackable, or they may just be independent.
'''
app_dir = Path(self.apps_dir, app_name)
call_dir = Path(app_dir, 'call')
@ -224,22 +358,23 @@ class ConfigManager:
if not call_dir.is_dir():
return
theme_prefixes = [
'any-any',
f'any-{scheme}',
f'{palette}-any',
f'{palette}-{scheme}'
prefix_order = [
('none' , 'none'),
('none' , scheme),
(palette , 'none'),
(palette , scheme),
]
# do it this way to keep order for downstream exec
script_list = []
for theme_prefix in theme_prefixes:
for script_path in call_dir.iterdir():
theme_part = script_path.stem
if theme_part == theme_prefix:
script_list.append(script_path)
pathnames = [path.name for path in call_dir.iterdir()]
ordered_matches = self.match_pathnames(
pathnames,
scheme,
palette,
prefix_order=prefix_order
)
return list(set(script_list))
# flip list to execute by decreasing specificity
return list(dict.fromkeys(map(lambda x:Path(call_dir, x[2]), ordered_matches)))[::-1]
def update_app_config(
self,
@ -320,19 +455,19 @@ class ConfigManager:
from_path.symlink_to(to_path)
links_succ.append((from_path, to_path))
# run matching scripts for app-specific reload
# TODO: store the status of this cmd & print with the messages
script_list = self.get_matching_scripts(
app_name,
scheme=scheme,
palette=palette,
# run matching scripts for app-specific reload
script_list = self.get_matching_scripts(
app_name,
scheme=scheme,
palette=palette,
)
for script in script_list:
print(Fore.BLUE + f'> Running script "{script.relative_to(self.config_dir)}"')
output = subprocess.check_output(str(script), shell=True)
print(
Fore.BLUE + Style.DIM + f'-> Captured script output "{output.decode().strip()}"' + Style.RESET_ALL
)
for script in script_list:
print(Fore.BLUE + f'> Running script "{script.relative_to(self.config_dir}"')
output = subprocess.check_output(str(script), shell=True)
print(
Fore.BLUE + Style.DIM + f'-> Captured script output "{output.decode().strip()}"' + Style.RESET
)
for from_p, to_p in links_succ:
from_p = from_p
@ -355,16 +490,16 @@ class ConfigManager:
app_list = list(self.app_registry.keys())
else:
# get requested apps that overlap with registry
app_list = [a for a in app_list if a in app_registry]
app_list = [a for a in apps if a in self.app_registry]
if not app_list:
print(f'None of the apps "apps" are registered, exiting')
print(f'None of the apps "{apps}" are registered, exiting')
return
for app_name in app_list:
self.update_app_config(
app_name,
app_settings=app_registry[app_name],
app_settings=self.app_registry[app_name],
scheme=scheme,
palette=palette,
)

View File

@ -6,4 +6,4 @@ def absolute_path(path: str | Path) -> Path:
return Path(path).expanduser().absolute()
def xdg_config_path():
return Path(BaseDirectory.save_config_path('autoconf'))
return Path(BaseDirectory.save_config_path('symconf'))

View File

@ -1 +0,0 @@
echo "> Testing script"

View File

@ -0,0 +1 @@
echo "> none-light ran"

View File

@ -0,0 +1 @@
echo "> none-none ran"

View File

@ -0,0 +1 @@
echo "> test-none ran"

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,131 @@
from pathlib import Path
from symconf import ConfigManager
config_dir = Path(
__file__, '..', 'test-config-dir/'
).resolve()
cm = ConfigManager(config_dir)
def test_config_map():
file_map = cm.app_config_map('test')
# from user
assert 'none-none.aaa' in file_map
assert 'none-light.aaa' in file_map
assert 'test-dark.bbb' in file_map
assert 'test-light.ccc' in file_map
# from generated
assert 'test-none.aaa' in file_map
def test_matching_configs_exact():
'''
Test matching exact palette and scheme. Given strict mode not set (allowing relaxation
to "none"), the order of matching should be
1. (none, none) :: none-none.aaa
2. (none, scheme) :: none-light.aaa
3. (palette, none) :: test-none.aaa & test-none.ddd
4. (palette, scheme) :: test-light.ccc
Yielding "test-none.aaa", "test-light.ccc", "test-none.ddd" (unique only on path name).
'''
any_light = cm.get_matching_configs(
'test',
palette='test',
scheme='light',
)
assert len(any_light) == 3
assert any_light['aaa'].name == 'test-none.aaa'
assert any_light['ccc'].name == 'test-light.ccc'
assert any_light['ddd'].name == 'test-none.ddd'
def test_matching_configs_any_palette():
'''
Test matching exact palette and scheme. Given strict mode not set (allowing relaxation
to "none"), the order of matching should be
1. (palette, none) :: test-none.aaa & test-none.ddd & none-none.aaa
2. (none, none) :: none-none.aaa
3. (palette, scheme) :: test-dark.bbb
4. (none, scheme) :: (nothing)
Yielding "none-none.aaa" (should always overwrite "test-none.aaa" due to "any"'s
preference for non-specific matches, i.e., "none"s), "test-none.ddd", "test-dark.bbb"
(unique only on path name).
'''
any_dark = cm.get_matching_configs(
'test',
palette='any',
scheme='dark',
)
assert len(any_dark) == 3
assert any_dark['aaa'].name == 'none-none.aaa'
assert any_dark['bbb'].name == 'test-dark.bbb'
assert any_dark['ddd'].name == 'test-none.ddd'
def test_matching_configs_any_scheme():
'''
Test matching exact palette and 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
2. (none, none) :: none-none.aaa
3. (palette, scheme) :: test-dark.bbb & test-light.ccc & test-none.aaa & test-none.ddd
4. (palette, none) :: test-none.aaa & test-none.ddd
Yielding "test-none.aaa", "test-none.ddd", "test-light.ccc", "test-dark.bbb"
'''
test_any = cm.get_matching_configs(
'test',
palette='test',
scheme='any',
)
assert len(test_any) == 4
assert test_any['aaa'].name == 'test-none.aaa'
assert test_any['bbb'].name == 'test-dark.bbb'
assert test_any['ccc'].name == 'test-light.ccc'
assert test_any['ddd'].name == 'test-none.ddd'
def test_matching_scripts():
'''
Test matching exact palette and scheme. Given strict mode not set (allowing relaxation
to "none"), the order of matching should be
1. (none, none) :: none-none.sh
2. (none, scheme) :: none-light.sh
3. (palette, none) :: test-none.sh
4. (palette, scheme) :: (nothing)
Yielding (ordered by dec specificity) "test-none.sh", "none-light.sh", "none-none.sh".
'''
test_any = cm.get_matching_scripts(
'test',
palette='test',
scheme='any',
)
assert len(test_any) == 3
assert test_any == ['test-none.sh', 'none-light.sh', 'none-none.sh']
any_light = cm.get_matching_scripts(
'test',
palette='any',
scheme='light',
)
assert len(any_light) == 3
assert any_light == ['test-none.sh', 'none-light.sh', 'none-none.sh']
any_dark = cm.get_matching_scripts(
'test',
palette='any',
scheme='dark',
)
assert len(any_dark) == 2
assert any_dark == ['test-none.sh', 'none-none.sh']

View File

@ -1,5 +1,5 @@
def test_imports():
from autoconf import ConfigManager
from symconf import ConfigManager
from autoconf import config
from autoconf import theme
from symconf import config
from symconf import theme