From 5da45c97d84aa164bb3e7e40bd722f0ef586110e Mon Sep 17 00:00:00 2001
From: "Sam G." <samgriesemer@gmail.com>
Date: Fri, 19 Apr 2024 15:50:31 -0700
Subject: [PATCH] initial commit (post-repo scrub)

---
 .gitignore                                    |   2 +
 README.md                                     | 233 ++++++++++++++++++
 autoconf/app_registry.toml                    |  42 ++++
 autoconf/gen_theme.py                         | 108 ++++++++
 autoconf/set_theme.py                         | 145 +++++++++++
 .../tone4/apps/kitty/generated/dark.conf      |  21 ++
 .../tone4/apps/kitty/generated/light.conf     |  21 ++
 .../tone4/apps/kitty/templates/dark.toml      |  40 +++
 .../tone4/apps/kitty/templates/light.toml     |  40 +++
 autoconf/themes/tone4/colors.json             | 107 ++++++++
 requirements.txt                              |   3 +
 11 files changed, 762 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 100644 autoconf/app_registry.toml
 create mode 100644 autoconf/gen_theme.py
 create mode 100644 autoconf/set_theme.py
 create mode 100644 autoconf/themes/tone4/apps/kitty/generated/dark.conf
 create mode 100644 autoconf/themes/tone4/apps/kitty/generated/light.conf
 create mode 100644 autoconf/themes/tone4/apps/kitty/templates/dark.toml
 create mode 100644 autoconf/themes/tone4/apps/kitty/templates/light.toml
 create mode 100644 autoconf/themes/tone4/colors.json
 create mode 100644 requirements.txt

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..98d3626
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+docs/_build
+.python-version
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2f7ff1d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,233 @@
+# local-config
+The `local-config` project is an attempt at wrangling the complexity of configuring many
+applications across one's Linux system. It provides a simple operational model for pulling
+many application config files into one place, as well as generating/setting color schemes
+across apps.
+
+Quick terminology rundown for theme-related items:
+
+- **Theme**: loose term referring generally to the overall aesthetic of a visual setting.
+  Ignoring stylistic changes (only applicable to some apps; example here might be a
+  a particular setting of the `waybar` layout), a theme is often just the wrapper term for
+  a choice of color _palette_ and _scheme_. For example, "tone4-light" could be a _theme_
+  setting for an app like `kitty`, referring to both a palette and scheme.
+- **Palette**: a set of base colors used to style text or other aspects of an app's
+  displayed assets
+- **Scheme**: an indication of lightness, typically either "light" or "dark.
+
+As far as managing settings across apps, there are current two useful classifications
+here:
+
+1. **Inseparable from theme**: some apps (e.g., `sway`, `waybar`) have color scheme
+   components effectively built in to their canonical configuration file. This can make it
+   hard to set themes dynamically, as it would likely require some involved
+   matching/substitution rules. This is not a level of complexity I'm willing to embrace,
+   so we simply split the config files according to theme and/or scheme.
+2. **Can load an external theme file**: some apps (e.g., `kitty`) have a clear mechanism
+   for loading themes. This typically implies some distinct color format, although usually
+   somewhat easy to generate (don't have to navigate non-color settings, for instance).
+   Such apps allow for an even less "invasive" config swapping process when setting a new
+   theme, as one can just swap out the external theme file.
+
+To be clear on operation implications here: apps of type (1) must have _manually
+maintained_ config variations according the desired themes. General theme settings must
+follow the naming scheme `<app-name>-<palette>-<scheme>.<ext>`. For example, if I wanted to set
+`sway` to a light variation (which, at the time of writing, would just entail changing a
+single background color), I must have explicitly created a `sway-tone4-light.conf` file
+that captures this setting. The canonical config file will then be symlinked to the
+theme-specific file when the theme is set. (Note that the palette in this example is pretty much
+irrelevant, but it needs to be present in order to match the overarching setting; here you
+can just think of the format being `<app-name>-<theme>.<ext>`, where `tone4-light` is the
+provided theme name.)
+
+For apps of type (2), the canonical config file can remain untouched so long as it refers
+to a fixed, generic theme file. For example, with `kitty`, my config file can point to a
+`current-theme.conf` file, which will be symlinked to a specific theme file here in
+`local-config` when a change is requested. This enables a couple of conveniences:
+
+- The true config directory on disk remains unpolluted with theme variants.
+- If the set theme is regenerated, there is no intervention necessary to propagate its
+  changes to the target app. The symlinked file itself will be updated when the theme
+  does, ensuring the latest theme version is always immediately available and pointed to
+  by the app.
+
+Keep in mind that some apps may fall into some grey area here, allowing some external
+customization but locking down other settings internally. In such instances, there's no
+need to overcomplicate things; just stick to explicit config variants under the type (1)
+umbrella. Type (2) only works for generated themes anyhow; even if the target app can load
+an external theme, type (1) should be used if preset themes are fixed.
+
+## Naming standards
+To keep things simple, we use a few fixed naming standards for setting app config files
+and their themed counterparts. The app registry requires each theme-eligible app to
+provide a config directory (`config_dir`), containing some canonical config file
+(`config_file`) and to serve as a place for theme-specific config variations. The
+following naming schemes must be used in order for theme switching to behave
+appropriately: 
+
+- When setting a theme for a particular app, the following variables will be available:
+    * `<app-name>`
+    * `<palette>`
+    * `<scheme>`
+- For apps with `external_theme = False`, config variants must named as
+  `<app-name>-<palette>-<scheme>.<ext>`, where `<ext>` is the app's default config file
+  extension.
+- For apps with `external_theme = True`, the file `<config-dir>/current-theme.conf` will
+  be used when symlinking the requested theme. The config file thus must point to this
+  file in order to change with the set theme.
+
+  Additionally, the theme symlink will be created from the file
+
+  ```
+  <local-config-root>/local-config/themes/<palette>/apps/<app-name>/generated/<scheme>.conf
+  ```
+
+  to `<config-dir>/current-theme.conf`. 
+
+## Directory structure
+
+- `local-config/`: main repo directory
+    * `config/`: app-specific configuration files. Each folder inside this directory is
+      app-specific, and the target of associated copy operations when a config sync is
+      performed. Nothing in this directory should pertain to any repo functionality; it
+      should only contain config files that originated elsewhere on the system.
+    * `themes/`: app-independent theme data files. Each folder in this directory should
+      correspond to a specific color palette and house any relevant color spec files
+      (currently likely be a `colors.json`). Also servers the output location for
+      generated theme files
+        * `<palette>/colors.json`: JSON formatted color key-value pairings for palette
+          colors. There's no standard here aside from the filename and format; downstream
+          app-specific TOML templates can be dependent on any key naming scheme within the
+          JSON.
+        + `<palette>/apps/<app-name>/templates/`: houses the TOML maps for the color
+          palette `<palette>` under app `<app-name>`. Files `<fname>.toml` will be mapped to
+          `<fname>.conf` in the theme output folder (below), so ensure the naming
+          standards align with those outlined above.
+        + `<palette>/apps/<app-name>/generated/`: output directory for generated scheme
+          variants. These are the symlink targets for dynamically set external themes.
+    * `app_registry.toml`: global application "registry" used by sync and theme-setting
+      scripts. This lets apps be dynamically added or removed from being eligible for
+      config-related operations.
+
+## Scripts
+
+`set_theme.py`: sets a theme across select apps.
+
+- Applies to specific app with `-a <app>` , or to all apps in the `app_registry.toml` with
+  `-a "*"`.
+- Uses symlinks to set canonical config files to theme-based variations. Which files get
+  set depends on the _app type_ (see above), which really just boils down to whether
+  theming (1) can be specified with an external format, and (2) if it depends on
+  auto-generated theme files from within `local-config`.
+- Palette and scheme are specified as expected. They are used to infer proper paths
+  according to naming and structure standards.
+- Real config files will never be overwritten. To begin setting themes with the script,
+  you must delete the canonical config file expected by the app (and specified in the app
+  registry) to allow the first symlink to be set. From there on out, symlinks will be
+  automatically flushed.
+- A report will be provided on which apps were successfully set to the requested theme,
+  along with the file stems. A number of checks are in place for the existence of involved
+  files and directories. Overall, the risk of overwritting a real config file is low; we
+  only flush existing symlinks, and if the would-be target for the requested theme (be it
+  from an auto-generated theme file, or from a manually manage config variant) doesn't
+  exist, that app's config will be completed skipped. Essentially, everything must be in
+  perfect shape before the symlink trigger is officially pulled.
+
+
+`gen_theme.py`: generates theme files for palettes by mapping their color definitions
+through app-specific templates. These templates specific how to relate an app's theme
+variables to the color names provided by the template.
+
+- An app and palette are the two required parameters. If no template or output paths are
+  provided, they will be inferred according to the theme path standards seen above.
+- The `--template` argument can be a directory or a file, depending on what theme files
+  you'd like to render.
+- The `--output` path, if specified, must be a directory. Generated theme files take on
+  a name with the same stem as their source template, but using the `.conf` extension.
+- The TOML templates should make config variable names to JSON dot-notation accessors. If
+  color definitions are nested, the dot notation should be properly expanded by the script
+  when mapping the colors to keyword values.
+- There are a number of checks for existing paths, even those inferred (e.g., template and
+  output) from the palette and app. If the appropriate setup hasn't been followed, the
+  script will fail. Make sure the `theme` folder in question and it's nested `app`
+  directory are correctly setup before running the script. (Perhaps down the line there
+  are some easy auto-setup steps to take here, but I'm not making that jump now.)
+- TODO: open up different app "writers," or make it easy to extend output syntax based on
+  the app in question. This would like be as simple as mapping app names to
+  line-generating functions, which accept the keyword and color (among other items). This
+  can be fleshed out as needed.
+
+`sync.sh`: copies relevant configuration files from local paths into the `local-config`
+subpath. Markdown files in the docs directory then reference the local copies of these
+files, meaning the documentation updates dynamically when the configuration files do. That
+is, the (possibly extracted) config snippets will change with the current state of my
+system config without any manual intervention of the documentation files.
+
+### Specific theme-setting example
+To make clear how the theme setting script works on my system, the following breaks down
+exactly what steps are taken to exert as much scheme control as possible. Everything at this
+point is wrapped up in a single `make set-<palette>-<scheme>` call; suppose we're
+currently running the dark scheme (see first image) and I run `make set-tone4-light`:
+
+![
+  Starting point; have a GTK app (GNOME files), `kitty`, and Firefox (with the
+  system-dependent default theme set). In Firefox, I have open `localsys` with its
+  scheme-mode to set to "auto," which should reflect the theme setting picked up by the
+  browser (and note the white tab icon).
+](_static/set-theme-1.png)
+
+_(Starting point; have a GTK app (GNOME files), `kitty`, and Firefox (with the
+system-dependent default theme set). In Firefox, I have open `localsys` with its
+scheme-mode to set to "auto," which should reflect the theme setting picked up by the
+browser (and note the white tab icon).)_
+
+1. `set_theme.py` is invoked. Global settings are applied first, based on my OS (`Linux`),
+   which calls
+
+   ```
+   gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'
+   ```
+
+   controlling settings for GTK apps and other `desktop-portal`-aware programs. This
+   yields the following:
+
+   ![Portal-aware apps changed, config apps not yet set](_static/set-theme-2.png)
+
+   _(Portal-aware apps changed, config apps not yet set. Scheme-aware sites are updated
+   without page refresh.)_
+2. Specific application styles are set. For now the list is small, including `kitty`,
+   `waybar`, and `sway`. `kitty` is the only type (2) application here, whereas the other
+   two are type (1).
+
+   a. For the type (1) apps, the canonical config files as specified in the app registry
+      are symlinked to their light variants. For `sway`, this is `~/.config/sway/config`,
+      and if we look at the `file`:
+
+      ```sh
+      .config/sway/config: symbolic link to ~/.config/sway/sway-tone4-light
+      ```
+   b. For the type (2) apps, just the `current-theme.conf` file is symlinked to the
+      relevant palette-scheme file. `kitty` is such an app, with a supported theme file
+      for `tone4`, and those files have been auto-generated via `gen_theme.py`. Looking at
+      this file under the `kitty` config directory:
+
+      ```sh
+      .config/kitty/current-theme.conf: symbolic link to ~/Documents/projects/local-config/local-config/themes/tone4/apps/kitty/generated/light.conf
+      ```
+
+      The `kitty.conf` file isn't changed, as all palette-related items are specified in
+      the theme file. (Note that the general notion of a "theme" could include changes to
+      other stylistic aspects, like the font family; this would likely require some hybrid
+      type 1-2 approach not yet implemented).
+3. Live application instances are reloaded, according to the registered `refresh_cmd`s.
+   Here the apps with style/config files set in step (2) are reloaded to reflect those
+   changes. Again, in this example, this is `kitty`, `sway`, and the `waybar`.
+
+   ![Final light setting: portal-dependent apps _and_ config-based apps changed](_static/set-theme-3.png)
+
+   _(Final light setting: portal-dependent apps _and_ config-based apps changed)_
+4. `set_theme.py` provides a report for the actions taken; in this case, the following was
+   printed:
+
+   ![`set_theme.py` output](_static/set-theme-4.png)
+   _(`set_theme.py` output)_
diff --git a/autoconf/app_registry.toml b/autoconf/app_registry.toml
new file mode 100644
index 0000000..00c39fd
--- /dev/null
+++ b/autoconf/app_registry.toml
@@ -0,0 +1,42 @@
+# 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'
diff --git a/autoconf/gen_theme.py b/autoconf/gen_theme.py
new file mode 100644
index 0000000..dac99e0
--- /dev/null
+++ b/autoconf/gen_theme.py
@@ -0,0 +1,108 @@
+import argparse
+import inspect
+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
+
+
+parser = argparse.ArgumentParser(
+    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.'
+)
+args = parser.parse_args()
+
+# separation sequences to use base on app
+app_sep_map = {
+    'kitty': ' ',
+}
+
+def generate_theme_files():
+    basepath = get_running_path()
+
+    # set arg conditional variables 
+    palette_path  = Path(basepath, 'themes', args.palette)
+    colors_path   = Path(palette_path, 'colors.json')
+    theme_app     = args.app
+
+    template_path = None
+    output_path   = None
+
+    if args.template is None:
+        template_path = Path(palette_path, 'apps', theme_app, 'templates')
+    else:
+        template_path = Path(args.template).resolve()
+
+    if args.output is None:
+        output_path = Path(palette_path, 'apps', theme_app, 'generated')
+    else:
+        output_path = Path(args.output).resolve()
+
+    # check paths
+    if not colors_path.exists():
+        print(f'Resolved colors path [{colors_path}] doesn\'t exist, exiting')
+        return
+    
+    if not template_path.exists():
+        print(f'Template path [{template_path}] doesn\'t exist, exiting')
+        return
+
+    if not output_path.exists() or not output_path.is_dir():
+        print(f'Output path [{output_path}] doesn\'t exist or not a directory, exiting')
+        return
+
+    print(f'Using palette colors [{colors_path}]')
+    print(f'-> with templates in [{template_path}]')
+    print(f'-> to output path [{output_path}]\n')
+
+    # load external files (JSON, TOML)
+    colors_json = json.load(colors_path.open())
+
+    # get all matching TOML files
+    template_list = [template_path]
+    if template_path.is_dir():
+        template_list = template_path.rglob('*.toml')
+
+    for template_path in template_list:
+        template_toml = toml.load(template_path.open('rb'))
+
+        # lookup app-specific config separator
+        config_sep = app_sep_map.get(theme_app, ' ')
+        output_lines = []
+        for config_key, color_key in template_toml.items():
+            color_value = colors_json
+            for _key in color_key.split('.'):
+                color_value = color_value.get(_key, {})
+            output_lines.append(f'{config_key}{config_sep}{color_value}') 
+
+        output_file = Path(output_path, template_path.stem).with_suffix('.conf')
+        output_file.write_text('\n'.join(output_lines))
+        print(f'[{len(output_lines)}] lines written to [{output_file}] for app [{theme_app}]')
+
+if __name__ == '__main__':
+    generate_theme_files()
diff --git a/autoconf/set_theme.py b/autoconf/set_theme.py
new file mode 100644
index 0000000..09b1129
--- /dev/null
+++ b/autoconf/set_theme.py
@@ -0,0 +1,145 @@
+import argparse
+import inspect
+import subprocess
+import json
+import os
+import tomllib as toml
+from pathlib import Path
+
+from colorama import Fore
+
+
+parser = argparse.ArgumentParser(
+    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'
+)
+args = parser.parse_args()
+
+
+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 local-config
+            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/themes/tone4/apps/kitty/generated/dark.conf b/autoconf/themes/tone4/apps/kitty/generated/dark.conf
new file mode 100644
index 0000000..2f3772f
--- /dev/null
+++ b/autoconf/themes/tone4/apps/kitty/generated/dark.conf
@@ -0,0 +1,21 @@
+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
new file mode 100644
index 0000000..972d9b9
--- /dev/null
+++ b/autoconf/themes/tone4/apps/kitty/generated/light.conf
@@ -0,0 +1,21 @@
+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
new file mode 100644
index 0000000..9b8900d
--- /dev/null
+++ b/autoconf/themes/tone4/apps/kitty/templates/dark.toml
@@ -0,0 +1,40 @@
+# 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
new file mode 100644
index 0000000..9e862d9
--- /dev/null
+++ b/autoconf/themes/tone4/apps/kitty/templates/light.toml
@@ -0,0 +1,40 @@
+# 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
new file mode 100644
index 0000000..27cb9b3
--- /dev/null
+++ b/autoconf/themes/tone4/colors.json
@@ -0,0 +1,107 @@
+{
+  "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/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..600e05f
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+sphinx
+furo
+myst-parser