complete first revision of large-scale refactor, vastly simplified config model

This commit is contained in:
Sam G. 2024-07-03 01:38:52 -07:00
parent 0de501ac04
commit 73816ecd67
31 changed files with 776 additions and 456 deletions

52
REFACTOR.md Normal file
View 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

View File

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

View File

@ -1,22 +1,101 @@
import argparse
from gen_theme import add_gen_subparser
from set_theme import add_set_subparser
import util
#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(
'autoconf',
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(
'-c', '--config-dir',
default = util.xdg_config_path(),
type = util.absolute_path,
help = 'Path to config directory'
)
# add subparsers
subparsers = parser.get_subparsers()
add_gen_subparser(subparsers)
#add_gen_subparser(subparsers)
add_set_subparser(subparsers)
args = parser.parse_args()
if __name__ == '__main__':
args = parser.parse_args()
if 'func' in args:
args.func(args)
else:

370
autoconf/config.py Normal file
View 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,
)

View File

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

View File

@ -4,41 +4,6 @@ import json
import tomllib as toml
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
app_sep_map = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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
View File

@ -0,0 +1,9 @@
# {{ fullname | escape }}
```{automodule}
{{ fullname }}
:members:
:undoc-members:
:show-inheritance:
:imported-members:
```

View File

@ -0,0 +1,8 @@
{{ fullname | escape | underline}}
.. automodule:: {{ fullname }}
:members:
:undoc-members:
:show-inheritance:
:imported-members:

38
docs/conf.py Normal file
View 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
View 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
View 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

View File

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

View 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#

View File

View File

@ -0,0 +1 @@
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'

View File

@ -0,0 +1 @@
gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'

View File

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

View File

0
tests/test_config.py Normal file
View File