├── .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 |
--------------------------------------------------------------------------------