From fd32cfd5bae9555867ca0e1743b248e7926dae55 Mon Sep 17 00:00:00 2001 From: "Sam G." Date: Fri, 5 Jul 2024 02:06:05 -0700 Subject: [PATCH] overhaul ConfigManager, add basic tests, add basic docs --- README.md | 238 +--------------- REFACTOR.md | 1 + autoconf/__init__.py | 1 - docs/conf.py | 17 +- docs/index.md | 8 +- docs/reference/archive.md | 233 ++++++++++++++++ docs/reference/configuring.md | 170 +++++++++++ docs/reference/documentation/index.md | 3 - .../reference/matching.md | 0 docs/reference/usage.md | 6 + pyproject.toml | 2 + symconf/__init__.py | 5 + {autoconf => symconf}/__main__.py | 16 +- {autoconf => symconf}/config.py | 263 +++++++++++++----- {autoconf => symconf}/theme.py | 0 {autoconf => symconf}/util.py | 2 +- .../user/any-light.conf.ini => __init__.py} | 0 .../apps/kitty/call/any-any.sh | 1 - .../apps/test/call/none-light.sh | 1 + .../apps/test/call/none-none.sh | 1 + .../apps/test/call/test-none.sh | 1 + .../generated/test-none.aaa} | 0 .../generated/test-none.ddd} | 0 .../apps/test/user/none-light.aaa | 0 .../apps/test/user/none-none.aaa | 0 .../apps/test/user/test-dark.bbb | 0 .../apps/test/user/test-light.ccc | 0 tests/test-config-dir/sym_tgt/aaa | 1 + tests/test-config-dir/sym_tgt/bbb | 1 + tests/test-config-dir/sym_tgt/ccc | 1 + tests/test-config-dir/sym_tgt/ddd | 1 + tests/test_config.py | 131 +++++++++ tests/test_imports.py | 6 +- 33 files changed, 791 insertions(+), 319 deletions(-) delete mode 100644 autoconf/__init__.py create mode 100644 docs/reference/archive.md create mode 100644 docs/reference/configuring.md rename tests/test-config-dir/apps/kitty/user/any-any.conf.ini => docs/reference/matching.md (100%) create mode 100644 docs/reference/usage.md create mode 100644 symconf/__init__.py rename {autoconf => symconf}/__main__.py (91%) rename {autoconf => symconf}/config.py (54%) rename {autoconf => symconf}/theme.py (100%) rename {autoconf => symconf}/util.py (74%) rename tests/{test-config-dir/apps/kitty/user/any-light.conf.ini => __init__.py} (100%) delete mode 100755 tests/test-config-dir/apps/kitty/call/any-any.sh create mode 100755 tests/test-config-dir/apps/test/call/none-light.sh create mode 100755 tests/test-config-dir/apps/test/call/none-none.sh create mode 100755 tests/test-config-dir/apps/test/call/test-none.sh rename tests/test-config-dir/apps/{kitty/user/spec-dark.conf => test/generated/test-none.aaa} (100%) rename tests/test-config-dir/apps/{kitty/user/test-light.conf.aba => test/generated/test-none.ddd} (100%) create mode 100644 tests/test-config-dir/apps/test/user/none-light.aaa create mode 100644 tests/test-config-dir/apps/test/user/none-none.aaa create mode 100644 tests/test-config-dir/apps/test/user/test-dark.bbb create mode 100644 tests/test-config-dir/apps/test/user/test-light.ccc create mode 120000 tests/test-config-dir/sym_tgt/aaa create mode 120000 tests/test-config-dir/sym_tgt/bbb create mode 120000 tests/test-config-dir/sym_tgt/ccc create mode 120000 tests/test-config-dir/sym_tgt/ddd diff --git a/README.md b/README.md index 6116535..82d269b 100644 --- a/README.md +++ b/README.md @@ -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 `--.`. 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 `-.`, 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: - * `` - * `` - * `` -- For apps with `external_theme = False`, config variants must named as - `--.`, where `` is the app's default config file - extension. -- For apps with `external_theme = True`, the file `/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/themes//apps//generated/.conf - ``` - - to `/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 - * `/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. - + `/apps//templates/`: houses the TOML maps for the color - palette `` under app ``. Files `.toml` will be mapped to - `.conf` in the theme output folder (below), so ensure the naming - standards align with those outlined above. - + `/apps//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 ` , 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--` 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)_ diff --git a/REFACTOR.md b/REFACTOR.md index 000ad36..7c6a759 100644 --- a/REFACTOR.md +++ b/REFACTOR.md @@ -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 diff --git a/autoconf/__init__.py b/autoconf/__init__.py deleted file mode 100644 index 4c18f26..0000000 --- a/autoconf/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from autoconf.config import ConfigManager diff --git a/docs/conf.py b/docs/conf.py index 0b3cf88..9ce7475 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,7 +6,7 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = '' +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 diff --git a/docs/index.md b/docs/index.md index aa6cc03..68231e9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 diff --git a/docs/reference/archive.md b/docs/reference/archive.md new file mode 100644 index 0000000..d0597c0 --- /dev/null +++ b/docs/reference/archive.md @@ -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 `--.`. 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 `-.`, 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: + * `` + * `` + * `` +- For apps with `external_theme = False`, config variants must named as + `--.`, where `` is the app's default config file + extension. +- For apps with `external_theme = True`, the file `/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/themes//apps//generated/.conf + ``` + + to `/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 + * `/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. + + `/apps//templates/`: houses the TOML maps for the color + palette `` under app ``. Files `.toml` will be mapped to + `.conf` in the theme output folder (below), so ensure the naming + standards align with those outlined above. + + `/apps//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 ` , 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--` 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)_ diff --git a/docs/reference/configuring.md b/docs/reference/configuring.md new file mode 100644 index 0000000..c11f54f --- /dev/null +++ b/docs/reference/configuring.md @@ -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//` 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 +-. +``` + +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 + +└── 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 `` 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 + +└── 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.] +# DEFINE *ONE* OF THE FOLLOWING +config_dir = "" +# OR +config_map = { + "" = "" + "" = "" + # ... +} +``` + +(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 `` (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/ + └── / +    ├── user/ # user managed + │   └── none-none. +    ├── generated/ # automatically populated + │   └── none-none. +    ├── templates/ # config templates + │   └── none-none.template +    └── call/ # reload scripts +    └── none-none.sh +``` + +## Misc remarks + +### Multiple config files with same path name diff --git a/docs/reference/documentation/index.md b/docs/reference/documentation/index.md index a14cdde..58b65a4 100644 --- a/docs/reference/documentation/index.md +++ b/docs/reference/documentation/index.md @@ -1,8 +1,5 @@ # Documentation ```{toctree} -:hidden: - sphinx ``` - diff --git a/tests/test-config-dir/apps/kitty/user/any-any.conf.ini b/docs/reference/matching.md similarity index 100% rename from tests/test-config-dir/apps/kitty/user/any-any.conf.ini rename to docs/reference/matching.md diff --git a/docs/reference/usage.md b/docs/reference/usage.md new file mode 100644 index 0000000..808059c --- /dev/null +++ b/docs/reference/usage.md @@ -0,0 +1,6 @@ +# Usage + +```{toctree} +:hidden: + +``` diff --git a/pyproject.toml b/pyproject.toml index 2986866..ac9b73b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/symconf/__init__.py b/symconf/__init__.py new file mode 100644 index 0000000..e8c2987 --- /dev/null +++ b/symconf/__init__.py @@ -0,0 +1,5 @@ +from symconf.config import ConfigManager + +from symconf import config +from symconf import theme +from symconf import util diff --git a/autoconf/__main__.py b/symconf/__main__.py similarity index 91% rename from autoconf/__main__.py rename to symconf/__main__.py index 316b369..da493be 100644 --- a/autoconf/__main__.py +++ b/symconf/__main__.py @@ -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) diff --git a/autoconf/config.py b/symconf/config.py similarity index 54% rename from autoconf/config.py rename to symconf/config.py index e654a20..dc2dee1 100644 --- a/autoconf/config.py +++ b/symconf/config.py @@ -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 ``-`` is the "theme part" and ```` 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-` and `-any` are both available, in which case the latter + ``none-`` and ``-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, ) diff --git a/autoconf/theme.py b/symconf/theme.py similarity index 100% rename from autoconf/theme.py rename to symconf/theme.py diff --git a/autoconf/util.py b/symconf/util.py similarity index 74% rename from autoconf/util.py rename to symconf/util.py index 00b78a8..d5d9cf0 100644 --- a/autoconf/util.py +++ b/symconf/util.py @@ -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')) diff --git a/tests/test-config-dir/apps/kitty/user/any-light.conf.ini b/tests/__init__.py similarity index 100% rename from tests/test-config-dir/apps/kitty/user/any-light.conf.ini rename to tests/__init__.py diff --git a/tests/test-config-dir/apps/kitty/call/any-any.sh b/tests/test-config-dir/apps/kitty/call/any-any.sh deleted file mode 100755 index fd9c92e..0000000 --- a/tests/test-config-dir/apps/kitty/call/any-any.sh +++ /dev/null @@ -1 +0,0 @@ -echo "> Testing script" diff --git a/tests/test-config-dir/apps/test/call/none-light.sh b/tests/test-config-dir/apps/test/call/none-light.sh new file mode 100755 index 0000000..11e6dfd --- /dev/null +++ b/tests/test-config-dir/apps/test/call/none-light.sh @@ -0,0 +1 @@ +echo "> none-light ran" diff --git a/tests/test-config-dir/apps/test/call/none-none.sh b/tests/test-config-dir/apps/test/call/none-none.sh new file mode 100755 index 0000000..48d877e --- /dev/null +++ b/tests/test-config-dir/apps/test/call/none-none.sh @@ -0,0 +1 @@ +echo "> none-none ran" diff --git a/tests/test-config-dir/apps/test/call/test-none.sh b/tests/test-config-dir/apps/test/call/test-none.sh new file mode 100755 index 0000000..c1d5a0c --- /dev/null +++ b/tests/test-config-dir/apps/test/call/test-none.sh @@ -0,0 +1 @@ +echo "> test-none ran" diff --git a/tests/test-config-dir/apps/kitty/user/spec-dark.conf b/tests/test-config-dir/apps/test/generated/test-none.aaa similarity index 100% rename from tests/test-config-dir/apps/kitty/user/spec-dark.conf rename to tests/test-config-dir/apps/test/generated/test-none.aaa diff --git a/tests/test-config-dir/apps/kitty/user/test-light.conf.aba b/tests/test-config-dir/apps/test/generated/test-none.ddd similarity index 100% rename from tests/test-config-dir/apps/kitty/user/test-light.conf.aba rename to tests/test-config-dir/apps/test/generated/test-none.ddd diff --git a/tests/test-config-dir/apps/test/user/none-light.aaa b/tests/test-config-dir/apps/test/user/none-light.aaa new file mode 100644 index 0000000..e69de29 diff --git a/tests/test-config-dir/apps/test/user/none-none.aaa b/tests/test-config-dir/apps/test/user/none-none.aaa new file mode 100644 index 0000000..e69de29 diff --git a/tests/test-config-dir/apps/test/user/test-dark.bbb b/tests/test-config-dir/apps/test/user/test-dark.bbb new file mode 100644 index 0000000..e69de29 diff --git a/tests/test-config-dir/apps/test/user/test-light.ccc b/tests/test-config-dir/apps/test/user/test-light.ccc new file mode 100644 index 0000000..e69de29 diff --git a/tests/test-config-dir/sym_tgt/aaa b/tests/test-config-dir/sym_tgt/aaa new file mode 120000 index 0000000..bbfcb06 --- /dev/null +++ b/tests/test-config-dir/sym_tgt/aaa @@ -0,0 +1 @@ +/home/smgr/Documents/projects/olog/symconf/tests/test-config-dir/apps/test/user/none-none.aaa \ No newline at end of file diff --git a/tests/test-config-dir/sym_tgt/bbb b/tests/test-config-dir/sym_tgt/bbb new file mode 120000 index 0000000..9fe65f3 --- /dev/null +++ b/tests/test-config-dir/sym_tgt/bbb @@ -0,0 +1 @@ +/home/smgr/Documents/projects/olog/symconf/tests/test-config-dir/apps/test/user/test-dark.bbb \ No newline at end of file diff --git a/tests/test-config-dir/sym_tgt/ccc b/tests/test-config-dir/sym_tgt/ccc new file mode 120000 index 0000000..8bf12e3 --- /dev/null +++ b/tests/test-config-dir/sym_tgt/ccc @@ -0,0 +1 @@ +/home/smgr/Documents/projects/olog/symconf/tests/test-config-dir/apps/test/user/test-light.ccc \ No newline at end of file diff --git a/tests/test-config-dir/sym_tgt/ddd b/tests/test-config-dir/sym_tgt/ddd new file mode 120000 index 0000000..ee78729 --- /dev/null +++ b/tests/test-config-dir/sym_tgt/ddd @@ -0,0 +1 @@ +/home/smgr/Documents/projects/olog/symconf/tests/test-config-dir/apps/test/generated/test-none.ddd \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py index e69de29..ab490b1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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'] diff --git a/tests/test_imports.py b/tests/test_imports.py index 2d9ee35..fd7487f 100644 --- a/tests/test_imports.py +++ b/tests/test_imports.py @@ -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