├── .gitignore ├── .travis.yml ├── LICENSE ├── README.rst ├── pybase16.py ├── pybase16_builder ├── __init__.py ├── builder.py ├── cli.py ├── injector.py ├── shared.py └── updater.py ├── setup.py └── tests ├── test_config ├── test_pybase16.py └── test_scheme.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | __pycache__/ 3 | .pytest_cache/ 4 | dist/ 5 | build/ 6 | 7 | sources 8 | schemes 9 | output 10 | templates 11 | sources.yaml 12 | .tags 13 | .cache 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: python 3 | dist: xenial 4 | python: 5 | - '3.5' 6 | - '3.6' 7 | - '3.7' 8 | install: 9 | - pip install . 10 | - pip install pytest 11 | script: pytest 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Pu Anlai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/InspectorMustache/base16-builder-python.svg?branch=master 2 | :target: https://travis-ci.org/InspectorMustache/base16-builder-python 3 | 4 | Deprecated 5 | ========== 6 | 7 | Use `base24-builder-python `_ instead. Sorry to anyone who's been waiting for me to update this repo. 8 | 9 | base16-builder-python 10 | ===================== 11 | 12 | Finally, a base16 builder that doesn't require me to install anything new. 13 | 14 | Installation 15 | ------------ 16 | As this project uses async/await syntax, the lowest supported Python version is 3.5. 17 | :: 18 | 19 | pip install pybase16-builder 20 | 21 | If you don't want to clutter your computer with something that you're just going to use once you can also just clone this repository and use the provided pybase16.py file. 22 | 23 | Usage 24 | ----- 25 | There are three modes of operation: 26 | :: 27 | 28 | pybase16 update 29 | pybase16 build 30 | pybase16 inject 31 | 32 | Basic Usage 33 | ^^^^^^^^^^^ 34 | If you just want to build all base16 colorschemes and then pick out the ones you need, simply run: 35 | :: 36 | 37 | pybase16 update 38 | pybase16 build 39 | 40 | Once the process is finished, you can find all colorschemes in a folder named output located in the current working directory. 41 | 42 | For a more detailed explanation of the individual commands, read on. 43 | 44 | Update 45 | ^^^^^^ 46 | Downloads all base16 schemes and templates to the current working directory. 47 | The source files, i.e. the files pointing to the scheme and template repositories (see `builder.md `_) will also be updated by default. If you want to use your own versions of these files (to exclude specific repositories, for example), you can prevent the builder from updating the source files by using the :code:`-c/--custom` option. 48 | You can use :code:`-v/--verbose` for more detailed output. 49 | 50 | Build 51 | ^^^^^ 52 | Builds base16 colorschemes for all schemes and templates. This requires the directory structure and files created by the update operation to be present in the working directory. This operation accepts four parameters: 53 | 54 | * :code:`-s/--scheme` restricts building to specific schemes 55 | 56 | Can be specified more than once. Each argument must match a scheme. Wildcards can be used but must be escaped properly so they are not expanded by the shell. 57 | 58 | * :code:`-t/--template` restricts building to specific templates 59 | 60 | Can be specified more than once. Each argument must correspond to a folder name in the templates directory. 61 | 62 | * :code:`-o/--output` specifies a path where built colorschemes will be placed 63 | 64 | If this option is not specified, an "output" folder in the current working directory will be created and used. 65 | 66 | * :code:`-v/--verbose` increases verbosity 67 | 68 | With this option specified the builder prints out the name of each scheme as it's built. 69 | 70 | Example: 71 | :: 72 | 73 | pybase16 build -t dunst -s atelier-heath-light -o /tmp/output 74 | 75 | Inject 76 | ^^^^^^ 77 | This operation provides an easier way to quickly insert a specific colorscheme into one or more config files. In order for the builder to locate the necessary files, this command relies on the folder structure created by the update command. The command accepts two parameters: 78 | 79 | * :code:`-s/--scheme` specifies the scheme you wish to inject 80 | 81 | Refers to the scheme that should be inserted. You can use wildcards and the same restrictions as with update apply. A pattern that matches more than one scheme will cause an error. 82 | 83 | * :code:`-f/--file` specifies the file(s) into which you wish the scheme to be inserted 84 | 85 | Can be specified more than once. Each argument must be specified as a path to a config file that features proper injection markers (see below). 86 | 87 | You will need to prepare your configuration files so that the script knows where to insert the colorscheme. This is done by including two lines in the file 88 | :: 89 | 90 | # %%base16_template: TEMPLATE_NAME##SUBTEMPLATE_NAME %% 91 | 92 | Everything in-between these two lines will be replaced with the colorscheme. 93 | 94 | # %%base16_template_end%% 95 | 96 | Both lines can feature arbitrary characters before the first two percentage signs. This is so as to accomodate different commenting styles. Both lines need to end exactly as demonstrated above, however. "TEMPLATE_NAME" and "SUBTEMPLATE_NAME" are the exception to this. Replace TEMPLATE_NAME with the name of the template you wish to insert, for example "gnome-terminal". This must correspond to a folder in the templates directory. Replace SUBTEMPLATE_NAME with the name of the subtemplate as it is defined at the top level of the template's config.yaml file (see `file.md `_ for details), for example "default-256". If you omit the subtemplate name (don't omit "##" though), "default" is assumed. 97 | 98 | An example of an i3 config file prepared in such a way can be found `here `_. 99 | 100 | Specify the name of the scheme you wish to inject with the -s option. Use the -f option for each file into which you want to inject the scheme. 101 | 102 | As an example, here's the command I use to globally change the color scheme in all applications that support it: 103 | :: 104 | 105 | pybase16 inject -s ocean -f ~/.gtkrc-2.0.mine -f ~/.config/dunst/dunstrc -f ~/.config/i3/config -f ~/.config/termite/config -f ~/.config/zathura/zathurarc 106 | 107 | Exit 108 | ^^^^ 109 | The program exits with exit code 1 if it encountered a general error and with 2 if one or more build or update tasks produced a warning or an error. 110 | -------------------------------------------------------------------------------- /pybase16.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import pybase16_builder 3 | 4 | if __name__ == '__main__': 5 | pybase16_builder.run() 6 | -------------------------------------------------------------------------------- /pybase16_builder/__init__.py: -------------------------------------------------------------------------------- 1 | from .cli import run 2 | -------------------------------------------------------------------------------- /pybase16_builder/builder.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | import aiofiles 4 | import pystache 5 | from glob import glob 6 | from .shared import get_yaml_dict, rel_to_cwd, JobOptions, verb_msg, compat_event_loop 7 | 8 | 9 | class TemplateGroup(object): 10 | """Representation of a template group, i.e. a group of templates specified 11 | in a config.yaml.""" 12 | 13 | def __init__(self, base_path): 14 | self.base_path = base_path 15 | self.name = os.path.basename(base_path.rstrip("/")) 16 | self.templates = self.get_templates() 17 | 18 | def get_templates(self): 19 | """ 20 | Return a list of template_dicts based on the config.yaml in 21 | $self.base_path. Keys correspond to templates and values represent 22 | further settings regarding each template. A pystache object containing 23 | the parsed corresponding mustache file is added to the sub-dictionary. 24 | """ 25 | config_path = rel_to_cwd(self.base_path, "templates", "config.yaml") 26 | templates = get_yaml_dict(config_path) 27 | for temp, sub in templates.items(): 28 | mustache_path = os.path.join( 29 | get_parent_dir(config_path), "{}.mustache".format(temp) 30 | ) 31 | sub["parsed"] = get_pystache_parsed(mustache_path) 32 | return templates 33 | 34 | 35 | def get_parent_dir(base_dir, level=1): 36 | "Get the directory $level levels above $base_dir." 37 | while level > 0: 38 | base_dir = os.path.dirname(base_dir) 39 | level -= 1 40 | return base_dir 41 | 42 | 43 | def get_pystache_parsed(mustache_file): 44 | """Return a ParsedTemplate instance based on the contents of 45 | $mustache_file.""" 46 | with open(mustache_file, "r", encoding="utf-8") as file_: 47 | parsed = pystache.parse(file_.read()) 48 | return parsed 49 | 50 | 51 | def get_template_dirs(): 52 | """Return a set of all template directories.""" 53 | temp_glob = rel_to_cwd("templates", "**", "templates", "config.yaml") 54 | temp_groups = glob(temp_glob) 55 | temp_groups = [get_parent_dir(path, 2) for path in temp_groups] 56 | return set(temp_groups) 57 | 58 | 59 | def get_scheme_dirs(): 60 | """Return a set of all scheme directories.""" 61 | scheme_glob = rel_to_cwd("schemes", "**", "*.yaml") 62 | scheme_groups = glob(scheme_glob) 63 | scheme_groups = [get_parent_dir(path) for path in scheme_groups] 64 | return set(scheme_groups) 65 | 66 | 67 | def get_scheme_files(patterns=None): 68 | """Return a list of all (or those matching $pattern) yaml (scheme) 69 | files.""" 70 | patterns = patterns or ["*"] 71 | pattern_list = ["{}.yaml".format(pattern) for pattern in patterns] 72 | scheme_files = [] 73 | for scheme_path in get_scheme_dirs(): 74 | for pattern in pattern_list: 75 | file_paths = glob(os.path.join(scheme_path, pattern)) 76 | scheme_files.extend(file_paths) 77 | 78 | return scheme_files 79 | 80 | 81 | def reverse_hex(hex_str): 82 | """Reverse a hex foreground string into its background version.""" 83 | hex_str = "".join([hex_str[i : i + 2] for i in range(0, len(hex_str), 2)][::-1]) 84 | return hex_str 85 | 86 | 87 | def format_scheme(scheme, slug): 88 | """Change $scheme so it can be applied to a template.""" 89 | scheme["scheme-name"] = scheme.pop("scheme") 90 | scheme["scheme-author"] = scheme.pop("author") 91 | scheme["scheme-slug"] = slug 92 | bases = ["base{:02X}".format(x) for x in range(0, 16)] 93 | for base in bases: 94 | scheme["{}-hex".format(base)] = scheme.pop(base) 95 | scheme["{}-hex-r".format(base)] = scheme["{}-hex".format(base)][0:2] 96 | scheme["{}-hex-g".format(base)] = scheme["{}-hex".format(base)][2:4] 97 | scheme["{}-hex-b".format(base)] = scheme["{}-hex".format(base)][4:6] 98 | scheme["{}-hex-bgr".format(base)] = reverse_hex(scheme["{}-hex".format(base)]) 99 | 100 | scheme["{}-rgb-r".format(base)] = str(int(scheme["{}-hex-r".format(base)], 16)) 101 | scheme["{}-rgb-g".format(base)] = str(int(scheme["{}-hex-g".format(base)], 16)) 102 | scheme["{}-rgb-b".format(base)] = str(int(scheme["{}-hex-b".format(base)], 16)) 103 | 104 | scheme["{}-dec-r".format(base)] = str( 105 | int(scheme["{}-rgb-r".format(base)]) / 255 106 | ) 107 | scheme["{}-dec-g".format(base)] = str( 108 | int(scheme["{}-rgb-g".format(base)]) / 255 109 | ) 110 | scheme["{}-dec-b".format(base)] = str( 111 | int(scheme["{}-rgb-b".format(base)]) / 255 112 | ) 113 | 114 | 115 | def slugify(scheme_file): 116 | """Format $scheme_file_name to be used as a slug variable.""" 117 | scheme_file_name = os.path.basename(scheme_file) 118 | if scheme_file_name.endswith(".yaml"): 119 | scheme_file_name = scheme_file_name[:-5] 120 | return scheme_file_name.lower().replace(" ", "-") 121 | 122 | 123 | async def build_single(scheme_file, job_options): 124 | """Build colorscheme from $scheme_file using $job_options. Return True if 125 | completed without warnings. Otherwise false.""" 126 | scheme = get_yaml_dict(scheme_file) 127 | scheme_slug = slugify(scheme_file) 128 | format_scheme(scheme, scheme_slug) 129 | scheme_name = scheme["scheme-name"] 130 | warn = False # set this for feedback to the caller 131 | 132 | if job_options.verbose: 133 | print('Building colorschemes for scheme "{}"...'.format(scheme_name)) 134 | 135 | for temp_group in job_options.templates: 136 | 137 | for _, sub in temp_group.templates.items(): 138 | output_dir = os.path.join( 139 | job_options.base_output_dir, temp_group.name, sub["output"] 140 | ) 141 | try: 142 | os.makedirs(output_dir) 143 | except FileExistsError: 144 | pass 145 | 146 | if sub["extension"] is not None: 147 | filename = "base16-{}{}".format(scheme_slug, sub["extension"]) 148 | else: 149 | filename = "base16-{}".format(scheme_slug) 150 | 151 | build_path = os.path.join(output_dir, filename) 152 | 153 | # include a warning for files being overwritten to comply with 154 | # base16 0.9.1 155 | if os.path.isfile(build_path): 156 | verb_msg("File {} exists and will be overwritten.".format(build_path)) 157 | warn = True 158 | 159 | async with aiofiles.open(build_path, "w", encoding="utf-8") as file_: 160 | file_content = pystache.render(sub["parsed"], scheme) 161 | await file_.write(file_content) 162 | 163 | if job_options.verbose: 164 | print('Built colorschemes for scheme "{}".'.format(scheme_name)) 165 | 166 | return not (warn) 167 | 168 | 169 | async def build_single_task(scheme_file, job_options): 170 | """Worker thread for picking up scheme files from $queue and building b16 171 | templates using $templates until it receives None.""" 172 | try: 173 | return await build_single(scheme_file, job_options) 174 | except Exception as e: 175 | verb_msg("{}: {!s}".format(scheme_file, e), lvl=2) 176 | return False 177 | 178 | 179 | async def build_scheduler(scheme_files, job_options): 180 | """Create a task list from scheme_files and run tasks asynchronously.""" 181 | task_list = [build_single_task(f, job_options) for f in scheme_files] 182 | return await asyncio.gather(*task_list) 183 | 184 | 185 | def build(templates=None, schemes=None, base_output_dir=None, verbose=False): 186 | """Main build function to initiate building process.""" 187 | template_dirs = templates or get_template_dirs() 188 | scheme_files = get_scheme_files(schemes) 189 | base_output_dir = base_output_dir or rel_to_cwd("output") 190 | 191 | # raise LookupError if there is not at least one template or scheme 192 | # to work with 193 | if not template_dirs or not scheme_files: 194 | raise LookupError 195 | 196 | # raise PermissionError if user has no write acces for $base_output_dir 197 | try: 198 | os.makedirs(base_output_dir) 199 | except FileExistsError: 200 | pass 201 | 202 | if not os.access(base_output_dir, os.W_OK | os.X_OK): 203 | raise PermissionError 204 | 205 | templates = [TemplateGroup(path) for path in template_dirs] 206 | 207 | job_options = JobOptions( 208 | base_output_dir=base_output_dir, templates=templates, verbose=verbose 209 | ) 210 | 211 | with compat_event_loop() as event_loop: 212 | results = event_loop.run_until_complete( 213 | build_scheduler(scheme_files, job_options) 214 | ) 215 | 216 | print("Finished building process.") 217 | return all(results) 218 | -------------------------------------------------------------------------------- /pybase16_builder/cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | from . import updater, builder, injector 4 | from .shared import rel_to_cwd, err_print 5 | 6 | 7 | def catch_keyboard_interrupt(func): 8 | """Decorator for catching KeyboardInterrupt and quitting gracefully.""" 9 | 10 | def decorated(*args, **kwargs): 11 | try: 12 | func(*args, **kwargs) 13 | except KeyboardInterrupt: 14 | err_print("Interrupt signal received.") 15 | 16 | return decorated 17 | 18 | 19 | @catch_keyboard_interrupt 20 | def build_mode(arg_namespace): 21 | """Check command line arguments and run build function.""" 22 | custom_temps = arg_namespace.template or [] 23 | temp_paths = [rel_to_cwd("templates", temp) for temp in custom_temps] 24 | 25 | try: 26 | result = builder.build( 27 | templates=temp_paths, 28 | schemes=arg_namespace.scheme, 29 | base_output_dir=arg_namespace.output, 30 | verbose=arg_namespace.verbose, 31 | ) 32 | # return with exit code 2 if there were any non-fatal incidents during 33 | sys.exit(0 if result else 2) 34 | 35 | except (LookupError, PermissionError) as exception: 36 | if isinstance(exception, LookupError): 37 | err_print( 38 | "Necessary resources for building not found in current " 39 | "working directory." 40 | ) 41 | if isinstance(exception, PermissionError): 42 | err_print("Lacking necessary access permissions for output directory.") 43 | 44 | 45 | @catch_keyboard_interrupt 46 | def inject_mode(arg_namespace): 47 | """Check command line arguments and run build function.""" 48 | try: 49 | injector.inject_into_files(arg_namespace.scheme, arg_namespace.file) 50 | except ( 51 | IndexError, 52 | FileNotFoundError, 53 | LookupError, 54 | PermissionError, 55 | IsADirectoryError, 56 | ValueError, 57 | ) as exception: 58 | if isinstance(exception, ValueError): 59 | err_print( 60 | "Pattern {} matches more than one scheme.".format(*arg_namespace.scheme) 61 | ) 62 | elif isinstance(exception, IndexError): 63 | err_print( 64 | '"{}" has no valid injection marker lines.'.format(exception.args[0]) 65 | ) 66 | elif isinstance(exception, FileNotFoundError): 67 | err_print( 68 | 'Lacking resource "{}" to complete operation.'.format( 69 | exception.filename 70 | ) 71 | ) 72 | elif isinstance(exception, PermissionError): 73 | err_print("No write permission for current working directory.") 74 | elif isinstance(exception, IsADirectoryError): 75 | err_print( 76 | '"{}" is a directory. Provide a *.yaml scheme file instead.'.format( 77 | exception.filename 78 | ) 79 | ) 80 | elif isinstance(exception, LookupError): 81 | err_print('No scheme "{}" found.'.format(*arg_namespace.scheme)) 82 | 83 | 84 | @catch_keyboard_interrupt 85 | def update_mode(arg_namespace): 86 | """Check command line arguments and run update function.""" 87 | try: 88 | result = updater.update( 89 | custom_sources=arg_namespace.custom, verbose=arg_namespace.verbose 90 | ) 91 | # return with exit code 2 if there were any non-fatal incidents during 92 | # update 93 | sys.exit(0 if result else 2) 94 | 95 | except (PermissionError, FileNotFoundError) as exception: 96 | if isinstance(exception, PermissionError): 97 | err_print("No write permission for current working directory.") 98 | if isinstance(exception, FileNotFoundError): 99 | err_print( 100 | "Necessary resources for updating not found in current " 101 | "working directory." 102 | ) 103 | 104 | 105 | def run(): 106 | arg_namespace = argparser.parse_args() 107 | arg_namespace.func(arg_namespace) 108 | 109 | 110 | argparser = argparse.ArgumentParser(prog="pybase16") 111 | subparsers = argparser.add_subparsers(dest="mode") 112 | subparsers.required = True # workaround for versions <3.7 113 | 114 | update_parser = subparsers.add_parser( 115 | "update", help="update: download all base16 scheme and template repositories" 116 | ) 117 | update_parser.set_defaults(func=update_mode) 118 | update_parser.add_argument( 119 | "-c", 120 | "--custom", 121 | action="store_const", 122 | const=True, 123 | help="update repositories but don't update source files", 124 | ) 125 | update_parser.add_argument( 126 | "-v", "--verbose", action="store_const", const=True, help="increase verbosity" 127 | ) 128 | 129 | build_parser = subparsers.add_parser( 130 | "build", help="build: build base16 colorschemes from templates" 131 | ) 132 | build_parser.set_defaults(func=build_mode) 133 | build_parser.add_argument( 134 | "-o", "--output", help="specifiy a target directory for the build output" 135 | ) 136 | build_parser.add_argument( 137 | "-t", 138 | "--template", 139 | action="append", 140 | metavar="TEMP", 141 | help="restrict operation to specific templates (must correspond to a directory in ./templates); can be specified more than once", 142 | ) 143 | build_parser.add_argument( 144 | "-s", 145 | "--scheme", 146 | action="append", 147 | help="restrict operation to specific schemes; (properly escaped) wildcards allowed", 148 | ) 149 | build_parser.add_argument( 150 | "-v", "--verbose", action="store_const", const=True, help="increase verbosity" 151 | ) 152 | 153 | inject_parser = subparsers.add_parser( 154 | "inject", help="inject: inject a colorscheme into one or multiple files" 155 | ) 156 | inject_parser.set_defaults(func=inject_mode) 157 | inject_parser.add_argument( 158 | "-f", 159 | "--file", 160 | action="append", 161 | required=True, 162 | help="provide paths to files into which you wish to inject a colorscheme; can be specified more than once", 163 | ) 164 | inject_parser.add_argument( 165 | "-s", 166 | "--scheme", 167 | action="append", 168 | required=True, 169 | help="select a scheme; allows for wildcards", 170 | ) 171 | -------------------------------------------------------------------------------- /pybase16_builder/injector.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pystache 3 | from . import builder 4 | from .shared import rel_to_cwd, get_yaml_dict 5 | 6 | TEMP_NEEDLE = re.compile(r"^.*%%base16_template:([^%]+)%%$") 7 | TEMP_END_NEEDLE = re.compile(r"^.*%%base16_template_end%%$") 8 | 9 | 10 | class Recipient: 11 | """Represents a file into which a base16 scheme is to be injected.""" 12 | 13 | def __init__(self, path): 14 | self.path = path 15 | self.content = self._get_file_content(self.path) 16 | self.temp = self._get_temp(self.content) 17 | 18 | def _get_file_content(self, path): 19 | """Return a string representation file content at $path.""" 20 | with open(path, "r", encoding="utf-8") as file_: 21 | content = file_.read() 22 | return content 23 | 24 | def _get_temp(self, content): 25 | """Get the string that points to a specific base16 scheme.""" 26 | temp = None 27 | for line in content.split("\n"): 28 | 29 | # make sure there's both start and end line 30 | if not temp: 31 | match = TEMP_NEEDLE.match(line) 32 | if match: 33 | temp = match.group(1).strip() 34 | continue 35 | else: 36 | match = TEMP_END_NEEDLE.match(line) 37 | if match: 38 | return temp 39 | 40 | raise IndexError(self.path) 41 | 42 | def get_colorscheme(self, scheme_file): 43 | """Return a string object with the colorscheme that is to be 44 | inserted.""" 45 | scheme = get_yaml_dict(scheme_file) 46 | scheme_slug = builder.slugify(scheme_file) 47 | builder.format_scheme(scheme, scheme_slug) 48 | 49 | try: 50 | temp_base, temp_sub = self.temp.split("##") 51 | except ValueError: 52 | temp_base, temp_sub = (self.temp.strip("##"), "default") 53 | 54 | temp_path = rel_to_cwd("templates", temp_base) 55 | temp_group = builder.TemplateGroup(temp_path) 56 | try: 57 | single_temp = temp_group.templates[temp_sub] 58 | except KeyError: 59 | raise FileNotFoundError(None, None, self.path + " (sub-template)") 60 | 61 | colorscheme = pystache.render(single_temp["parsed"], scheme) 62 | return colorscheme 63 | 64 | def inject_scheme(self, b16_scheme): 65 | """Inject string $b16_scheme into self.content.""" 66 | # correctly formatted start and end of block should have already been 67 | # ascertained by _get_temp 68 | content_lines = self.content.split("\n") 69 | b16_scheme_lines = b16_scheme.split("\n") 70 | start_line = None 71 | for num, line in enumerate(content_lines): 72 | if not start_line: 73 | match = TEMP_NEEDLE.match(line) 74 | if match: 75 | start_line = num + 1 76 | else: 77 | match = TEMP_END_NEEDLE.match(line) 78 | if match: 79 | end_line = num 80 | 81 | # put lines back together 82 | new_content_lines = ( 83 | content_lines[0:start_line] + b16_scheme_lines + content_lines[end_line:] 84 | ) 85 | self.content = "\n".join(new_content_lines) 86 | 87 | def write(self): 88 | """Write content back to file.""" 89 | with open(self.path, "w", encoding="utf-8") as file_: 90 | file_.write(self.content) 91 | 92 | 93 | def inject_into_files(scheme, files): 94 | """Inject $scheme into list $files.""" 95 | scheme_files = builder.get_scheme_files(scheme) 96 | if len(scheme_files) == 0: 97 | raise FileNotFoundError(None, None, scheme) 98 | if len(scheme_files) > 1: 99 | raise ValueError 100 | 101 | for file_ in files: 102 | rec = Recipient(file_) 103 | colorscheme = rec.get_colorscheme(*scheme_files) 104 | rec.inject_scheme(colorscheme) 105 | rec.write() 106 | -------------------------------------------------------------------------------- /pybase16_builder/shared.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import asyncio 4 | import yaml 5 | from collections import namedtuple 6 | from contextlib import contextmanager 7 | 8 | 9 | class JobOptions: 10 | """Container for options related to job processing""" 11 | 12 | def __init__(self, **kwargs): 13 | for k, v in kwargs.items(): 14 | setattr(self, k, v) 15 | 16 | 17 | CWD = os.path.realpath(os.getcwd()) 18 | ACodes = namedtuple("ACodes", ["red", "yellow", "bold", "end"]) 19 | acodes = ACodes(red="\033[31m", yellow="\033[33m", bold="\033[1m", end="\033[0m") 20 | 21 | 22 | @contextmanager 23 | def compat_event_loop(): 24 | """OS agnostic context manager for an event loop.""" 25 | if sys.platform.startswith("win"): 26 | asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) 27 | 28 | event_loop = asyncio.get_event_loop() 29 | 30 | if event_loop.is_closed(): 31 | event_loop = asyncio.new_event_loop() 32 | asyncio.set_event_loop(event_loop) 33 | 34 | yield event_loop 35 | 36 | event_loop.close() 37 | 38 | 39 | def rel_to_cwd(*args): 40 | """Get absolute real path of $path with $CWD as base.""" 41 | return os.path.join(CWD, *args) 42 | 43 | 44 | def get_yaml_dict(yaml_file): 45 | """Return a yaml_dict from reading yaml_file. If yaml_file is empty or 46 | doesn't exist, return an empty dict instead.""" 47 | try: 48 | with open(yaml_file, "r", encoding="utf-8") as file_: 49 | yaml_dict = yaml.safe_load(file_.read()) or {} 50 | return yaml_dict 51 | except FileNotFoundError: 52 | return {} 53 | 54 | 55 | def err_print(msg, exit_code=1): 56 | """Print $msg and exit with $exit_code.""" 57 | print(msg, file=sys.stderr) 58 | sys.exit(exit_code) 59 | 60 | 61 | def verb_msg(msg, lvl=1): 62 | """Print a warning ($lvl=1) or an error ($lvl=2) message.""" 63 | if lvl == 1: 64 | print( 65 | "{0.yellow}{0.bold}Warning{0.end}:\n{1}".format(acodes, msg), 66 | file=sys.stderr, 67 | ) 68 | elif lvl == 2: 69 | print("{0.red}{0.bold}Error{0.end}:\n{1}".format(acodes, msg), file=sys.stderr) 70 | -------------------------------------------------------------------------------- /pybase16_builder/updater.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import shutil 4 | import asyncio 5 | from .shared import get_yaml_dict, rel_to_cwd, verb_msg, compat_event_loop 6 | 7 | 8 | def write_sources_file(): 9 | """Write a sources.yaml file to current working dir.""" 10 | file_content = ( 11 | "schemes: " 12 | "https://github.com/chriskempson/base16-schemes-source.git\n" 13 | "templates: " 14 | "https://github.com/chriskempson/base16-templates-source.git" 15 | ) 16 | file_path = rel_to_cwd("sources.yaml") 17 | with open(file_path, "w", encoding="utf-8") as file_: 18 | file_.write(file_content) 19 | 20 | 21 | async def git_clone(git_url, path, verbose=False): 22 | """Clone git repository at $git_url to $path. Return True if succesful, 23 | otherwise False.""" 24 | if verbose: 25 | print("Cloning {}...".format(git_url)) 26 | if os.path.exists(os.path.join(path, ".git")): 27 | # get rid of local repo if it already exists 28 | shutil.rmtree(path) 29 | 30 | os.makedirs(path, exist_ok=True) 31 | 32 | proc_env = os.environ.copy() 33 | proc_env["GIT_TERMINAL_PROMPT"] = "0" 34 | git_proc = await asyncio.create_subprocess_exec( 35 | "git", "clone", git_url, path, stderr=asyncio.subprocess.PIPE, env=proc_env 36 | ) 37 | stdout, stderr = await git_proc.communicate() 38 | 39 | if git_proc.returncode != 0: 40 | # remove created directory if it's empty 41 | try: 42 | os.rmdir(path) 43 | except OSError: 44 | pass 45 | 46 | verb_msg("{}:\n{}".format(git_url, stderr.decode("utf-8"))) 47 | return False 48 | elif verbose: 49 | print("Cloned {}".format(git_url)) 50 | return True 51 | 52 | 53 | async def git_clone_scheduler(yaml_file, base_dir, verbose=False): 54 | """Create task list for clone jobs and run them asynchronously.""" 55 | jobs = generate_jobs_from_yaml(yaml_file, base_dir) 56 | task_list = [git_clone(*args_, verbose=verbose) for args_ in jobs] 57 | return await asyncio.gather(*task_list) 58 | 59 | 60 | def generate_jobs_from_yaml(yaml_file, base_dir): 61 | yaml_dict = get_yaml_dict(yaml_file) 62 | for key, value in yaml_dict.items(): 63 | yield (value, rel_to_cwd(base_dir, key)) 64 | 65 | 66 | def update(custom_sources=False, verbose=False): 67 | """Update function to be called from cli.py""" 68 | if not shutil.which("git"): 69 | print("Git executable not found in $PATH.") 70 | sys.exit(1) 71 | 72 | results = [] 73 | with compat_event_loop() as event_loop: 74 | if not custom_sources: 75 | print("Creating sources.yaml…") 76 | write_sources_file() 77 | 78 | print("Cloning sources…") 79 | sources_file = rel_to_cwd("sources.yaml") 80 | r = event_loop.run_until_complete( 81 | git_clone_scheduler(sources_file, rel_to_cwd("sources"), verbose=verbose) 82 | ) 83 | results.append(r) 84 | 85 | print("Cloning templates…") 86 | r = event_loop.run_until_complete( 87 | git_clone_scheduler( 88 | rel_to_cwd("sources", "templates", "list.yaml"), 89 | rel_to_cwd("templates"), 90 | verbose=verbose, 91 | ) 92 | ) 93 | results.append(r) 94 | 95 | print("Cloning schemes…") 96 | r = event_loop.run_until_complete( 97 | git_clone_scheduler( 98 | rel_to_cwd("sources", "schemes", "list.yaml"), 99 | rel_to_cwd("schemes"), 100 | verbose=verbose, 101 | ) 102 | ) 103 | results.append(r) 104 | 105 | return all(results) 106 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="pybase16-builder", 5 | version="0.2.8", 6 | description="A base16 colorscheme builder for Python", 7 | long_description=open("README.rst").read(), 8 | url="https://github.com/InspectorMustache/pybase16-builder", 9 | packages=["pybase16_builder"], 10 | author="Pu Anlai", 11 | license="MIT", 12 | classifiers=[ 13 | "Development Status :: 3 - Alpha", 14 | "Intended Audience :: End Users/Desktop", 15 | "Topic :: Other/Nonlisted Topic", 16 | "License :: OSI Approved :: MIT License", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3 :: Only", 19 | ], 20 | keywords="base16", 21 | install_requires=["pystache", "pyyaml", "aiofiles"], 22 | python_requires=">=3.5", 23 | entry_points={"console_scripts": ["pybase16 = pybase16_builder.cli:run"]}, 24 | ) 25 | -------------------------------------------------------------------------------- /tests/test_config: -------------------------------------------------------------------------------- 1 | # This file has been auto-generated by i3-config-wizard(1). 2 | # It will not be overwritten, so edit it as you like. 3 | # 4 | # Should you change your keyboard layout some time, delete 5 | # this file and re-run i3-config-wizard(1). 6 | # 7 | 8 | # i3 config file (v4) 9 | # 10 | # Please see http://i3wm.org/docs/userguide.html for a complete reference! 11 | # 12 | # %%base16_template: i3##colors %% 13 | ## Base16 Ocean 14 | # Author: Chris Kempson (http://chriskempson.com) 15 | # 16 | # You can use these variables anywhere in the i3 configuration file. 17 | 18 | set $base00 #2b303b 19 | set $base01 #343d46 20 | set $base02 #4f5b66 21 | set $base03 #65737e 22 | set $base04 #a7adba 23 | set $base05 #c0c5ce 24 | set $base06 #dfe1e8 25 | set $base07 #eff1f5 26 | set $base08 #bf616a 27 | set $base09 #d08770 28 | set $base0A #ebcb8b 29 | set $base0B #a3be8c 30 | set $base0C #96b5b4 31 | set $base0D #8fa1b3 32 | set $base0E #b48ead 33 | set $base0F #ab7967 34 | 35 | # %%base16_template_end%% 36 | 37 | # Basic color configuration using the Base16 variables for windows and borders. 38 | # Property Name Border BG Text Indicator Child Border 39 | client.focused $base0D $base0D $base00 $base0E $base0D 40 | client.focused_inactive $base01 $base01 $base05 $base01 $base01 41 | client.unfocused $base01 $base00 $base05 $base01 $base01 42 | client.urgent $base08 $base08 $base00 $base08 $base08 43 | client.placeholder $base00 $base00 $base05 $base00 $base00 44 | client.background $base07 45 | 46 | 47 | set $font Liberation Sans 48 | 49 | set $i3confdir "$HOME/.config/i3" 50 | set $mod Mod4 51 | 52 | set $i3b_up20 "pkill -RTMIN+20 i3blocks" 53 | # never let windows take focus 54 | focus_on_window_activation urgent 55 | ## configure window decorations 56 | # first the border 57 | hide_edge_borders smart 58 | new_window pixel 5 59 | 60 | # next the title font 61 | font pango:$font 8 62 | 63 | # Format the window title beyond the font 64 | for_window [class=".*"] title_format " %title" 65 | 66 | # Gaps 67 | # gaps edge_gaps on 68 | gaps inner 15 69 | gaps outer -6 70 | smart_gaps on 71 | 72 | # This font is widely installed, provides lots of unicode glyphs, right-to-left 73 | # text rendering and scalability on retina/hidpi displays (thanks to pango). 74 | #font pango:DejaVu Sans Mono 8 75 | 76 | # Before i3 v4.8, we used to recommend this one as the default: 77 | # font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1 78 | # The font above is very space-efficient, that is, it looks good, sharp and 79 | # clear in small sizes. However, its unicode glyph coverage is limited, the old 80 | # X core fonts rendering does not support right-to-left and this being a bitmap 81 | # font, it doesn’t scale on retina/hidpi displays. 82 | 83 | # Use Mouse+$mod to drag floating windows to their wanted position 84 | floating_modifier $mod 85 | 86 | # start a terminal 87 | bindsym $mod+Return exec i3-sensible-terminal 88 | 89 | # kill focused window 90 | bindsym $mod+q kill 91 | 92 | # kill window by middle-clicking its title bar 93 | bindsym --release button2 kill 94 | 95 | # start a program launcher 96 | bindsym $mod+r exec j4-dmenu-desktop --no-generic --usage-log=$i3confdir/j4-dmenu-desktop.log --dmenu="dmenu -fn '$font' -i -b -nb \\$base01 -nf \\$base05 -sb \\$base0D -sf \\$base00" 97 | 98 | # start the password finder 99 | bindsym $mod+y exec fish $i3confdir/scripts/pass_get.fish -l 100 | bindsym $mod+x exec fish $i3confdir/scripts/pass_get.fish 101 | 102 | # change focus 103 | bindsym $mod+h focus left 104 | bindsym $mod+j focus down 105 | bindsym $mod+k focus up 106 | bindsym $mod+l focus right 107 | 108 | # alternatively, you can use the cursor keys: 109 | bindsym $mod+Left focus left 110 | bindsym $mod+Down focus down 111 | bindsym $mod+Up focus up 112 | bindsym $mod+Right focus right 113 | 114 | # move focused window 115 | bindsym $mod+Shift+h move left 116 | bindsym $mod+Shift+j move down 117 | bindsym $mod+Shift+k move up 118 | bindsym $mod+Shift+l move right 119 | 120 | # alternatively, you can use the cursor keys: 121 | bindsym $mod+Shift+Left move left 122 | bindsym $mod+Shift+Down move down 123 | bindsym $mod+Shift+Up move up 124 | bindsym $mod+Shift+Right move right 125 | 126 | # split in horizontal orientation 127 | bindsym $mod+odiaeresis split h 128 | 129 | # split in vertical orientation 130 | bindsym $mod+v split v 131 | 132 | # enter fullscreen mode for the focused container 133 | bindsym $mod+f fullscreen toggle 134 | 135 | # change container layout (stacked, tabbed, toggle split) 136 | bindsym $mod+s layout stacking 137 | bindsym $mod+w layout tabbed 138 | bindsym $mod+e layout toggle split 139 | 140 | # toggle tiling / floating 141 | bindsym $mod+Insert floating toggle 142 | 143 | # change focus between tiling / floating windows 144 | # bindsym $mod+space focus mode_toggle 145 | 146 | # focus the parent container 147 | bindsym $mod+p focus parent 148 | 149 | # focus the child container 150 | bindsym $mod+Shift+p focus child 151 | 152 | # define workspaces 153 | set $ws1 "1  " 154 | set $ws2 "2 " 155 | set $ws3 "3 " 156 | set $ws4 "4 " 157 | set $ws5 "5 " 158 | set $ws6 "6 " 159 | set $ws7 "7 " 160 | set $ws8 "8 " 161 | set $ws9 "9 " 162 | set $ws10 "10 " 163 | 164 | # switch to workspace 165 | bindsym $mod+1 workspace $ws1 166 | bindsym $mod+2 workspace $ws2 167 | bindsym $mod+3 workspace $ws3 168 | bindsym $mod+4 workspace $ws4 169 | bindsym $mod+5 workspace $ws5 170 | bindsym $mod+6 workspace $ws6 171 | bindsym $mod+7 workspace $ws7 172 | bindsym $mod+8 workspace $ws8 173 | bindsym $mod+9 workspace $ws9 174 | bindsym $mod+0 workspace $ws10 175 | bindsym $mod+Tab workspace back_and_forth 176 | 177 | # move focused container to workspace 178 | bindsym $mod+Shift+1 move container to workspace $ws1 179 | bindsym $mod+Shift+2 move container to workspace $ws2 180 | bindsym $mod+Shift+3 move container to workspace $ws3 181 | bindsym $mod+Shift+4 move container to workspace $ws4 182 | bindsym $mod+Shift+5 move container to workspace $ws5 183 | bindsym $mod+Shift+6 move container to workspace $ws6 184 | bindsym $mod+Shift+7 move container to workspace $ws7 185 | bindsym $mod+Shift+8 move container to workspace $ws8 186 | bindsym $mod+Shift+9 move container to workspace $ws9 187 | bindsym $mod+Shift+0 move container to workspace $ws10 188 | bindsym $mod+Shift+Tab move container to workspace back_and_forth 189 | 190 | # move focused container to workspace and switch to workspace 191 | bindsym $mod+Shift+Ctrl+1 move container to workspace $ws1; workspace $ws1 192 | bindsym $mod+Shift+Ctrl+2 move container to workspace $ws2; workspace $ws2 193 | bindsym $mod+Shift+Ctrl+3 move container to workspace $ws3; workspace $ws3 194 | bindsym $mod+Shift+Ctrl+4 move container to workspace $ws4; workspace $ws4 195 | bindsym $mod+Shift+Ctrl+5 move container to workspace $ws5; workspace $ws5 196 | bindsym $mod+Shift+Ctrl+6 move container to workspace $ws6; workspace $ws6 197 | bindsym $mod+Shift+Ctrl+7 move container to workspace $ws7; workspace $ws7 198 | bindsym $mod+Shift+Ctrl+8 move container to workspace $ws8; workspace $ws8 199 | bindsym $mod+Shift+Ctrl+9 move container to workspace $ws9; workspace $ws9 200 | bindsym $mod+Shift+Ctrl+0 move container to workspace $ws10; workspace $ws10 201 | bindsym $mod+Shift+Ctrl+Tab move container to workspace back_and_forth; workspace back_and_forth 202 | 203 | # jump to urgent window 204 | bindsym $mod+space [urgent=latest] focus 205 | # restart/reload i3 inplace 206 | bindsym $mod+Shift+Ctrl+r restart 207 | bindsym $mod+Shift+r reload 208 | # shortcuts for clicking blocklets 209 | bindsym $mod+Escape exec BLOCK_BUTTON=1 ${HOME}/.config/i3blocks/blocklets/shutdown.sh 210 | bindsym $mod+t exec BLOCK_BUTTON=1 python ${HOME}/.config/i3blocks/blocklets/todo.txt 211 | 212 | # workspace assignments 213 | assign [class="Inox"] $ws2 214 | assign [class="Firefox"] $ws2 215 | assign [class="Chromium"] $ws2 216 | assign [class="TelegramDesktop"] $ws3 217 | assign [class="Thunderbird"] $ws3 218 | assign [class="Thunar"] $ws4 219 | assign [title="LibreOffice" window_type="splash"] $ws5 220 | assign [class="(?i).*libreoffice.*"] $ws5 221 | assign [class="keepassx" window_type="normal"] $ws9 222 | assign [class="Guayadeque"] $ws10 223 | assign [class="Vlc"] $ws10 224 | 225 | # window arrangement fixes 226 | for_window [class="Zotero" title="Schnellformatierung Zitation"] floating enable 227 | for_window [class="Yad"] floating enable 228 | for_window [class="Pinentry"] floating enable 229 | 230 | ## modes 231 | set $mode_music "music control" 232 | set $mode_resize "resize" 233 | # dedicated mode to control the audio player 234 | mode $mode_music { 235 | bindsym h exec playerctl previous; exec $i3b_up20 236 | bindsym l exec playerctl next; exec $i3b_up20 237 | bindsym space exec playerctl play-pause; exec $i3b_up20 238 | bindsym g exec guayadeque 239 | bindsym Return exec guayadeque; workspace $ws10; mode "default" 240 | 241 | # back to normal: Enter or Escape or repeat mode keysequence 242 | bindsym Escape mode "default" 243 | bindsym $mod+m mode "default" 244 | } 245 | 246 | # resize window (you can also use the mouse for that) 247 | mode $mode_resize { 248 | # These bindings trigger as soon as you enter the resize mode 249 | 250 | # Pressing left will shrink the window’s width. 251 | # Pressing right will grow the window’s width. 252 | # Pressing up will shrink the window’s height. 253 | # Pressing down will grow the window’s height. 254 | bindsym h resize shrink width 5 px or 5 ppt 255 | bindsym j resize grow height 5 px or 5 ppt 256 | bindsym k resize shrink height 5 px or 5 ppt 257 | bindsym l resize grow width 5 px or 5 ppt 258 | 259 | # same bindings, but for the arrow keys 260 | bindsym Left resize shrink width 10 px or 10 ppt 261 | bindsym Down resize grow height 10 px or 10 ppt 262 | bindsym Up resize shrink height 10 px or 10 ppt 263 | bindsym Right resize grow width 10 px or 10 ppt 264 | 265 | # back to normal: Enter or Escape 266 | bindsym Escape mode "default" 267 | bindsym $mod+plus mode "default" 268 | } 269 | 270 | bindsym $mod+plus mode $mode_resize 271 | bindsym $mod+m mode $mode_music 272 | 273 | # Start i3bar to display a workspace bar (plus the system information i3status 274 | # finds out, if available) 275 | bar { 276 | status_command i3blocks 277 | position top 278 | font pango:$font, FontAwesome 11 279 | 280 | colors { 281 | background $base00 282 | separator $base01 283 | statusline $base04 284 | 285 | # State Border BG Text 286 | focused_workspace $base0D $base0D $base00 287 | active_workspace $base03 $base03 $base00 288 | inactive_workspace $base01 $base01 $base05 289 | urgent_workspace $base08 $base08 $base00 290 | binding_mode $base0A $base0A $base00 291 | } 292 | } 293 | 294 | 295 | exec compton -f 296 | exec unclutter 297 | exec feh --bg-fill $HOME/Bilder/wallpaper.jpg 298 | exec ibus-daemon -drx 299 | exec xset -b 300 | exec numlockx on 301 | exec setxkbmap -layout de -variant nodeadkeys -option compose:menu -option caps:escape 302 | -------------------------------------------------------------------------------- /tests/test_pybase16.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | import pytest 5 | from pybase16_builder import shared, updater, builder, injector 6 | 7 | 8 | @pytest.fixture(scope='module') 9 | def clean_dir(): 10 | """Remove all files/folders in the working dir that were not present before 11 | running the test.""" 12 | orig_struct = os.listdir(shared.CWD) 13 | yield 14 | for file_ in os.listdir(shared.CWD): 15 | if file_ not in orig_struct: 16 | try: 17 | shutil.rmtree(file_) 18 | except NotADirectoryError: 19 | os.remove(file_) 20 | 21 | 22 | @pytest.fixture(scope='function') 23 | def clean_config(): 24 | config = shared.rel_to_cwd(os.path.join('tests', 'test_config')) 25 | with open(config, 'r') as file_: 26 | orig_content = file_.read() 27 | yield config 28 | with open(config, 'w') as file_: 29 | file_.write(orig_content) 30 | 31 | 32 | def test_update(clean_dir): 33 | updater.update() 34 | 35 | sources_path = shared.rel_to_cwd('sources.yaml') 36 | template_path = shared.rel_to_cwd('sources', 'templates', 'list.yaml') 37 | scheme_path = shared.rel_to_cwd('sources', 'schemes', 'list.yaml') 38 | directories = {sources_path: shared.rel_to_cwd('sources'), 39 | template_path: shared.rel_to_cwd('templates'), 40 | scheme_path: shared.rel_to_cwd('schemes')} 41 | 42 | # assume there's a corresponding git directory for every key in a yaml file 43 | for yaml_file, dir_ in directories.items(): 44 | yaml_dict = shared.get_yaml_dict(yaml_file) 45 | for key in yaml_dict.keys(): 46 | # assert there's a corresponding directory 47 | assert key in os.listdir(dir_) 48 | key_dir = os.path.join(dir_, key) 49 | # assert it's a git repo 50 | assert '.git' in os.listdir(key_dir) 51 | 52 | 53 | def test_ressource_dirs(clean_dir): 54 | # assert there's a config.yaml for every template group 55 | for temp_path in builder.get_template_dirs(): 56 | conf_path = shared.rel_to_cwd(temp_path, 'templates', 'config.yaml') 57 | assert os.path.exists(conf_path) 58 | 59 | # assert there's at least one yaml file for each scheme group 60 | for scheme_path in builder.get_scheme_dirs(): 61 | yaml_glob = shared.rel_to_cwd(scheme_path, '*.yaml') 62 | assert len(yaml_glob) >= 1 63 | 64 | # assert get_scheme_files only returns yaml_files 65 | scheme_files = builder.get_scheme_files() 66 | for scheme_file in scheme_files: 67 | assert scheme_file[-5:] == '.yaml' 68 | 69 | 70 | def test_build(clean_dir): 71 | builder.build() 72 | template_dirs = builder.get_template_dirs() 73 | templates = [builder.TemplateGroup(path) for path in template_dirs] 74 | for temp_group in templates: 75 | for temp, sub in temp_group.templates.items(): 76 | output_dir = builder.rel_to_cwd('output', temp_group.name, 77 | sub['output']) 78 | # assert proper paths for each template were created 79 | assert os.path.exists(output_dir) 80 | # assert these directories aren't empty. so at least something 81 | # happened 82 | assert len(os.listdir(output_dir)) > 0 83 | 84 | 85 | def test_custom_build(clean_dir): 86 | """Test building with specific parameters.""" 87 | dunst_temp_path = shared.rel_to_cwd('templates', 'dunst') 88 | base_output_dir = tempfile.mktemp() 89 | builder.build(templates=[dunst_temp_path], schemes=['atelier-heath-light'], 90 | base_output_dir=base_output_dir) 91 | 92 | dunst_temps = builder.TemplateGroup(dunst_temp_path).get_templates() 93 | # out_dirs = [dunst_temps[temp]['output'] for temp in dunst_temps.keys()] 94 | for temp, sub in dunst_temps.items(): 95 | out_path = os.path.join(base_output_dir, 'dunst', 96 | sub['output']) 97 | theme_file = 'base16-atelier-heath-light{}'.format(sub['extension']) 98 | out_file = os.path.join(out_path, theme_file) 99 | 100 | assert os.path.exists(out_file) 101 | assert len(os.listdir(out_path)) == 1 102 | 103 | 104 | def test_inject(clean_config): 105 | """Test injection mode.""" 106 | test_injection = 'TEST\nINJECT\nSTRING' 107 | rec = injector.Recipient(clean_config) 108 | assert rec.temp == 'i3##colors' 109 | 110 | # test colorscheme return 111 | scheme_files = builder.get_scheme_files(['atelier-heath-light']) 112 | colorscheme = rec.get_colorscheme(*scheme_files) 113 | assert colorscheme 114 | 115 | # test injection 116 | rec.inject_scheme(test_injection) 117 | rec.write() 118 | with open(clean_config) as file_: 119 | content = file_.read() 120 | matches = content.find(test_injection) 121 | assert matches > 0 122 | -------------------------------------------------------------------------------- /tests/test_scheme.yaml: -------------------------------------------------------------------------------- 1 | scheme: "Cupertino" 2 | author: "Defman21" 3 | base00: "ffffff" # White 4 | base01: "c0c0c0" 5 | base02: "c0c0c0" 6 | base03: "808080" 7 | base04: "808080" 8 | base05: "404040" 9 | base06: "404040" 10 | base07: "5e5e5e" # Black 11 | base08: "c41a15" # Red 12 | base09: "eb8500" # Orange 13 | base0A: "826b28" # Yellow 14 | base0B: "007400" # Green 15 | base0C: "318495" # Cyan 16 | base0D: "0000ff" # Blue 17 | base0E: "a90d91" # Purple 18 | base0F: "826b28" # Brown 19 | --------------------------------------------------------------------------------