├── lib ├── __init__.py └── package_search.py ├── tests ├── __init__.py ├── test_json.py └── validate_json_format.py ├── setup.cfg ├── .travis.yml ├── scheme_editor.sublime-settings ├── Default.sublime-commands ├── .gitignore ├── README.md └── color_scheme_editor.py /lib/__init__.py: -------------------------------------------------------------------------------- 1 | """Library.""" 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit Tests.""" 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore=D202,D203,D401 3 | max-line-length=120 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.3" 4 | install: 5 | - pip install flake8 6 | - pip install flake8-docstrings 7 | - pip install pep8-naming 8 | - pip install pytest 9 | script: 10 | - py.test . 11 | - flake8 . 12 | -------------------------------------------------------------------------------- /scheme_editor.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // Enable debugging in the log file: subclrschm.log 3 | "debug": false, 4 | 5 | // Enable or disable live editing 6 | // (live editing saves to the file right after changes are made) 7 | // This is not enabled by default for open with file picker and new themes 8 | "live_edit": true, 9 | 10 | // Enable or disable direct editing 11 | // All files are copied to a temp location before editing. 12 | // If direct edit is enabled, the file will be edited directly 13 | // except in cases where the theme file is inside a sublime-packages 14 | // archive 15 | "direct_edit": false, 16 | 17 | // Allow multiple instances. By default, the editor will only allow one 18 | // instance, and if other instances are opened, there arguments will be sent 19 | // to the one already open. 20 | "multiple_instances": false, 21 | 22 | // Path of subclrschm app 23 | // Just setup call to the app. No need to setup app options as that is controlled 24 | // by the plugin. 25 | "editor": { 26 | "windows": ["python", "-m", "subclrschm"], 27 | "osx": ["python", "-m", "subclrschm"], 28 | "linux": ["python", "-m", "subclrschm"] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/test_json.py: -------------------------------------------------------------------------------- 1 | """Test JSON.""" 2 | import unittest 3 | from . import validate_json_format 4 | import os 5 | import fnmatch 6 | 7 | 8 | class TestSettings(unittest.TestCase): 9 | """Test JSON settings.""" 10 | 11 | def _get_json_files(self, pattern, folder='.'): 12 | """Get json files.""" 13 | 14 | for root, dirnames, filenames in os.walk(folder): 15 | for filename in fnmatch.filter(filenames, pattern): 16 | yield os.path.join(root, filename) 17 | for dirname in [d for d in dirnames if d not in ('.svn', '.git', '.tox')]: 18 | for f in self._get_json_files(pattern, os.path.join(root, dirname)): 19 | yield f 20 | 21 | def test_json_settings(self): 22 | """Test each JSON file.""" 23 | 24 | patterns = ( 25 | '*.sublime-settings', 26 | '*.sublime-keymap', 27 | '*.sublime-commands', 28 | '*.sublime-menu', 29 | '*.sublime-theme' 30 | ) 31 | 32 | for pattern in patterns: 33 | for f in self._get_json_files(pattern): 34 | print(f) 35 | self.assertFalse( 36 | validate_json_format.CheckJsonFormat(False, True).check_format(f), 37 | "%s does not comform to expected format!" % f 38 | ) 39 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | // Open the current theme in the editor 3 | // (copy to separate location first and set to current theme) 4 | { 5 | "caption": "SchemeEditor: Edit Scheme (file picker)", 6 | "command": "scheme_editor", 7 | "args": { "live_edit": false } 8 | }, 9 | // Creates a new theme in the editor 10 | // (copy to separate location first and set to current theme) 11 | { 12 | "caption": "SchemeEditor: Create New Scheme", 13 | "command": "scheme_editor", 14 | "args": { "action": "new", "live_edit": false } 15 | }, 16 | // Open the current theme in the editor 17 | // (copy to separate location first and set to current theme) 18 | { 19 | "caption": "SchemeEditor: Edit Current Scheme", 20 | "command": "scheme_editor", 21 | "args": { "action": "current" } 22 | }, 23 | // Search plugins for themes and choose one to edit 24 | { 25 | "caption": "SchemeEditor: Edit installed scheme", 26 | "command": "scheme_editor_get_scheme" 27 | }, 28 | // Open log file in Sublime Text 29 | { 30 | "caption": "SchemeEditor: Get Editor Log", 31 | "command": "scheme_editor_log" 32 | }, 33 | // Clear Temp Folder 34 | { 35 | "caption": "SchemeEditor: Clear Temp Folder", 36 | "command": "scheme_clear_temp" 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SchemeEditor 2 | ================= 3 | 4 | Sublime Color Scheme Editor for Sublime Text 5 | 6 | **NOTE**: As Sublime Text has moved away from tmTheme in favor of their new sublime-color-scheme format, motivation to continue working on this tool is not what it used to be. For the forseable future, all development is halted on this project. 7 | 8 | ## Supported Platforms 9 | 10 | Windows 11 | 12 | ![Windows](https://github.com/facelessuser/subclrschm/blob/master/docs/src/markdown/images/CSE_WIN.png) 13 | 14 | macOS 15 | 16 | ![macOS](https://github.com/facelessuser/subclrschm/blob/master/docs/src/markdown/images/CSE_OSX.png) 17 | 18 | Linux 19 | 20 | ![Linux](https://github.com/facelessuser/subclrschm/blob/master/docs/src/markdown/images/CSE_NIX.png) 21 | 22 | ## Installation 23 | 24 | - Install this package in your Sublime `Packages` folder. 25 | - Pip install [subclrschm](https://pypi.python.org/pypi/subclrschm/) version 2.0.2+ in your local Python installation. See install instructions here: http://facelessuser.github.io/subclrschm/installation/. 26 | - Configure your `scheme_editor.sublime-settings` file to call `subclrschm`. 27 | 28 | ```js 29 | // Path of subclrschm app 30 | // Just setup call to the app. No need to setup app options as that is controlled 31 | // by the plugin. 32 | "editor": { 33 | "windows": ["python", "-m", "subclrschm"], 34 | "osx": ["python", "-m", "subclrschm"], 35 | "linux": ["python", "-m", "subclrschm"] 36 | } 37 | ``` 38 | 39 | ## Optional Settings 40 | 41 | ```js 42 | // Enable or disable live editing 43 | // (live editing saves to the file right after changes are made) 44 | // This is not enabled by default for open with file picker and new themes 45 | "live_edit": true, 46 | 47 | // Enable or disable direct editing 48 | // All files are copied to a temp location before editing. 49 | // If direct edit is enabled, the file will be edited directly 50 | // except in cases where the theme file is inside a sublime-packages 51 | // archive 52 | "direct_edit": false, 53 | 54 | // Allow multiple instances. By default, the editor will only allow one 55 | // instance, and if other instances are opened, there arguments will be sent 56 | // to the one already open. 57 | "multiple_instances": false, 58 | ``` 59 | 60 | ## Usage 61 | 62 | All commands are available via the command palette. Here are the included commands: 63 | 64 | ```js 65 | // Open the current theme in the editor 66 | // (copy to separate location first and set to current theme) 67 | { 68 | "caption": "SchemeEditor: Edit Scheme (file picker)", 69 | "command": "scheme_editor", 70 | "args": { "live_edit": false } 71 | }, 72 | // Creates a new theme in the editor 73 | // (copy to separate location first and set to current theme) 74 | { 75 | "caption": "SchemeEditor: Create New Scheme", 76 | "command": "scheme_editor", 77 | "args": { "action": "new", "live_edit": false } 78 | }, 79 | // Open the current theme in the editor 80 | // (copy to separate location first and set to current theme) 81 | { 82 | "caption": "SchemeEditor: Edit Current Scheme", 83 | "command": "scheme_editor", 84 | "args": { "action": "current" } 85 | }, 86 | // Search plugins for themes and choose one to edit 87 | { 88 | "caption": "SchemeEditor: Edit installed scheme", 89 | "command": "scheme_editor_get_scheme" 90 | }, 91 | // Open log file in Sublime Text 92 | { 93 | "caption": "SchemeEditor: Get Editor Log", 94 | "command": "scheme_editor_log" 95 | }, 96 | // Clear Temp Folder 97 | { 98 | "caption": "SchemeEditor: Clear Temp Folder", 99 | "command": "scheme_clear_temp" 100 | } 101 | ``` 102 | 103 | ## Questions 104 | 105 | *How can I get undefined scopes into the editor?* 106 | 107 | ScopeHunter is a plugin I wrote that you can configure to put the scope of what is under the cursor into your clipboard (it also can show you a lot of other stuff, but you configure it how you want it to work). So you can use that to quickly get the scope into your clipboard, then you can open up the scheme editor. 108 | 109 | The SublimeText3 documentation has a page on scope naming: http://www.sublimetext.com/docs/3/scope_naming.html. 110 | 111 | ## License 112 | SchemeEditor plugin is released under the MIT license. 113 | 114 | Copyright (c) 2013 - 2017 Isaac Muse 115 | 116 | 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: 117 | 118 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 119 | 120 | 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. 121 | -------------------------------------------------------------------------------- /tests/validate_json_format.py: -------------------------------------------------------------------------------- 1 | """ 2 | Validate JSON format. 3 | 4 | Licensed under MIT 5 | Copyright (c) 2012-2015 Isaac Muse 6 | """ 7 | import re 8 | import codecs 9 | import json 10 | 11 | RE_LINE_PRESERVE = re.compile(r"\r?\n", re.MULTILINE) 12 | RE_COMMENT = re.compile( 13 | r'''(?x) 14 | (?P 15 | /\*[^*]*\*+(?:[^/*][^*]*\*+)*/ # multi-line comments 16 | | [ \t]*//(?:[^\r\n])* # single line comments 17 | ) 18 | | (?P 19 | "(?:\\.|[^"\\])*" # double quotes 20 | | '(?:\\.|[^'\\])*' # single quotes 21 | | .[^/"']* # everything else 22 | ) 23 | ''', 24 | re.DOTALL 25 | ) 26 | RE_TRAILING_COMMA = re.compile( 27 | r'''(?x) 28 | ( 29 | (?P 30 | , # trailing comma 31 | (?P[\s\r\n]*) # white space 32 | (?P\]) # bracket 33 | ) 34 | | (?P 35 | , # trailing comma 36 | (?P[\s\r\n]*) # white space 37 | (?P\}) # bracket 38 | ) 39 | ) 40 | | (?P 41 | "(?:\\.|[^"\\])*" # double quoted string 42 | | '(?:\\.|[^'\\])*' # single quoted string 43 | | .[^,"']* # everything else 44 | ) 45 | ''', 46 | re.DOTALL 47 | ) 48 | RE_LINE_INDENT_TAB = re.compile(r'^((\t+)?[^ \t\r\n][^\r\n]*)?\r?\n$') 49 | RE_LINE_INDENT_SPACE = re.compile(r'^(((?: {4})+)?[^ \t\r\n][^\r\n]*)?\r?\n$') 50 | RE_TRAILING_SPACES = re.compile(r'^.*?[ \t]+\r?\n?$') 51 | 52 | 53 | E_MALFORMED = "E0" 54 | E_COMMENTS = "E1" 55 | E_COMMA = "E2" 56 | W_NL_START = "W1" 57 | W_NL_END = "W2" 58 | W_INDENT = "W3" 59 | W_TRAILING_SPACE = "W4" 60 | 61 | 62 | VIOLATION_MSG = { 63 | E_MALFORMED: 'JSON content is malformed.', 64 | E_COMMENTS: 'Comments are not part of the JSON spec.', 65 | E_COMMA: 'Dangling comma found.', 66 | W_NL_START: 'Unnecessary newlines at the start of file.', 67 | W_NL_END: 'Missing a new line at the end of the file.', 68 | W_INDENT: 'Indentation Error.', 69 | W_TRAILING_SPACE: 'Trailing whitespace.' 70 | } 71 | 72 | 73 | class CheckJsonFormat(object): 74 | """ 75 | Test JSON for format irregularities. 76 | 77 | - Trailing spaces. 78 | - Inconsistent indentation. 79 | - New lines at end of file. 80 | - Unnecessary newlines at start of file. 81 | - Trailing commas. 82 | - Malformed JSON. 83 | """ 84 | 85 | def __init__(self, use_tabs=False, allow_comments=False): 86 | """Setup the settings.""" 87 | 88 | self.use_tabs = use_tabs 89 | self.allow_comments = allow_comments 90 | self.fail = False 91 | 92 | def index_lines(self, text): 93 | """Index the char range of each line.""" 94 | 95 | self.line_range = [] 96 | count = 1 97 | last = 0 98 | for m in re.finditer('\n', text): 99 | self.line_range.append((last, m.end(0) - 1, count)) 100 | last = m.end(0) 101 | count += 1 102 | 103 | def get_line(self, pt): 104 | """Get the line from char index.""" 105 | 106 | line = None 107 | for r in self.line_range: 108 | if pt >= r[0] and pt <= r[1]: 109 | line = r[2] 110 | break 111 | return line 112 | 113 | def check_comments(self, text): 114 | """ 115 | Check for JavaScript comments. 116 | 117 | Log them and strip them out so we can continue. 118 | """ 119 | 120 | def remove_comments(group): 121 | return ''.join([x[0] for x in RE_LINE_PRESERVE.findall(group)]) 122 | 123 | def evaluate(m): 124 | text = '' 125 | g = m.groupdict() 126 | if g["code"] is None: 127 | if not self.allow_comments: 128 | self.log_failure(E_COMMENTS, self.get_line(m.start(0))) 129 | text = remove_comments(g["comments"]) 130 | else: 131 | text = g["code"] 132 | return text 133 | 134 | content = ''.join(map(lambda m: evaluate(m), RE_COMMENT.finditer(text))) 135 | return content 136 | 137 | def check_dangling_commas(self, text): 138 | """ 139 | Check for dangling commas. 140 | 141 | Log them and strip them out so we can continue. 142 | """ 143 | 144 | def check_comma(g, m, line): 145 | # ,] -> ] or ,} -> } 146 | self.log_failure(E_COMMA, line) 147 | if g["square_comma"] is not None: 148 | return g["square_ws"] + g["square_bracket"] 149 | else: 150 | return g["curly_ws"] + g["curly_bracket"] 151 | 152 | def evaluate(m): 153 | g = m.groupdict() 154 | return check_comma(g, m, self.get_line(m.start(0))) if g["code"] is None else g["code"] 155 | 156 | return ''.join(map(lambda m: evaluate(m), RE_TRAILING_COMMA.finditer(text))) 157 | 158 | def log_failure(self, code, line=None): 159 | """ 160 | Log failure. 161 | 162 | Log failure code, line number (if available) and message. 163 | """ 164 | 165 | if line: 166 | print("%s: Line %d - %s" % (code, line, VIOLATION_MSG[code])) 167 | else: 168 | print("%s: %s" % (code, VIOLATION_MSG[code])) 169 | self.fail = True 170 | 171 | def check_format(self, file_name): 172 | """Initiate teh check.""" 173 | 174 | self.fail = False 175 | with codecs.open(file_name, encoding='utf-8') as f: 176 | count = 1 177 | for line in f: 178 | if count == 1 and line.strip() == '': 179 | self.log_failure(W_NL_START, count) 180 | if not line.endswith('\n'): 181 | self.log_failure(W_NL_END, count) 182 | if RE_TRAILING_SPACES.match(line): 183 | self.log_failure(W_TRAILING_SPACE, count) 184 | if self.use_tabs: 185 | if (RE_LINE_INDENT_TAB if self.use_tabs else RE_LINE_INDENT_SPACE).match(line) is None: 186 | self.log_failure(W_INDENT, count) 187 | count += 1 188 | f.seek(0) 189 | text = f.read() 190 | 191 | self.index_lines(text) 192 | text = self.check_comments(text) 193 | self.index_lines(text) 194 | text = self.check_dangling_commas(text) 195 | try: 196 | json.loads(text) 197 | except Exception as e: 198 | self.log_failure(E_MALFORMED) 199 | print(e) 200 | return self.fail 201 | 202 | 203 | if __name__ == "__main__": 204 | import sys 205 | cjf = CheckJsonFormat(False, True) 206 | cjf.check_format(sys.argv[1]) 207 | -------------------------------------------------------------------------------- /lib/package_search.py: -------------------------------------------------------------------------------- 1 | """ 2 | Submlime Text Package File Search. 3 | 4 | Licensed under MIT 5 | Copyright (c) 2012 Isaac Muse 6 | """ 7 | import sublime 8 | import re 9 | from os import walk, listdir 10 | from os.path import basename, dirname, isdir, join, normpath, splitext, exists 11 | from fnmatch import fnmatch 12 | import zipfile 13 | 14 | __all__ = ( 15 | "sublime_package_paths", 16 | "scan_for_packages", 17 | "packagename", 18 | "get_packages", 19 | "get_packages_location", 20 | "get_package_contents", 21 | "PackageSearch" 22 | ) 23 | 24 | EXCLUDE_PATTERN = re.compile(r"(?:/|^)(?:[^/]*\.(?:pyc|pyo)|\.git|\.svn|\.hg|\.DS_Store)(?=$|/)") 25 | 26 | 27 | def sublime_package_paths(): 28 | """Get all the locations where plugins live.""" 29 | 30 | return [ 31 | sublime.installed_packages_path(), 32 | join(dirname(sublime.executable_path()), "Packages"), 33 | sublime.packages_path() 34 | ] 35 | 36 | 37 | def scan_for_packages(file_path, archives=False): 38 | """Look for zipped and unzipped plugins.""" 39 | 40 | if archives: 41 | plugins = [join(file_path, item) for item in listdir(file_path) if fnmatch(item, "*.sublime-package")] 42 | else: 43 | plugins = [join(file_path, item) for item in listdir(file_path) if isdir(join(file_path, item))] 44 | 45 | return plugins 46 | 47 | 48 | def packagename(pth, normalize=False): 49 | """Get the package name from the path.""" 50 | 51 | if isdir(pth): 52 | name = basename(pth) 53 | else: 54 | name = splitext(basename(pth))[0] 55 | return name.lower() if sublime.platform() == "windows" and normalize else name 56 | 57 | 58 | def get_packages_location(): 59 | """Get all packages. Optionally disable resolving override packages.""" 60 | 61 | installed_pth, default_pth, user_pth = sublime_package_paths() 62 | 63 | installed_pkgs = scan_for_packages(installed_pth, archives=True) 64 | default_pkgs = scan_for_packages(default_pth, archives=True) 65 | user_pkgs = scan_for_packages(user_pth) 66 | 67 | return default_pkgs, installed_pkgs, user_pkgs 68 | 69 | 70 | def get_folder_resources(folder_pkg, pkg_name, content_folders, content_files): 71 | """Get resources in folder.""" 72 | 73 | if exists(folder_pkg): 74 | for base, dirs, files in walk(folder_pkg): 75 | file_objs = [] 76 | for d in dirs[:]: 77 | if EXCLUDE_PATTERN.search(d) is not None: 78 | dirs.remove(d) 79 | for f in files: 80 | if EXCLUDE_PATTERN.search(f) is None: 81 | file_name = join(base, f).replace(folder_pkg, "Packages/%s" % pkg_name, 1).replace("\\", "/") 82 | file_objs.append(file_name) 83 | content_files.append(file_name) 84 | if len(file_objs) == 0 and len(dirs) == 0: 85 | content_folders.append(base.replace(folder_pkg, "Packages/%s" % pkg_name, 1).replace("\\", "/") + "/") 86 | 87 | 88 | def in_list(x, l): 89 | """Find if x (string) is in l (list).""" 90 | 91 | found = False 92 | if sublime.platform() == "windows": 93 | for item in l: 94 | if item.lower() == x.lower(): 95 | found = True 96 | break 97 | else: 98 | found = x in l 99 | return found 100 | 101 | 102 | def get_zip_resources(zip_pkg, pkg_name, content_folders, content_files): 103 | """Get resources in archive that are not already in the lists.""" 104 | 105 | if exists(zip_pkg): 106 | with zipfile.ZipFile(zip_pkg, 'r') as z: 107 | for item in z.infolist(): 108 | file_name = item.filename 109 | if EXCLUDE_PATTERN.search(file_name) is None: 110 | package_name = "Packages/%s/%s" % (pkg_name, file_name) 111 | if package_name.endswith('/'): 112 | if not in_list(package_name, content_folders): 113 | content_folders.append(package_name) 114 | elif not package_name.endswith('/'): 115 | if not in_list(package_name, content_files): 116 | content_files.append(package_name) 117 | 118 | 119 | def get_package_contents(pkg): 120 | """Get contents of package.""" 121 | 122 | m = re.match(r"^Packages/([^/]*)/?$", pkg) 123 | assert(m is not None) 124 | pkg = m.group(1) 125 | installed_pth, default_pth, user_pth = sublime_package_paths() 126 | content_files = [] 127 | content_folders = [] 128 | 129 | get_folder_resources(join(user_pth, pkg), pkg, content_folders, content_files) 130 | get_zip_resources(join(installed_pth, "%s.sublime-package" % pkg), pkg, content_folders, content_files) 131 | get_zip_resources(join(default_pth, "%s.sublime-package" % pkg), pkg, content_folders, content_files) 132 | 133 | return content_folders + content_files 134 | 135 | 136 | def get_packages(): 137 | """Get the package names.""" 138 | 139 | installed_pth, default_pth, user_pth = sublime_package_paths() 140 | 141 | installed_pkgs = scan_for_packages(installed_pth, archives=True) 142 | default_pkgs = scan_for_packages(default_pth, archives=True) 143 | user_pkgs = scan_for_packages(user_pth) 144 | 145 | pkgs = [] 146 | for pkg_type in [user_pkgs, installed_pkgs, default_pkgs]: 147 | for pkg in pkg_type: 148 | name = packagename(pkg) 149 | if not in_list(name, pkgs): 150 | pkgs.append(name) 151 | 152 | pkgs.sort() 153 | 154 | return pkgs 155 | 156 | 157 | class PackageSearch(object): 158 | """Search packages.""" 159 | 160 | def pre_process(self, **kwargs): 161 | """Preprocess event.""" 162 | 163 | return kwargs 164 | 165 | def on_select(self, value, settings): 166 | """On select event.""" 167 | 168 | def process_file(self, value, settings): 169 | """Handle processing the file.""" 170 | 171 | ################ 172 | # Qualify Files 173 | ################ 174 | def find_files(self, files, file_path, pattern, settings, regex): 175 | """Find the file that matches the pattern.""" 176 | 177 | for f in files: 178 | if regex: 179 | if re.match(pattern, f[0], re.IGNORECASE) is not None: 180 | settings.append([f[0].replace(file_path, "").lstrip("\\").lstrip("/"), f[1]]) 181 | else: 182 | if fnmatch(f[0], pattern): 183 | settings.append([f[0].replace(file_path, "").lstrip("\\").lstrip("/"), f[1]]) 184 | 185 | ################ 186 | # Zipped 187 | ################ 188 | def walk_zip(self, settings, plugin, pattern, regex): 189 | """Walk the archived files within the plugin.""" 190 | 191 | with zipfile.ZipFile(plugin[0], 'r') as z: 192 | zipped = [(join(basename(plugin[0]), normpath(fn)), plugin[1]) for fn in sorted(z.namelist())] 193 | self.find_files(zipped, "", pattern, settings, regex) 194 | 195 | def get_zip_packages(self, settings, file_path, package_type, pattern, regex=False): 196 | """Get all the archived plugins in the plugin folder.""" 197 | 198 | plugins = [ 199 | (join(file_path, item), package_type) for item in listdir(file_path) if fnmatch(item, "*.sublime-package") 200 | ] 201 | for plugin in plugins: 202 | self.walk_zip(settings, plugin, pattern.strip(), regex) 203 | 204 | def search_zipped_files(self, settings, pattern, regex): 205 | """Search the plugin folders for archived plugins.""" 206 | 207 | st_packages = sublime_package_paths() 208 | self.get_zip_packages(settings, st_packages[0], "Installed", pattern, regex) 209 | self.get_zip_packages(settings, st_packages[1], "Default", pattern, regex) 210 | 211 | ################ 212 | # Unzipped 213 | ################ 214 | def walk(self, settings, file_path, plugin, package_type, pattern, regex=False): 215 | """Walk the files within the plugin.""" 216 | 217 | for base, dirs, files in walk(plugin): 218 | files = [(join(base, f), package_type) for f in files] 219 | self.find_files(files, file_path, pattern, settings, regex) 220 | 221 | def get_unzipped_packages(self, settings, file_path, package_type, pattern, regex=False): 222 | """Get all of the plugins in the plugin folder.""" 223 | 224 | plugins = [join(file_path, item) for item in listdir(file_path) if isdir(join(file_path, item))] 225 | for plugin in plugins: 226 | self.walk(settings, file_path, plugin, package_type, pattern.strip(), regex) 227 | 228 | def search_unzipped_files(self, settings, pattern, regex): 229 | """Search the plugin folders for unzipped packages.""" 230 | 231 | st_packages = sublime_package_paths() 232 | self.get_unzipped_packages(settings, st_packages[2], "Packages", pattern, regex) 233 | 234 | ################ 235 | # Search All 236 | ################ 237 | def find_raw(self, pattern, regex=False): 238 | """Search all packages regardless of whether it is being overridden.""" 239 | 240 | settings = [] 241 | self.search_unzipped_files(settings, pattern, regex) 242 | self.zipped_idx = len(settings) 243 | self.search_zipped_files(settings, pattern, regex) 244 | 245 | self.window.show_quick_panel( 246 | settings, 247 | lambda x: self.process_file(x, settings=settings) 248 | ) 249 | 250 | ################ 251 | # Search Override 252 | ################ 253 | def find(self, pattern, regex): 254 | """Search just the active packages. Not the ones that have been overridden.""" 255 | 256 | resources = [] 257 | if not regex: 258 | resources = sublime.find_resources(pattern) 259 | else: 260 | temp = sublime.find_resources("*") 261 | for t in temp: 262 | if re.match(pattern, t, re.IGNORECASE) is not None: 263 | resources.append(t) 264 | 265 | self.window.show_quick_panel( 266 | resources, 267 | lambda x: self.process_file(x, settings=resources), 268 | 0, 269 | 0, 270 | lambda x: self.on_select(x, settings=resources) 271 | ) 272 | 273 | def search(self, **kwargs): 274 | """Search packages.""" 275 | 276 | kwargs = self.pre_process(**kwargs) 277 | pattern = kwargs.get("pattern", None) 278 | regex = kwargs.get("regex", False) 279 | self.find_all = kwargs.get("find_all", False) 280 | 281 | if not self.find_all: 282 | self.find(pattern, regex) 283 | else: 284 | self.find_raw(pattern, regex) 285 | -------------------------------------------------------------------------------- /color_scheme_editor.py: -------------------------------------------------------------------------------- 1 | """Scheme Editor.""" 2 | import sublime 3 | import sublime_plugin 4 | import sys 5 | import os 6 | import subprocess 7 | 8 | from .lib.package_search import PackageSearch 9 | 10 | TEMP_FOLDER = "SchemeEditorTemp" 11 | TEMP_PATH = "Packages/User/%s" % TEMP_FOLDER 12 | PLUGIN_SETTINGS = 'scheme_editor.sublime-settings' 13 | PREFERENCES = 'Preferences.sublime-settings' 14 | SCHEME = "color_scheme" 15 | 16 | 17 | MSGS = { 18 | "access": '''Scheme Editor: 19 | There was a problem calling subclrschm. 20 | ''', 21 | 22 | "temp": '''Scheme Editor: 23 | Could not copy theme file to temp directory. 24 | ''', 25 | 26 | "new": '''Scheme Editor: 27 | Could not create new theme. 28 | ''' 29 | } 30 | 31 | if sys.platform.startswith('win'): 32 | _PLATFORM = "windows" 33 | elif sys.platform == "darwin": 34 | _PLATFORM = "osx" 35 | else: 36 | _PLATFORM = "linux" 37 | 38 | 39 | def get_environ(): 40 | """Get environment and force utf-8.""" 41 | 42 | import os 43 | env = {} 44 | env.update(os.environ) 45 | 46 | if _PLATFORM != 'windows': 47 | shell = env['SHELL'] 48 | p = subprocess.Popen( 49 | [shell, '-l', '-c', 'echo "#@#@#${PATH}#@#@#"'], 50 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE 51 | ) 52 | result = p.communicate()[0].decode('utf8').split('#@#@#') 53 | if len(result) > 1: 54 | bin_paths = result[1].split(':') 55 | if len(bin_paths): 56 | env['PATH'] = ':'.join(bin_paths) 57 | 58 | env['PYTHONIOENCODING'] = 'utf8' 59 | env['LANG'] = 'en_US.UTF-8' 60 | env['LC_CTYPE'] = 'en_US.UTF-8' 61 | 62 | return env 63 | 64 | 65 | def load_resource(resource, binary=False): 66 | """Load the given resource.""" 67 | 68 | bfr = None 69 | if not binary: 70 | bfr = sublime.load_resource(resource) 71 | else: 72 | bfr = sublime.load_binary_resource(resource) 73 | return bfr 74 | 75 | 76 | class SchemeEditorCommand(sublime_plugin.ApplicationCommand): 77 | """Color scheme editor command.""" 78 | 79 | def init_settings(self, action, select_theme): 80 | """Initialize the settings.""" 81 | 82 | init_okay = True 83 | # Get current color scheme 84 | self.p_settings = sublime.load_settings(PLUGIN_SETTINGS) 85 | self.settings = sublime.load_settings(PREFERENCES) 86 | self.direct_edit = bool(self.p_settings.get("direct_edit", False)) 87 | self.scheme_file = self.settings.get(SCHEME, None) 88 | self.actual_scheme_file = None 89 | self.file_select = False 90 | 91 | if action == "select": 92 | if select_theme is not None: 93 | self.scheme_file = select_theme 94 | else: 95 | init_okay = False 96 | return init_okay 97 | 98 | def prepare_theme(self, action): 99 | """Prepare the theme to be edited.""" 100 | 101 | if action != "new" and self.scheme_file is not None and (action == "current" or action == "select"): 102 | # Get real path 103 | self.actual_scheme_file = os.path.join( 104 | os.path.dirname(sublime.packages_path()), os.path.normpath(self.scheme_file) 105 | ) 106 | 107 | # If scheme cannot be found, it is most likely in an archived package 108 | if ( 109 | not os.path.exists(self.actual_scheme_file) or 110 | (not self.direct_edit and not self.scheme_file.startswith(TEMP_PATH)) 111 | ): 112 | # Create temp folder 113 | zipped_themes = os.path.join(sublime.packages_path(), "User", TEMP_FOLDER) 114 | if not os.path.exists(zipped_themes): 115 | os.makedirs(zipped_themes) 116 | 117 | # Read theme file into memory and write out to the temp directory 118 | text = load_resource(self.scheme_file, binary=True) 119 | self.actual_scheme_file = os.path.join(zipped_themes, os.path.basename(self.scheme_file)) 120 | try: 121 | with open(self.actual_scheme_file, "wb") as f: 122 | f.write(text) 123 | except: 124 | sublime.error_message(MSGS["temp"]) 125 | return 126 | 127 | # Load unarchived theme 128 | self.settings.set(SCHEME, "%s/%s" % (TEMP_PATH, os.path.basename(self.scheme_file))) 129 | elif action == "select": 130 | self.settings.set(SCHEME, self.scheme_file) 131 | elif action != "new" and action != "select": 132 | self.file_select = True 133 | 134 | def is_live_edit(self, live_edit): 135 | """Check if we should use live edit.""" 136 | 137 | return ( 138 | (live_edit is None and bool(self.p_settings.get("live_edit", True))) or 139 | (live_edit is not None and live_edit) 140 | ) 141 | 142 | def is_actual_scheme_file(self): 143 | """Check if actual scheme file.""" 144 | 145 | return self.actual_scheme_file is not None and os.path.exists(self.actual_scheme_file) 146 | 147 | def run(self, action=None, select_theme=None, live_edit=None): 148 | """Run subclrschm.""" 149 | 150 | # Init settings. Bail if returned an issue 151 | if not self.init_settings(action, select_theme): 152 | return 153 | 154 | # Prepare the theme to be edited 155 | # Copy to a temp location if desired before editing 156 | self.prepare_theme(action) 157 | 158 | # Call the editor with the theme file 159 | try: 160 | cmd = ( 161 | self.p_settings.get('editor', {}).get(sublime.platform(), ['python', '-m', 'subclrschm']) + 162 | (["--debug"] if bool(self.p_settings.get("debug", False)) else []) + 163 | (["-n"] if action == "new" else []) + 164 | (["-s"] if self.file_select else []) + 165 | (["-L"] if self.is_live_edit(live_edit) else []) + 166 | ["-l", os.path.join(sublime.packages_path(), "User")] + 167 | ([self.actual_scheme_file] if self.is_actual_scheme_file() else []) 168 | ) 169 | print(cmd) 170 | subprocess.Popen( 171 | cmd, 172 | env=get_environ() 173 | ) 174 | except Exception as e: 175 | print("SchemeEditor: " + str(e)) 176 | sublime.error_message(MSGS["access"]) 177 | 178 | 179 | class SchemeEditorGetSchemeCommand(sublime_plugin.WindowCommand, PackageSearch): 180 | """Get color scheme files.""" 181 | 182 | def on_select(self, value, settings): 183 | """Process selected menu item.""" 184 | 185 | if value != -1: 186 | preferences = sublime.load_settings(PREFERENCES) 187 | preferences.set(SCHEME, settings[value]) 188 | 189 | def process_file(self, value, settings): 190 | """Process the file.""" 191 | 192 | if value != -1: 193 | if self.edit: 194 | sublime.run_command( 195 | "scheme_editor", 196 | {"action": "select", "select_theme": settings[value]} 197 | ) 198 | else: 199 | preferences = sublime.load_settings(PREFERENCES) 200 | preferences.set(SCHEME, settings[value]) 201 | else: 202 | if self.current_color_scheme is not None: 203 | preferences = sublime.load_settings(PREFERENCES) 204 | preferences.set(SCHEME, self.current_color_scheme) 205 | 206 | def pre_process(self, **kwargs): 207 | """Pre-process actions.""" 208 | 209 | self.edit = kwargs.get("edit", True) 210 | self.current_color_scheme = sublime.load_settings("Preferences.sublime-settings").get("color_scheme") 211 | return {"pattern": "*.tmTheme"} 212 | 213 | def run(self, **kwargs): 214 | """Run the command.""" 215 | 216 | self.search(**kwargs) 217 | 218 | 219 | class SchemeEditorLogCommand(sublime_plugin.WindowCommand): 220 | """Color scheme editor log command.""" 221 | 222 | def run(self): 223 | """Run the command.""" 224 | 225 | log = os.path.join(sublime.packages_path(), "User", "subclrschm.log") 226 | if os.path.exists(log): 227 | self.window.open_file(log) 228 | 229 | 230 | class SchemeEditorClearTempCommand(sublime_plugin.ApplicationCommand): 231 | """Color scheme editor clear temp folder command.""" 232 | 233 | def run(self): 234 | """Run the command.""" 235 | 236 | current_scheme = sublime.load_settings(PREFERENCES).get(SCHEME) 237 | using_temp = current_scheme.startswith(TEMP_PATH) 238 | folder = os.path.join(sublime.packages_path(), "User", TEMP_FOLDER) 239 | for f in os.listdir(folder): 240 | pth = os.path.join(folder, f) 241 | try: 242 | if ( 243 | os.path.isfile(pth) and 244 | ( 245 | not using_temp or ( 246 | os.path.basename(pth) != os.path.basename(current_scheme) 247 | ) 248 | ) 249 | ): 250 | os.unlink(pth) 251 | except: 252 | print("SchemeEditor: Could not remove %s!" % pth) 253 | 254 | 255 | def delete_old_binary(): 256 | """Delete old binary.""" 257 | 258 | import shutil 259 | 260 | binpath = os.path.normpath("${Packages}/User/subclrschm").replace("${Packages}", sublime.packages_path()) 261 | osbinpath = os.path.join(binpath, "subclrschm-bin-%s" % sublime.platform()) 262 | try: 263 | if os.path.exists(binpath): 264 | if os.path.isdir(binpath): 265 | if os.path.exists(osbinpath): 266 | shutil.rmtree(osbinpath, onerror=on_rm_error) 267 | else: 268 | os.unlink(binpath) 269 | except Exception as e: 270 | print("SchemeEditor: " + str(e)) 271 | 272 | 273 | def on_rm_error(func, path, exc_info): 274 | """Try and handle rare windows delete issue gracefully.""" 275 | 276 | import stat 277 | 278 | # excvalue = exc_info[1] 279 | if func in (os.rmdir, os.unlink): 280 | os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777 281 | try: 282 | func(path) 283 | except: 284 | if sublime.platform() == "windows": 285 | # Why are you being so stubborn windows? 286 | # This situation only randomly occurs 287 | print("SchemeEditor: Windows is being stubborn...go through rmdir to remove temp folder") 288 | cmd = ["rmdir", "/S", path] 289 | startupinfo = subprocess.STARTUPINFO() 290 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 291 | process = subprocess.Popen( 292 | cmd, 293 | startupinfo=startupinfo, 294 | stdout=subprocess.PIPE, 295 | stderr=subprocess.STDOUT, 296 | stdin=subprocess.PIPE, 297 | shell=False 298 | ) 299 | returncode = process.returncode 300 | if returncode: 301 | print("SchemeEditor: Why won't you play nice, Windows!") 302 | print("SchemeEditor:\n" + str(process.communicate()[0])) 303 | raise 304 | else: 305 | raise 306 | else: 307 | raise 308 | 309 | 310 | def init_plugin(): 311 | """Init the plugin.""" 312 | 313 | delete_old_binary() 314 | p_settings = sublime.load_settings(PLUGIN_SETTINGS) 315 | p_settings.clear_on_change('reload') 316 | p_settings.add_on_change('reload', init_plugin) 317 | 318 | 319 | def plugin_loaded(): 320 | """Load the plugin.""" 321 | 322 | sublime.set_timeout(init_plugin, 3000) 323 | --------------------------------------------------------------------------------