├── VERSION ├── requirements.txt ├── dev_requirements.txt ├── src └── kittytheme │ ├── __init__.py │ └── kittytheme.py ├── l.txt ├── try.sh ├── d.txt ├── README.rst ├── setup.py └── LICENSE.rst /VERSION: -------------------------------------------------------------------------------- 1 | 0.6 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | pep257==0.7.0 2 | pycodestyle==2.5.0 3 | pylint==2.3.1 4 | -------------------------------------------------------------------------------- /src/kittytheme/__init__.py: -------------------------------------------------------------------------------- 1 | """Kitty Theme Changer : Helper script to change Kitty Terminal Themes.""" 2 | -------------------------------------------------------------------------------- /l.txt: -------------------------------------------------------------------------------- 1 | 3024_Day 2 | AtomOneLight 3 | ayu_light 4 | Belafonte_Day 5 | C64 6 | CLRS 7 | Grass 8 | gruvbox_light 9 | Man_Page 10 | Novel 11 | Ocean 12 | PencilLight 13 | Piatto_Light 14 | Solarized_Light 15 | Spring 16 | Tango_Light 17 | Tomorrow 18 | -------------------------------------------------------------------------------- /try.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | profile_list=$1 4 | if [[ ! -f ${profile_list} ]]; then 5 | echo "Please provide profiles file"; 6 | exit 1; 7 | fi 8 | 9 | for profile in $(cat ${profile_list}); do 10 | echo $profile; 11 | kitty-theme -T $profile; 12 | read -p "enter to continue"; 13 | done 14 | -------------------------------------------------------------------------------- /d.txt: -------------------------------------------------------------------------------- 1 | 3024_Night 2 | AdventureTime 3 | Afterglow 4 | AlienBlood 5 | Alucard 6 | Apprentice 7 | Argonaut 8 | Arthur 9 | AtelierSulphurpool 10 | Atom 11 | ayu 12 | ayu_mirage 13 | Batman 14 | Belafonte_Night 15 | BirdsOfParadise 16 | Blazer 17 | Borland 18 | Bright_Lights 19 | Broadcast 20 | Brogrammer 21 | Chalk 22 | Chalkboard 23 | Ciapre 24 | Cobalt2 25 | Cobalt_Neon 26 | CrayonPonyFish 27 | Dark_Pastel 28 | Darkside 29 | Desert 30 | DimmedMonokai 31 | DotGov 32 | Dracula 33 | Dumbledore 34 | Duotone_Dark 35 | Earthsong 36 | Elemental 37 | ENCOM 38 | Espresso 39 | Espresso_Libre 40 | Fideloper 41 | FishTank 42 | Flat 43 | Flatland 44 | Floraverse 45 | FrontEndDelight 46 | FunForrest 47 | Galaxy 48 | Github 49 | Glacier 50 | GoaBase 51 | Grape 52 | gruvbox_dark 53 | Hardcore 54 | Harper 55 | Highway 56 | Hipster_Green 57 | Homebrew 58 | Hurtado 59 | Hybrid 60 | IC_Green_PPL 61 | IC_Orange_PPL 62 | idleToes 63 | IR_Black 64 | Jackie_Brown 65 | Japanesque 66 | Jellybeans 67 | JetBrains_Darcula 68 | Kibble 69 | Later_This_Evening 70 | Lavandula 71 | LiquidCarbon 72 | LiquidCarbonTransparent 73 | LiquidCarbonTransparentInverse 74 | Material 75 | MaterialDark 76 | Mathias 77 | Medallion 78 | Misterioso 79 | Molokai 80 | MonaLisa 81 | Monokai 82 | Monokai_Classic 83 | Monokai_Pro 84 | Monokai_Pro_(Filter_Machine) 85 | Monokai_Pro_(Filter_Octagon) 86 | Monokai_Pro_(Filter_Ristretto) 87 | Monokai_Pro_(Filter_Spectrum) 88 | Monokai_Soda 89 | N0tch2k 90 | Neopolitan 91 | Neutron 92 | NightLion_v1 93 | NightLion_v2 94 | Nova 95 | Obsidian 96 | OceanicMaterial 97 | Ollie 98 | OneDark 99 | Parasio_Dark 100 | PaulMillr 101 | PencilDark 102 | Pnevma 103 | Pro 104 | Red_Alert 105 | Red_Sands 106 | Relaxed_Afterglow 107 | Renault_Style 108 | Renault_Style_Light 109 | Rippedcasts 110 | Royal 111 | Seafoam_Pastel 112 | SeaShells 113 | Seti 114 | Shaman 115 | Slate 116 | Smyck 117 | snazzy 118 | SoftServer 119 | Solarized_Darcula 120 | Solarized_Dark 121 | Solarized_Dark_-_Patched 122 | Solarized_Dark_Higher_Contrast 123 | Source_Code_X 124 | Spacedust 125 | SpaceGray 126 | SpaceGray_Eighties 127 | SpaceGray_Eighties_Dull 128 | Spiderman 129 | Square 130 | Sundried 131 | Symfonic 132 | Tango_Dark 133 | Teerb 134 | Thayer_Bright 135 | The_Hulk 136 | Tomorrow_Night 137 | Tomorrow_Night_Blue 138 | Tomorrow_Night_Bright 139 | Tomorrow_Night_Eighties 140 | ToyChest 141 | Treehouse 142 | Twilight 143 | Ubuntu 144 | Urple 145 | Vaughn 146 | VibrantInk 147 | WarmNeon 148 | Wez 149 | WildCherry 150 | Wombat 151 | Wryan 152 | Zenburn 153 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Kitty Theme Changer 3 | =================== 4 | 5 | :author: Curtis Sand 6 | :author_email: curtissand@gmail.com 7 | :repository: https://github.com/fretboardfreak/kitty-theme-changer.git 8 | 9 | Change Kitty Terminal Themes Easily! 10 | 11 | Kitty Theme Changer provides simple CLI script that will help you test out and 12 | change theme configuration files for the Kitty Terminal Emulator. 13 | 14 | ---- 15 | 16 | Table of Contents: 17 | 18 | #. `Installation`_ 19 | 20 | #. `Configuration`_ 21 | 22 | #. `Tips and Tricks`_ 23 | 24 | #. `First Run`_ 25 | 26 | #. `Kitty Configuration Tips`_ 27 | 28 | ---- 29 | 30 | For more information on the terminal visit: https://github.com/kovidgoyal/kitty 31 | 32 | The Kitty Theme Changer script has been tested with the collection of themes 33 | found in the kitty-themes repository: https://github.com/dexpota/kitty-themes 34 | 35 | Installation 36 | ============ 37 | 38 | The recommended method for installing the Kitty Theme Changer is to use ``pip`` 39 | to install the package into your python environment. This can be done safely 40 | inside a python virtual environment or using the ``--user`` flag on ``pip`` 41 | install to install the script in your home directory. 42 | 43 | Home directory install:: 44 | 45 | pip install --user git+git://github.com/fretboardfreak/kitty-theme-changer.git@master 46 | 47 | Note that you *can* install the Kitty Theme Changer into the system's core 48 | python installation but I feel that python package management is a bit cleaner 49 | if user installed packages are kept out of the system directories. 50 | 51 | Inside a Python Virtual Environment:: 52 | 53 | python -m venv my_python_env 54 | my_python_env/bin/pip install git+git://github.com/fretboardfreak/kitty-theme-changer.git@master 55 | 56 | 57 | .. note:: The above URI will install the latest development version of the 58 | Kitty Theme Changer. To instead install one of the tagged releases 59 | replace ``@master`` with ``@RELEASE`` where the text RELEASE is the 60 | tagged version (e.g. ``...kitty-theme-changer.git@0.1``). 61 | 62 | As an alternative you could clone this repository and execute the script 63 | directly. This method would not leverage the setuptools entrypoint and would 64 | depend on you ensuring that the script is available in your PATH variable 65 | yourself. 66 | 67 | Configuration 68 | ============= 69 | 70 | 1. First step in configuring the Kitty Theme Changer is to download a set of 71 | themes for kitty (recommendation: https://github.com/dexpota/kitty-themes). 72 | 73 | 2. Second step is to configure kitty to include a theme config 74 | file. To do this add the line ``include ./theme.conf`` in your 75 | kitty.conf file. 76 | 77 | 3. Third and final step is to create the Kitty Theme Changer 78 | config file which will point to the correct paths for the 79 | themes you've collected. 80 | 81 | The Kitty theme changer uses a simple python module as 82 | a configuration file. By default this is '~/.kittythemechanger.py'. 83 | The list of required variables and their types are: 84 | 85 | - theme_dir (pathlib.Path): Directory of Kitty theme.conf files. 86 | 87 | - conf_dir (pathlib.Path): Directory Kitty looks in for theme.conf 88 | 89 | - theme_link (pathlib.Path): Symlink file Kitty loads from kitty.conf 90 | 91 | - light_theme_link (pathlib.Path): Symlink to a 'light' theme config file. 92 | 93 | - dark_theme_link (pathlib.Path): Symlink to a 'dark' theme config file. 94 | 95 | - socket (str): a Kitty compatible socket string for the '--listen-on' flag. See 'man kitty'. 96 | 97 | An example .kittythemechanger.py file is shown below:: 98 | 99 | '''A config module for the Kitty Theme Changer Tool.''' 100 | from pathlib import Path 101 | from os import getpid 102 | from psutil import process_iter 103 | 104 | theme_dir = Path('~/kitty-themes/themes').expanduser() 105 | conf_dir = Path('~/.config/kitty').expanduser() 106 | theme_link = conf_dir.joinpath('theme.conf') 107 | light_theme_link = conf_dir.joinpath('light-theme.conf') 108 | dark_theme_link = conf_dir.joinpath('dark-theme.conf') 109 | 110 | def kitty_pid(): 111 | ps = {x.pid: x for x in psutil.process_iter(['name', 'pid', 'ppid'])} 112 | cp = ps[getpid()] 113 | while cp.name() != 'kitty': 114 | cp = cp.parent() 115 | return cp.pid 116 | 117 | socket = 'unix:/tmp/kitty-socket-{}'.format(kitty_pid()) 118 | 119 | Tips and Tricks 120 | =============== 121 | 122 | First Run 123 | --------- 124 | 125 | On the first run the Kitty Theme Changer script will randomly choose a theme to 126 | set as both the light and dark theme. It does this to create the 3 symlinks in 127 | the kitty configuration directory pointed to by the config file. One pointing 128 | to a light theme, one pointing to a dark theme and a third that ties the kitty 129 | configuration with one of the light or dark links. (kitty.conf -> theme.conf -> 130 | light-theme.conf -> actual theme file). You can prevent a random theme from 131 | being chosen by creating the light and dark symlink files manually (the file 132 | names are set in your Kitty Theme Changer configuration file.) or you can 133 | simply set your themes to your preference after the first run. 134 | 135 | Kitty Configuration Tips 136 | ------------------------ 137 | 138 | - Kitty Terminal Emulator Site: https://sw.kovidgoyal.net/kitty/index.html 139 | - Kitty Terminal Configuration Docs: https://sw.kovidgoyal.net/kitty/conf.html 140 | 141 | The main features of the Kitty Theme Changer tool - listing themes, setting a 142 | dark or light theme, toggling between configured themes - can be used without 143 | any additional tweaks to the Kitty Terminal config. 144 | 145 | However, the "--test" and "--live" features require some settings in order to 146 | work correctly. 147 | 148 | - Kitty Remote Control: The remote control feature must be turned on. Either 149 | with a value of "yes" or a value of "socket-only" to limit remote control 150 | commands to only use the socket specified in the "--listen-on" flag when 151 | running kitty. :: 152 | 153 | allow_remote_control yes 154 | 155 | - Kitty Socket: Any launchers or aliases that you use to start kitty should 156 | include a "--listen-on" option. The socket string that you choose for the 157 | "--listen-on" flag should match the socket string in your Kitty Theme Changer 158 | configuration file. You can also use "listen_on unix:/tmp/kitty-socket" in kitty.conf 159 | 160 | - Single Instance/Instance Groups: For the "--live" feature to change the color 161 | theme for all running windows it is useful to run kitty with the 162 | ``--single-instance`` option turned on. 163 | 164 | If you want the Kitty Theme Changer to modify only a set of kitty windows 165 | then you can make all those windows part of the same Kitty instance using the 166 | ``--instance-group GROUPNAME`` flag. 167 | 168 | 169 | .. EOF README 170 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """The setuptools deployment script.""" 2 | # Copyright 2019 Curtis Sand 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | import os 16 | import subprocess 17 | import distutils.log 18 | import unittest 19 | from distutils.cmd import Command 20 | from setuptools import setup 21 | from setuptools import find_packages 22 | from setuptools.command.test import test 23 | from pathlib import Path 24 | 25 | 26 | def install_requires(): 27 | """Read the requirements for installing the project.""" 28 | path = Path('requirements.txt') 29 | if not path.exists(): 30 | return "" 31 | with open(path, 'r') as reqf: 32 | return reqf.read().splitlines() 33 | 34 | 35 | def tests_require(): 36 | """Read the requirements for testing the project.""" 37 | path = Path('dev_requirements.txt') 38 | if not path.exists(): 39 | return "" 40 | with open(path, 'r') as dev_req: 41 | return dev_req.read().splitlines() 42 | 43 | 44 | def readme(): 45 | """Read the readme file for the long description.""" 46 | with open('README.rst', 'r') as readmef: 47 | return readmef.read() 48 | 49 | 50 | class SetupCommand(Command): 51 | """Base command for distutils in the project.""" 52 | 53 | def initialize_options(self): 54 | """Set defaults for options.""" 55 | pass 56 | 57 | def finalize_options(self): 58 | """Post-process options.""" 59 | pass 60 | 61 | def run(self): 62 | """Run the custom setup command.""" 63 | pass 64 | 65 | def _run_command(self, command): 66 | """Execute the command.""" 67 | self.announce('running command: %s' % str(command), 68 | level=distutils.log.INFO) 69 | try: 70 | subprocess.check_call(command) 71 | except subprocess.CalledProcessError as exc: 72 | self.announce('Non-zero returncode "%s": %s' % 73 | (exc.returncode, exc.cmd), 74 | level=distutils.log.INFO) 75 | return 1 76 | 77 | @property 78 | def setup_path(self): 79 | """Get the path to the setup.py script.""" 80 | return os.path.dirname(__file__) 81 | 82 | @property 83 | def software_package(self): 84 | """Get the path to the software parkace.""" 85 | return os.path.join(self.setup_path, 'src/kittytheme') 86 | 87 | @property 88 | def test_paths(self): 89 | """Get the list of paths for python files that should be tested.""" 90 | return [self.software_package, __file__] 91 | 92 | 93 | class DevelopmentCommand(SetupCommand): 94 | """Base command for project development and testing.""" 95 | 96 | pass 97 | 98 | 99 | class PylintCommand(DevelopmentCommand): 100 | """A custom command to run Pylint on all Python source files.""" 101 | 102 | description = "Run pylint on the python sources." 103 | user_options = [ 104 | ('pylint-rcfile=', None, 'path to Pylint config file.') 105 | ] 106 | 107 | def __init__(self, *args, **kwargs): 108 | """Initialize the class.""" 109 | super().__init__(*args, **kwargs) 110 | self.pylint_rcfile = "" 111 | 112 | def initialize_options(self): 113 | """Set defaults for options.""" 114 | self.pylint_rcfile = '' 115 | 116 | def finalize_options(self): 117 | """Post-process options.""" 118 | if self.pylint_rcfile: 119 | assert os.path.exists(self.pylint_rcfile), ( 120 | 'Cannot find config file "%s"' % self.pylint_rcfile) 121 | 122 | def run(self): 123 | """Prepare and run the Pylint command.""" 124 | super().run() 125 | command = ['pylint'] 126 | rcfile = self.pylint_rcfile 127 | if not rcfile and os.path.exists('pylintrc'): 128 | rcfile = 'pylintrc' 129 | if rcfile: 130 | command.append('--rcfile=%s' % rcfile) 131 | command.extend([test_path for test_path in self.test_paths 132 | if test_path != __file__]) 133 | self._run_command(command) 134 | 135 | 136 | class PycodestyleCommand(DevelopmentCommand): 137 | """A custom command to run pycodestyle on all python source files.""" 138 | 139 | description = "Run pycodestyle on all python sources." 140 | user_options = [] 141 | 142 | def run(self): 143 | """Run the pycodestyle tool.""" 144 | super().run() 145 | self._run_command(['pycodestyle', '--statistics', '--verbose'] + 146 | self.test_paths) 147 | 148 | 149 | class Pep257Command(DevelopmentCommand): 150 | """A custom command to run pep257 on all Python source files.""" 151 | 152 | description = "Run pep257 on all python sources." 153 | user_options = [] 154 | 155 | def run(self): 156 | """Run the pep257 checker.""" 157 | super().run() 158 | self._run_command(['pep257', '--count', '--verbose'] + 159 | self.test_paths) 160 | 161 | 162 | class UnitTestCommand(DevelopmentCommand): 163 | """A custom command for running unit tests.""" 164 | 165 | description = "Run the unittests." 166 | user_options = [] 167 | 168 | def run(self): 169 | """Run the unittests.""" 170 | super().run() 171 | # load test suite 172 | test_loader = unittest.defaultTestLoader 173 | setup_dir, _ = os.path.split(__file__) 174 | test_suite = test_loader.discover(os.path.join(setup_dir, 'src/tests')) 175 | 176 | # run the tests 177 | test_runner = unittest.TextTestRunner(verbosity=3) 178 | test_runner.run(test_suite) 179 | 180 | 181 | class DevInstallCommand(SetupCommand): 182 | """A custom command to install the development requirements.""" 183 | 184 | description = "Install the development requirements." 185 | user_options = [] 186 | 187 | def run(self): 188 | """Install the development requirements.""" 189 | if self.distribution.install_requires: 190 | self.distribution.fetch_build_eggs( 191 | self.distribution.install_requires) 192 | if self.distribution.tests_require: 193 | self.distribution.fetch_build_eggs( 194 | self.distribution.tests_require) 195 | 196 | 197 | class Test(test): 198 | """Combine unittest, pycodestyle and pylint checks all into one command.""" 199 | 200 | description = "Run pycodestyle, pylint and unittest commands together." 201 | user_options = [('interactive', 'i', 'Enable interactive pauses.')] 202 | 203 | def __init__(self, *args, **kwargs): 204 | """Add instance variables for the Test subclass.""" 205 | self.interactive = None 206 | super().__init__(*args, **kwargs) 207 | 208 | def _interactive_pause(self): 209 | """Pause for the use to press enter. So interactive.""" 210 | if self.interactive: 211 | input('enter to continue...') 212 | 213 | def run(self): 214 | """Run all tests and checkers for project.""" 215 | # Skip parent method to avoid reinstalling packages 216 | # super().run() 217 | self.run_command('pycodestyle') 218 | self._interactive_pause() 219 | self.run_command('pep257') 220 | self._interactive_pause() 221 | self.run_command('pylint') 222 | self._interactive_pause() 223 | # disabled until there are unit tests to execute 224 | # self.run_command('unittest') 225 | # self._interactive_pause() 226 | 227 | 228 | setup( 229 | name='kitty-theme-changer', 230 | version='0.6', 231 | description='A CLI tool changing the Kitty Terminal Theme configuration.', 232 | long_description=readme(), 233 | url='https://github.com/fretboardfreak/kitty-theme-changer.git', 234 | author='Curtis Sand', 235 | author_email='curtissand@gmail.com', 236 | license='Apache', 237 | package_dir={'': 'src'}, 238 | packages=find_packages('src'), 239 | entry_points={ 240 | 'console_scripts': ['kitty-theme=kittytheme.kittytheme:main'] 241 | }, 242 | use_2to3=False, 243 | install_requires=install_requires(), 244 | zip_safe=True, 245 | include_package_data=True, 246 | test_suite='kittytheme.tests', 247 | tests_require=tests_require(), 248 | keywords='kitty terminal themes config', 249 | classifiers=[ 250 | 'Environment :: Console', 251 | 'Intended Audience :: Developers', 252 | 'Intended Audience :: System Administrators', 253 | 'License :: OSI Approved :: Apache Software License', 254 | 'Natural Language :: English', 255 | 'Programming Language :: Python :: 3 :: Only', 256 | 'Topic :: Terminals :: Terminal Emulators/X Terminals', 257 | ], 258 | cmdclass={ 259 | 'pylint': PylintCommand, 260 | 'pycodestyle': PycodestyleCommand, 261 | 'unittest': UnitTestCommand, 262 | 'pep257': Pep257Command, 263 | 'test': Test, 264 | 'dev': DevInstallCommand}) 265 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Apache License 3 | ============== 4 | 5 | Version 2.0, January 2004 6 | http://www.apache.org/licenses/ 7 | 8 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 9 | 10 | 1. Definitions. 11 | 12 | "License" shall mean the terms and conditions for use, reproduction, 13 | and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by 16 | the copyright owner that is granting the License. 17 | 18 | "Legal Entity" shall mean the union of the acting entity and all 19 | other entities that control, are controlled by, or are under common 20 | control with that entity. For the purposes of this definition, 21 | "control" means (i) the power, direct or indirect, to cause the 22 | direction or management of such entity, whether by contract or 23 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 24 | outstanding shares, or (iii) beneficial ownership of such entity. 25 | 26 | "You" (or "Your") shall mean an individual or Legal Entity 27 | exercising permissions granted by this License. 28 | 29 | "Source" form shall mean the preferred form for making modifications, 30 | including but not limited to software source code, documentation 31 | source, and configuration files. 32 | 33 | "Object" form shall mean any form resulting from mechanical 34 | transformation or translation of a Source form, including but 35 | not limited to compiled object code, generated documentation, 36 | and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or 39 | Object form, made available under the License, as indicated by a 40 | copyright notice that is included in or attached to the work 41 | (an example is provided in the Appendix below). 42 | 43 | "Derivative Works" shall mean any work, whether in Source or Object 44 | form, that is based on (or derived from) the Work and for which the 45 | editorial revisions, annotations, elaborations, or other modifications 46 | represent, as a whole, an original work of authorship. For the purposes 47 | of this License, Derivative Works shall not include works that remain 48 | separable from, or merely link (or bind by name) to the interfaces of, 49 | the Work and Derivative Works thereof. 50 | 51 | "Contribution" shall mean any work of authorship, including 52 | the original version of the Work and any modifications or additions 53 | to that Work or Derivative Works thereof, that is intentionally 54 | submitted to Licensor for inclusion in the Work by the copyright owner 55 | or by an individual or Legal Entity authorized to submit on behalf of 56 | the copyright owner. For the purposes of this definition, "submitted" 57 | means any form of electronic, verbal, or written communication sent 58 | to the Licensor or its representatives, including but not limited to 59 | communication on electronic mailing lists, source code control systems, 60 | and issue tracking systems that are managed by, or on behalf of, the 61 | Licensor for the purpose of discussing and improving the Work, but 62 | excluding communication that is conspicuously marked or otherwise 63 | designated in writing by the copyright owner as "Not a Contribution." 64 | 65 | "Contributor" shall mean Licensor and any individual or Legal Entity 66 | on behalf of whom a Contribution has been received by Licensor and 67 | subsequently incorporated within the Work. 68 | 69 | 2. Grant of Copyright License. Subject to the terms and conditions of 70 | this License, each Contributor hereby grants to You a perpetual, 71 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 72 | copyright license to reproduce, prepare Derivative Works of, 73 | publicly display, publicly perform, sublicense, and distribute the 74 | Work and such Derivative Works in Source or Object form. 75 | 76 | 3. Grant of Patent License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | (except as stated in this section) patent license to make, have made, 80 | use, offer to sell, sell, import, and otherwise transfer the Work, 81 | where such license applies only to those patent claims licensable 82 | by such Contributor that are necessarily infringed by their 83 | Contribution(s) alone or by combination of their Contribution(s) 84 | with the Work to which such Contribution(s) was submitted. If You 85 | institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work 87 | or a Contribution incorporated within the Work constitutes direct 88 | or contributory patent infringement, then any patent licenses 89 | granted to You under this License for that Work shall terminate 90 | as of the date such litigation is filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the 93 | Work or Derivative Works thereof in any medium, with or without 94 | modifications, and in Source or Object form, provided that You 95 | meet the following conditions: 96 | 97 | (a) You must give any other recipients of the Work or 98 | Derivative Works a copy of this License; and 99 | 100 | (b) You must cause any modified files to carry prominent notices 101 | stating that You changed the files; and 102 | 103 | (c) You must retain, in the Source form of any Derivative Works 104 | that You distribute, all copyright, patent, trademark, and 105 | attribution notices from the Source form of the Work, 106 | excluding those notices that do not pertain to any part of 107 | the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" text file as part of its 110 | distribution, then any Derivative Works that You distribute must 111 | include a readable copy of the attribution notices contained 112 | within such NOTICE file, excluding those notices that do not 113 | pertain to any part of the Derivative Works, in at least one 114 | of the following places: within a NOTICE text file distributed 115 | as part of the Derivative Works; within the Source form or 116 | documentation, if provided along with the Derivative Works; or, 117 | within a display generated by the Derivative Works, if and 118 | wherever such third-party notices normally appear. The contents 119 | of the NOTICE file are for informational purposes only and 120 | do not modify the License. You may add Your own attribution 121 | notices within Derivative Works that You distribute, alongside 122 | or as an addendum to the NOTICE text from the Work, provided 123 | that such additional attribution notices cannot be construed 124 | as modifying the License. 125 | 126 | You may add Your own copyright statement to Your modifications and 127 | may provide additional or different license terms and conditions 128 | for use, reproduction, or distribution of Your modifications, or 129 | for any such Derivative Works as a whole, provided Your use, 130 | reproduction, and distribution of the Work otherwise complies with 131 | the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, 134 | any Contribution intentionally submitted for inclusion in the Work 135 | by You to the Licensor shall be under the terms and conditions of 136 | this License, without any additional terms or conditions. 137 | Notwithstanding the above, nothing herein shall supersede or modify 138 | the terms of any separate license agreement you may have executed 139 | with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. This License does not grant permission to use the trade 142 | names, trademarks, service marks, or product names of the Licensor, 143 | except as required for reasonable and customary use in describing the 144 | origin of the Work and reproducing the content of the NOTICE file. 145 | 146 | 7. Disclaimer of Warranty. Unless required by applicable law or 147 | agreed to in writing, Licensor provides the Work (and each 148 | Contributor provides its Contributions) on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 150 | implied, including, without limitation, any warranties or conditions 151 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 152 | PARTICULAR PURPOSE. You are solely responsible for determining the 153 | appropriateness of using or redistributing the Work and assume any 154 | risks associated with Your exercise of permissions under this License. 155 | 156 | 8. Limitation of Liability. In no event and under no legal theory, 157 | whether in tort (including negligence), contract, or otherwise, 158 | unless required by applicable law (such as deliberate and grossly 159 | negligent acts) or agreed to in writing, shall any Contributor be 160 | liable to You for damages, including any direct, indirect, special, 161 | incidental, or consequential damages of any character arising as a 162 | result of this License or out of the use or inability to use the 163 | Work (including but not limited to damages for loss of goodwill, 164 | work stoppage, computer failure or malfunction, or any and all 165 | other commercial damages or losses), even if such Contributor 166 | has been advised of the possibility of such damages. 167 | 168 | 9. Accepting Warranty or Additional Liability. While redistributing 169 | the Work or Derivative Works thereof, You may choose to offer, 170 | and charge a fee for, acceptance of support, warranty, indemnity, 171 | or other liability obligations and/or rights consistent with this 172 | License. However, in accepting such obligations, You may act only 173 | on Your own behalf and on Your sole responsibility, not on behalf 174 | of any other Contributor, and only if You agree to indemnify, 175 | defend, and hold each Contributor harmless for any liability 176 | incurred by, or claims asserted against, such Contributor by reason 177 | of your accepting any such warranty or additional liability. 178 | 179 | END OF TERMS AND CONDITIONS 180 | 181 | APPENDIX: How to apply the Apache License to your work. 182 | 183 | To apply the Apache License to your work, attach the following 184 | boilerplate notice, with the fields enclosed by brackets "{}" 185 | replaced with your own identifying information. (Don't include 186 | the brackets!) The text should be enclosed in the appropriate 187 | comment syntax for the file format. We also recommend that a 188 | file or class name and description of purpose be included on the 189 | same "printed page" as the copyright notice for easier 190 | identification within third-party archives. 191 | 192 | Copyright {yyyy} {name of copyright owner} 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | -------------------------------------------------------------------------------- /src/kittytheme/kittytheme.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Helper for setting and switching Kitty Terminal Themes.""" 3 | 4 | # Copyright 2020 Curtis Sand 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | import argparse 19 | import sys 20 | import random 21 | 22 | from importlib.util import spec_from_file_location 23 | from importlib.util import module_from_spec 24 | from pathlib import Path 25 | from subprocess import call 26 | 27 | 28 | VERSION = "0.6" 29 | VERBOSE = False 30 | DEBUG = False 31 | DEBUG_OUTPUT_PREFIX = 'debug: ' 32 | 33 | DEFAULT_CONFIG = '~/.kittythemechanger.py' 34 | 35 | 36 | def main(): 37 | """Main script logic.""" 38 | # parse the command line arguments 39 | parser = build_argument_parser() 40 | args = parser.parse_args() 41 | dprint(f'parsed args: {args}') 42 | 43 | if args.test and args.live: 44 | parser.error('The options "--live" and "--test" cannot be ' 45 | 'used together.') 46 | 47 | # if --help-config is used, print config help then exit 48 | if args.config_help: 49 | print_config_help() 50 | sys.exit(0) 51 | 52 | # load config module 53 | spec = spec_from_file_location(args.config.stem, args.config.as_posix()) 54 | config = module_from_spec(spec) 55 | spec.loader.exec_module(config) 56 | check_config(config) 57 | 58 | check_symlinks(config) 59 | 60 | # dispatch selected actions 61 | do_default = True 62 | if args.list: 63 | do_default = False 64 | dprint('calling action: list') 65 | Actions.list(args, config) 66 | 67 | if args.set_dark: 68 | do_default = False 69 | dprint('calling action: set_dark') 70 | Actions.set_dark(args, config) 71 | if args.set_light: 72 | do_default = False 73 | dprint('calling action: set_light') 74 | Actions.set_light(args, config) 75 | 76 | if args.show: 77 | do_default = False 78 | dprint('calling action: show') 79 | Actions.show(args, config) 80 | 81 | if args.test: 82 | do_default = False 83 | dprint('calling action: test') 84 | Actions.test(args, config) 85 | 86 | if args.toggle: 87 | do_default = False 88 | dprint('calling action: toggle') 89 | Actions.toggle(args, config) 90 | 91 | if args.live: 92 | do_default = False 93 | dprint('calling action: live') 94 | Actions.live(args, config) 95 | 96 | if do_default: # take default action 97 | dprint('no action provided: calling default action') 98 | Actions.show(args, config) 99 | 100 | return 0 101 | 102 | 103 | def build_argument_parser(): 104 | """Parse the command line arguments.""" 105 | parser = argparse.ArgumentParser(description=__doc__) 106 | parser.add_argument( 107 | '--version', help='Print the version and exit.', action='version', 108 | version='%(prog)s {}'.format(VERSION)) 109 | DebugAction.add_parser_argument(parser) 110 | VerboseAction.add_parser_argument(parser) 111 | parser.add_argument( 112 | '-c', '--config', type=existing_file, default=DEFAULT_CONFIG, 113 | help=('The configuration for the Kitty Theme Changer. See ' 114 | '"--help-config" for more info. Default: {}'.format( 115 | DEFAULT_CONFIG))) 116 | parser.add_argument( 117 | '-l', '--list', dest='list', action="store_true", 118 | default=False, help="List available themes.") 119 | parser.add_argument( 120 | '-s', '--show', dest='show', action="store_true", 121 | default=False, help="Show the current configuration.") 122 | parser.add_argument( 123 | '-T', '--test', dest='test', metavar="TEST_THEME", 124 | default='', help="Test a new theme in the current kitty session.") 125 | parser.add_argument( 126 | '-t', '--toggle', dest='toggle', action="store_true", 127 | default=False, help="Toggle between the dark and light themes.") 128 | parser.add_argument( 129 | '--setd', dest='set_dark', metavar='DARK_THEME', 130 | default='', help='Set the dark theme.') 131 | parser.add_argument( 132 | '--setl', dest='set_light', metavar='LIGHT_THEME', 133 | default='', help='Set the light theme.') 134 | parser.add_argument( 135 | '-L', '--live', dest='live', action='store_true', default=False, 136 | help='Update existing kitty sessions to use the config.') 137 | parser.add_argument( 138 | '--help-config', action='store_true', dest='config_help', 139 | default=False, 140 | help="Print info on how to configure Kitty Theme Changer.") 141 | 142 | return parser 143 | 144 | 145 | def existing_file(input_text): 146 | """Ensure the input text is an existing file path.""" 147 | dprint('input config file: {}'.format(input_text)) 148 | filepath = Path(input_text).expanduser() 149 | if not filepath.exists(): 150 | raise argparse.ArgumentTypeError( 151 | 'The configuration file must exist. Use --help-config to see ' 152 | 'how to configure the Kitty Theme Changer.') 153 | return filepath 154 | 155 | 156 | def check_config(config): 157 | """Check that the config module has the required attributes.""" 158 | required_config_attributes = [ 159 | ('theme_dir', Path), 160 | ('conf_dir', Path), 161 | ('theme_link', Path), 162 | ('light_theme_link', Path), 163 | ('dark_theme_link', Path), 164 | ('socket', str) 165 | ] 166 | dprint('checking config: {}'.format(dir(config))) 167 | dprint(config.__file__) 168 | for attribute, type in required_config_attributes: 169 | check_status = True 170 | try: 171 | check_status = isinstance(getattr(config, attribute), type) 172 | except AttributeError: 173 | check_status = False 174 | if not check_status: 175 | raise Exception( 176 | 'The config module is missing a variable named ' 177 | '{} of type {}'.format(attribute, type)) 178 | 179 | 180 | def get_random_theme_config(theme_dir): 181 | """Randomly choose a theme file from the theme dir.""" 182 | try: 183 | ret_val = random.choice([i for i in theme_dir.glob('*conf')]) 184 | except IndexError: 185 | print('Error: cannot find any theme files ending in "conf" in the ' 186 | f'directory "{theme_dir}". Please follow the instructions ' 187 | 'given with "--help-config" to ensure the theme changer is ' 188 | 'configured correctly.') 189 | sys.exit(1) 190 | return ret_val 191 | 192 | 193 | def check_symlinks(config): 194 | """Check that the three theme symlinks exist or create them.""" 195 | dprint('Checking that the theme symlinks exist.') 196 | if not config.dark_theme_link.exists(): 197 | dprint('dark theme link does not exist, creating it.') 198 | config.dark_theme_link.symlink_to( 199 | get_random_theme_config(config.theme_dir)) 200 | if not config.light_theme_link.exists(): 201 | dprint('light theme link does not exist, creating it.') 202 | config.light_theme_link.symlink_to( 203 | get_random_theme_config(config.theme_dir)) 204 | if not config.theme_link.exists(): 205 | dprint('main theme link does not exist, creating it.') 206 | config.theme_link.symlink_to(config.dark_theme_link) 207 | 208 | 209 | def list_themes(args, config): 210 | """List the available themes.""" 211 | print('Available Kitty Themes:') 212 | dprint('Looking for themes in: {}'.format(config.theme_dir)) 213 | for theme in sorted(config.theme_dir.glob('*conf'), 214 | key=lambda s: s.stem.lower()): 215 | print(' {}'.format(theme.stem)) 216 | 217 | 218 | def show_config(args, config): 219 | """Show the current theme configuration.""" 220 | dprint('looking at symlink target of {}'.format(config.theme_link)) 221 | theme = config.theme_link.resolve().stem 222 | light_theme = config.light_theme_link.resolve().stem 223 | dark_theme = config.dark_theme_link.resolve().stem 224 | dark_active = '***' if dark_theme == theme else '' 225 | light_active = '***' if light_theme == theme else '' 226 | print('{}dark theme: {}{}'.format( 227 | dark_active, dark_theme, dark_active)) 228 | print('{}light theme: {}{}'.format( 229 | light_active, light_theme, light_active)) 230 | 231 | 232 | def get_theme_file(theme_name, config): 233 | """From a theme name, get the filename of an existing file. 234 | 235 | Return None if the file does not exist. 236 | """ 237 | theme_file = None 238 | for fname in config.theme_dir.glob('*.conf'): 239 | # Allow case insensitive theme name inputs 240 | if fname.stem.lower() == theme_name.lower(): 241 | theme_file = fname 242 | 243 | if not theme_file or not theme_file.exists(): 244 | print(f'Provided theme, "{theme_name}" does not exist. Use the ' 245 | '--list option to see available themes.') 246 | sys.exit(1) 247 | dprint('theme_file: {}'.format(theme_file)) 248 | return theme_file 249 | 250 | 251 | def test_theme(args, config): 252 | """Test the given theme in the current kitty session.""" 253 | theme_file = get_theme_file(args.test, config) 254 | vprint('Changing theme of current kitty window to: {}'.format( 255 | theme_file.name)) 256 | cmd = ['kitty', '@', '--to={}'.format(config.socket), 'set-colors', 257 | theme_file.as_posix()] 258 | dprint('executing: {}'.format(' '.join(cmd))) 259 | call(cmd) 260 | 261 | 262 | def toggle_themes(args, config): 263 | """Toggle the themes between light and dark.""" 264 | vprint('Toggling configured theme between light and dark.') 265 | if config.light_theme_link.resolve() == config.theme_link.resolve(): 266 | # light theme: reconfig to dark theme 267 | dprint('light theme configured. enabling dark theme.') 268 | config.theme_link.unlink() 269 | config.theme_link.symlink_to(config.dark_theme_link) 270 | elif config.dark_theme_link.resolve() == config.theme_link.resolve(): 271 | # dark theme: reconfig to light theme 272 | dprint('dark theme configured. enabling light theme.') 273 | config.theme_link.unlink() 274 | config.theme_link.symlink_to(config.light_theme_link) 275 | else: 276 | print('Configured theme.conf does not match configured ' 277 | 'light-theme.conf or dark-theme.conf. Setting theme to dark.') 278 | config.theme_link.unlink() 279 | config.theme_link.symlink_to(config.dark_theme_link) 280 | 281 | 282 | def set_dark_theme(args, config): 283 | """Set the default theme to the configured dark theme.""" 284 | theme_file = get_theme_file(args.set_dark, config) 285 | dprint('existing dark theme is: {}'.format( 286 | config.dark_theme_link.resolve())) 287 | vprint('Changing configured dark theme to {}'.format(theme_file.name)) 288 | config.dark_theme_link.unlink() 289 | config.dark_theme_link.symlink_to(theme_file) 290 | 291 | 292 | def set_light_theme(args, config): 293 | """Set the default theme to the configured light theme.""" 294 | theme_file = get_theme_file(args.set_light, config) 295 | dprint('existing light theme is: {}'.format( 296 | config.light_theme_link.resolve())) 297 | vprint('Changing configured light theme to {}'.format(theme_file.name)) 298 | config.light_theme_link.unlink() 299 | config.light_theme_link.symlink_to(theme_file) 300 | 301 | 302 | def make_theme_live(args, config): 303 | """Update all existing kitty sessions to use the configured theme.""" 304 | vprint('Changing theme of all running kitty windows to: {}'.format( 305 | config.theme_link.resolve().name)) 306 | cmd = ['kitty', '@', '--to={}'.format(config.socket), 'set-colors', 307 | '--all', config.theme_link.as_posix()] 308 | dprint('executing: {}'.format(' '.join(cmd))) 309 | call(cmd) 310 | 311 | 312 | def print_config_help(): 313 | """Print a help message about configuring Kitty Theme Changer.""" 314 | msg = "Configuring Kitty Theme Changer\n\n" 315 | msg += "1. First step in configuring the Kitty Theme Changer is to download a set of\n" 316 | msg += " themes for kitty (recommendation: https://github.com/dexpota/kitty-themes).\n\n" 317 | msg += "2. Second step is to configure kitty to include a theme config\n" 318 | msg += " file. To do this add the line ``include ./theme.conf`` in your\n" 319 | msg += " kitty.conf file.\n\n" 320 | msg += "3. Third and final step is to create the Kitty Theme Changer\n" 321 | msg += " config file which will point to the correct paths for the\n" 322 | msg += " themes you've collected.\n\n" 323 | msg += " The Kitty theme changer uses a simple python module as\n" 324 | msg += " a configuration file. By default this is '~/.kittythemechanger.py'.\n" 325 | msg += " The list of required variables and their types are:\n\n" 326 | msg += " - theme_dir (pathlib.Path): Directory of Kitty theme.conf files.\n" 327 | msg += " - conf_dir (pathlib.Path): Directory Kitty looks in for theme.conf\n" 328 | msg += " - theme_link (pathlib.Path): Symlink file Kitty loads from kitty.conf\n" 329 | msg += " - light_theme_link (pathlib.Path): Symlink to a 'light' theme config file.\n" 330 | msg += " - dark_theme_link (pathlib.Path): Symlink to a 'dark' theme config file.\n" 331 | msg += " - socket (str): a Kitty compatible socket string for the '--listen-on' flag. See 'man kitty'.\n\n" 332 | msg += " An example .kittythemechanger.py file is shown below::\n\n" 333 | msg += " '''A config module for the Kitty Theme Changer Tool.'''\n" 334 | msg += " from pathlib import Path\n" 335 | msg += " theme_dir = Path('~/kitty-themes/themes').expanduser()\n" 336 | msg += " conf_dir = Path('~/.config/kitty').expanduser()\n" 337 | msg += " theme_link = conf_dir.joinpath('theme.conf')\n" 338 | msg += " light_theme_link = conf_dir.joinpath('light-theme.conf')\n" 339 | msg += " dark_theme_link = conf_dir.joinpath('dark-theme.conf')\n" 340 | msg += " socket = 'unix:/tmp/kittysocket'\n" 341 | print(msg) 342 | 343 | 344 | class Actions: 345 | """A container object to hold the actions this script can perform.""" 346 | 347 | list = list_themes 348 | show = show_config 349 | test = test_theme 350 | toggle = toggle_themes 351 | set_dark = set_dark_theme 352 | set_light = set_light_theme 353 | live = make_theme_live 354 | 355 | 356 | def dprint(msg): 357 | """Conditionally print a debug message.""" 358 | if DEBUG: 359 | print('{}{}'.format(DEBUG_OUTPUT_PREFIX, msg)) 360 | 361 | 362 | def vprint(msg): 363 | """Conditionally print a verbose message.""" 364 | if VERBOSE: 365 | print(msg) 366 | 367 | 368 | class DebugAction(argparse.Action): 369 | """Enable the debugging output mechanism.""" 370 | 371 | sflag = '-d' 372 | flag = '--debug' 373 | help = 'Enable debugging output.' 374 | 375 | @classmethod 376 | def add_parser_argument(cls, parser): 377 | """Add arguments for this action to an argparse parser object.""" 378 | parser.add_argument(cls.sflag, cls.flag, help=cls.help, action=cls) 379 | 380 | def __init__(self, option_strings, dest, **kwargs): 381 | """Initialize this Action object.""" 382 | super(DebugAction, self).__init__(option_strings, dest, nargs=0, 383 | default=False, **kwargs) 384 | 385 | def __call__(self, parser, namespace, values, option_string=None): 386 | """Set DEBUG to True for this execution.""" 387 | global DEBUG 388 | DEBUG = True 389 | setattr(namespace, self.dest, True) 390 | 391 | 392 | class VerboseAction(DebugAction): 393 | """Enable the verbose output mechanism.""" 394 | 395 | sflag = '-v' 396 | flag = '--verbose' 397 | help = 'Enable verbose output.' 398 | 399 | def __call__(self, parser, namespace, values, option_string=None): 400 | """Set VERBOSE to True for this execution.""" 401 | global VERBOSE 402 | VERBOSE = True 403 | setattr(namespace, self.dest, True) 404 | 405 | 406 | if __name__ == '__main__': 407 | try: 408 | sys.exit(main()) 409 | except SystemExit: 410 | sys.exit(0) 411 | except KeyboardInterrupt: 412 | print('...interrupted by user, exiting.') 413 | sys.exit(1) 414 | except Exception as exc: 415 | if DEBUG: 416 | raise 417 | else: 418 | print('Unhandled Error:\n{}'.format(exc)) 419 | sys.exit(1) 420 | --------------------------------------------------------------------------------