├── .gitignore ├── .gitmodules ├── MANIFEST.in ├── README.md ├── getchs └── __init__.py ├── iterminator ├── __init__.py ├── colorscheme.py ├── iterminator.py └── version.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | *.egg-info 3 | __pycache__/ 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "iterminator/iTerm2-Color-Schemes"] 2 | path = iterminator/iTerm2-Color-Schemes 3 | url = https://github.com/mbadolato/iTerm2-Color-Schemes.git 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include iterminator/iTerm2-Color-Schemes * 2 | prune iterminator/iTerm2-Color-Schemes/backgrounds 3 | prune iterminator/iTerm2-Color-Schemes/konsole 4 | prune iterminator/iTerm2-Color-Schemes/screenshots 5 | prune iterminator/iTerm2-Color-Schemes/terminal 6 | prune iterminator/iTerm2-Color-Schemes/terminator 7 | prune iterminator/iTerm2-Color-Schemes/xrdb 8 | include iterminator/version.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### Command-line color scheme selector for iTerm2 2 | 3 | ```sh 4 | pip install iterminator 5 | ``` 6 | 7 | ##### Examples 8 | 9 | - `iterminator` 10 | 11 | Use right/left or j/k or n/p to move forwards/backwards through color schemes. 12 | 13 | - `iterminator -a 2.5` 14 | 15 | Cycle through schemes automatically at 2.5 schemes/second. Use space to pause, right/left or j/k or n/p to move forwards/backwards through color schemes. 16 | 17 | - `iterminator -s cobalt2` 18 | 19 | Select a single scheme (case-insensitive substring matching). 20 | 21 | - `iterminator -i` 22 | 23 | Select color scheme interactively with tab completion. 24 | 25 | - `iterminator -l` 26 | 27 | List color schemes. 28 | 29 | - `iterminator --help` 30 | 31 | Show help. 32 | 33 | 34 | ##### Credits and contributions 35 | 36 | The color schemes (and the script to issue the iterm2 escape sequences) are 37 | from https://github.com/mbadolato/iTerm2-Color-Schemes, which is included as a 38 | submodule. All credit for the schemes goes to the original scheme authors and 39 | to the iTerm2-Color-Schemes project. To add a new scheme, please first create a 40 | pull request against iTerm2-Color-Schemes to add your scheme, and then open a 41 | pull request or issue against this repo to update the submodule. 42 | -------------------------------------------------------------------------------- /getchs/__init__.py: -------------------------------------------------------------------------------- 1 | from select import select 2 | from sys import stdin 3 | from time import sleep 4 | import fcntl 5 | import os 6 | import termios 7 | import tty 8 | 9 | 10 | POLL_INTERVAL = 0.1 11 | 12 | CTRL_C = "\x03" 13 | LEFT = "\x1b[D" 14 | RIGHT = "\x1b[C" 15 | UP = "\x1b[A" 16 | DOWN = "\x1b[B" 17 | 18 | 19 | def setNonBlocking(fd): 20 | """ 21 | Set the file description of the given file descriptor to non-blocking. 22 | 23 | Copied from twisted.internet.fdesc.setNonBlocking() 24 | https://github.com/twisted/twisted 25 | """ 26 | flags = fcntl.fcntl(fd, fcntl.F_GETFL) 27 | flags = flags | os.O_NONBLOCK 28 | fcntl.fcntl(fd, fcntl.F_SETFL, flags) 29 | 30 | 31 | def getchs(): 32 | """ 33 | Block until there are bytes to read on stdin and then return them all. 34 | 35 | Adapted from getch() 36 | https://github.com/joeyespo/py-getch. 37 | """ 38 | fd = stdin.fileno() 39 | old = termios.tcgetattr(fd) 40 | setNonBlocking(fd) 41 | try: 42 | tty.setraw(fd) 43 | while not select([stdin], [], [], 0)[0]: 44 | sleep(POLL_INTERVAL) 45 | return stdin.read() 46 | finally: 47 | termios.tcsetattr(fd, termios.TCSADRAIN, old) 48 | 49 | 50 | if __name__ == "__main__": 51 | while True: 52 | chars = getchs() 53 | if chars == CTRL_C: 54 | exit(0) 55 | else: 56 | print("%r" % chars, map(ord, chars)) 57 | -------------------------------------------------------------------------------- /iterminator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dandavison/iterminator/69e2406e565f1c4c051c46a18ea7d37ed7933e40/iterminator/__init__.py -------------------------------------------------------------------------------- /iterminator/colorscheme.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | import os 3 | from plistlib import readPlist 4 | 5 | 6 | class Scheme(object): 7 | """ 8 | An iTerm2 color scheme. 9 | """ 10 | 11 | def __init__(self, path): 12 | self.path = path 13 | self.name = os.path.basename(self.path).split(".")[0] 14 | self.scheme = self.parse() 15 | 16 | def background(self): 17 | background = self.scheme["Background Color"] 18 | return ( 19 | background["Red Component"], 20 | background["Green Component"], 21 | background["Blue Component"], 22 | ) 23 | 24 | def is_light(self): 25 | rgb = self.background() 26 | h, l, s = colorsys.rgb_to_hls(*rgb) 27 | return l >= 0.5 28 | 29 | def parse(self): 30 | return readPlist(self.path) 31 | 32 | def __repr__(self): 33 | return self.name 34 | -------------------------------------------------------------------------------- /iterminator/iterminator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from collections import deque 3 | from threading import Thread 4 | from time import sleep 5 | import argparse 6 | import os 7 | import random 8 | import subprocess 9 | import sys 10 | 11 | try: 12 | import readline 13 | except ImportError: 14 | import gnureadline as readline 15 | 16 | import getchs 17 | 18 | from iterminator.colorscheme import Scheme 19 | 20 | 21 | class ColorSchemeSelector(object): 22 | """ 23 | An interactive iTerm2 color scheme selector. 24 | """ 25 | 26 | # Animation control keys 27 | PAUSE = {" "} 28 | NEXT = {"j", "n", getchs.RIGHT, getchs.DOWN} 29 | PREV = {"k", "p", getchs.LEFT, getchs.UP} 30 | QUIT = {"q", "\r", getchs.CTRL_C} 31 | 32 | def __init__(self, quiet=True): 33 | self.quiet = quiet 34 | self.repo_dir = os.path.join(os.path.dirname(__file__), "iTerm2-Color-Schemes") 35 | schemes_dir = self.repo_dir + "/schemes" 36 | self.schemes = deque( 37 | Scheme(os.path.join(schemes_dir, scheme_file)) 38 | for scheme_file in sorted(os.listdir(schemes_dir), key=str.lower) 39 | if scheme_file.endswith(".itermcolors") 40 | ) 41 | self._post_change_schemes_hook() 42 | 43 | self.animation_control = Thread(target=self.control) 44 | self.paused = False 45 | self.quitting = False 46 | 47 | def _post_change_schemes_hook(self): 48 | self.name_to_scheme = {s.name: s for s in self.schemes} 49 | self.scheme_names = [s.name for s in self.schemes] 50 | self.blank = " " * max(len(s.name) for s in self.schemes) 51 | 52 | def filter_light_or_dark(self, is_light): 53 | self.schemes = deque(scheme for scheme in self.schemes if scheme.is_light() == is_light) 54 | self._post_change_schemes_hook() 55 | 56 | @property 57 | def scheme(self): 58 | return self.schemes[0] 59 | 60 | def next(self): 61 | self.schemes.rotate(-1) 62 | 63 | def prev(self): 64 | self.schemes.rotate(+1) 65 | 66 | def goto(self, scheme): 67 | self.schemes.rotate(-list(self.schemes).index(scheme)) 68 | 69 | def shuffle(self): 70 | random.shuffle(self.schemes) 71 | self._post_change_schemes_hook() 72 | 73 | def apply(self): 74 | """ 75 | Apply current scheme to current iTerm2 session. 76 | """ 77 | subprocess.check_call([self.repo_dir + "/tools/preview.rb", self.scheme.path]) 78 | 79 | def say(self, msg): 80 | if not self.quiet: 81 | if len(msg) > len(self.blank): 82 | self.blank = " " * len(msg) 83 | sys.stdout.write(msg) 84 | sys.stdout.flush() 85 | 86 | def tell(self): 87 | sys.stdout.write("\r%s\r%s" % (self.blank, self.scheme)) 88 | sys.stdout.flush() 89 | 90 | def animate(self, speed, shuffle): 91 | """ 92 | Cycle through schemes automatically. 93 | 94 | Keys can be used to pause, and go forwards/backwards. 95 | """ 96 | if shuffle: 97 | self.shuffle() 98 | self.animation_control.start() 99 | self.prev() 100 | while True: 101 | if self.quitting: 102 | self.quit() 103 | elif self.paused: 104 | sleep(0.1) 105 | else: 106 | self.next() 107 | self.apply() 108 | self.tell() 109 | sleep(1.0 / speed) 110 | 111 | def control(self): 112 | while True: 113 | chars = getchs.getchs() 114 | if chars in self.PAUSE: 115 | self.paused = not self.paused 116 | elif chars in self.NEXT: 117 | self.next() 118 | self.apply() 119 | self.tell() 120 | elif chars in self.PREV: 121 | self.prev() 122 | self.apply() 123 | self.tell() 124 | elif chars in self.QUIT: 125 | self.quitting = True 126 | break 127 | 128 | def quit(self): 129 | self.animation_control.join() 130 | print("\n") 131 | sys.exit(0) 132 | 133 | def select(self): 134 | """ 135 | Select a color theme interactively. 136 | """ 137 | readline.set_completer(self.complete) 138 | readline.set_completer_delims("") 139 | readline.parse_and_bind("tab: complete") 140 | readline.parse_and_bind("set completion-ignore-case on") 141 | readline.parse_and_bind("set completion-query-items -1") 142 | readline.parse_and_bind("set show-all-if-ambiguous on") 143 | readline.parse_and_bind('"\e[C": menu-complete') 144 | readline.parse_and_bind('"\e[D": menu-complete-backward') 145 | 146 | while True: 147 | try: 148 | self.goto(self.name_to_scheme[raw_input()]) 149 | except KeyboardInterrupt: 150 | print("\n") 151 | sys.exit(0) 152 | except KeyError: 153 | sys.exit(1) 154 | else: 155 | self.apply() 156 | 157 | def complete(self, text, state): 158 | """ 159 | Return state'th completion for current input. 160 | 161 | This is the standard readline completion function. 162 | https://docs.python.org/2/library/readline.html 163 | 164 | text: the current input 165 | state: an integer specifying which of the matches for the current input 166 | should be returned 167 | """ 168 | if state == 0: 169 | # First call for current input; compute and cache completions 170 | if text: 171 | self.current_matches = self.get_matches(text) 172 | 173 | if len(self.current_matches) == 1: 174 | # Unique match; apply scheme and return the completion 175 | [completion] = self.current_matches 176 | self.goto(self.name_to_scheme[completion]) 177 | self.apply() 178 | return completion 179 | else: 180 | self.current_matches = self.scheme_names 181 | try: 182 | completion = self.current_matches[state] 183 | except IndexError: 184 | completion = None 185 | 186 | return completion 187 | 188 | def get_matches(self, query): 189 | """ 190 | Return matches for current readline input. 191 | """ 192 | if query.endswith("$"): 193 | query = query[:-1] 194 | match_operator = lambda query, name: query.lower() == name.lower() 195 | else: 196 | match_operator = lambda query, name: query.lower() in name.lower() 197 | 198 | return [name for name in self.scheme_names if match_operator(query, name)] 199 | 200 | 201 | DEFAULT_HELP_MESSAGE = "Use left/right or j/k or n/p to select color schemes" 202 | 203 | 204 | def parse_arguments(): 205 | selector = ColorSchemeSelector() 206 | arg_parser = argparse.ArgumentParser( 207 | description=( 208 | "Color theme selector for iTerm2.\n\n%s" ", or supply one of the arguments below." 209 | ) 210 | % DEFAULT_HELP_MESSAGE, 211 | epilog=( 212 | "The color schemes are from " 213 | "https://github.com/mbadolato/iTerm2-Color-Schemes, which is " 214 | "included as a git submodule in this project. All credit for the " 215 | "schemes goes to the original scheme authors and to the " 216 | "iTerm2-Color-Schemes project. To add a new scheme, please first " 217 | "create a pull request against iTerm2-Color-Schemes to add your " 218 | "scheme, and then open a pull request or issue against " 219 | "https://github.com/dandavison/iterminator to update the " 220 | "submodule." 221 | ), 222 | formatter_class=argparse.RawTextHelpFormatter, 223 | ) 224 | 225 | arg_parser.add_argument( 226 | "-a", 227 | "--animation-speed", 228 | nargs="?", 229 | metavar="speed", 230 | type=float, 231 | const=1.0, 232 | help=( 233 | "Cycle through color schemes automatically.\n" 234 | "Optional value is animation speed (schemes/second)\n" 235 | "Key bindings:\n" 236 | "space - pause/unpause\n" 237 | "right arrow, j, n - next scheme\n" 238 | "left arrow, k, p - previous scheme\n" 239 | "return - quit\n\n" 240 | ), 241 | ) 242 | 243 | arg_parser.add_argument( 244 | "--dark", action="store_true", help="Restrict to dark background themes\n\n" 245 | ) 246 | 247 | arg_parser.add_argument( 248 | "-i", 249 | "--interactive", 250 | action="store_true", 251 | help=( 252 | "Select color scheme with tab-completion.\n" 253 | "Key bindings:\n" 254 | "tab - complete\n" 255 | "right arrow - next completion\n" 256 | "left arrow - previous completion\n" 257 | "return - select\n" 258 | "Plus the usual emacs-based readline defaults such as\n" 259 | "ctrl a - beginning of line\n" 260 | "ctrl k - kill to end of line\n\n" 261 | ), 262 | ) 263 | 264 | arg_parser.add_argument( 265 | "--light", action="store_true", help="Restrict to light background themes\n\n" 266 | ) 267 | 268 | arg_parser.add_argument( 269 | "-l", "--list", action="store_true", help="List available color schemes\n\n" 270 | ) 271 | 272 | arg_parser.add_argument( 273 | "-q", 274 | "--quiet", 275 | action="store_true", 276 | help="Don't display initial key bindings help message\n\n", 277 | ) 278 | 279 | arg_parser.add_argument( 280 | "-r", "--random", action="store_true", help="Select a random color scheme\n\n" 281 | ) 282 | 283 | arg_parser.add_argument("-v", "--version", action="store_true", help="Show version\n\n") 284 | 285 | arg_parser.add_argument( 286 | "-s", "--scheme", help="Available choices are\n%s" % " | ".join(selector.scheme_names) 287 | ) 288 | 289 | return arg_parser.parse_args() 290 | 291 | 292 | def main(): 293 | if os.getenv("TMUX"): 294 | error("Please detach from your tmux session before running this command.") 295 | 296 | args = parse_arguments() 297 | selector = ColorSchemeSelector(quiet=args.quiet) 298 | 299 | if args.dark and args.light: 300 | error("Don't request both --light and --dark") 301 | if args.dark: 302 | selector.filter_light_or_dark(False) 303 | elif args.light: 304 | selector.filter_light_or_dark(True) 305 | 306 | if args.animation_speed: 307 | 308 | try: 309 | selector.animate(args.animation_speed, args.random) 310 | except KeyboardInterrupt: 311 | print("\n") 312 | sys.exit(0) 313 | 314 | if args.interactive: 315 | 316 | selector.say("Tab to complete color scheme names\n") 317 | selector.select() 318 | 319 | elif args.list: 320 | 321 | for scheme in selector.schemes: 322 | print(scheme) 323 | 324 | elif args.random: 325 | 326 | selector.shuffle() 327 | selector.apply() 328 | print(selector.scheme) 329 | 330 | elif args.scheme: 331 | 332 | names = selector.get_matches(args.scheme) 333 | if len(names) == 0: 334 | error("No matches") 335 | elif len(names) > 1: 336 | # Check whether it's an exact match as well as a substring 337 | names_2 = selector.get_matches(args.scheme + "$") 338 | if len(names_2) == 1: 339 | names = names_2 340 | 341 | if len(names) == 1: 342 | [name] = names 343 | scheme = selector.name_to_scheme[name] 344 | selector.goto(scheme) 345 | selector.apply() 346 | print(selector.scheme) 347 | elif len(names) > 1: 348 | error("Multiple matches: %s" % ", ".join(names)) 349 | 350 | elif args.version: 351 | with open(os.path.join(os.path.dirname(__file__), "version.txt")) as fp: 352 | print(fp.read().strip()) 353 | 354 | else: 355 | 356 | try: 357 | selector.say(DEFAULT_HELP_MESSAGE) 358 | selector.prev() 359 | selector.control() 360 | except KeyboardInterrupt: 361 | print("\n") 362 | sys.exit(0) 363 | 364 | 365 | def error(msg): 366 | print(msg, file=sys.stderr) 367 | sys.exit(1) 368 | 369 | 370 | if __name__ == "__main__": 371 | main() 372 | -------------------------------------------------------------------------------- /iterminator/version.txt: -------------------------------------------------------------------------------- 1 | 0.3.0 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | 6 | 7 | setup( 8 | name="iterminator", 9 | version=( 10 | open(os.path.join(os.path.dirname(__file__), "iterminator", "version.txt")).read().strip() 11 | ), 12 | author="Dan Davison", 13 | author_email="dandavison7@gmail.com", 14 | description="Command-line color scheme selector for iTerm2", 15 | packages=find_packages(), 16 | include_package_data=True, 17 | zip_safe=False, 18 | install_requires=["gnureadline>=8.0.0"], 19 | entry_points={"console_scripts": ["iterminator = iterminator.iterminator:main"]}, 20 | ) 21 | --------------------------------------------------------------------------------