complete first revision of large-scale refactor, vastly simplified config model
This commit is contained in:
parent
0de501ac04
commit
73816ecd67
52
REFACTOR.md
Normal file
52
REFACTOR.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
- Require all app names be set somewhere in the app registry, even if no variables need to
|
||||||
|
be supplied (such as "gnome," which just calls a script based on scheme). This helps
|
||||||
|
explicitly define the scope of "*" for applications, and can allow users to keep their
|
||||||
|
config files without involving them in any `autoconf` call.
|
||||||
|
- `scheme: auto` is a valid specification, but it will always resolve to either `light` or
|
||||||
|
`dark`. "auto" is not a valid scheme choice when it comes to naming, but it can be used
|
||||||
|
when making `autoconf` calls to set palettes that reflect the scheme that is currently
|
||||||
|
set (just think of the analogous setting in the browser: auto just means to infer and
|
||||||
|
set the local scheme to whatever the system scheme is set to).
|
||||||
|
- Due `/call`, we no longer have explicit, hard-coded scheme commends in `set_theme.py`.
|
||||||
|
Calls to GNOME's portal or to MacOS system settings can now just be seen as another
|
||||||
|
"app" with a configurable script, reactive to the passed scheme option.
|
||||||
|
- Here's the palette/scheme spec model
|
||||||
|
* If specific `scheme` and `palette` are provided, files prefixed with
|
||||||
|
`<scheme>-<palette>`, `any-<palette>`, or `<scheme>-any` are matched, in that order,
|
||||||
|
for each unique file tail, for each app.
|
||||||
|
* If `palette` is provided by `scheme` is not, it defaults to `auto` and will attempt to
|
||||||
|
infer a specific value, yielding the same case as above.
|
||||||
|
* If `scheme` cannot be inferred when `auto`, or is explicitly set to `any`, only
|
||||||
|
`any-<palette>` file prefixes are matched. The idea here is that `any` indicates that
|
||||||
|
a theme file is explicitly indifferent to the specification of that option, and won't
|
||||||
|
interfere in an unintended way. The term `none` would work exactly the same here;
|
||||||
|
`any` seems like it might be misleading, indicating it will match with any specific
|
||||||
|
palette. In any case (no pun intended), palettes should create files with an `any`
|
||||||
|
scheme if want to be considered as a possible setting when the `scheme` is any option,
|
||||||
|
i.e., `light/dark/any`.
|
||||||
|
* The same goes for `palette`, although it will default to `any` when unspecified. Thus,
|
||||||
|
only commands/files that change `scheme` will be considered when `palette` isn't
|
||||||
|
given. (I suppose we could also consider an `auto` default here that attempts to
|
||||||
|
determine app-specific palettes that are currently set, and switch to their opposite
|
||||||
|
`scheme` counterparts if available. You could still explicitly provide `any` to ensure
|
||||||
|
you just isolate the `scheme` switch, but `auto` could allow something like a dark to
|
||||||
|
light switch that applies to gnome (only supports scheme), changes kitty to
|
||||||
|
"tone4-light" (a light counterpart the currently set palette is available), and
|
||||||
|
Discord remains the same (as a hypothetical app for which we've created a dark palette
|
||||||
|
but no a light one)). I guess the main takeaway with `any`/`auto` is the following: if
|
||||||
|
`auto` can resolve to the concrete option currently set for a given app, behave as if
|
||||||
|
that option was given. When `any` is provided (or `auto` fails to infer a concrete
|
||||||
|
setting), _isolate_ that property (either `scheme` or `palette`) and ensure it doesn't
|
||||||
|
change (even when another might, and doing so by only matching theme files that have
|
||||||
|
actually used `any`, indicating they actually deliver on the agreed upon behavior
|
||||||
|
here).
|
||||||
|
* If neither are given, (depending on what we decide), both would be `auto` and should
|
||||||
|
do nothing (simply determine the `scheme` and `palette` currently set, requiring no
|
||||||
|
updates). If both are `any`, this should also do nothing; `any` is meant to "freeze"
|
||||||
|
that property, so we'd just be freezing both of the possible variables. One or both of
|
||||||
|
these options could serve as a meaningful refresh, however, either re-symlinking the
|
||||||
|
relevant/expected files and/or calling the refresh commands for each apps to ensure
|
||||||
|
expected settings are freshly applied.
|
||||||
|
- Config TOML accepts either `config_dir` or `config_map`, nothing else and only one of
|
||||||
|
the two.
|
||||||
|
- Refresh scripts should likely specify a shell shabang at the top of the file
|
@ -0,0 +1 @@
|
|||||||
|
from autoconf.config import ConfigManager
|
@ -1,22 +1,101 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from gen_theme import add_gen_subparser
|
import util
|
||||||
from set_theme import add_set_subparser
|
#from gen_theme import generate_theme_files
|
||||||
|
from autoconf.config import ConfigManager
|
||||||
|
|
||||||
|
|
||||||
|
def add_set_subparser(subparsers):
|
||||||
|
def update_app_settings(args):
|
||||||
|
cm = ConfigManager(args.config_dif)
|
||||||
|
cm.update_apps(
|
||||||
|
apps=args.apps,
|
||||||
|
scheme=args.scheme,
|
||||||
|
palette=args.palette,
|
||||||
|
)
|
||||||
|
|
||||||
|
parser = subparsers.add_parser(
|
||||||
|
'set',
|
||||||
|
description='Generate theme files for various applications. Uses a template (in TOML ' \
|
||||||
|
+ 'format) to map application-specific config keywords to colors (in JSON ' \
|
||||||
|
+ 'format).'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-p', '--palette',
|
||||||
|
required = False,
|
||||||
|
default = "any",
|
||||||
|
help = 'Palette name, must match a folder in themes/'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-s', '--scheme',
|
||||||
|
required = False,
|
||||||
|
default = "any",
|
||||||
|
help = 'Preferred lightness scheme, either "light" or "dark".'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-a', '--apps',
|
||||||
|
required = False,
|
||||||
|
default = "any",
|
||||||
|
type = lambda s: s.split(',') if s != '*' else s
|
||||||
|
help = 'Application target for theme. App must be present in the registry. ' \
|
||||||
|
+ 'Use "*" to apply to all registered apps'
|
||||||
|
)
|
||||||
|
parser.set_defaults(func=update_app_settings)
|
||||||
|
|
||||||
|
def add_gen_subparser(subparsers):
|
||||||
|
parser = subparsers.add_parser(
|
||||||
|
'gen',
|
||||||
|
description='Generate theme files for various applications. Uses a template (in TOML ' \
|
||||||
|
+ 'format) to map application-specific config keywords to colors (in JSON ' \
|
||||||
|
+ 'format).'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-a', '--app',
|
||||||
|
required=True,
|
||||||
|
help='Application target for theme. Supported: ["kitty"]'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-p', '--palette',
|
||||||
|
required=True,
|
||||||
|
help='Palette to use for template mappings. Uses local "theme/<palette>/colors.json".'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-t', '--template',
|
||||||
|
default=None,
|
||||||
|
help='Path to TOML template file. If omitted, app\'s default template path is used.' \
|
||||||
|
+ 'If a directory is provided, all TOML files in the folder will be used.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-o', '--output',
|
||||||
|
default=None,
|
||||||
|
help='Output file path for theme. If omitted, app\'s default theme output path is used.'
|
||||||
|
)
|
||||||
|
parser.set_defaults(func=generate_theme_files)
|
||||||
|
|
||||||
|
|
||||||
|
# central argparse entry point
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
'autoconf',
|
'autoconf',
|
||||||
description='Generate theme files for various applications. Uses a template (in TOML ' \
|
description='Generate theme files for various applications. Uses a template (in TOML ' \
|
||||||
+ 'format) to map application-specific config keywords to colors (in JSON ' \
|
+ 'format) to map application-specific config keywords to colors (in JSON ' \
|
||||||
+ 'format).'
|
+ 'format).'
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-c', '--config-dir',
|
||||||
|
default = util.xdg_config_path(),
|
||||||
|
type = util.absolute_path,
|
||||||
|
help = 'Path to config directory'
|
||||||
|
)
|
||||||
|
|
||||||
|
# add subparsers
|
||||||
subparsers = parser.get_subparsers()
|
subparsers = parser.get_subparsers()
|
||||||
add_gen_subparser(subparsers)
|
#add_gen_subparser(subparsers)
|
||||||
add_set_subparser(subparsers)
|
add_set_subparser(subparsers)
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
if 'func' in args:
|
if 'func' in args:
|
||||||
args.func(args)
|
args.func(args)
|
||||||
else:
|
else:
|
||||||
|
370
autoconf/config.py
Normal file
370
autoconf/config.py
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import inspect
|
||||||
|
import tomllib
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from colorama import Fore, Back, Style
|
||||||
|
|
||||||
|
from autoconf import util
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigManager:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config_dir=None,
|
||||||
|
disable_registry=False,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Configuration manager class
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
config_dir: config parent directory housing expected files (registry,
|
||||||
|
app-specific conf files, etc). Defaults to
|
||||||
|
``"$XDG_CONFIG_HOME/autoconf/"``.
|
||||||
|
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.
|
||||||
|
'''
|
||||||
|
if config_dir == None:
|
||||||
|
config_dir = util.xdg_config_path()
|
||||||
|
|
||||||
|
self.config_dir = util.absolute_path(config_dir)
|
||||||
|
self.apps_dir = Path(self.config_dir, 'apps')
|
||||||
|
|
||||||
|
self.app_registry = {}
|
||||||
|
|
||||||
|
self._check_paths()
|
||||||
|
|
||||||
|
if not disable_registry:
|
||||||
|
self._check_registry()
|
||||||
|
|
||||||
|
def _check_paths(self):
|
||||||
|
'''
|
||||||
|
Check necessary paths for existence.
|
||||||
|
|
||||||
|
Regardless of programmatic use or ``disable_registry``, we need to a valid
|
||||||
|
``config_dir`` and it must have an ``apps/`` subdirectory (otherwise there are
|
||||||
|
simply no files to act on, not even when manually providing app settings).
|
||||||
|
'''
|
||||||
|
# throw error if config dir doesn't exist
|
||||||
|
if not self.config_dir.exists():
|
||||||
|
raise ValueError(
|
||||||
|
f'Config directory "{self.config_dir}" doesn\'t exist.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# throw error if apps dir doesn't exist or is empty
|
||||||
|
if not self.apps_dir.exists() or not list(self.apps_dir.iterdir()):
|
||||||
|
raise ValueError(
|
||||||
|
f'Config directory "{self.config_dir}" must have an "apps/" subdirectory.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_registry(self):
|
||||||
|
registry_path = Path(self.config_dir, 'app_registry.toml')
|
||||||
|
|
||||||
|
self.app_registry = {}
|
||||||
|
if not registry_path.exists():
|
||||||
|
print(
|
||||||
|
Fore.YELLOW \
|
||||||
|
+ f'No registry file found at expected location "{registry_path}"'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
app_registry = tomllib.load(registry_path.open('rb'))
|
||||||
|
|
||||||
|
if 'app' not in app_registry:
|
||||||
|
print(
|
||||||
|
Fore.YELLOW \
|
||||||
|
+ f'Registry file found but is either empty or incorrectly formatted (no "app" key).'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.app_registry = app_registry.get('app', {})
|
||||||
|
|
||||||
|
def resolve_scheme(self, scheme):
|
||||||
|
# if scheme == 'auto':
|
||||||
|
# os_cmd_groups = {
|
||||||
|
# 'Linux': (
|
||||||
|
# "gsettings get org.gnome.desktop.interface color-scheme",
|
||||||
|
# lambda r: r.split('-')[1][:-1],
|
||||||
|
# ),
|
||||||
|
# 'Darwin': (),
|
||||||
|
# }
|
||||||
|
|
||||||
|
# osname = os.uname().sysname
|
||||||
|
# os_group = os_cmd_groups.get(osname, [])
|
||||||
|
|
||||||
|
# for cmd in cmd_list:
|
||||||
|
# subprocess.check_call(cmd.format(scheme=scheme).split())
|
||||||
|
|
||||||
|
# return scheme
|
||||||
|
|
||||||
|
if scheme == 'auto':
|
||||||
|
return 'any'
|
||||||
|
|
||||||
|
return scheme
|
||||||
|
|
||||||
|
def resolve_palette(self, palette):
|
||||||
|
if palette == 'auto':
|
||||||
|
return 'any'
|
||||||
|
|
||||||
|
return palette
|
||||||
|
|
||||||
|
def app_config_map(self, app_name):
|
||||||
|
'''
|
||||||
|
Get the config map for a provided app.
|
||||||
|
|
||||||
|
The config map is a dict mapping from config file **path names** to their absolute
|
||||||
|
path locations. That is,
|
||||||
|
|
||||||
|
```sh
|
||||||
|
<config_path_name> -> <config_dir>/apps/<app_name>/<subdir>/<palette>-<scheme>.<config_path_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures we have unique config names pointing to appropriate locations (which
|
||||||
|
is mostly important when the same config file names are present across ``user``
|
||||||
|
and ``generated`` subdirectories).
|
||||||
|
'''
|
||||||
|
# first look in "generated", then overwrite with "user"
|
||||||
|
file_map = {}
|
||||||
|
app_dir = Path(self.apps_dir, app_name)
|
||||||
|
for subdir in ['generated', 'user']:
|
||||||
|
subdir_path = Path(app_dir, subdir)
|
||||||
|
|
||||||
|
if not subdir_path.is_dir():
|
||||||
|
continue
|
||||||
|
|
||||||
|
for conf_file in subdir_path.iterdir():
|
||||||
|
file_map[conf_file.name] = conf_file
|
||||||
|
|
||||||
|
return file_map
|
||||||
|
|
||||||
|
def get_matching_configs(
|
||||||
|
self,
|
||||||
|
app_name,
|
||||||
|
scheme='auto',
|
||||||
|
palette='auto',
|
||||||
|
) -> dict[str, str]:
|
||||||
|
'''
|
||||||
|
Get app config files that match the provided scheme and palette.
|
||||||
|
|
||||||
|
Unique config file path names are written to the file map in order of specificity.
|
||||||
|
All config files follow the naming scheme ``<palette>-<scheme>.<path-name>``,
|
||||||
|
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
|
||||||
|
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
|
||||||
|
will overwrite the former).
|
||||||
|
'''
|
||||||
|
app_dir = Path(self.apps_dir, app_name)
|
||||||
|
|
||||||
|
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}'
|
||||||
|
]
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
return matching_file_map
|
||||||
|
|
||||||
|
def get_matching_scripts(
|
||||||
|
self,
|
||||||
|
app_name,
|
||||||
|
scheme='any',
|
||||||
|
palette='any',
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Execute matching scripts in the app's ``call/`` directory.
|
||||||
|
|
||||||
|
Scripts need to be placed in
|
||||||
|
|
||||||
|
```sh
|
||||||
|
<config_dir>/apps/<app_name>/call/<palette>-<scheme>.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
and are matched using the same heuristic employed by config file symlinking
|
||||||
|
procedure (see ``get_matching_configs()``).
|
||||||
|
'''
|
||||||
|
app_dir = Path(self.apps_dir, app_name)
|
||||||
|
call_dir = Path(app_dir, 'call')
|
||||||
|
|
||||||
|
if not call_dir.is_dir():
|
||||||
|
return
|
||||||
|
|
||||||
|
theme_prefixes = [
|
||||||
|
'any-any',
|
||||||
|
f'any-{scheme}',
|
||||||
|
f'{palette}-any',
|
||||||
|
f'{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)
|
||||||
|
|
||||||
|
return list(set(script_list))
|
||||||
|
|
||||||
|
def update_app_config(
|
||||||
|
self,
|
||||||
|
app_name,
|
||||||
|
app_settings = None,
|
||||||
|
scheme = 'any',
|
||||||
|
palette = 'any',
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Perform full app config update process, applying symlinks and running scripts.
|
||||||
|
|
||||||
|
Note that this explicitly accepts app settings to override or act in place of
|
||||||
|
missing app details in the app registry file. This is mostly to provide more
|
||||||
|
programmatic control and test settings without needing them present in the
|
||||||
|
registry file. The ``update_apps()`` method, however, **will** more strictly
|
||||||
|
filter out those apps not in the registry, accepting a list of app keys that
|
||||||
|
ultimately call this method.
|
||||||
|
|
||||||
|
Note: symlinks point **from** the target location **to** the known internal config
|
||||||
|
file; can be a little confusing.
|
||||||
|
'''
|
||||||
|
if app_settings is None:
|
||||||
|
app_settings = self.app_registry.get(app_name, {})
|
||||||
|
|
||||||
|
if 'config_dir' in app_settings and 'config_map' in app_settings:
|
||||||
|
print(f'App "{app_name}" incorrectly configured, skipping')
|
||||||
|
return
|
||||||
|
|
||||||
|
to_symlink: list[tuple[Path, Path]] = []
|
||||||
|
file_map = self.get_matching_configs(
|
||||||
|
app_name,
|
||||||
|
scheme=scheme,
|
||||||
|
palette=palette,
|
||||||
|
)
|
||||||
|
if 'config_dir' in app_settings:
|
||||||
|
for config_tail, full_path in file_map.items():
|
||||||
|
to_symlink.append((
|
||||||
|
util.absolute_path(Path(app_settings['config_dir'], config_tail)), # point from real config dir
|
||||||
|
full_path, # to internal config location
|
||||||
|
))
|
||||||
|
elif 'config_map' in app_settings:
|
||||||
|
for config_tail, full_path in file_map.items():
|
||||||
|
# app's config map points config tails to absolute paths
|
||||||
|
if config_tail in app_settings['config_map']:
|
||||||
|
to_symlink.append((
|
||||||
|
abs_pat(Path(app_settings['config_map'][config_tail])), # point from real config path
|
||||||
|
full_path, # to internal config location
|
||||||
|
))
|
||||||
|
|
||||||
|
links_succ = []
|
||||||
|
links_fail = []
|
||||||
|
for from_path, to_path in to_symlink:
|
||||||
|
if not to_path.exists():
|
||||||
|
print(f'Internal config path "{to_path}" doesn\'t exist, skipping')
|
||||||
|
links_fail.append((from_path, to_path))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not from_path.parent.exists():
|
||||||
|
print(f'Target config parent directory for "{from_path}" doesn\'t exist, skipping')
|
||||||
|
links_fail.append((from_path, to_path))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# if config file being symlinked exists & isn't already a symlink (i.e.,
|
||||||
|
# previously set by this script), throw an error.
|
||||||
|
if from_path.exists() and not from_path.is_symlink():
|
||||||
|
print(
|
||||||
|
f'Symlink target "{from_path}" exists and isn\'t a symlink, NOT overwriting;' \
|
||||||
|
+ ' please first manually remove this file so a symlink can be set.'
|
||||||
|
)
|
||||||
|
links_fail.append((from_path, to_path))
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# if path doesn't exist, or exists and is symlink, remove the symlink in
|
||||||
|
# preparation for the new symlink setting
|
||||||
|
from_path.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
#print(f'Linking [{from_path}] -> [{to_path}]')
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
to_p = to_p.relative_to(self.config_dir)
|
||||||
|
print(Fore.GREEN + f'> {app_name} :: {from_p} -> {to_p}')
|
||||||
|
|
||||||
|
for from_p, to_p in links_fail:
|
||||||
|
from_p = from_p
|
||||||
|
to_p = to_p.relative_to(self.config_dir)
|
||||||
|
print(Fore.RED + f'> {app_name} :: {from_p} -> {to_p}')
|
||||||
|
|
||||||
|
def update_apps(
|
||||||
|
self,
|
||||||
|
apps: str | list[str] = '*',
|
||||||
|
scheme = 'any',
|
||||||
|
palette = 'any',
|
||||||
|
):
|
||||||
|
if apps == '*':
|
||||||
|
# get all registered apps
|
||||||
|
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]
|
||||||
|
|
||||||
|
if not app_list:
|
||||||
|
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],
|
||||||
|
scheme=scheme,
|
||||||
|
palette=palette,
|
||||||
|
)
|
@ -1,146 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import inspect
|
|
||||||
import subprocess
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import tomllib as toml
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from colorama import Fore
|
|
||||||
|
|
||||||
def add_set_subparser(subparsers):
|
|
||||||
parser = subparsers.add_parser(
|
|
||||||
'set',
|
|
||||||
description='Generate theme files for various applications. Uses a template (in TOML ' \
|
|
||||||
+ 'format) to map application-specific config keywords to colors (in JSON ' \
|
|
||||||
+ 'format).'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-p', '--palette',
|
|
||||||
required=True,
|
|
||||||
help='Palette name, must match a folder in themes/'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-s', '--scheme',
|
|
||||||
required=True,
|
|
||||||
help='Preferred lightness scheme, either "light" or "dark".'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-a', '--app',
|
|
||||||
required=True,
|
|
||||||
help='Application target for theme. App must be present in the registry. ' \
|
|
||||||
+ 'Use "*" to apply to all registered apps'
|
|
||||||
)
|
|
||||||
parser.set_defaults(func=update_theme_settings)
|
|
||||||
|
|
||||||
|
|
||||||
def get_running_path():
|
|
||||||
calling_module = inspect.getmodule(inspect.stack()[-1][0])
|
|
||||||
return Path(calling_module.__file__).parent
|
|
||||||
|
|
||||||
def os_scheme_settings(scheme):
|
|
||||||
'''
|
|
||||||
Groups of settings/commands to invoke globally based on the provided `scheme`. This
|
|
||||||
may control things like default app light/dark behavior, for instance.
|
|
||||||
'''
|
|
||||||
os_cmd_groups = {
|
|
||||||
'Linux': [
|
|
||||||
"gsettings set org.gnome.desktop.interface color-scheme 'prefer-{scheme}'",
|
|
||||||
],
|
|
||||||
'Darwin': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
if scheme not in ['light', 'dark']: return
|
|
||||||
osname = os.uname().sysname
|
|
||||||
cmd_list = os_cmd_groups.get(osname, [])
|
|
||||||
|
|
||||||
for cmd in cmd_list:
|
|
||||||
subprocess.check_call(cmd.format(scheme=scheme).split())
|
|
||||||
|
|
||||||
def update_theme_settings():
|
|
||||||
osname = os.uname().sysname
|
|
||||||
basepath = get_running_path()
|
|
||||||
app_registry = toml.load(Path(basepath, 'app_registry.toml').open('rb'))
|
|
||||||
app_registry = app_registry.get('app', {})
|
|
||||||
|
|
||||||
if args.app not in app_registry and args.app != '*':
|
|
||||||
print(f'App {args.app} not registered, exiting')
|
|
||||||
return
|
|
||||||
|
|
||||||
app_list = []
|
|
||||||
if args.app == '*':
|
|
||||||
app_list = list(app_registry.items())
|
|
||||||
else:
|
|
||||||
app_list = [(args.app, app_registry[args.app])]
|
|
||||||
|
|
||||||
links_succ = {}
|
|
||||||
links_fail = {}
|
|
||||||
for app_name, app_settings in app_list:
|
|
||||||
config_dir = Path(app_settings['config_dir']).expanduser()
|
|
||||||
config_file = app_settings['config_file']
|
|
||||||
config_path = Path(config_dir, config_file)
|
|
||||||
|
|
||||||
if osname not in app_settings['supported_oses']:
|
|
||||||
print(f'OS [{osname}] not support for app [{app_name}]')
|
|
||||||
continue
|
|
||||||
|
|
||||||
if app_settings['external_theme']:
|
|
||||||
# symlink from "current-theme.conf" in app's config-dir ...
|
|
||||||
from_conf_path = Path(config_dir, 'current-theme.conf')
|
|
||||||
|
|
||||||
# ... to appropriate generated theme path here in autoconf
|
|
||||||
to_conf_path = Path(
|
|
||||||
basepath,
|
|
||||||
f'themes/{args.palette}/apps/{app_name}/generated/{args.scheme}.conf'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# symlink from the canonical config file ...
|
|
||||||
from_conf_path = config_path
|
|
||||||
|
|
||||||
# ... to appropriate theme variant
|
|
||||||
to_conf_path = Path(
|
|
||||||
config_dir,
|
|
||||||
f'{app_name}-{args.palette}-{args.scheme}{config_path.suffix}'
|
|
||||||
)
|
|
||||||
|
|
||||||
if not to_conf_path.exists():
|
|
||||||
print(
|
|
||||||
f'Expected symlink target [{to_conf_path}] doesn\'t exist, skipping'
|
|
||||||
)
|
|
||||||
links_fail[app_name] = (from_conf_path.name, to_conf_path.name)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# if config file being symlinked exists & isn't already a symlink (i.e.,
|
|
||||||
# previously set by this script), throw an error.
|
|
||||||
if from_conf_path.exists() and not from_conf_path.is_symlink():
|
|
||||||
print(
|
|
||||||
f'Symlink origin [{from_conf_path}] exists and isn\'t a symlink; please ' \
|
|
||||||
+ 'first manually remove this file so this script can set the symlink.'
|
|
||||||
)
|
|
||||||
links_fail[app_name] = (from_conf_path.name, to_conf_path.name)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
# if path doesn't exist, or exists and is symlink, remove the symlink in
|
|
||||||
# preparation for the new symlink setting
|
|
||||||
from_conf_path.unlink(missing_ok=True)
|
|
||||||
|
|
||||||
print(f'Linking [{from_conf_path}] -> [{to_conf_path}]')
|
|
||||||
|
|
||||||
# run color scheme live-reload for app, if available
|
|
||||||
# TODO: store the status of this cmd & print with the messages
|
|
||||||
if 'refresh_cmd' in app_settings:
|
|
||||||
subprocess.check_call(app_settings['refresh_cmd'], shell=True)
|
|
||||||
|
|
||||||
from_conf_path.symlink_to(to_conf_path)
|
|
||||||
links_succ[app_name] = (from_conf_path.name, to_conf_path.name)
|
|
||||||
|
|
||||||
for app, (from_p, to_p) in links_succ.items():
|
|
||||||
print(Fore.GREEN + f'> {app} :: {from_p} -> {to_p}')
|
|
||||||
|
|
||||||
for app, (from_p, to_p) in links_fail.items():
|
|
||||||
print(Fore.RED + f'> {app} :: {from_p} -> {to_p}')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
os_scheme_settings(args.scheme)
|
|
||||||
update_theme_settings()
|
|
@ -4,41 +4,6 @@ import json
|
|||||||
import tomllib as toml
|
import tomllib as toml
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
def get_running_path():
|
|
||||||
calling_module = inspect.getmodule(inspect.stack()[-1][0])
|
|
||||||
return Path(calling_module.__file__).parent
|
|
||||||
|
|
||||||
|
|
||||||
def add_gen_subparser(subparsers):
|
|
||||||
parser = subparsers.add_parser(
|
|
||||||
'gen',
|
|
||||||
description='Generate theme files for various applications. Uses a template (in TOML ' \
|
|
||||||
+ 'format) to map application-specific config keywords to colors (in JSON ' \
|
|
||||||
+ 'format).'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-a', '--app',
|
|
||||||
required=True,
|
|
||||||
help='Application target for theme. Supported: ["kitty"]'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-p', '--palette',
|
|
||||||
required=True,
|
|
||||||
help='Palette to use for template mappings. Uses local "theme/<palette>/colors.json".'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-t', '--template',
|
|
||||||
default=None,
|
|
||||||
help='Path to TOML template file. If omitted, app\'s default template path is used.' \
|
|
||||||
+ 'If a directory is provided, all TOML files in the folder will be used.'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-o', '--output',
|
|
||||||
default=None,
|
|
||||||
help='Output file path for theme. If omitted, app\'s default theme output path is used.'
|
|
||||||
)
|
|
||||||
parser.set_defaults(func=generate_theme_files)
|
|
||||||
|
|
||||||
|
|
||||||
# separation sequences to use base on app
|
# separation sequences to use base on app
|
||||||
app_sep_map = {
|
app_sep_map = {
|
@ -1,21 +0,0 @@
|
|||||||
background #1b1b1b
|
|
||||||
foreground #c6c6c6
|
|
||||||
selection_background #303030
|
|
||||||
selection_foreground #ababab
|
|
||||||
cursor #d4d4d4
|
|
||||||
color0 #262626
|
|
||||||
color8 #262626
|
|
||||||
color1 #ed625d
|
|
||||||
color9 #ed625d
|
|
||||||
color2 #34a543
|
|
||||||
color10 #34a543
|
|
||||||
color3 #a68f03
|
|
||||||
color11 #a68f03
|
|
||||||
color4 #618eff
|
|
||||||
color12 #618eff
|
|
||||||
color5 #ed625d
|
|
||||||
color13 #ed625d
|
|
||||||
color6 #618eff
|
|
||||||
color14 #618eff
|
|
||||||
color7 #d4d4d4
|
|
||||||
color15 #d4d4d4
|
|
@ -1,21 +0,0 @@
|
|||||||
background #f1f1f1
|
|
||||||
foreground #262626
|
|
||||||
selection_background #ababab
|
|
||||||
selection_foreground #303030
|
|
||||||
cursor #262626
|
|
||||||
color0 #1b1b1b
|
|
||||||
color8 #1b1b1b
|
|
||||||
color1 #c25244
|
|
||||||
color9 #c25244
|
|
||||||
color2 #298835
|
|
||||||
color10 #298835
|
|
||||||
color3 #897600
|
|
||||||
color11 #897600
|
|
||||||
color4 #4e75d4
|
|
||||||
color12 #4e75d4
|
|
||||||
color5 #c25244
|
|
||||||
color13 #c25244
|
|
||||||
color6 #4e75d4
|
|
||||||
color14 #4e75d4
|
|
||||||
color7 #d4d4d4
|
|
||||||
color15 #d4d4d4
|
|
@ -1,40 +0,0 @@
|
|||||||
# base settings
|
|
||||||
background = 'black.bg+1'
|
|
||||||
foreground = 'black.fg+3'
|
|
||||||
|
|
||||||
selection_background = 'black.bg+3'
|
|
||||||
selection_foreground = 'black.m+4'
|
|
||||||
|
|
||||||
cursor = 'black.fg+2'
|
|
||||||
|
|
||||||
# black
|
|
||||||
color0 = 'black.bg+2'
|
|
||||||
color8 = 'black.bg+2'
|
|
||||||
|
|
||||||
# red
|
|
||||||
color1 = 'red.m+2'
|
|
||||||
color9 = 'red.m+2'
|
|
||||||
|
|
||||||
# green
|
|
||||||
color2 = 'green.m+2'
|
|
||||||
color10 = 'green.m+2'
|
|
||||||
|
|
||||||
# yellow
|
|
||||||
color3 = 'yellow.m+2'
|
|
||||||
color11 = 'yellow.m+2'
|
|
||||||
|
|
||||||
# blue
|
|
||||||
color4 = 'blue.m+2'
|
|
||||||
color12 = 'blue.m+2'
|
|
||||||
|
|
||||||
# purple (red)
|
|
||||||
color5 = 'red.m+2'
|
|
||||||
color13 = 'red.m+2'
|
|
||||||
|
|
||||||
# cyan (blue)
|
|
||||||
color6 = 'blue.m+2'
|
|
||||||
color14 = 'blue.m+2'
|
|
||||||
|
|
||||||
## white
|
|
||||||
color7 = 'black.fg+2'
|
|
||||||
color15 = 'black.fg+2'
|
|
@ -1,40 +0,0 @@
|
|||||||
# base settings
|
|
||||||
background = 'black.fg+1'
|
|
||||||
foreground = 'black.bg+2'
|
|
||||||
|
|
||||||
selection_background = 'black.m+4'
|
|
||||||
selection_foreground = 'black.bg+3'
|
|
||||||
|
|
||||||
cursor = 'black.bg+2'
|
|
||||||
|
|
||||||
# black
|
|
||||||
color0 = 'black.bg+1'
|
|
||||||
color8 = 'black.bg+1'
|
|
||||||
|
|
||||||
# red
|
|
||||||
color1 = 'red.m+0'
|
|
||||||
color9 = 'red.m+0'
|
|
||||||
|
|
||||||
# green
|
|
||||||
color2 = 'green.m+0'
|
|
||||||
color10 = 'green.m+0'
|
|
||||||
|
|
||||||
# yellow
|
|
||||||
color3 = 'yellow.m+0'
|
|
||||||
color11 = 'yellow.m+0'
|
|
||||||
|
|
||||||
# blue
|
|
||||||
color4 = 'blue.m+0'
|
|
||||||
color12 = 'blue.m+0'
|
|
||||||
|
|
||||||
# purple (red)
|
|
||||||
color5 = 'red.m+0'
|
|
||||||
color13 = 'red.m+0'
|
|
||||||
|
|
||||||
# cyan (blue)
|
|
||||||
color6 = 'blue.m+0'
|
|
||||||
color14 = 'blue.m+0'
|
|
||||||
|
|
||||||
## white
|
|
||||||
color7 = 'black.fg+2'
|
|
||||||
color15 = 'black.fg+2'
|
|
@ -1,107 +0,0 @@
|
|||||||
{
|
|
||||||
"red": {
|
|
||||||
"bg+0": "#1c0d0a",
|
|
||||||
"bg+1": "#2d1412",
|
|
||||||
"bg+2": "#401a17",
|
|
||||||
"bg+3": "#531f1d",
|
|
||||||
"bg+4": "#652624",
|
|
||||||
"m-4": "#782d2b",
|
|
||||||
"m-3": "#8b3633",
|
|
||||||
"m-2": "#9d3e3b",
|
|
||||||
"m-1": "#b14643",
|
|
||||||
"m+0": "#c25244",
|
|
||||||
"m+1": "#d95854",
|
|
||||||
"m+2": "#ed625d",
|
|
||||||
"m+3": "#f27870",
|
|
||||||
"m+4": "#f68c83",
|
|
||||||
"fg+4": "#faa097",
|
|
||||||
"fg+3": "#fdb3ab",
|
|
||||||
"fg+2": "#fec6c0",
|
|
||||||
"fg+1": "#ffdad3",
|
|
||||||
"fg+0": "#ffede9"
|
|
||||||
},
|
|
||||||
"yellow": {
|
|
||||||
"bg+0": "#151100",
|
|
||||||
"bg+1": "#211b00",
|
|
||||||
"bg+2": "#2d2500",
|
|
||||||
"bg+3": "#393000",
|
|
||||||
"bg+4": "#453b00",
|
|
||||||
"m-4": "#524600",
|
|
||||||
"m-3": "#5f5100",
|
|
||||||
"m-2": "#6d5d00",
|
|
||||||
"m-1": "#7a6900",
|
|
||||||
"m+0": "#897600",
|
|
||||||
"m+1": "#978200",
|
|
||||||
"m+2": "#a68f03",
|
|
||||||
"m+3": "#b29c34",
|
|
||||||
"m+4": "#bfaa53",
|
|
||||||
"fg+4": "#cbb770",
|
|
||||||
"fg+3": "#d6c58c",
|
|
||||||
"fg+2": "#e1d3a9",
|
|
||||||
"fg+1": "#ebe2c5",
|
|
||||||
"fg+0": "#f5f0e2"
|
|
||||||
},
|
|
||||||
"green": {
|
|
||||||
"bg+0": "#081406",
|
|
||||||
"bg+1": "#0e1f0c",
|
|
||||||
"bg+2": "#0f2c10",
|
|
||||||
"bg+3": "#103814",
|
|
||||||
"bg+4": "#114518",
|
|
||||||
"m-4": "#15521d",
|
|
||||||
"m-3": "#1a5f23",
|
|
||||||
"m-2": "#1e6c29",
|
|
||||||
"m-1": "#247a2f",
|
|
||||||
"m+0": "#298835",
|
|
||||||
"m+1": "#2e973b",
|
|
||||||
"m+2": "#34a543",
|
|
||||||
"m+3": "#56b15a",
|
|
||||||
"m+4": "#71bc72",
|
|
||||||
"fg+4": "#8ac789",
|
|
||||||
"fg+3": "#a2d3a0",
|
|
||||||
"fg+2": "#badeb8",
|
|
||||||
"fg+1": "#d1e9cf",
|
|
||||||
"fg+0": "#e8f4e7"
|
|
||||||
},
|
|
||||||
"blue": {
|
|
||||||
"bg+0": "#0f101b",
|
|
||||||
"bg+1": "#151b2f",
|
|
||||||
"bg+2": "#1a2544",
|
|
||||||
"bg+3": "#202f59",
|
|
||||||
"bg+4": "#263a6e",
|
|
||||||
"m-4": "#2d4582",
|
|
||||||
"m-3": "#355196",
|
|
||||||
"m-2": "#3d5daa",
|
|
||||||
"m-1": "#4669bf",
|
|
||||||
"m+0": "#4e75d4",
|
|
||||||
"m+1": "#5781ea",
|
|
||||||
"m+2": "#618eff",
|
|
||||||
"m+3": "#7b9bff",
|
|
||||||
"m+4": "#91a9ff",
|
|
||||||
"fg+4": "#a6b7ff",
|
|
||||||
"fg+3": "#b9c5ff",
|
|
||||||
"fg+2": "#cbd3ff",
|
|
||||||
"fg+1": "#dde1ff",
|
|
||||||
"fg+0": "#eef0fe"
|
|
||||||
},
|
|
||||||
"black": {
|
|
||||||
"bg+0": "#111111",
|
|
||||||
"bg+1": "#1b1b1b",
|
|
||||||
"bg+2": "#262626",
|
|
||||||
"bg+3": "#303030",
|
|
||||||
"bg+4": "#3b3b3b",
|
|
||||||
"m-4": "#474747",
|
|
||||||
"m-3": "#525252",
|
|
||||||
"m-2": "#5e5e5e",
|
|
||||||
"m-1": "#6a6a6a",
|
|
||||||
"m+0": "#777777",
|
|
||||||
"m+1": "#848484",
|
|
||||||
"m+2": "#919191",
|
|
||||||
"m+3": "#9e9e9e",
|
|
||||||
"m+4": "#ababab",
|
|
||||||
"fg+4": "#b9b9b9",
|
|
||||||
"fg+3": "#c6c6c6",
|
|
||||||
"fg+2": "#d4d4d4",
|
|
||||||
"fg+1": "#e2e2e2",
|
|
||||||
"fg+0": "#f1f1f1"
|
|
||||||
}
|
|
||||||
}
|
|
9
autoconf/util.py
Normal file
9
autoconf/util.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from xdg import BaseDirectory
|
||||||
|
|
||||||
|
|
||||||
|
def absolute_path(path: str | Path) -> Path:
|
||||||
|
return Path(path).expanduser().absolute()
|
||||||
|
|
||||||
|
def xdg_config_path():
|
||||||
|
return Path(BaseDirectory.save_config_path('autoconf'))
|
@ -1,42 +0,0 @@
|
|||||||
# App registry -- apps eligible for theme switching
|
|
||||||
#
|
|
||||||
# Each app must be register under the "app" directive, i.e., as "[app.<app-name>]"
|
|
||||||
#
|
|
||||||
# Option details:
|
|
||||||
# - external_theme: if False (default), indicates an app of type 1 as per the README. That
|
|
||||||
# is, an external theme file cannot be used, and theme switching will involve switch the
|
|
||||||
# canonical config setting. If True, the app's theme can be set through an external theme
|
|
||||||
# file.
|
|
||||||
# - support_os: OSes that support theme switching according to the implemented paths.
|
|
||||||
# Accepts a list of `uname -s` strings to match system.
|
|
||||||
# - refresh_cmd: a command to run for live refreshing the application's color scheme after
|
|
||||||
# it's been set for running instances.
|
|
||||||
#
|
|
||||||
# Default example
|
|
||||||
# [app.default]
|
|
||||||
# external_theme = False
|
|
||||||
# config_dir = '~/.config/default/'
|
|
||||||
# config_file = 'default.conf'
|
|
||||||
# refresh_cmd = 'app reload-config'
|
|
||||||
#
|
|
||||||
|
|
||||||
[app.kitty]
|
|
||||||
external_theme = true
|
|
||||||
config_dir = '~/.config/kitty/'
|
|
||||||
config_file = 'kitty.conf'
|
|
||||||
supported_oses = ['Linux', 'Darwin']
|
|
||||||
refresh_cmd = 'kill -s USR1 $(pgrep kitty)'
|
|
||||||
|
|
||||||
[app.sway]
|
|
||||||
external_theme = false
|
|
||||||
config_dir = '~/.config/sway/'
|
|
||||||
config_file = 'config'
|
|
||||||
supported_oses = ['Linux']
|
|
||||||
#refresh_cmd = 'swaymsg reload'
|
|
||||||
|
|
||||||
[app.waybar]
|
|
||||||
external_theme = false
|
|
||||||
config_dir = '~/.config/waybar/'
|
|
||||||
config_file = 'style.css'
|
|
||||||
supported_oses = ['Linux']
|
|
||||||
refresh_cmd = 'swaymsg reload'
|
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line, and also
|
||||||
|
# from the environment for the first two.
|
||||||
|
SPHINXOPTS ?=
|
||||||
|
SPHINXBUILD ?= sphinx-build
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
9
docs/_templates/autosummary.md
vendored
Normal file
9
docs/_templates/autosummary.md
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# {{ fullname | escape }}
|
||||||
|
|
||||||
|
```{automodule}
|
||||||
|
{{ fullname }}
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
:imported-members:
|
||||||
|
```
|
8
docs/_templates/autosummary/module.rst
vendored
Normal file
8
docs/_templates/autosummary/module.rst
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{{ fullname | escape | underline}}
|
||||||
|
|
||||||
|
.. automodule:: {{ fullname }}
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
:imported-members:
|
||||||
|
|
38
docs/conf.py
Normal file
38
docs/conf.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
#
|
||||||
|
# For the full list of built-in configuration values, see the documentation:
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
|
project = '<project-name>'
|
||||||
|
copyright = '2024, Sam Griesemer'
|
||||||
|
author = 'Sam Griesemer'
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
||||||
|
extensions = [
|
||||||
|
"sphinx.ext.autodoc",
|
||||||
|
"sphinx.ext.autosummary",
|
||||||
|
"sphinx.ext.viewcode",
|
||||||
|
"myst_parser",
|
||||||
|
]
|
||||||
|
autosummary_generate = True
|
||||||
|
autosummary_imported_members = True
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
html_theme = 'furo'
|
||||||
|
html_static_path = ['_static']
|
||||||
|
#html_sidebars = {
|
||||||
|
# '**': ['/modules.html'],
|
||||||
|
#}
|
||||||
|
|
29
docs/index.md
Normal file
29
docs/index.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# `autoconf` package docs
|
||||||
|
{ref}`genindex`
|
||||||
|
{ref}`modindex`
|
||||||
|
{ref}`search`
|
||||||
|
|
||||||
|
```{eval-rst}
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
# list modules here for quick links
|
||||||
|
```
|
||||||
|
|
||||||
|
```{toctree}
|
||||||
|
:maxdepth: 3
|
||||||
|
:caption: Autoref
|
||||||
|
|
||||||
|
_autoref/autoconf.rst
|
||||||
|
```
|
||||||
|
|
||||||
|
```{toctree}
|
||||||
|
:maxdepth: 3
|
||||||
|
:caption: Contents
|
||||||
|
|
||||||
|
reference/documentation/index
|
||||||
|
reference/site/index
|
||||||
|
```
|
||||||
|
|
||||||
|
```{include} ../README.md
|
||||||
|
```
|
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
pushd %~dp0
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=sphinx-build
|
||||||
|
)
|
||||||
|
set SOURCEDIR=.
|
||||||
|
set BUILDDIR=_build
|
||||||
|
|
||||||
|
%SPHINXBUILD% >NUL 2>NUL
|
||||||
|
if errorlevel 9009 (
|
||||||
|
echo.
|
||||||
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||||
|
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||||
|
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||||
|
echo.may add the Sphinx directory to PATH.
|
||||||
|
echo.
|
||||||
|
echo.If you don't have Sphinx installed, grab it from
|
||||||
|
echo.https://www.sphinx-doc.org/
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
|
goto end
|
||||||
|
|
||||||
|
:help
|
||||||
|
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
|
|
||||||
|
:end
|
||||||
|
popd
|
8
docs/reference/documentation/index.md
Normal file
8
docs/reference/documentation/index.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Documentation
|
||||||
|
|
||||||
|
```{toctree}
|
||||||
|
:hidden:
|
||||||
|
|
||||||
|
sphinx
|
||||||
|
```
|
||||||
|
|
111
docs/reference/documentation/sphinx.md
Normal file
111
docs/reference/documentation/sphinx.md
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# Sphinx
|
||||||
|
The primary driver of this package's documentation is Sphinx's `autodoc` extension,
|
||||||
|
using the [Furo theme][1].
|
||||||
|
|
||||||
|
**High-level details**:
|
||||||
|
|
||||||
|
- `sphinx-apidoc` generates package-based documentation to the `_autoref/` directory,
|
||||||
|
with navigation available under "Autoref" in the sidebar.
|
||||||
|
- Markdown-based documentation files are manually written under the `reference/`
|
||||||
|
directory, showing up under "Contents" in the sidebar.
|
||||||
|
|
||||||
|
## Detailed directory structure
|
||||||
|
All files are placed under `docs/sphinx`:
|
||||||
|
|
||||||
|
- `_`-prefixed are Sphinx-managed directories
|
||||||
|
* `_build/html/` houses output HTML files
|
||||||
|
* `_autoref/` is the target for module-based RST files written by `autodoc`
|
||||||
|
- `reference/`: houses all manually written documentation (totally separate from
|
||||||
|
auto-generated package docs)
|
||||||
|
- `conf.py`: single Sphinx configuration file
|
||||||
|
- `index.md`: documentation index, setups up a persistent sidebar across all other pages
|
||||||
|
|
||||||
|
For manually written documentation under `reference/`, topics are nested as needed. Within
|
||||||
|
a nested directory `reference/<topic>`, an `index.md` should created with content like:
|
||||||
|
|
||||||
|
```
|
||||||
|
# <Topic>
|
||||||
|
|
||||||
|
\`\`\`{toctree}
|
||||||
|
:hidden:
|
||||||
|
|
||||||
|
sub-topic-1.rst
|
||||||
|
sub-topic-2.rst
|
||||||
|
...
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
This will add the nested directory to the sidebar navigation, using the name set under the
|
||||||
|
top-level header. See [Markdown syntax][#markdown-syntax] for more details on the syntax.
|
||||||
|
|
||||||
|
## Sphinx autodoc
|
||||||
|
Sphinx's `autodoc` extension allows automatic generation of documents according to
|
||||||
|
(Python) subpackage structure and available docstrings. A few notes here:
|
||||||
|
|
||||||
|
- In the `conf.py` file, autodoc is enabled by adding `"sphinx.ext.autodoc"` to
|
||||||
|
the extensions list. `"sphinx.ext.viewcode"` can also be added to provide
|
||||||
|
links to source code.
|
||||||
|
- Documents are actually generated by calling the `sphinx-apidoc` CLI command. The
|
||||||
|
current Makefile uses the following call:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sphinx-apidoc --module-first -o docs/sphinx/_autoref/ localsys
|
||||||
|
```
|
||||||
|
|
||||||
|
This writes the automatically generated docs for modules in the package at the
|
||||||
|
local directory `localsys/` to the `docs/sphinx/_autoref` directory. These are
|
||||||
|
reStructuredText files by default.
|
||||||
|
* `--module-first` places the module-level descriptions at the top of the module page.
|
||||||
|
By default, this is placed at the bottom (oddly), and can be obscured by large lists
|
||||||
|
of subpackages if this flag isn't provided.
|
||||||
|
* See available `sphinx-apidoc` options [here][2], as well as more advanced config
|
||||||
|
[here][3].
|
||||||
|
|
||||||
|
|
||||||
|
## Markdown syntax
|
||||||
|
The `myst_parser` extension enables Markdown (or something close to it) to be used when
|
||||||
|
writing documentation files. The Sphinx directives can be difficult to track, and
|
||||||
|
they change slightly under the MyST Markdown syntax. The following are a few common
|
||||||
|
blocks:
|
||||||
|
|
||||||
|
**Page hierarchies**: the following will generate link hierarchy according to the provided
|
||||||
|
pages:
|
||||||
|
|
||||||
|
```
|
||||||
|
\`\`\`{toctree}
|
||||||
|
:maxdepth: <n>
|
||||||
|
:caption: <caption>
|
||||||
|
:hidden:
|
||||||
|
|
||||||
|
example-file-1
|
||||||
|
example-file-2
|
||||||
|
example-dir/index
|
||||||
|
...
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
- `:maxdepth:` limits the depth of nesting
|
||||||
|
- `:caption:` title for the group of pages
|
||||||
|
- `:hidden:` if provided, links will only show in the sidebar (hidden on the page)
|
||||||
|
- Constituent files: listed files will be rendered as a link directly. If a listed file
|
||||||
|
has a `{toctree}` directive, this tree will be rendered in place of the page's link as a
|
||||||
|
dropdown. The dropdown will be named according to the file's top-level heading, and
|
||||||
|
clicking directly on the dropdown header will show that page's content. Files found in
|
||||||
|
the tree will be placed as links under the dropdown, recursively subject to same rules
|
||||||
|
described here.
|
||||||
|
|
||||||
|
**Include files**: the following will include file content
|
||||||
|
pages:
|
||||||
|
|
||||||
|
```
|
||||||
|
\`\`\`{include} README.md
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reference directives**
|
||||||
|
|
||||||
|
|
||||||
|
[1]: https://pradyunsg.me/furo/
|
||||||
|
[2]: https://www.sphinx-doc.org/en/master/man/sphinx-apidoc.html
|
||||||
|
[3]: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#
|
||||||
|
|
0
tests/test-config-dir/app_registry.toml
Normal file
0
tests/test-config-dir/app_registry.toml
Normal file
1
tests/test-config-dir/apps/gnome/call/any-dark.sh
Normal file
1
tests/test-config-dir/apps/gnome/call/any-dark.sh
Normal file
@ -0,0 +1 @@
|
|||||||
|
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'
|
1
tests/test-config-dir/apps/gnome/call/any-light.sh
Normal file
1
tests/test-config-dir/apps/gnome/call/any-light.sh
Normal file
@ -0,0 +1 @@
|
|||||||
|
gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'
|
1
tests/test-config-dir/apps/kitty/call/any-any.sh
Executable file
1
tests/test-config-dir/apps/kitty/call/any-any.sh
Executable file
@ -0,0 +1 @@
|
|||||||
|
echo "> Testing script"
|
0
tests/test-config-dir/conf.ini
Normal file
0
tests/test-config-dir/conf.ini
Normal file
0
tests/test_config.py
Normal file
0
tests/test_config.py
Normal file
Loading…
Reference in New Issue
Block a user