-
Notifications
You must be signed in to change notification settings - Fork 1
/
skins.py
408 lines (342 loc) · 14.7 KB
/
skins.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# -*- coding: utf-8 -*-
import os.path
import sublime
import sublime_plugin
PREF = "Preferences"
PREF_EXT = ".sublime-settings"
PREF_USER = PREF + PREF_EXT
PREF_SKIN = "Skins" + PREF_EXT
def decode_resource(name):
"""Load and decode sublime text resource.
Arguments:
name - Name of the resource file to load.
returns:
This function always returns a valid dict object of the decoded
resource. The returned object is empty if something goes wrong.
"""
try:
return sublime.decode_value(sublime.load_resource(name)) or {}
except Exception as e:
message = "Skins: loading %s failed with %s" % (name, e)
sublime.status_message(message)
print(message)
return {}
def validate_skin(skin_data, fallback_theme=None, fallback_colors=None):
"""Check skin integrity and return the boolean result.
For a skin to be valid at least 'color_scheme' or 'theme' must exist.
If one of both values is invalid, it may be replaced with a fallback value.
Otherwise SublimeText's behavior when loading the skin is unpredictable.
SublimeLinter automatically creates and applies patched color schemes if
they doesn't contain linter icon scopes. To ensure not to break this
feature this function ensures not to apply such a hacked color scheme
directly so SublimeLinter can do his job correctly.
Arguments:
skin_data (dict):
JSON object with all settings to apply for the skin.
fallback_theme (string):
A valid theme name to inject into skin_data, if skin_data does not
contain a valid one.
fallback_colors (string):
A valid color_scheme path to inject into skin_data, if skin_data
does not contain a valid one.
"""
# check theme file
theme_name = skin_data[PREF].get("theme")
theme_ok = theme_name and sublime.find_resources(theme_name)
# check color scheme
color_scheme_ok = False
color_scheme_name = skin_data[PREF].get("color_scheme")
if color_scheme_name:
path, tail = os.path.split(color_scheme_name)
name = tail.replace(" (SL)", "")
color_schemes = sublime.find_resources(name)
if color_schemes:
# Try to find the exact path from *.skins file
resource_path = "/".join((path, name))
for found in color_schemes:
if found == resource_path:
color_scheme_ok = True
break
# Use the first found color scheme which matches 'name'
if not color_scheme_ok:
skin_data[PREF]["color_scheme"] = color_schemes[0]
color_scheme_ok = True
valid = theme_ok or color_scheme_ok
if valid:
if fallback_theme and not theme_ok:
skin_data[PREF]["theme"] = fallback_theme
if fallback_colors and not color_scheme_ok:
skin_data[PREF]["color_scheme"] = fallback_colors
return valid
def load_user_skins():
"""Open the 'Saved Skins.skins' and read all valid skins from it."""
return {name: data
for name, data in decode_resource(
"Packages/User/Saved Skins.skins").items()
if validate_skin(data)}
def save_user_skins(skins):
"""Save the skins to the 'Saved Skins.skins'."""
user_skins_file = os.path.join(
sublime.packages_path(), "User", "Saved Skins.skins")
with open(user_skins_file, "w", encoding="utf-8") as file:
file.write(sublime.encode_value(skins, True))
class SetSkinCommand(sublime_plugin.WindowCommand):
"""Implements the 'set_skin' command."""
# A sublime.Settings object of the global Sublime Text settings
prefs = None
# The last selected row index - used to debounce the search so we
# aren't apply a new theme with every keypress
last_selected = -1
def run(self, package=None, name=None):
"""Apply all visual settings stored in a skin.
If 'set_skin' is called with both args 'package' and 'name',
the provided information will be used to directly switch to
the desired skin.
sublime.run_command("set_skin", {
"package": "User", "name": "Preset 1"})
If 'package' is a string but name is not, a quick panel with all
skins provided by the package is displayed.
If at least one of the args is not a string, a quick panel with all
available skins is displayed.
sublime.run_command("set_skin")
Arguments:
package (string): name of the package providing the skin or (User)
name (string): name of the skin in the <skins>.skins file
"""
if not self.prefs:
self.prefs = sublime.load_settings(PREF_USER)
if isinstance(package, str):
if isinstance(name, str):
# directly apply new skin
for skins_file in sublime.find_resources("*.skins"):
if package in skins_file:
skin = decode_resource(skins_file).get(name)
if validate_skin(skin):
self.set_skin(package, name, skin)
else:
# show only skins provided by the package
self.show_quick_panel(filter=package)
else:
# prepare and show quick panel asynchronous
self.show_quick_panel()
def show_quick_panel(self, filter=None):
"""Display a quick panel with all available skins."""
initial_color = self.prefs.get("color_scheme")
initial_theme = self.prefs.get("theme")
initial_skin = self.prefs.get("skin")
initial_selected = -1
# a dictionary with all preferences to restore on abort
initial_prefs = {}
# the icon to display next to the skin name
icon = "💦 "
# the package and skin name to display in the quick panel
items = []
# the skin objects with all settings
skins = []
# Create the lists of all available skins.
for skins_file in sublime.find_resources("*.skins"):
package = skins_file.split("/", 2)[1]
if filter and filter != package:
continue
for name, skin in decode_resource(skins_file).items():
if validate_skin(skin, initial_theme, initial_color):
if initial_skin == "/".join((package, name)):
initial_selected = len(items)
items.append([icon + name, package])
skins.append(skin)
def on_done(index):
"""Apply selected skin if user pressed enter or revert changes.
Arguments:
index (int): Index of the selected skin if user pressed ENTER
or -1 if user aborted by pressing ESC.
"""
if index == -1:
for key, val in initial_prefs.items():
if val:
self.prefs.set(key, val)
else:
self.prefs.erase(key)
sublime.save_settings(PREF_USER)
return
name, package = items[index]
self.set_skin(package, name.strip(icon), skins[index])
def on_highlight(index):
"""Temporarily apply new skin, if quick panel selection changed.
Arguments:
index (int): Index of the highlighted skin.
"""
if index == -1:
return
self.last_selected = index
def preview_skin():
# The selected row has changed since the timeout was created.
if index != self.last_selected:
return
for key, val in skins[index][PREF].items():
# backup settings before changing the first time
if key not in initial_prefs:
initial_prefs[key] = self.prefs.get(key)
if val:
self.prefs.set(key, val)
else:
self.prefs.erase(key)
# start timer to delay the preview a little bit
sublime.set_timeout_async(preview_skin, 250)
self.window.show_quick_panel(
items=items, selected_index=initial_selected,
flags=sublime.KEEP_OPEN_ON_FOCUS_LOST,
on_select=on_done, on_highlight=on_highlight)
def set_skin(self, package, name, skin):
"""Apply all skin settings.
Arguments:
package (string): name of the package providing the skin or (User)
name (string): name of the skin in the <skins>.skins file
skin (dict): all settings to apply
"""
self.prefs.set("skin", "/".join((package, name)))
for pkg_name, pkg_prefs in skin.items():
try:
pkgs = sublime.load_settings(pkg_name + PREF_EXT)
for key, val in pkg_prefs.items():
if isinstance(val, dict):
new = pkgs.get(key)
new.update(val)
val = new
if val:
pkgs.set(key, val)
else:
pkgs.erase(key)
sublime.save_settings(pkg_name + PREF_EXT)
except Exception:
pass
class DeleteUserSkinCommand(sublime_plugin.WindowCommand):
"""Implements the 'delete_user_skin' command."""
def is_visible(self):
"""Show command only if user skins exist."""
return any(
validate_skin(data) for data in decode_resource(
"Packages/User/Saved Skins.skins").values())
def run(self, name=None):
"""Delete a user defined skin or show quick panel to select one.
Arguments:
name (string): The name of the skin to delete.
"""
skins = load_user_skins()
if not skins:
return
def delete_skin(skin):
"""Delete the skin from 'Saved Skins.skins' file."""
if skin not in skins.keys():
sublime.status_message("Skin not deleted!")
return
del skins[skin]
save_user_skins(skins)
sublime.status_message("Skin %s deleted!" % skin)
if name:
return delete_skin(name)
# the icon to display next to the skin name
icon = "🚮 "
# built quick panel items
items = [[
icon + skin,
"Delete existing skin."
] for skin in sorted(skins.keys())]
def on_done(index):
"""A quick panel item was selected."""
if index >= 0:
delete_skin(items[index][0].lstrip(icon))
# display a quick panel with all user skins
self.window.show_quick_panel(
items=items, on_select=on_done,
flags=sublime.KEEP_OPEN_ON_FOCUS_LOST)
class SaveUserSkinCommand(sublime_plugin.WindowCommand):
"""Implements the 'save_user_skin' command."""
def run(self, name=None):
"""Save visual settings as user defined skin.
If the command is called without arguments, it shows an input panel
to ask the user for the desired name to save the skin as.
sublime.run_command("save_user_skin")
The command can be called to save the current skin
with a predefined name:
sublime.run_command("save_user_skin", {"name": "Preset 1"})
Arguments:
name (string): If not None this names the skin to save the current
visual settings as.
"""
skins = load_user_skins()
def save_skin(name):
"""Save the skin with provided name."""
# Compose the new skin by loading all settings from all existing
# <pkg_name>.sublime-settings files defined in <template>.
template = sublime.load_settings(PREF_SKIN).get("skin-template")
new_skin = {}
for pkg_name, css in template.items():
val = self.transform(decode_resource(
"Packages/User/%s.sublime-settings" % pkg_name), css)
if val:
new_skin[pkg_name] = val
# Check whether the minimum requirements are met.
if not validate_skin(new_skin):
sublime.status_message("Invalid skin %s not saved!" % name)
return
# Save the skin.
skins[name] = new_skin
save_user_skins(skins)
sublime.status_message("Saved skin %s!" % name)
if name:
return save_skin(name)
# the icon to display next to the skin name
icon = "🔃 "
# built quick panel items
items = [[
"💾 Save as new skin ...",
"Enter the name in the following input panel, please."
]] + [[
icon + skin,
"Update existing skin."
] for skin in sorted(skins.keys())]
def on_done(index):
"""A quick panel item was selected."""
if index == 0:
# Save as new skin ...
self.window.show_input_panel(
"Enter skins name:", "", save_skin, None, None)
elif index > 0:
# Update existing skin.
save_skin(items[index][0].lstrip(icon))
# display a quick panel with all user skins
self.window.show_quick_panel(
items=items, on_select=on_done,
flags=sublime.KEEP_OPEN_ON_FOCUS_LOST)
@classmethod
def transform(cls, json, css):
"""Filter JSON object by a stylesheet.
This function transforms the <json> object by recursively
parsing it and returning only the child objects whose keys
match the values in the cascaded stylesheet <css>.
Arguments:
json The data source to filter
css The stylesheet used as filter
Each <object> must exist in <json>.
Each <key> and its value is read from <json> and
added to the returned object.
EXAMPLE:
<object> : [<key>, <key>, ...],
<object> : {
<object> : [<key>, <key>, ...]
}
"""
if json and css:
if isinstance(css, dict):
node = {}
for key, style in css.items():
value = cls.transform(json[key], style)
# do not add empty objects
if value:
node[key] = value
return node
if isinstance(css, list):
return {key: json[key] for key in css if key in json}
elif css in json:
return {css: json[css]}
return None