├── .python-version ├── Preferences.sublime-settings ├── Default (OSX).sublime-keymap ├── Default (Linux).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.skins ├── Default.sublime-commands ├── Skins.sublime-settings ├── Main.sublime-menu ├── LICENSE ├── README.md └── skins.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 -------------------------------------------------------------------------------- /Preferences.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // The UI skin set by Skins package 3 | "skin": "" 4 | } -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": [ "super+f12" ], 4 | "command": "set_skin" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": [ "ctrl+f12" ], 4 | "command": "set_skin" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": [ "ctrl+f12" ], 4 | "command": "set_skin" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /Default.skins: -------------------------------------------------------------------------------- 1 | { 2 | "Monokai (Adaptive)": { 3 | "Preferences": { 4 | "color_scheme": "Packages/Color Scheme - Default/Monokai.tmTheme", 5 | "theme": "Adaptive.sublime-theme" 6 | } 7 | }, 8 | "Monokai": { 9 | "Preferences": { 10 | "color_scheme": "Packages/Color Scheme - Default/Monokai.tmTheme", 11 | "theme": "Default.sublime-theme" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "UI: Select Skin", 4 | "command": "set_skin" 5 | }, 6 | { 7 | "caption": "UI: Delete Skin", 8 | "command": "delete_user_skin" 9 | }, 10 | { 11 | "caption": "UI: Save Skin", 12 | "command": "save_user_skin" 13 | }, 14 | { 15 | "caption": "Preferences: Skins Settings", 16 | "command": "edit_settings", 17 | "args": 18 | { 19 | "base_file": "${packages}/Skins/Skins.sublime-settings", 20 | "default": "{\n\t$0\n}\n" 21 | } 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /Skins.sublime-settings: -------------------------------------------------------------------------------- 1 | // A list of .sublime-settings and the structure of the 2 | // settings to store from and apply to from a *.sublime-themepack 3 | { 4 | "skin-template": 5 | { 6 | "Preferences": 7 | [ 8 | "color_scheme", 9 | "theme", 10 | "font_face", 11 | "font_size", 12 | "git_gutter_theme" 13 | ], 14 | "GitGutter": 15 | [ 16 | "theme" 17 | ], 18 | "SublimeLinter": 19 | { 20 | "user": 21 | [ 22 | "error_color", 23 | "gutter_theme", 24 | "warning_color" 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "preferences", 4 | "children": 5 | [ 6 | { 7 | "caption": "Package Settings", 8 | "mnemonic": "P", 9 | "id": "package-settings", 10 | "children": 11 | [ 12 | { 13 | "caption": "Skins", 14 | "children": 15 | [ 16 | { 17 | "caption": "Settings", 18 | "command": "edit_settings", 19 | "args": 20 | { 21 | "base_file": "${packages}/Skins/Skins.sublime-settings", 22 | "default": "{\n\t$0\n}\n" 23 | } 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | ] 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016, Deathaxe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Skins](https://github.com/deathaxe/sublime-skins) 2 | 3 | [![License](https://img.shields.io/github/license/deathaxe/sublime-skins.svg?style=flat-square)](LICENSE) 4 | [![Package Control](https://img.shields.io/packagecontrol/dt/Skins.svg?style=flat-square)](https://packagecontrol.io/packages/Skins) 5 | 6 | **Skins** gives users the ability to change their current **Sublime Text** color scheme and theme with a single command. When a skin is selected, a certain set of settings is applied to Sublime Text. Skins can be provided in theme packages such as [Boxy Theme](https://github.com/ihodev/sublime-boxy) or they can be created by users themselves by saving the current settings to a new User Skin. 7 | 8 | ![screenshot](https://cloud.githubusercontent.com/assets/16542113/25050093/aae66aa0-2145-11e7-9edd-acd019ac5610.gif) 9 | 10 | ### End Users 11 | 12 | #### General Usage 13 | 14 | 1. Open the Command Palette 15 | 2. Type one of the following three commands: 16 | * `UI: Select Skin` 17 | * `UI: Save Skin` 18 | * `UI: Delete Skin`. 19 | 20 | ##### Keyboard shortcuts 21 | 22 | To quickly open the `UI: Select Skin` menu use: 23 | 24 | * Ctrl+F12 on Windows / Linux 25 | * Super+F12 on macOS 26 | 27 | #### Settings 28 | 29 | By default the following settings are stored by `Save User Skin` 30 | 31 | * `color_scheme` 32 | * `theme` 33 | * `font_face` 34 | * `font_size` 35 | 36 | To edit the settings 37 | 38 | 1. Open the Command Palette 39 | 2. Type `Preferences: Skins Settings` 40 | 41 | The settings are stored in `Packages/User/Skins.sublime-settings`. 42 | 43 | **Example** 44 | 45 | ```json 46 | "skin-template": 47 | { 48 | // List of settings to load from / save to Preferences.sublime-settings 49 | "Preferences": 50 | [ 51 | "color_scheme", 52 | "theme", 53 | "font_face", 54 | "font_size" 55 | ], 56 | // List of settings to load from / save to SublimeLinter.sublime-settings 57 | "SublimeLinter": 58 | { 59 | "user": 60 | [ 61 | "error_color", 62 | "gutter_theme", 63 | "warning_color" 64 | ] 65 | } 66 | } 67 | ``` 68 | 69 | ### Theme Developers 70 | 71 | #### General 72 | 73 | **Skins** parses all `*.skins` files in all packages. They are expected to store a collection of settings for sublime text and other packages. More than one skins file can exist in a package. The name of the file does not matter, but the names of the skins inside must be unique per package. The quick panel will show these names. The `Package` providing it is displayed in the second row as a kind of description. 74 | 75 | #### File Format 76 | 77 | ```json 78 | { 79 | // skin 80 | "Boxy Tomorrow (Green)": { 81 | // Packages/User/Preferences.sublime-settings 82 | "Preferences": { 83 | "color_scheme": "Packages/Boxy Theme/schemes/Boxy Tomorrow.tmTheme", 84 | "theme": "Boxy Tomorrow.sublime-theme", 85 | "theme_accent_green" : true, 86 | "theme_accent_orange": null, 87 | "theme_accent_purple": null 88 | }, 89 | // Packages/User/SublimeLinter.sublime-settings 90 | "SublimeLinter": { 91 | "user": { 92 | // ... 93 | } 94 | } 95 | }, 96 | 97 | // skin 98 | "Monokai 2": { 99 | // ... 100 | }, 101 | 102 | // ... 103 | } 104 | ``` 105 | 106 | Each child node of a skin represents the settings to be written to a `Packages/User/*.sublime-settings` file. Therefore settings can be provided not only for `Sublime Text` but for any installed package such as `SublimeLinter`. A skin must at least contain the `Preferences` node with `color_scheme` or `theme` settings to be valid but may include any other setting accepted by `Sublime Text`. 107 | 108 | Settings with `null` value, are deleted in the sublime-settings files. 109 | 110 | #### Commands 111 | 112 | `Skins` exports the following `commands` to directly interact with all available skins. They can be used to create key bindings or command shortcuts to the most frequent used skins. 113 | 114 | ##### Set Skins 115 | 116 | To open a quick panel with all available skins call: 117 | 118 | ```json 119 | "command": "set_skin" 120 | "args": { } 121 | ``` 122 | 123 | To open a quick panel with all skins provided by a single package call: 124 | 125 | ```json 126 | "command": "set_skin", 127 | "args": { "package": "Skins" } 128 | ``` 129 | 130 | To directly apply a certain predefined skin call: 131 | 132 | ```json 133 | "command": "set_skin", 134 | "args": { "package": "Skins", "name": "Monokai" } 135 | ``` 136 | 137 | To apply a saved user skin call: 138 | 139 | ```json 140 | "command": "set_skin", 141 | "args": { "package": "User", "name": "Preset 01" } 142 | ``` 143 | 144 | 145 | ##### Save Skins 146 | 147 | The following example will directly save the current look and feel as `Preset 01` in the `Packages/User/Saved Skins.skins` file. 148 | 149 | ```json 150 | "command": "save_user_skin", 151 | "args": { "name": "Preset 01" } 152 | ``` 153 | 154 | ##### Delete Skins 155 | 156 | The following example will directly delete `Preset 01` from the `Packages/User/Saved Skins.skins` file. 157 | 158 | ```json 159 | "command": "delete_user_skin", 160 | "args": { "name": "Preset 01" } 161 | ``` 162 | 163 | ### Inspired by 164 | 165 | * [`chmln/Theme Menu Switcher`](https://github.com/chmln/sublime-text-theme-switcher-menu) 166 | * [`chrislongo/QuickThemes`](https://github.com/chrislongo/QuickThemes) 167 | -------------------------------------------------------------------------------- /skins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os.path 3 | import sublime 4 | import sublime_plugin 5 | 6 | PREF = "Preferences" 7 | PREF_EXT = ".sublime-settings" 8 | PREF_USER = PREF + PREF_EXT 9 | PREF_SKIN = "Skins" + PREF_EXT 10 | 11 | 12 | def decode_resource(name): 13 | """Load and decode sublime text resource. 14 | 15 | Arguments: 16 | name - Name of the resource file to load. 17 | 18 | returns: 19 | This function always returns a valid dict object of the decoded 20 | resource. The returned object is empty if something goes wrong. 21 | """ 22 | try: 23 | return sublime.decode_value(sublime.load_resource(name)) or {} 24 | except Exception as e: 25 | message = "Skins: loading %s failed with %s" % (name, e) 26 | sublime.status_message(message) 27 | print(message) 28 | return {} 29 | 30 | 31 | def validate_skin(skin_data, fallback_theme=None, fallback_colors=None): 32 | """Check skin integrity and return the boolean result. 33 | 34 | For a skin to be valid at least 'color_scheme' or 'theme' must exist. 35 | If one of both values is invalid, it may be replaced with a fallback value. 36 | Otherwise SublimeText's behavior when loading the skin is unpredictable. 37 | 38 | SublimeLinter automatically creates and applies patched color schemes if 39 | they doesn't contain linter icon scopes. To ensure not to break this 40 | feature this function ensures not to apply such a hacked color scheme 41 | directly so SublimeLinter can do his job correctly. 42 | 43 | Arguments: 44 | skin_data (dict): 45 | JSON object with all settings to apply for the skin. 46 | fallback_theme (string): 47 | A valid theme name to inject into skin_data, if skin_data does not 48 | contain a valid one. 49 | fallback_colors (string): 50 | A valid color_scheme path to inject into skin_data, if skin_data 51 | does not contain a valid one. 52 | """ 53 | # check theme file 54 | theme_name = skin_data[PREF].get("theme") 55 | theme_ok = theme_name and sublime.find_resources(theme_name) 56 | # check color scheme 57 | color_scheme_ok = False 58 | color_scheme_name = skin_data[PREF].get("color_scheme") 59 | if color_scheme_name: 60 | path, tail = os.path.split(color_scheme_name) 61 | name = tail.replace(" (SL)", "") 62 | color_schemes = sublime.find_resources(name) 63 | if color_schemes: 64 | # Try to find the exact path from *.skins file 65 | resource_path = "/".join((path, name)) 66 | for found in color_schemes: 67 | if found == resource_path: 68 | color_scheme_ok = True 69 | break 70 | # Use the first found color scheme which matches 'name' 71 | if not color_scheme_ok: 72 | skin_data[PREF]["color_scheme"] = color_schemes[0] 73 | color_scheme_ok = True 74 | valid = theme_ok or color_scheme_ok 75 | if valid: 76 | if fallback_theme and not theme_ok: 77 | skin_data[PREF]["theme"] = fallback_theme 78 | if fallback_colors and not color_scheme_ok: 79 | skin_data[PREF]["color_scheme"] = fallback_colors 80 | return valid 81 | 82 | 83 | def load_user_skins(): 84 | """Open the 'Saved Skins.skins' and read all valid skins from it.""" 85 | return {name: data 86 | for name, data in decode_resource( 87 | "Packages/User/Saved Skins.skins").items() 88 | if validate_skin(data)} 89 | 90 | 91 | def save_user_skins(skins): 92 | """Save the skins to the 'Saved Skins.skins'.""" 93 | user_skins_file = os.path.join( 94 | sublime.packages_path(), "User", "Saved Skins.skins") 95 | with open(user_skins_file, "w", encoding="utf-8") as file: 96 | file.write(sublime.encode_value(skins, True)) 97 | 98 | 99 | class SetSkinCommand(sublime_plugin.WindowCommand): 100 | """Implements the 'set_skin' command.""" 101 | 102 | # A sublime.Settings object of the global Sublime Text settings 103 | prefs = None 104 | 105 | # The last selected row index - used to debounce the search so we 106 | # aren't apply a new theme with every keypress 107 | last_selected = -1 108 | 109 | def run(self, package=None, name=None): 110 | """Apply all visual settings stored in a skin. 111 | 112 | If 'set_skin' is called with both args 'package' and 'name', 113 | the provided information will be used to directly switch to 114 | the desired skin. 115 | 116 | sublime.run_command("set_skin", { 117 | "package": "User", "name": "Preset 1"}) 118 | 119 | If 'package' is a string but name is not, a quick panel with all 120 | skins provided by the package is displayed. 121 | 122 | If at least one of the args is not a string, a quick panel with all 123 | available skins is displayed. 124 | 125 | sublime.run_command("set_skin") 126 | 127 | Arguments: 128 | package (string): name of the package providing the skin or (User) 129 | name (string): name of the skin in the .skins file 130 | """ 131 | if not self.prefs: 132 | self.prefs = sublime.load_settings(PREF_USER) 133 | 134 | if isinstance(package, str): 135 | if isinstance(name, str): 136 | # directly apply new skin 137 | for skins_file in sublime.find_resources("*.skins"): 138 | if package in skins_file: 139 | skin = decode_resource(skins_file).get(name) 140 | if validate_skin(skin): 141 | self.set_skin(package, name, skin) 142 | else: 143 | # show only skins provided by the package 144 | self.show_quick_panel(filter=package) 145 | else: 146 | # prepare and show quick panel asynchronous 147 | self.show_quick_panel() 148 | 149 | def show_quick_panel(self, filter=None): 150 | """Display a quick panel with all available skins.""" 151 | initial_color = self.prefs.get("color_scheme") 152 | initial_theme = self.prefs.get("theme") 153 | initial_skin = self.prefs.get("skin") 154 | initial_selected = -1 155 | # a dictionary with all preferences to restore on abort 156 | initial_prefs = {} 157 | # the icon to display next to the skin name 158 | icon = "💦 " 159 | # the package and skin name to display in the quick panel 160 | items = [] 161 | # the skin objects with all settings 162 | skins = [] 163 | 164 | # Create the lists of all available skins. 165 | for skins_file in sublime.find_resources("*.skins"): 166 | package = skins_file.split("/", 2)[1] 167 | if filter and filter != package: 168 | continue 169 | for name, skin in decode_resource(skins_file).items(): 170 | if validate_skin(skin, initial_theme, initial_color): 171 | if initial_skin == "/".join((package, name)): 172 | initial_selected = len(items) 173 | items.append([icon + name, package]) 174 | skins.append(skin) 175 | 176 | def on_done(index): 177 | """Apply selected skin if user pressed enter or revert changes. 178 | 179 | Arguments: 180 | index (int): Index of the selected skin if user pressed ENTER 181 | or -1 if user aborted by pressing ESC. 182 | """ 183 | if index == -1: 184 | for key, val in initial_prefs.items(): 185 | if val: 186 | self.prefs.set(key, val) 187 | else: 188 | self.prefs.erase(key) 189 | sublime.save_settings(PREF_USER) 190 | return 191 | name, package = items[index] 192 | self.set_skin(package, name.strip(icon), skins[index]) 193 | 194 | def on_highlight(index): 195 | """Temporarily apply new skin, if quick panel selection changed. 196 | 197 | Arguments: 198 | index (int): Index of the highlighted skin. 199 | """ 200 | if index == -1: 201 | return 202 | 203 | self.last_selected = index 204 | 205 | def preview_skin(): 206 | # The selected row has changed since the timeout was created. 207 | if index != self.last_selected: 208 | return 209 | for key, val in skins[index][PREF].items(): 210 | # backup settings before changing the first time 211 | if key not in initial_prefs: 212 | initial_prefs[key] = self.prefs.get(key) 213 | if val: 214 | self.prefs.set(key, val) 215 | else: 216 | self.prefs.erase(key) 217 | # start timer to delay the preview a little bit 218 | sublime.set_timeout_async(preview_skin, 250) 219 | 220 | self.window.show_quick_panel( 221 | items=items, selected_index=initial_selected, 222 | flags=sublime.KEEP_OPEN_ON_FOCUS_LOST, 223 | on_select=on_done, on_highlight=on_highlight) 224 | 225 | def set_skin(self, package, name, skin): 226 | """Apply all skin settings. 227 | 228 | Arguments: 229 | package (string): name of the package providing the skin or (User) 230 | name (string): name of the skin in the .skins file 231 | skin (dict): all settings to apply 232 | """ 233 | self.prefs.set("skin", "/".join((package, name))) 234 | for pkg_name, pkg_prefs in skin.items(): 235 | try: 236 | pkgs = sublime.load_settings(pkg_name + PREF_EXT) 237 | for key, val in pkg_prefs.items(): 238 | if isinstance(val, dict): 239 | new = pkgs.get(key) 240 | new.update(val) 241 | val = new 242 | if val: 243 | pkgs.set(key, val) 244 | else: 245 | pkgs.erase(key) 246 | sublime.save_settings(pkg_name + PREF_EXT) 247 | except Exception: 248 | pass 249 | 250 | 251 | class DeleteUserSkinCommand(sublime_plugin.WindowCommand): 252 | """Implements the 'delete_user_skin' command.""" 253 | 254 | def is_visible(self): 255 | """Show command only if user skins exist.""" 256 | return any( 257 | validate_skin(data) for data in decode_resource( 258 | "Packages/User/Saved Skins.skins").values()) 259 | 260 | def run(self, name=None): 261 | """Delete a user defined skin or show quick panel to select one. 262 | 263 | Arguments: 264 | name (string): The name of the skin to delete. 265 | """ 266 | skins = load_user_skins() 267 | if not skins: 268 | return 269 | 270 | def delete_skin(skin): 271 | """Delete the skin from 'Saved Skins.skins' file.""" 272 | if skin not in skins.keys(): 273 | sublime.status_message("Skin not deleted!") 274 | return 275 | del skins[skin] 276 | save_user_skins(skins) 277 | sublime.status_message("Skin %s deleted!" % skin) 278 | 279 | if name: 280 | return delete_skin(name) 281 | 282 | # the icon to display next to the skin name 283 | icon = "🚮 " 284 | # built quick panel items 285 | items = [[ 286 | icon + skin, 287 | "Delete existing skin." 288 | ] for skin in sorted(skins.keys())] 289 | 290 | def on_done(index): 291 | """A quick panel item was selected.""" 292 | if index >= 0: 293 | delete_skin(items[index][0].lstrip(icon)) 294 | 295 | # display a quick panel with all user skins 296 | self.window.show_quick_panel( 297 | items=items, on_select=on_done, 298 | flags=sublime.KEEP_OPEN_ON_FOCUS_LOST) 299 | 300 | 301 | class SaveUserSkinCommand(sublime_plugin.WindowCommand): 302 | """Implements the 'save_user_skin' command.""" 303 | 304 | def run(self, name=None): 305 | """Save visual settings as user defined skin. 306 | 307 | If the command is called without arguments, it shows an input panel 308 | to ask the user for the desired name to save the skin as. 309 | 310 | sublime.run_command("save_user_skin") 311 | 312 | The command can be called to save the current skin 313 | with a predefined name: 314 | 315 | sublime.run_command("save_user_skin", {"name": "Preset 1"}) 316 | 317 | Arguments: 318 | name (string): If not None this names the skin to save the current 319 | visual settings as. 320 | """ 321 | skins = load_user_skins() 322 | 323 | def save_skin(name): 324 | """Save the skin with provided name.""" 325 | 326 | # Compose the new skin by loading all settings from all existing 327 | # .sublime-settings files defined in