suplemon-editor 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. suplemon/__init__.py +0 -0
  2. suplemon/__main__.py +3 -0
  3. suplemon/cli.py +65 -0
  4. suplemon/config/defaults.json +153 -0
  5. suplemon/config/keymap.json +63 -0
  6. suplemon/config.py +324 -0
  7. suplemon/cursor.py +135 -0
  8. suplemon/editor.py +568 -0
  9. suplemon/file.py +193 -0
  10. suplemon/help.py +327 -0
  11. suplemon/helpers.py +104 -0
  12. suplemon/hex2xterm.py +23 -0
  13. suplemon/key_mappings.py +239 -0
  14. suplemon/lexer.py +88 -0
  15. suplemon/line.py +51 -0
  16. suplemon/linelight/__init__.py +0 -0
  17. suplemon/linelight/color_map.py +9 -0
  18. suplemon/linelight/css.py +21 -0
  19. suplemon/linelight/diff.py +17 -0
  20. suplemon/linelight/html.py +19 -0
  21. suplemon/linelight/js.py +21 -0
  22. suplemon/linelight/json.py +15 -0
  23. suplemon/linelight/lua.py +10 -0
  24. suplemon/linelight/md.py +19 -0
  25. suplemon/linelight/php.py +27 -0
  26. suplemon/linelight/py.py +27 -0
  27. suplemon/logger.py +90 -0
  28. suplemon/main.py +771 -0
  29. suplemon/module_loader.py +98 -0
  30. suplemon/modules/application_state.py +84 -0
  31. suplemon/modules/autocomplete.py +114 -0
  32. suplemon/modules/autodocstring.py +142 -0
  33. suplemon/modules/battery.py +118 -0
  34. suplemon/modules/bulk_delete.py +75 -0
  35. suplemon/modules/clock.py +22 -0
  36. suplemon/modules/comment.py +59 -0
  37. suplemon/modules/config.py +20 -0
  38. suplemon/modules/date.py +22 -0
  39. suplemon/modules/diff.py +42 -0
  40. suplemon/modules/eval.py +43 -0
  41. suplemon/modules/hostname.py +34 -0
  42. suplemon/modules/input_test.py +23 -0
  43. suplemon/modules/keymap.py +20 -0
  44. suplemon/modules/linter.py +274 -0
  45. suplemon/modules/lower.py +21 -0
  46. suplemon/modules/lstrip.py +20 -0
  47. suplemon/modules/paste.py +49 -0
  48. suplemon/modules/reload.py +16 -0
  49. suplemon/modules/replace_all.py +23 -0
  50. suplemon/modules/reverse.py +22 -0
  51. suplemon/modules/rstrip.py +19 -0
  52. suplemon/modules/save.py +16 -0
  53. suplemon/modules/save_all.py +19 -0
  54. suplemon/modules/sort_lines.py +41 -0
  55. suplemon/modules/strip.py +19 -0
  56. suplemon/modules/system_clipboard.py +121 -0
  57. suplemon/modules/tabstospaces.py +18 -0
  58. suplemon/modules/toggle_whitespace.py +18 -0
  59. suplemon/modules/upper.py +21 -0
  60. suplemon/prompt.py +319 -0
  61. suplemon/suplemon_module.py +210 -0
  62. suplemon/themes/8colors.tmTheme +179 -0
  63. suplemon/themes/monokai.tmTheme +284 -0
  64. suplemon/themes.py +201 -0
  65. suplemon/ui.py +656 -0
  66. suplemon/viewer.py +1004 -0
  67. suplemon_editor-0.3.0.dist-info/METADATA +515 -0
  68. suplemon_editor-0.3.0.dist-info/RECORD +72 -0
  69. suplemon_editor-0.3.0.dist-info/WHEEL +5 -0
  70. suplemon_editor-0.3.0.dist-info/entry_points.txt +3 -0
  71. suplemon_editor-0.3.0.dist-info/licenses/LICENSE +21 -0
  72. suplemon_editor-0.3.0.dist-info/top_level.txt +1 -0
suplemon/file.py ADDED
@@ -0,0 +1,193 @@
1
+ # -*- encoding: utf-8
2
+ """
3
+ File object for storing an opened file and editor.
4
+ """
5
+
6
+ import os
7
+ import time
8
+ import logging
9
+
10
+ from .helpers import parse_path
11
+
12
+
13
+ class File:
14
+ def __init__(self, app=None):
15
+ self.app = app
16
+ self.logger = logging.getLogger(__name__)
17
+ self.name = ""
18
+ self.fpath = ""
19
+ self.data = None
20
+ self.read_only = False # Currently unused
21
+ self.last_save = None # Time of last save
22
+ self.opened = time.time() # Time of last open
23
+ self.editor = None
24
+ self.writable = True
25
+ self.is_help = False
26
+
27
+ def _path(self):
28
+ """Get the full path of the file."""
29
+ return os.path.join(self.fpath, self.name)
30
+
31
+ def get_encoding(self):
32
+ """Get the encoding to read and write this file with.
33
+
34
+ Comes from the editor's default_encoding setting. Without this the
35
+ files were opened with whatever the locale happened to be, so the
36
+ setting did nothing and a non UTF-8 locale mangled the contents.
37
+
38
+ :return: Name of the encoding to use.
39
+ :rtype: str
40
+ """
41
+ default = "utf-8"
42
+ if self.app is None:
43
+ return default
44
+ try:
45
+ return self.app.config["editor"]["default_encoding"] or default
46
+ except (KeyError, TypeError):
47
+ return default
48
+
49
+ def path(self):
50
+ """Get the full path of the file."""
51
+ # TODO: deprecate in favour of get_path()
52
+ return self._path()
53
+
54
+ def get_name(self):
55
+ """Get the file name."""
56
+ return self.name
57
+
58
+ def get_path(self):
59
+ """Get the full path of the file."""
60
+ return self._path()
61
+
62
+ def get_extension(self):
63
+ parts = self.name.split(".")
64
+ if len(parts) < 2:
65
+ return ""
66
+ return parts[-1]
67
+
68
+ def get_editor(self):
69
+ """Get the associated editor."""
70
+ return self.editor
71
+
72
+ def set_name(self, name):
73
+ """Set the file name."""
74
+ # TODO: sanitize
75
+ self.name = name
76
+ self.update_editor_extension()
77
+
78
+ def set_path(self, path):
79
+ """Set the file path. Relative paths are sanitized."""
80
+ self.fpath, self.name = parse_path(path)
81
+ self.update_editor_extension()
82
+
83
+ def set_data(self, data):
84
+ """Set the file data and apply to editor if it exists."""
85
+ self.data = data
86
+ if self.editor:
87
+ self.editor.set_data(data)
88
+
89
+ def set_editor(self, editor):
90
+ """The editor instance set its file extension."""
91
+ self.editor = editor
92
+ self.update_editor_extension()
93
+
94
+ def on_load(self):
95
+ """Does checks after file is loaded."""
96
+ self.writable = os.access(self._path(), os.W_OK)
97
+ if not self.writable:
98
+ self.logger.info("File not writable.")
99
+
100
+ def update_editor_extension(self):
101
+ """Set the editor file extension from the current file name."""
102
+ if not self.editor:
103
+ return False
104
+ ext = self.get_extension()
105
+ if len(ext) >= 1:
106
+ self.editor.set_file_extension(ext)
107
+
108
+ def save(self):
109
+ """Write the editor data to file."""
110
+ data = self.editor.get_data()
111
+ try:
112
+ with open(self._path(), "w", encoding=self.get_encoding()) as f:
113
+ f.write(data)
114
+ except (OSError, LookupError, UnicodeError):
115
+ self.logger.exception('Failed writing file "{file}"'.format(file=self._path()))
116
+ return False
117
+ self.data = data
118
+ self.last_save = time.time()
119
+ self.writable = os.access(self._path(), os.W_OK)
120
+ return True
121
+
122
+ def load(self, read=True):
123
+ """Try to read the actual file and load the data into the editor instance."""
124
+ if not read:
125
+ return True
126
+ path = self._path()
127
+ if not os.path.isfile(path):
128
+ self.logger.debug("Given path isn't a file.")
129
+ return False
130
+ data = self._read(path)
131
+ if data is False:
132
+ return False
133
+ self.data = data
134
+ self.editor.set_data(data)
135
+ self.on_load()
136
+ return True
137
+
138
+ def _read(self, path):
139
+ data = self._read_text(path)
140
+ if data is False:
141
+ self.logger.warning("Normal file read failed.")
142
+ data = self._read_binary(path)
143
+ if data is False:
144
+ self.logger.warning("Fallback file read failed.")
145
+ return False
146
+ return data
147
+
148
+ def _read_text(self, file):
149
+ # Read text file
150
+ try:
151
+ with open(self._path(), encoding=self.get_encoding()) as f:
152
+ return f.read()
153
+ except (OSError, LookupError, UnicodeDecodeError):
154
+ self.logger.exception('Failed reading file "{file}"'.format(file=file))
155
+ return False
156
+
157
+ def _read_binary(self, file):
158
+ # Read binary file and try to autodetect encoding
159
+ try:
160
+ f = open(self._path(), "rb")
161
+ data = f.read()
162
+ f.close()
163
+ import chardet
164
+ detection = chardet.detect(data)
165
+ charenc = detection["encoding"]
166
+ if charenc is None:
167
+ self.logger.warning("Failed to detect file encoding.")
168
+ return False
169
+ self.logger.info("Trying to decode with encoding '{0}'".format(charenc))
170
+ return data.decode(charenc)
171
+ except Exception:
172
+ self.logger.warning("Failed reading binary file!", exc_info=True)
173
+ return False
174
+
175
+ def reload(self):
176
+ """Reload file data."""
177
+ return self.load()
178
+
179
+ def is_changed(self):
180
+ """Check if the editor data is different from the file."""
181
+ return self.editor.get_data() != self.data
182
+
183
+ def is_changed_on_disk(self):
184
+ path = self._path()
185
+ if os.path.isfile(path):
186
+ data = self._read(path)
187
+ if data != self.data:
188
+ return True
189
+ return False
190
+
191
+ def is_writable(self):
192
+ """Check if the file is writable."""
193
+ return self.writable
suplemon/help.py ADDED
@@ -0,0 +1,327 @@
1
+ """
2
+ Help text for Suplemon
3
+ """
4
+
5
+ help_text = """
6
+ # Suplemon Help
7
+
8
+ *Contents*
9
+ 1. General description
10
+ 2. User interface
11
+ 3. Selecting text
12
+ 4. Default keyboard shortcuts
13
+ 5. Commands
14
+
15
+ ## 1. General description
16
+ Suplemon is designed to be an easy, intuitive and powerful text editor.
17
+ It emulates some features from Sublime Text and the user interface of Nano.
18
+ Multi cursor editing is a core feature. Suplemon also supports extensions
19
+ so you can customize it to work how you want.
20
+
21
+ ## 2. User interface
22
+ The user interface is designed to be as intuitive and informative as possible.
23
+ There are two status bars, one at the top and one at the bottom. The top bar
24
+ shows the program version, a clock, and a list of opened files. The bottom bar
25
+ shows status messages and handles input for commands. Above the bottom status
26
+ bar there is a list of most common keyboard shortcuts.
27
+
28
+ ## 3. Selecting text
29
+ Suplemon has no text selection. There is no way to select a range of
30
+ characters, with or without a mouse, and nothing highlights as you move.
31
+ What it has instead is cursors, and the unit of work is the whole line.
32
+
33
+ Copy (Ctrl + C) and cut (Ctrl + X) act on every line that has a cursor on
34
+ it. Where the cursor sits within the line makes no difference: one cursor
35
+ anywhere on a line copies that entire line. So "selecting" more text means
36
+ putting more cursors down.
37
+
38
+ * Alt + Down / Up
39
+ > Add a cursor on the next or previous line. This is the usual way to
40
+ > take several lines: press Alt + Down twice, then Ctrl + C, to copy
41
+ > three lines.
42
+
43
+ * Ctrl + D
44
+ > Add a cursor at the next occurance of the word the cursor is on.
45
+ > Press it repeatedly to gather up matches one at a time.
46
+
47
+ * Ctrl + A
48
+ > Add a cursor at every occurance of the last search term at once.
49
+ > Search with Ctrl + F or Ctrl + D first to set the term.
50
+
51
+ * ESC
52
+ > Collapse back to a single cursor when you're done.
53
+
54
+ Pasting (Ctrl + V) behaves in one of two ways. With a single cursor and
55
+ several copied lines, the lines are inserted as new lines. With several
56
+ cursors, or a single copied line, the text is inserted at each cursor
57
+ position instead, and a single copied line is repeated into every cursor.
58
+ That last case is what makes multi cursor editing useful: copy one line,
59
+ put ten cursors down, and paste it into all ten at once.
60
+
61
+ The practical consequence is that you cannot copy part of a line. If you
62
+ need a fragment, copy the whole line and trim what you don't want after
63
+ pasting it. This is the habit that takes the longest to unlearn if you're
64
+ coming from an editor with ordinary selection.
65
+
66
+ Copy and paste share text with other applications when a clipboard tool
67
+ such as xsel, xclip, wl-clipboard or pbcopy is installed. Without one they
68
+ still work, but only within Suplemon.
69
+
70
+
71
+ ## 4. Default keyboard shortcuts
72
+ The default keyboard shortcuts imitate those of common graphical editors.
73
+ Most shortcuts are also shown at the bottom in the legend area. Here's
74
+ the complete reference.
75
+
76
+
77
+ * F1 / Ctrl + H
78
+ > Show this help. On terminals where the backspace key sends ^H, Ctrl + H
79
+ > arrives as backspace and can't reach the help, so F1 always works.
80
+
81
+ * Ctrl + Q
82
+ > Exit
83
+
84
+ * Ctrl + W
85
+ > Close file or tab
86
+
87
+ * Ctrl + C
88
+ > Copy line(s) to buffer
89
+
90
+ * Ctrl + X
91
+ > Cut line(s) to buffer
92
+
93
+ * Ctrl + V
94
+ > Insert buffer
95
+
96
+ * Ctrl + K
97
+ > Duplicate line
98
+
99
+ * Ctrl + G
100
+ > Go to line number or file (type the beginning of a filename to switch to it).
101
+ > You can also use 'filena:42' to go to line 42 in filename.py etc.
102
+
103
+ * Ctrl + F
104
+ > Search for a string or regular expression (configurable)
105
+
106
+ * Ctrl + D
107
+ > Search for next occurance or find the word the cursor is on. Adds a new cursor at each new occurance.
108
+
109
+ * Ctrl + A
110
+ > Add a cursor at every occurance of the last search term. Search with
111
+ > Ctrl + F or Ctrl + D first, otherwise there is no term to look for.
112
+
113
+ * Ctrl + T
114
+ > Trim whitespace
115
+
116
+ * Alt + Arrow Key
117
+ > Add new curor in arrow direction
118
+
119
+ * Ctrl + Left / Right
120
+ > Jump to previous or next word or line
121
+
122
+ * Ctrl + Up / Down
123
+ > Jump three lines up or down
124
+
125
+ * ESC
126
+ > Revert to a single cursor / Cancel input prompt
127
+
128
+ * Alt + Page Up
129
+ > Move line(s) up
130
+
131
+ * Alt + Page Down
132
+ > Move line(s) down
133
+
134
+ * Tab / Shift + Tab
135
+ > Indent or unindent line(s)
136
+
137
+ * Ctrl + S
138
+ > Save current file
139
+
140
+ * F3
141
+ > Save file with new name
142
+
143
+ * F2
144
+ > Reload current file
145
+
146
+ * Ctrl + N
147
+ > New empty file
148
+
149
+ * Ctrl + O
150
+ > Open file
151
+
152
+ * Ctrl + W
153
+ > Close file
154
+
155
+ * F4 / Ctrl + Page Up / Ctrl + Alt + Page Up
156
+ > Switch to next file. Wraps around, so F4 alone cycles through them all.
157
+
158
+ * Shift + F4 / Ctrl + Page Down / Ctrl + Alt + Page Down
159
+ > Switch to previous file. Terminals often keep Ctrl + Page Up and Down
160
+ > for their own tabs, hence the alternatives. Shift + F4 and the
161
+ > Ctrl + Alt pair need a terminfo entry that describes those keys, which
162
+ > older ones (FreeBSD's among them) do not have; F4 works everywhere.
163
+
164
+ * Ctrl + E
165
+ > Run a command.
166
+
167
+ * F5 / Ctrl + Z
168
+ > Undo
169
+
170
+ * F6 / Ctrl + Y
171
+ > Redo
172
+
173
+ * F7
174
+ > Toggle visible whitespace
175
+
176
+ * F8
177
+ > Toggle mouse mode
178
+
179
+ * F9
180
+ > Toggle line numbers
181
+
182
+ * F10
183
+ > Toggle visible line endings. They start hidden; the character drawn is
184
+ > the "line_end_char" setting in your config.
185
+
186
+ * F11
187
+ > Toggle syntax highlighting. Bound but not implemented yet, so pressing
188
+ > it currently does nothing.
189
+
190
+ * F12
191
+ > Toggle full screen
192
+
193
+ ### Mouse shortcuts
194
+
195
+ * Left Click
196
+ > Set cursor at mouse position. Reverts to a single cursor.
197
+
198
+ * Right Click
199
+ > Add a cursor at mouse position.
200
+
201
+ * Scroll Wheel Up / Down
202
+ > Scroll up & down.
203
+
204
+
205
+ ## 5. Commands
206
+ Commands are special operations that can be performed (e.g. remove whitespace
207
+ or convert line to uppercase). Each command can be run by pressing Ctrl + E
208
+ and then typing the command name. Commands are extensions and are stored in
209
+ the modules folder in the Suplemon installation.
210
+
211
+ * autocomplete
212
+
213
+ A simple autocompletion module.
214
+
215
+ This adds autocomplete support for the tab key. It uses a word
216
+ list scanned from all open files for completions. By default it suggests
217
+ the shortest possible match. If there are no matches, the tab action is
218
+ run normally.
219
+
220
+ * autodocstring
221
+
222
+ Simple module for adding docstring placeholders.
223
+
224
+ This module is intended to generate docstrings for Python functions.
225
+ It adds placeholders for descriptions, arguments and return data.
226
+ Function arguments are crudely parsed from the function definition
227
+ and return statements are scanned from the function body.
228
+
229
+ * bulk_delete
230
+
231
+ Bulk delete lines and characters.
232
+ Asks what direction to delete in by default.
233
+
234
+ Add 'up' to delete lines above highest cursor.
235
+ Add 'down' to delete lines below lowest cursor.
236
+ Add 'left' to delete characters to the left of all cursors.
237
+ Add 'right' to delete characters to the right of all cursors.
238
+
239
+ * comment
240
+
241
+ Toggle line commenting based on current file syntax.
242
+
243
+ * config
244
+
245
+ Shortcut for openning the config files.
246
+
247
+ * diff
248
+
249
+ View a diff of the current file compared to it's on disk version.
250
+
251
+ * eval
252
+
253
+ Evaluate a python expression and show the result in the status bar.
254
+
255
+ If no expression is provided the current line(s) are evaluated and
256
+ replaced with the evaluation result.
257
+
258
+ * keymap
259
+
260
+ Shortcut to openning the keymap config file.
261
+
262
+ * linter
263
+
264
+ Linter for suplemon.
265
+
266
+ * lower
267
+
268
+ Transform current lines to lower case.
269
+
270
+ * lstrip
271
+
272
+ Trim whitespace from beginning of current lines.
273
+
274
+ * paste
275
+
276
+ Toggle paste mode (helpful when pasting over SSH if auto indent is enabled)
277
+
278
+ * reload
279
+
280
+ Reload all add-on modules.
281
+
282
+ * replace_all
283
+
284
+ Replace all occurrences in all files of given text with given replacement.
285
+
286
+ * reverse
287
+
288
+ Reverse text on current line(s).
289
+
290
+ * rstrip
291
+
292
+ Trim whitespace from the end of lines.
293
+
294
+ * save
295
+
296
+ Save the current file.
297
+
298
+ * save_all
299
+
300
+ Save all currently open files. Asks for confirmation.
301
+
302
+ * sort_lines
303
+
304
+ Sort current lines.
305
+
306
+ Sorts alphabetically by default.
307
+ Add 'length' to sort by length.
308
+ Add 'reverse' to reverse the sorting.
309
+
310
+ * strip
311
+
312
+ Trim whitespace from start and end of lines.
313
+
314
+ * tabstospaces
315
+
316
+ Convert tab characters to spaces in the entire file.
317
+
318
+ * toggle_whitespace
319
+
320
+ Toggle visually showing whitespace.
321
+
322
+ * upper
323
+
324
+ Transform current lines to upper case.
325
+
326
+
327
+ """
suplemon/helpers.py ADDED
@@ -0,0 +1,104 @@
1
+ # -*- encoding: utf-8
2
+ """
3
+ Various helper constants and functions.
4
+ """
5
+
6
+ import os
7
+ import re
8
+ import sys
9
+ import time
10
+ import traceback
11
+
12
+
13
+ def curr_time():
14
+ """Current time in %H:%M"""
15
+ return time.strftime("%H:%M")
16
+
17
+
18
+ def curr_time_sec():
19
+ """Current time in %H:%M:%S"""
20
+ return time.strftime("%H:%M:%S")
21
+
22
+
23
+ def multisplit(data, delimiters):
24
+ pattern = "|".join(map(re.escape, delimiters))
25
+ return re.split(pattern, data)
26
+
27
+
28
+ def get_error_info():
29
+ """Return info about last error."""
30
+ msg = "{0}\n{1}".format(str(traceback.format_exc()), str(sys.exc_info()))
31
+ return msg
32
+
33
+
34
+ def get_string_between(start, stop, s):
35
+ """Search string for a substring between two delimeters. False if not found."""
36
+ i1 = s.find(start)
37
+ if i1 == -1:
38
+ return False
39
+ s = s[i1 + len(start):]
40
+ i2 = s.find(stop)
41
+ if i2 == -1:
42
+ return False
43
+ s = s[:i2]
44
+ return s
45
+
46
+
47
+ def whitespace(line):
48
+ """Return index of first non whitespace character on a line.
49
+
50
+ Tabs count as whitespace. Only spaces used to, which meant tab indented
51
+ files behaved as though they had no indentation at all.
52
+ """
53
+ i = 0
54
+ for char in str(line):
55
+ if char not in " \t":
56
+ break
57
+ i += 1
58
+ return i
59
+
60
+
61
+ def leading_whitespace(line):
62
+ """Return the indentation of a line, as written.
63
+
64
+ Use this instead of whitespace() when the indentation is going to be
65
+ reproduced, so that tabs stay tabs and spaces stay spaces.
66
+ """
67
+ line = str(line)
68
+ return line[:whitespace(line)]
69
+
70
+
71
+ def parse_path(path):
72
+ """Parse a relative path and return full directory and filename as a tuple.
73
+
74
+ expanduser handles a bare "~" as well as "~/...", which the previous
75
+ manual prefix check did not: "~" on its own was treated as a filename
76
+ in the current directory.
77
+ """
78
+ path = os.path.expanduser(path)
79
+ ab = os.path.abspath(path)
80
+ parts = os.path.split(ab)
81
+ return parts
82
+
83
+
84
+ def get_filename_cursor_pos(name):
85
+ default = {
86
+ "name": name,
87
+ "row": 0,
88
+ "col": 0,
89
+ }
90
+
91
+ m = re.match(r"(.*?):(\d+):?(\d+)?", name)
92
+
93
+ if not m:
94
+ return default
95
+
96
+ groups = m.groups()
97
+ if not groups[0]:
98
+ return default
99
+
100
+ return {
101
+ "name": groups[0],
102
+ "row": abs(int(groups[1])-1) if groups[1] else 0,
103
+ "col": abs(int(groups[2])-1) if groups[2] else 0,
104
+ }
suplemon/hex2xterm.py ADDED
@@ -0,0 +1,23 @@
1
+
2
+ # Default color levels for the color cube
3
+ cubelevels = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
4
+ # Generate a list of midpoints of the above list
5
+ snaps = [(x+y)/2 for x, y in list(zip(cubelevels, [0]+cubelevels))[1:]]
6
+
7
+
8
+ def hex_to_rgb(value):
9
+ value = value.lstrip("#")
10
+ lv = len(value)
11
+ return tuple(int(value[i:i + lv // 3], 16) for i in range(0, lv, lv // 3))
12
+
13
+
14
+ def hex_to_xterm(hex):
15
+ """
16
+ Converts hex color values to the nearest equivalent xterm-256 color.
17
+ """
18
+ rgb = hex_to_rgb(hex)
19
+ r, g, b = rgb
20
+ # Using list of snap points, convert RGB value to cube indexes
21
+ r, g, b = map(lambda x: len(tuple(s for s in snaps if s < x)), (r, g, b))
22
+ # Simple colorcube transform
23
+ return r*36 + g*6 + b + 16