├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── LICENSE.md ├── Main.sublime-menu ├── SchemrFavorites.sublime-settings ├── lib └── plist_parser.py ├── readme.md └── schemr.py /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["alt+f5"], "command": "schemr_list_schemes" }, 3 | { "keys": ["alt+f7"], "command": "schemr_cycle_schemes", "args": {"direction": "prev"}}, 4 | { "keys": ["alt+f8"], "command": "schemr_cycle_schemes", "args": {"direction": "next"}}, 5 | { "keys": ["alt+f10"], "command": "schemr_cycle_schemes", "args": {"direction": "rand"}}, 6 | 7 | { "keys": ["shift+alt+f5"], "command": "schemr_list_favorite_schemes" }, 8 | { "keys": ["shift+alt+f7"], "command": "schemr_cycle_favorite_schemes", "args": {"direction": "prev"}}, 9 | { "keys": ["shift+alt+f8"], "command": "schemr_cycle_favorite_schemes", "args": {"direction": "next"}}, 10 | { "keys": ["shift+alt+f10"], "command": "schemr_cycle_favorite_schemes", "args": {"direction": "rand"}} 11 | ] 12 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["alt+f5"], "command": "schemr_list_schemes" }, 3 | { "keys": ["alt+f7"], "command": "schemr_cycle_schemes", "args": {"direction": "prev"}}, 4 | { "keys": ["alt+f8"], "command": "schemr_cycle_schemes", "args": {"direction": "next"}}, 5 | { "keys": ["alt+f10"], "command": "schemr_cycle_schemes", "args": {"direction": "rand"}}, 6 | 7 | { "keys": ["shift+alt+f5"], "command": "schemr_list_favorite_schemes" }, 8 | { "keys": ["shift+alt+f7"], "command": "schemr_cycle_favorite_schemes", "args": {"direction": "prev"}}, 9 | { "keys": ["shift+alt+f8"], "command": "schemr_cycle_favorite_schemes", "args": {"direction": "next"}}, 10 | { "keys": ["shift+alt+f10"], "command": "schemr_cycle_favorite_schemes", "args": {"direction": "rand"}} 11 | ] 12 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["alt+f5"], "command": "schemr_list_schemes" }, 3 | { "keys": ["alt+f7"], "command": "schemr_cycle_schemes", "args": {"direction": "prev"}}, 4 | { "keys": ["alt+f8"], "command": "schemr_cycle_schemes", "args": {"direction": "next"}}, 5 | { "keys": ["alt+f10"], "command": "schemr_cycle_schemes", "args": {"direction": "rand"}}, 6 | 7 | { "keys": ["shift+alt+f5"], "command": "schemr_list_favorite_schemes" }, 8 | { "keys": ["shift+alt+f7"], "command": "schemr_cycle_favorite_schemes", "args": {"direction": "prev"}}, 9 | { "keys": ["shift+alt+f8"], "command": "schemr_cycle_favorite_schemes", "args": {"direction": "next"}}, 10 | { "keys": ["shift+alt+f10"], "command": "schemr_cycle_favorite_schemes", "args": {"direction": "rand"}} 11 | ] 12 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Schemr: List all schemes", 4 | "command": "schemr_list_schemes" 5 | }, 6 | { 7 | "caption": "Schemr: Next scheme", 8 | "command": "schemr_cycle_schemes", 9 | "args": {"direction": "next"} 10 | }, 11 | { 12 | "caption": "Schemr: Previous scheme", 13 | "command": "schemr_cycle_schemes", 14 | "args": {"direction": "prev"} 15 | }, 16 | { 17 | "caption": "Schemr: Random scheme", 18 | "command": "schemr_cycle_schemes", 19 | "args": {"direction": "rand"} 20 | }, 21 | { 22 | "caption": "Schemr: List favorite schemes", 23 | "command": "schemr_list_favorite_schemes" 24 | }, 25 | { 26 | "caption": "Schemr: Next favorite scheme", 27 | "command": "schemr_cycle_favorite_schemes", 28 | "args": {"direction": "next"} 29 | }, 30 | { 31 | "caption": "Schemr: Previous favorite scheme", 32 | "command": "schemr_cycle_favorite_schemes", 33 | "args": {"direction": "prev"} 34 | }, 35 | { 36 | "caption": "Schemr: Random favorite scheme", 37 | "command": "schemr_cycle_favorite_schemes", 38 | "args": {"direction": "rand"} 39 | }, 40 | { 41 | "caption": "Schemr: Add current scheme to favorites", 42 | "command": "schemr_favorite_current_scheme" 43 | }, 44 | { 45 | "caption": "Schemr: Remove current scheme from favorites", 46 | "command": "schemr_unfavorite_current_scheme" 47 | }, 48 | { 49 | "caption": "Schemr: Set scheme for current syntax", 50 | "command": "schemr_set_syntax_scheme" 51 | }, 52 | { 53 | "caption": "Schemr: Reset scheme for current syntax", 54 | "command": "schemr_reset_syntax_scheme" 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2015 Ben Weier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "mnemonic": "n", 5 | "id": "preferences", 6 | "children": 7 | [ 8 | { 9 | "caption": "Package Settings", 10 | "mnemonic": "P", 11 | "id": "package-settings", 12 | "children": 13 | [ 14 | { 15 | "caption": "Schemr", 16 | "children": 17 | [ 18 | { 19 | "command": "open_file", 20 | "args": { 21 | "file": "${packages}/Schemr/SchemrFavorites.sublime-settings" 22 | }, 23 | "caption": "Favorite Schemes – Default" 24 | }, 25 | 26 | { 27 | "command": "open_file", 28 | "args": { 29 | "file": "${packages}/User/SchemrFavorites.sublime-settings" 30 | }, 31 | "caption": "Favorite Schemes – User" 32 | }, 33 | 34 | { 35 | "caption": "-" 36 | }, 37 | 38 | { 39 | "command": "open_file", 40 | "args": { 41 | "file": "${packages}/Schemr/Default (OSX).sublime-keymap", 42 | "platform": "OSX" 43 | }, 44 | "caption": "Key Bindings – Default" 45 | }, 46 | { 47 | "command": "open_file", 48 | "args": { 49 | "file": "${packages}/Schemr/Default (Linux).sublime-keymap", 50 | "platform": "Linux" 51 | }, 52 | "caption": "Key Bindings – Default" 53 | }, 54 | { 55 | "command": "open_file", 56 | "args": { 57 | "file": "${packages}/Schemr/Default (Windows).sublime-keymap", 58 | "platform": "Windows" 59 | }, 60 | "caption": "Key Bindings – Default" 61 | }, 62 | { 63 | "command": "open_file", 64 | "args": { 65 | "file": "${packages}/User/Default (OSX).sublime-keymap", 66 | "platform": "OSX" 67 | }, 68 | "caption": "Key Bindings – User" 69 | }, 70 | { 71 | "command": "open_file", 72 | "args": { 73 | "file": "${packages}/User/Default (Linux).sublime-keymap", 74 | "platform": "Linux" 75 | }, 76 | "caption": "Key Bindings – User" 77 | }, 78 | { 79 | "command": "open_file", 80 | "args": { 81 | "file": "${packages}/User/Default (Windows).sublime-keymap", 82 | "platform": "Windows" 83 | }, 84 | "caption": "Key Bindings – User" 85 | } 86 | ] 87 | } 88 | ] 89 | } 90 | ] 91 | } 92 | ] 93 | -------------------------------------------------------------------------------- /SchemrFavorites.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // You can add the full paths of your favorite color schemes to this list, 3 | // e.g. "Packages/Color Scheme - Default/Monokai.tmTheme." Schemr also 4 | // modifies this list automatically when you use "Add current scheme to favorites" 5 | // or "Remove current scheme from favorites" from the command palette. 6 | "schemr_favorites": 7 | [ 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /lib/plist_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A `Property Lists`_ is a data representation used in Apple's Mac OS X as 3 | a convenient way to store standard object types, such as string, number, 4 | boolean, and container object. 5 | 6 | This file contains a class ``XmlPropertyListParser`` for parse 7 | a property list file and get back a python native data structure. 8 | 9 | :copyright: 2008 by Takanori Ishikawa 10 | :license: MIT (See LICENSE file for more details) 11 | 12 | .. _Property Lists: http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/ 13 | """ 14 | 15 | import re 16 | import sys 17 | 18 | 19 | if sys.version_info >= (3,): 20 | # Some forwards compatability 21 | basestring = str 22 | 23 | 24 | class PropertyListParseError(Exception): 25 | """Raised when parsing a property list is failed.""" 26 | pass 27 | 28 | 29 | class XmlPropertyListParser(object): 30 | """The ``XmlPropertyListParser`` class provides methods that 31 | convert `Property Lists`_ objects from xml format. 32 | Property list objects include ``string``, ``unicode``, 33 | ``list``, ``dict``, ``datetime``, and ``int`` or ``float``. 34 | 35 | :copyright: 2008 by Takanori Ishikawa 36 | :license: MIT License 37 | 38 | .. _Property List: http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/ 39 | """ 40 | 41 | def _assert(self, test, message): 42 | if not test: 43 | raise PropertyListParseError(message) 44 | 45 | # ------------------------------------------------ 46 | # SAX2: ContentHandler 47 | # ------------------------------------------------ 48 | def setDocumentLocator(self, locator): 49 | pass 50 | 51 | def startPrefixMapping(self, prefix, uri): 52 | pass 53 | 54 | def endPrefixMapping(self, prefix): 55 | pass 56 | 57 | def startElementNS(self, name, qname, attrs): 58 | pass 59 | 60 | def endElementNS(self, name, qname): 61 | pass 62 | 63 | def ignorableWhitespace(self, whitespace): 64 | pass 65 | 66 | def processingInstruction(self, target, data): 67 | pass 68 | 69 | def skippedEntity(self, name): 70 | pass 71 | 72 | def startDocument(self): 73 | self.__stack = [] 74 | self.__plist = self.__key = self.__characters = None 75 | # For reducing runtime type checking, 76 | # the parser caches top level object type. 77 | self.__in_dict = False 78 | 79 | def endDocument(self): 80 | self._assert(self.__plist is not None, "A top level element must be .") 81 | self._assert( 82 | len(self.__stack) is 0, 83 | "multiple objects at top level.") 84 | 85 | def startElement(self, name, attributes): 86 | if name in XmlPropertyListParser.START_CALLBACKS: 87 | XmlPropertyListParser.START_CALLBACKS[name](self, name, attributes) 88 | if name in XmlPropertyListParser.PARSE_CALLBACKS: 89 | self.__characters = [] 90 | 91 | def endElement(self, name): 92 | if name in XmlPropertyListParser.END_CALLBACKS: 93 | XmlPropertyListParser.END_CALLBACKS[name](self, name) 94 | if name in XmlPropertyListParser.PARSE_CALLBACKS: 95 | # Creates character string from buffered characters. 96 | content = ''.join(self.__characters) 97 | # For compatibility with ``xml.etree`` and ``plistlib``, 98 | # convert text string to ascii, if possible 99 | try: 100 | content = content.encode('ascii') 101 | except (UnicodeError, AttributeError): 102 | pass 103 | XmlPropertyListParser.PARSE_CALLBACKS[name](self, name, content) 104 | self.__characters = None 105 | 106 | def characters(self, content): 107 | if self.__characters is not None: 108 | self.__characters.append(content) 109 | 110 | # ------------------------------------------------ 111 | # XmlPropertyListParser private 112 | # ------------------------------------------------ 113 | def _push_value(self, value): 114 | if not self.__stack: 115 | self._assert(self.__plist is None, "Multiple objects at top level") 116 | self.__plist = value 117 | else: 118 | top = self.__stack[-1] 119 | #assert isinstance(top, (dict, list)) 120 | if self.__in_dict: 121 | k = self.__key 122 | if k is None: 123 | raise PropertyListParseError("Missing key for dictionary.") 124 | top[k] = value 125 | self.__key = None 126 | else: 127 | top.append(value) 128 | 129 | def _push_stack(self, value): 130 | self.__stack.append(value) 131 | self.__in_dict = isinstance(value, dict) 132 | 133 | def _pop_stack(self): 134 | self.__stack.pop() 135 | self.__in_dict = self.__stack and isinstance(self.__stack[-1], dict) 136 | 137 | def _start_plist(self, name, attrs): 138 | self._assert(not self.__stack and self.__plist is None, " more than once.") 139 | self._assert(attrs.get('version', '1.0') == '1.0', 140 | "version 1.0 is only supported, but was '%s'." % attrs.get('version')) 141 | 142 | def _start_array(self, name, attrs): 143 | v = list() 144 | self._push_value(v) 145 | self._push_stack(v) 146 | 147 | def _start_dict(self, name, attrs): 148 | v = dict() 149 | self._push_value(v) 150 | self._push_stack(v) 151 | 152 | def _end_array(self, name): 153 | self._pop_stack() 154 | 155 | def _end_dict(self, name): 156 | if self.__key is not None: 157 | raise PropertyListParseError("Missing value for key '%s'" % self.__key) 158 | self._pop_stack() 159 | 160 | def _start_true(self, name, attrs): 161 | self._push_value(True) 162 | 163 | def _start_false(self, name, attrs): 164 | self._push_value(False) 165 | 166 | def _parse_key(self, name, content): 167 | if not self.__in_dict: 168 | raise PropertyListParseError(" element must be in element.") 169 | self.__key = content 170 | 171 | def _parse_string(self, name, content): 172 | self._push_value(content) 173 | 174 | def _parse_data(self, name, content): 175 | import base64 176 | self._push_value(base64.b64decode(content)) 177 | 178 | # http://www.apple.com/DTDs/PropertyList-1.0.dtd says: 179 | # 180 | # Contents should conform to a subset of ISO 8601 181 | # (in particular, YYYY '-' MM '-' DD 'T' HH ':' MM ':' SS 'Z'. 182 | # Smaller units may be omitted with a loss of precision) 183 | DATETIME_PATTERN = re.compile(r"(?P\d\d\d\d)(?:-(?P\d\d)(?:-(?P\d\d)(?:T(?P\d\d)(?::(?P\d\d)(?::(?P\d\d))?)?)?)?)?Z$") 184 | 185 | def _parse_date(self, name, content): 186 | import datetime 187 | 188 | units = ('year', 'month', 'day', 'hour', 'minute', 'second', ) 189 | pattern = XmlPropertyListParser.DATETIME_PATTERN 190 | match = pattern.match(content) 191 | if not match: 192 | raise PropertyListParseError("Failed to parse datetime '%s'" % content) 193 | 194 | groups, components = match.groupdict(), [] 195 | for key in units: 196 | value = groups[key] 197 | if value is None: 198 | break 199 | components.append(int(value)) 200 | while len(components) < 3: 201 | components.append(1) 202 | 203 | d = datetime.datetime(*components) 204 | self._push_value(d) 205 | 206 | def _parse_real(self, name, content): 207 | self._push_value(float(content)) 208 | 209 | def _parse_integer(self, name, content): 210 | self._push_value(int(content)) 211 | 212 | START_CALLBACKS = { 213 | 'plist': _start_plist, 214 | 'array': _start_array, 215 | 'dict': _start_dict, 216 | 'true': _start_true, 217 | 'false': _start_false, 218 | } 219 | 220 | END_CALLBACKS = { 221 | 'array': _end_array, 222 | 'dict': _end_dict, 223 | } 224 | 225 | PARSE_CALLBACKS = { 226 | 'key': _parse_key, 227 | 'string': _parse_string, 228 | 'data': _parse_data, 229 | 'date': _parse_date, 230 | 'real': _parse_real, 231 | 'integer': _parse_integer, 232 | } 233 | 234 | # ------------------------------------------------ 235 | # XmlPropertyListParser 236 | # ------------------------------------------------ 237 | def _to_stream(self, io_or_string): 238 | if isinstance(io_or_string, basestring): 239 | # Creates a string stream for in-memory contents. 240 | from io import StringIO 241 | return StringIO(io_or_string) 242 | elif hasattr(io_or_string, 'read') and callable(getattr(io_or_string, 'read')): 243 | return io_or_string 244 | else: 245 | raise TypeError('Can\'t convert %s to file-like-object' % type(io_or_string)) 246 | 247 | def _parse_using_etree(self, xml_input): 248 | from xml.etree.cElementTree import iterparse 249 | 250 | parser = iterparse(self._to_stream(xml_input), events=('start', 'end')) 251 | self.startDocument() 252 | try: 253 | for action, element in parser: 254 | name = element.tag 255 | if action == 'start': 256 | if name in XmlPropertyListParser.START_CALLBACKS: 257 | XmlPropertyListParser.START_CALLBACKS[name](self, element.tag, element.attrib) 258 | elif action == 'end': 259 | if name in XmlPropertyListParser.END_CALLBACKS: 260 | XmlPropertyListParser.END_CALLBACKS[name](self, name) 261 | if name in XmlPropertyListParser.PARSE_CALLBACKS: 262 | XmlPropertyListParser.PARSE_CALLBACKS[name](self, name, element.text or "") 263 | element.clear() 264 | except SyntaxError as e: 265 | raise PropertyListParseError(e) 266 | 267 | self.endDocument() 268 | return self.__plist 269 | 270 | def _parse_using_sax_parser(self, xml_input): 271 | from xml.sax import make_parser, xmlreader, SAXParseException 272 | source = xmlreader.InputSource() 273 | source.setByteStream(self._to_stream(xml_input)) 274 | reader = make_parser() 275 | reader.setContentHandler(self) 276 | try: 277 | reader.parse(source) 278 | except SAXParseException as e: 279 | raise PropertyListParseError(e) 280 | 281 | return self.__plist 282 | 283 | def parse(self, xml_input): 284 | """Parse the property list (`.plist`, `.xml, for example) ``xml_input``, 285 | which can be either a string or a file-like object. 286 | 287 | >>> parser = XmlPropertyListParser() 288 | >>> parser.parse(r'' 289 | ... r'Python.py' 290 | ... r'') 291 | {'Python': '.py'} 292 | """ 293 | try: 294 | return self._parse_using_etree(xml_input) 295 | except ImportError: 296 | # No xml.etree.ccElementTree found. 297 | return self._parse_using_sax_parser(xml_input) 298 | 299 | 300 | def parse_string(io_or_string): 301 | """Parse a string (or a stream) and return the resulting object. 302 | """ 303 | return XmlPropertyListParser().parse(io_or_string) 304 | 305 | 306 | def parse_file(file_path): 307 | """Parse the specified file and return the resulting object. 308 | """ 309 | with open(file_path) as f: 310 | return XmlPropertyListParser().parse(f) 311 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # About 2 | [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/benweier/Schemr?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 3 | 4 | Schemr allows you to quickly change your color scheme using the command palette and keyboard shortcuts. With Schemr, you get commands to easily cycle forward, backward and randomly through your available color schemes. 5 | 6 | # Features 7 | * Full compatibility with Sublime Text 2 and 3. 8 | * Preview the selected color scheme as you navigate through the quick panel. [ST3 ONLY] 9 | * Color schemes can be favorited for even faster access. 10 | * Set syntax-specific color schemes for your favorite languages. Use your favorite schemes for your favorite languages! 11 | * Displays `[Dark]` or `[Light]` in the scheme list to easily filter by type. 12 | * Automatically loads all available `.tmTheme` files, including those found inside `.sublime-package` files. 13 | 14 | # Installation 15 | Install Schemr through [Package Control](https://packagecontrol.io/), or download and extract it into your Sublime Text `Packages` folder. 16 | 17 | # Contributors 18 | * [Max](https://github.com/SyntaxColoring) - Favorites support and code refactoring 19 | 20 | # Usage 21 | 22 | **Schemr: List schemes** displays all the available schemes in alphabetical order. 23 | 24 | * Default binding: Alt+F5 (Windows/Linux) Option+F5 (OSX) 25 | 26 | **Schemr: Next scheme** switches immediately to the alphabetically next color scheme. 27 | 28 | * Default binding: Alt+F7 (Windows/Linux) Option+F7 (OSX) 29 | 30 | **Schemr: Previous scheme** switches immediately to the alphabetically previous color scheme. 31 | 32 | * Default binding: Alt+F8 (Windows/Linux) Option+F8 (OSX) 33 | 34 | **Schemr: Random scheme** switches immediately to a random color scheme that you have installed. 35 | 36 | * Default binding: Alt+F10 (Windows/Linux) Option+F10 (OSX) 37 | 38 | ## Favorites 39 | 40 | **Schemr: Add current scheme to favorites** and **Schemr: Remove current scheme from favorites** add and remove the currently selected color scheme to your favorites list. 41 | 42 | * You can also edit your favorites list manually through **Preferences > Package Settings > Schemr**. 43 | 44 | **Schemr: List favorite schemes** displays your favorite schemes in alphabetical order. 45 | 46 | * Default binding: Alt+Shift+F5 (Windows/Linux) Option+Shift+F5 (OSX) 47 | 48 | **Schemr: Next favorite scheme** switches immediately to the alphabetically next color scheme in your favorites. 49 | 50 | * Default binding: Alt+Shift+F7 (Windows/Linux) Option+Shift+F7 (OSX) 51 | 52 | **Schemr: Previous favorite scheme** switches immediately to the alphabetically previous color scheme in your favorites. 53 | 54 | * Default binding: Alt+Shift+F8 (Windows/Linux) Option+Shift+F8 (OSX) 55 | 56 | **Schemr: Random favorite scheme** switches immediately to a random color scheme in your favorites. 57 | 58 | * Default binding: Alt+Shift+F10 (Windows/Linux) Option+Shift+F10 (OSX) 59 | 60 | ## Syntax Specific Settings 61 | 62 | Syntax specific color schemes will override the behavior of all other commands for listing and switching schemes! Reset the syntax specific scheme setting to return to the normal behavior. 63 | 64 | **Schemr: Set scheme for current syntax** displays the scheme selection list to choose a color scheme for the syntax mode of the current file. 65 | 66 | **Schemr: Reset scheme for current syntax** removes the color scheme setting for the syntax mode of the current file. Only available if a syntax specific color scheme has been set. 67 | 68 | # User Settings 69 | These settings are available to control some of Schemr's behavior. Add them to `Preferences.sublime-settings` if you wish to override the default value. 70 | 71 | `schemr_brightness_threshold`: Integer 0-255. Defaults to 100. 72 | 73 | The brightness theshold setting allows you to define where the cutoff occurs between Dark and Light themes. Higher values indicate increasing brightess approaching white, while lower values indicate decreasing brightess approaching black. 74 | 75 | `schemr_brightness_flags`: Boolean true|false. Defaults to true. 76 | 77 | The brightness flags setting allows you to disable the "[Dark]" or "[Light]" text that appears after the scheme name in the quick panel. Disabling this will turn off color scheme parsing entirely and may increase performance if you have a large number of schemes. 78 | 79 | `schemr_preview_selection`: Boolean true|false. Defaults to true. 80 | 81 | If you are using Sublime Text 3, you can enable/disable previewing the highlighted color scheme as you move through the scheme list. Some performance issues related to the SublimeLinter and Color Highlighter plugins may be resolved by disabling this setting. 82 | 83 | # Note about [SublimeLinter](https://packagecontrol.io/packages/SublimeLinter) and [Color Highlighter](https://packagecontrol.io/packages/Color%20Highlighter) 84 | 85 | To improve the user experience, Schemr filters schemes that contain `(SL)` or `(Color Highlighter)` from being listed or activated with Schemr commands. These schemes can still be enabled manually through the application menu or user settings file. 86 | 87 | If a color scheme does not define colors for the [SublimeLinter](https://packagecontrol.io/packages/SublimeLinter) or [Color Highlighter](https://packagecontrol.io/packages/Color%20Highlighter), the scheme file is extended and the written to a file in the `Packages/User` directory. If you switch between a lot of schemes this can quickly pollute the scheme list with many duplicates. Activate the base color scheme through Schemr and SublimeLinter/Color Highlighter will switch to their version automatically. 88 | -------------------------------------------------------------------------------- /schemr.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | import sys, os, re, zipfile 3 | from random import random 4 | 5 | is_ST2 = int(sublime.version()) < 3000 6 | 7 | if not is_ST2: 8 | import Schemr.lib.plist_parser as parser 9 | else: 10 | sys.path.append(os.path.join(os.path.dirname(__file__), 'lib')) 11 | import plist_parser as parser 12 | 13 | # Contains various common, internal functions for Schemr. 14 | class Schemr(object): 15 | _instance = None 16 | 17 | @classmethod 18 | def instance(cls): 19 | if not cls._instance: 20 | cls._instance = cls() 21 | return cls._instance 22 | 23 | def __init__(self): 24 | self.preferences = dict(filename = 'Preferences.sublime-settings', data = sublime.load_settings('Preferences.sublime-settings')) 25 | self.favorites = dict(filename = 'SchemrFavorites.sublime-settings', data = sublime.load_settings('SchemrFavorites.sublime-settings')) 26 | 27 | # Returns a list of all managed schemes. Each scheme is itself represented by a list 28 | # that contains, in order, (1) its pretty-printed name, (2) its path and (3) whether 29 | # or not it is favorited (True or False). 30 | def load_schemes(self): 31 | scheme_paths = [] 32 | favorites = self.get_favorites() 33 | 34 | try: # use find_resources() first for ST3. 35 | scheme_paths = sublime.find_resources('*.tmTheme') 36 | 37 | except: # fallback to walk() for ST2 38 | # Load the paths for schemes contained in zipped .sublime-package files. 39 | for root, dirs, files in os.walk(sublime.installed_packages_path()): 40 | for package in (package for package in files if package.endswith('.sublime-package')): 41 | zf = zipfile.ZipFile(os.path.join(sublime.installed_packages_path(), package)) 42 | for filename in (filename for filename in zf.namelist() if filename.endswith('.tmTheme')): 43 | filepath = os.path.join(root, package, filename).replace(sublime.installed_packages_path(), 'Packages').replace('.sublime-package', '').replace('\\', '/') 44 | scheme_paths.append(filepath) 45 | 46 | # Load the paths for schemes contained in folders. 47 | for root, dirs, files in os.walk(sublime.packages_path()): 48 | for filename in (filename for filename in files if filename.endswith('.tmTheme')): 49 | filepath = os.path.join(root, filename).replace(sublime.packages_path(), 'Packages').replace('\\', '/') 50 | scheme_paths.append(filepath) 51 | 52 | scheme_paths = self.filter_scheme_list(scheme_paths) 53 | 54 | # Given the paths of all the color schemes, add in the information for 55 | # the pretty-printed name and whether or not it's been favorited. 56 | schemes = [] 57 | for scheme_path in scheme_paths: 58 | scheme_name = self.filter_scheme_name(scheme_path) 59 | is_favorite = '' 60 | if scheme_path in favorites: is_favorite = u' \u2605' # Put a pretty star icon next to favorited schemes. :) 61 | schemes.append([scheme_name, scheme_path, is_favorite]) 62 | 63 | schemes.sort(key=lambda s: s[0].lower()) 64 | return schemes 65 | 66 | # Displayes the given schemes in a quick-panel, letting the user cycle through 67 | # them to preview them and possibly select one. The reason that this is a method 68 | # here instead of a free-standing command is that the "List all schemes" and 69 | # "List favorite schemes" commands function exactly the same except for the 70 | # underlying schemes that they operate on. This method exists to provide that 71 | # common listing functionality. 72 | def list_schemes(self, window, schemes, preferences): 73 | # Get the user-defined settings or return default values. 74 | schemr_brightness_theshold = self.preferences.get('data').get('schemr_brightness_theshold', 100) 75 | schemr_brightness_flags = self.preferences.get('data').get('schemr_brightness_flags', True) 76 | schemr_preview_selection = self.preferences.get('data').get('schemr_preview_selection', True) 77 | 78 | the_scheme_path = self.get_scheme(preferences) 79 | the_scheme_name = self.filter_scheme_name(the_scheme_path) 80 | 81 | # If the active scheme isn't part of the scheme list, then we can't skip the 82 | # selection to that point and the best we can do is start from the top of the list. 83 | try: 84 | the_index = [scheme[0] for scheme in schemes].index(the_scheme_name) 85 | except (ValueError): 86 | the_index = 0 87 | 88 | # Build the display list of color schemes. 89 | if schemr_brightness_flags: 90 | color_schemes = list() 91 | 92 | # Add a brightness flag to each scheme name if the luminance 93 | # is above or below the schemr_brightness_threshold value. 94 | for scheme in schemes: 95 | # Get the RGB value of the scheme background and convert to luminance value. 96 | rgb = self.parse_scheme(scheme[1]) 97 | flag = '' 98 | 99 | if rgb is not False: 100 | luminance = (0.2126 * rgb[0]) + (0.7152 * rgb[1]) + (0.0722 * rgb[2]) 101 | if luminance < schemr_brightness_theshold: 102 | flag = ' [Dark]' 103 | else: 104 | flag = ' [Light]' 105 | 106 | color_schemes.append([scheme[0] + flag + scheme[2], scheme[1]]) 107 | 108 | else: 109 | color_schemes = [[scheme[0] + scheme[2], scheme[1]] for scheme in schemes] 110 | 111 | # Set a selection flag to detect when the panel is first opened in some 112 | # versions of Sublime Text. This prevents the color scheme from 'flickering' 113 | # from one scheme to another as the panel jumps to the active selection. 114 | self.user_selected = False 115 | def on_highlight(index): 116 | if self.user_selected is True: 117 | self.set_scheme(color_schemes[index][1], preferences) 118 | else: 119 | self.user_selected = True 120 | 121 | try: # Attempt to enable preview-on-selection (only supported by Sublime Text 3). 122 | if schemr_preview_selection is True: 123 | window.show_quick_panel(color_schemes, lambda index: self.select_scheme(index, the_scheme_path, color_schemes, preferences), 0, the_index, on_highlight) 124 | else: 125 | window.show_quick_panel(color_schemes, lambda index: self.select_scheme(index, the_scheme_path, color_schemes, preferences), 0, the_index) 126 | except: 127 | window.show_quick_panel(color_schemes, lambda index: self.select_scheme(index, the_scheme_path, color_schemes, preferences)) 128 | 129 | def select_scheme(self, index, the_scheme_path, color_schemes, preferences): 130 | if index is -1: 131 | # Restore or erase the original scheme setting. 132 | if (the_scheme_path is not ''): 133 | self.set_scheme(the_scheme_path, preferences) 134 | sublime.save_settings(preferences.get('filename')) 135 | else: 136 | self.erase_scheme(preferences) 137 | else: 138 | # Persist the new scheme setting. 139 | self.set_scheme(color_schemes[index][1], preferences) 140 | sublime.save_settings(preferences.get('filename')) 141 | sublime.status_message('Scheme: ' + color_schemes[index][0]) 142 | 143 | # Cycles the scheme in the given direction ("next", "prev" or "rand"). 144 | def cycle_schemes(self, schemes, direction): 145 | the_scheme_name = self.filter_scheme_name(self.get_scheme(self.preferences)) 146 | num_of_schemes = len(schemes) 147 | 148 | # Try to find the current scheme path in the available schemes otherwise 149 | # start from the top of the list. Useful in case the user has manually 150 | # saved an invalid scheme path or the current scheme file is not available. 151 | try: 152 | the_index = [scheme[0] for scheme in schemes].index(the_scheme_name) 153 | except (ValueError): 154 | the_index = 0 155 | 156 | if direction == 'next': 157 | index = the_index + 1 if the_index < num_of_schemes - 1 else 0 158 | 159 | if direction == 'prev': 160 | index = the_index - 1 if the_index > 0 else num_of_schemes - 1 161 | 162 | if direction == 'rand': 163 | index = int(random() * len(schemes)) 164 | 165 | self.set_scheme(schemes[index][1], self.preferences) 166 | sublime.save_settings(self.preferences.get('filename')) 167 | sublime.status_message('Scheme: ' + schemes[index][0]) 168 | 169 | # Parse the scheme file for the background color and return the RGB values 170 | # in order to determine if the scheme is Dark or Light. Use load_resources() 171 | # first for ST3 or fallback to the absolute path for ST2. 172 | def parse_scheme(self, scheme_path): 173 | if not is_ST2: 174 | try: 175 | xml = sublime.load_resource(scheme_path) 176 | except: 177 | print('Error loading ' + scheme_path) 178 | return False 179 | try: 180 | plist = parser.parse_string(xml) 181 | except (parser.PropertyListParseError): 182 | print('Error parsing ' + scheme_path) 183 | return False 184 | else: 185 | xml = os.path.join(sublime.packages_path(), scheme_path.replace('Packages/', '')) 186 | try: 187 | plist = parser.parse_file(xml) 188 | except (parser.PropertyListParseError): 189 | print('Error parsing ' + scheme_path) 190 | return False 191 | 192 | try: 193 | background_color = plist['settings'][0]['settings']['background'].lstrip('#') 194 | except (KeyError): # tmTheme is missing a background color 195 | return False 196 | 197 | if len(background_color) is 3: 198 | # Shorthand value, e.g. #111 199 | # Repeat the values for correct base 16 conversion. 200 | r, g, b = [background_color[i:i+1] * 2 for i in range(0, 3)] 201 | else: 202 | # Full-length color value, e.g. #111111 or #FFEEEEEE 203 | # Here we assume the order of hex values is #RRGGBB 204 | # or #RRGGBBAA and only use six characters. 205 | r, g, b = [background_color[i:i+2] for i in range(0, 6, 2)] 206 | 207 | try: 208 | r, g, b = [int(n, 16) for n in (r, g, b)] 209 | except (ValueError): # Error converting the hex value 210 | return False 211 | 212 | return (r, g, b) 213 | 214 | def set_scheme(self, scheme, preferences): 215 | preferences.get('data').set('color_scheme', scheme) 216 | 217 | def get_scheme(self, preferences): 218 | return preferences.get('data').get('color_scheme', '') 219 | 220 | def erase_scheme(self, preferences): 221 | preferences.get('data').erase('color_scheme') 222 | 223 | def set_favorites(self, schemes): 224 | self.favorites.get('data').set('schemr_favorites', schemes) 225 | sublime.save_settings(self.favorites.get('filename')) 226 | 227 | def get_favorites(self): 228 | return self.favorites.get('data').get('schemr_favorites') 229 | 230 | def filter_scheme_name(self, scheme_path): 231 | regex = re.compile('(\ \(SL\))|(\ Color\ Highlighter)?.tmTheme', re.IGNORECASE) 232 | scheme_name = re.sub(regex, '', scheme_path).split('/').pop() 233 | return scheme_name 234 | 235 | def filter_scheme_list(self, scheme_list): 236 | # Filter schemes generated by known plugins. 237 | regex = re.compile('SublimeLinter|Color\ Highlighter|Colorsublime - Themes\/cache', re.IGNORECASE) 238 | return [scheme for scheme in scheme_list if not regex.search(scheme)] 239 | 240 | def find_scheme(self, scheme_path): 241 | scheme_name = self.filter_scheme_name(scheme_path) 242 | matching_paths = [path for name, path, favorited in self.load_schemes() if name == scheme_name] 243 | if len(matching_paths) is not 0: 244 | return matching_paths[0] 245 | else: 246 | return False 247 | 248 | # Called when Sublime API is ready [ST3]. 249 | def plugin_loaded(): 250 | Schemr.instance() 251 | 252 | # Display the full list of schemes available, regardless 253 | # of whether or not they are favorited. 254 | class SchemrListSchemesCommand(sublime_plugin.WindowCommand): 255 | def run(self): 256 | Schemr.instance().list_schemes(self.window, Schemr.instance().load_schemes(), Schemr.instance().preferences) 257 | 258 | # Display the list of schemes that have been favorited. 259 | # Only available if there are favorites to display. 260 | class SchemrListFavoriteSchemesCommand(sublime_plugin.WindowCommand): 261 | def run(self): 262 | Schemr.instance().list_schemes(self.window, [scheme for scheme in Schemr.instance().load_schemes() if scheme[2]], Schemr.instance().preferences) 263 | 264 | def is_enabled(self): 265 | return len(Schemr.instance().get_favorites()) > 0 266 | 267 | # SchemrFavoriteCurrentSchemeCommand and SchemrUnfavoriteCurrentSchemeCommand 268 | # work in conjunction. Only one is ever available to the user at a time, 269 | # depending on whether or not the active scheme is already favorited. 270 | class SchemrFavoriteCurrentSchemeCommand(sublime_plugin.WindowCommand): 271 | def run(self): 272 | the_scheme = Schemr.instance().find_scheme(Schemr.instance().get_scheme(Schemr.instance().preferences)) 273 | if the_scheme is not False: 274 | favorites = Schemr.instance().get_favorites() 275 | favorites.append(the_scheme) 276 | Schemr.instance().set_favorites(favorites) 277 | 278 | def is_enabled(self): 279 | return Schemr.instance().find_scheme(Schemr.instance().get_scheme(Schemr.instance().preferences)) not in Schemr.instance().get_favorites() 280 | 281 | class SchemrUnfavoriteCurrentSchemeCommand(sublime_plugin.WindowCommand): 282 | def run(self): 283 | the_scheme = Schemr.instance().find_scheme(Schemr.instance().get_scheme(Schemr.instance().preferences)) 284 | if the_scheme is not False: 285 | favorites = Schemr.instance().get_favorites() 286 | favorites.remove(the_scheme) 287 | Schemr.instance().set_favorites(favorites) 288 | 289 | def is_enabled(self): 290 | return Schemr.instance().find_scheme(Schemr.instance().get_scheme(Schemr.instance().preferences)) in Schemr.instance().get_favorites() 291 | 292 | # Cycles the full list of schemes that are available 293 | # regardless of whether or not they are favorited. 294 | class SchemrCycleSchemesCommand(sublime_plugin.WindowCommand): 295 | def run(self, direction): 296 | Schemr.instance().cycle_schemes(Schemr.instance().load_schemes(), direction) 297 | 298 | # Cycles the list of schemes that have been favorited. This command is 299 | # only available if the number of favorites is enough to cycle through. 300 | class SchemrCycleFavoriteSchemesCommand(sublime_plugin.WindowCommand): 301 | def run(self, direction): 302 | Schemr.instance().cycle_schemes([scheme for scheme in Schemr.instance().load_schemes() if scheme[2]], direction) 303 | 304 | def is_enabled(self): 305 | return len(Schemr.instance().get_favorites()) > 1 306 | 307 | class SchemrSetSyntaxSchemeCommand(sublime_plugin.TextCommand): 308 | def run(self, edit): 309 | syntax_path = self.view.settings().get('syntax') 310 | syntax_file = os.path.splitext(os.path.basename(syntax_path))[0] + '.sublime-settings' 311 | preferences = dict(filename = syntax_file, data = sublime.load_settings(syntax_file)) 312 | 313 | Schemr.instance().list_schemes(self.view.window(), Schemr.instance().load_schemes(), preferences) 314 | 315 | class SchemrResetSyntaxSchemeCommand(sublime_plugin.TextCommand): 316 | def run(self, edit): 317 | syntax_path = self.view.settings().get('syntax') 318 | syntax_file = os.path.splitext(os.path.basename(syntax_path))[0] + '.sublime-settings' 319 | 320 | sublime.load_settings(syntax_file).erase('color_scheme') 321 | sublime.save_settings(syntax_file) 322 | 323 | def is_enabled(self): 324 | syntax_path = self.view.settings().get('syntax') 325 | syntax_file = os.path.splitext(os.path.basename(syntax_path))[0] + '.sublime-settings' 326 | 327 | return sublime.load_settings(syntax_file).has('color_scheme') 328 | 329 | # These commands are provided for backwards-compatibility. 330 | # SchemrCycleSchemeCommand should be used instead. 331 | class SchemrNextSchemeCommand(sublime_plugin.WindowCommand): 332 | def run(self): 333 | self.window.run_command('schemr_cycle_schemes', {'direction': 'next'}) 334 | class SchemrPreviousSchemeCommand(sublime_plugin.WindowCommand): 335 | def run(self): 336 | self.window.run_command('schemr_cycle_schemes', {'direction': 'prev'}) 337 | class SchemrRandomSchemeCommand(sublime_plugin.WindowCommand): 338 | def run(self): 339 | self.window.run_command('schemr_cycle_schemes', {'direction': 'rand'}) 340 | 341 | if is_ST2: plugin_loaded() 342 | --------------------------------------------------------------------------------