diff --git a/REFACTOR.md b/REFACTOR.md new file mode 100644 index 0000000..638c972 --- /dev/null +++ b/REFACTOR.md @@ -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 + `-`, `any-`, or `-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-` 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 diff --git a/autoconf/__init__.py b/autoconf/__init__.py index e69de29..4c18f26 100644 --- a/autoconf/__init__.py +++ b/autoconf/__init__.py @@ -0,0 +1 @@ +from autoconf.config import ConfigManager diff --git a/autoconf/__main__.py b/autoconf/__main__.py index 4fc1567..316b369 100644 --- a/autoconf/__main__.py +++ b/autoconf/__main__.py @@ -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//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: diff --git a/autoconf/config.py b/autoconf/config.py new file mode 100644 index 0000000..e654a20 --- /dev/null +++ b/autoconf/config.py @@ -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 + -> /apps///-. + ``` + + 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 ``-.``, + 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 + 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 + 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 + /apps//call/-.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, + ) diff --git a/autoconf/set_theme.py b/autoconf/set_theme.py deleted file mode 100644 index c077924..0000000 --- a/autoconf/set_theme.py +++ /dev/null @@ -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() diff --git a/autoconf/gen_theme.py b/autoconf/theme.py similarity index 65% rename from autoconf/gen_theme.py rename to autoconf/theme.py index 12bd8fc..1e5a60a 100644 --- a/autoconf/gen_theme.py +++ b/autoconf/theme.py @@ -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//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 = { diff --git a/autoconf/themes/tone4/apps/kitty/generated/dark.conf b/autoconf/themes/tone4/apps/kitty/generated/dark.conf deleted file mode 100644 index 2f3772f..0000000 --- a/autoconf/themes/tone4/apps/kitty/generated/dark.conf +++ /dev/null @@ -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 \ No newline at end of file diff --git a/autoconf/themes/tone4/apps/kitty/generated/light.conf b/autoconf/themes/tone4/apps/kitty/generated/light.conf deleted file mode 100644 index 972d9b9..0000000 --- a/autoconf/themes/tone4/apps/kitty/generated/light.conf +++ /dev/null @@ -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 \ No newline at end of file diff --git a/autoconf/themes/tone4/apps/kitty/templates/dark.toml b/autoconf/themes/tone4/apps/kitty/templates/dark.toml deleted file mode 100644 index 9b8900d..0000000 --- a/autoconf/themes/tone4/apps/kitty/templates/dark.toml +++ /dev/null @@ -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' diff --git a/autoconf/themes/tone4/apps/kitty/templates/light.toml b/autoconf/themes/tone4/apps/kitty/templates/light.toml deleted file mode 100644 index 9e862d9..0000000 --- a/autoconf/themes/tone4/apps/kitty/templates/light.toml +++ /dev/null @@ -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' diff --git a/autoconf/themes/tone4/colors.json b/autoconf/themes/tone4/colors.json deleted file mode 100644 index 27cb9b3..0000000 --- a/autoconf/themes/tone4/colors.json +++ /dev/null @@ -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" - } -} diff --git a/autoconf/util.py b/autoconf/util.py new file mode 100644 index 0000000..00b78a8 --- /dev/null +++ b/autoconf/util.py @@ -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')) diff --git a/config/app_registry.toml b/config/app_registry.toml deleted file mode 100644 index 00c39fd..0000000 --- a/config/app_registry.toml +++ /dev/null @@ -1,42 +0,0 @@ -# App registry -- apps eligible for theme switching -# -# Each app must be register under the "app" directive, i.e., as "[app.]" -# -# 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' diff --git a/docs/_templates/autosummary.md b/docs/_templates/autosummary.md new file mode 100644 index 0000000..216f1df --- /dev/null +++ b/docs/_templates/autosummary.md @@ -0,0 +1,9 @@ +# {{ fullname | escape }} + +```{automodule} +{{ fullname }} +:members: +:undoc-members: +:show-inheritance: +:imported-members: +``` diff --git a/docs/_templates/autosummary/module.rst b/docs/_templates/autosummary/module.rst new file mode 100644 index 0000000..6d5a51d --- /dev/null +++ b/docs/_templates/autosummary/module.rst @@ -0,0 +1,8 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + :members: + :undoc-members: + :show-inheritance: + :imported-members: + diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..0b3cf88 --- /dev/null +++ b/docs/conf.py @@ -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 = '' +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'], +#} + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..aa6cc03 --- /dev/null +++ b/docs/index.md @@ -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 +``` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -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 diff --git a/docs/reference/documentation/index.md b/docs/reference/documentation/index.md new file mode 100644 index 0000000..a14cdde --- /dev/null +++ b/docs/reference/documentation/index.md @@ -0,0 +1,8 @@ +# Documentation + +```{toctree} +:hidden: + +sphinx +``` + diff --git a/docs/reference/documentation/sphinx.md b/docs/reference/documentation/sphinx.md new file mode 100644 index 0000000..33d6f27 --- /dev/null +++ b/docs/reference/documentation/sphinx.md @@ -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/`, an `index.md` should created with content like: + +``` +# + +\`\`\`{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: +: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# + diff --git a/tests/test-config-dir/app_registry.toml b/tests/test-config-dir/app_registry.toml new file mode 100644 index 0000000..e69de29 diff --git a/tests/test-config-dir/apps/gnome/call/any-dark.sh b/tests/test-config-dir/apps/gnome/call/any-dark.sh new file mode 100644 index 0000000..7f9dc22 --- /dev/null +++ b/tests/test-config-dir/apps/gnome/call/any-dark.sh @@ -0,0 +1 @@ +gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' diff --git a/tests/test-config-dir/apps/gnome/call/any-light.sh b/tests/test-config-dir/apps/gnome/call/any-light.sh new file mode 100644 index 0000000..fcabb84 --- /dev/null +++ b/tests/test-config-dir/apps/gnome/call/any-light.sh @@ -0,0 +1 @@ +gsettings set org.gnome.desktop.interface color-scheme 'prefer-light' diff --git a/tests/test-config-dir/apps/kitty/call/any-any.sh b/tests/test-config-dir/apps/kitty/call/any-any.sh new file mode 100755 index 0000000..fd9c92e --- /dev/null +++ b/tests/test-config-dir/apps/kitty/call/any-any.sh @@ -0,0 +1 @@ +echo "> Testing script" diff --git a/tests/test-config-dir/apps/kitty/user/any-any.conf.ini b/tests/test-config-dir/apps/kitty/user/any-any.conf.ini new file mode 100644 index 0000000..e69de29 diff --git a/tests/test-config-dir/apps/kitty/user/any-light.conf.ini b/tests/test-config-dir/apps/kitty/user/any-light.conf.ini new file mode 100644 index 0000000..e69de29 diff --git a/tests/test-config-dir/apps/kitty/user/spec-dark.conf b/tests/test-config-dir/apps/kitty/user/spec-dark.conf new file mode 100644 index 0000000..e69de29 diff --git a/tests/test-config-dir/apps/kitty/user/test-light.conf.aba b/tests/test-config-dir/apps/kitty/user/test-light.conf.aba new file mode 100644 index 0000000..e69de29 diff --git a/tests/test-config-dir/conf.ini b/tests/test-config-dir/conf.ini new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..e69de29