GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

RainmeterColorPickCommand.__run_picker()   B
last analyzed

Complexity

Conditions 6

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 10
Bugs 0 Features 0
Metric Value
cc 6
c 10
b 0
f 0
dl 0
loc 28
rs 7.5384
1
"""
2
This module is about the integration with the color picker.
3
4
The color picker can detect a color in a substring
5
and launch a tool to display the current color,
6
change it and thus also replace the old color.
7
8
It supports both ways Rainmeter defines color.
9
10
* RRGGBB
11
* RRGGBBAA
12
* RRR,GGG,BBB
13
* RRR,GGG,BBB,AAA
14
15
which is hexadecimal and decimal format.
16
"""
17
18
import os
19
import re
20
import subprocess
21
22
import sublime
23
import sublime_plugin
24
25
from . import logger
26
from .color import converter
27
28
29
class RainmeterReplaceColorCommand(sublime_plugin.TextCommand):  # pylint: disable=R0903; we only need one method
30
31
    """
32
    Replace a region with a text.
33
34
    This command is required because the edit objects passed to TextCommand
35
    are not transferable. We have to call this from the other command
36
    to get a valid edit object.
37
    """
38
39
    def run(self, edit, **args):
40
        """
41
        Method is provided by Sublime Text through the super class TextCommand.
42
43
        This is run automatically if you initialize the command
44
        through an "command": "rainmeter_replace_color" command
45
        with additional arguments:
46
47
        * low: start of the region to replace
48
        * high: end of the region to replace
49
        * output: text which will replace the region
50
        """
51
        low = args["low"]
52
        high = args["high"]
53
        output = args["output"]
54
55
        region = sublime.Region(low, high)
56
        original_str = self.view.substr(region)
57
        self.view.replace(edit, region, output)
58
59
        logger.info("Replacing '" + original_str + "' with '" + output + "'")
60
61
62
# encodes RRR,GGG,BBB,AAA with optional alpha channel and supporting all numbers from 0 to 999
63
# converter will check for 255
64
# numbers can be spaced anyway
65
DEC_COLOR_EXP = re.compile(r"(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d{1,3}))?")
66
# support lower and upper case hexadecimal with optional alpha channel
67
HEX_COLOR_EXP = re.compile(r"(?:[0-9a-fA-F]{2}){3,4}")
68
69
70
def color_or_default(color):
71
    """If the color is none it returns FFFFFFFF."""
72
    return "FFFFFFFF" if color is None else color
73
74
75
class RainmeterColorPickCommand(sublime_plugin.TextCommand):  # pylint: disable=R0903; we only need one method
76
    """Sublime Text integration running this through an action."""
77
78
    def run(self, dummy_edit, **dummy_args):
79
        """
80
        Method is provided by Sublime Text through the super class TextCommand.
81
82
        This is run automatically if you initialize the command
83
        through an "command": "rainmeter_color_pick" command.
84
        """
85
        sublime.set_timeout_async(self.__run_picker, 0)
86
87
    def __get_first_selection(self):
88
        selections = self.view.sel()
89
        first_selection = selections[0]
90
91
        return first_selection
92
93
    def __get_selected_line_index(self):
94
        first_selection = self.__get_first_selection()
95
        selection_start = first_selection.begin()
96
        line_cursor = self.view.line(selection_start)
97
        line_index = line_cursor.begin()
98
99
        return line_index
100
101
    def __get_selected_line_content(self):
102
        first_selection = self.__get_first_selection()
103
        selection_start = first_selection.begin()
104
        line_cursor = self.view.line(selection_start)
105
        line_content = self.view.substr(line_cursor)
106
107
        return line_content
108
109
    @staticmethod
110
    def __get_selected_dec_or_none(caret, line_index, line_content):
111
        # catch case with multiple colors in same line
112
        for match in DEC_COLOR_EXP.finditer(line_content):
113
            low = line_index + match.start()
114
            high = line_index + match.end()
115
116
            # need to shift the caret to the current line
117
            if low <= caret <= high:
118
                rgba_raw = match.groups()
119
                rgba = [int(color) for color in rgba_raw if color is not None]
120
                hexes = converter.rgbs_to_hexes(rgba)
121
                hex_string = converter.hexes_to_string(hexes)
122
                with_alpha = converter.convert_hex_to_hex_with_alpha(hex_string)
123
                has_alpha = len(rgba) == 4
124
125
                return low, high, with_alpha, True, False, has_alpha
126
127
        return None
128
129
    @staticmethod
130
    def __get_selected_hex_or_none(caret, line_index, line_content):
131
        # we can find multiple color values in the same row
132
        # after iterating through the single elements
133
        # we can use start() and end() of each match to determine the length
134
        # and thus the area the caret had to be in,
135
        # to identify th1e one we are currently in
136
        for match in HEX_COLOR_EXP.finditer(line_content):
137
            low = line_index + match.start()
138
            high = line_index + match.end()
139
140
            if low <= caret <= high:
141
                hex_values = match.group(0)
142
                is_lower = hex_values.islower()
143
                # color picker requires RGBA
144
                with_alpha = converter.convert_hex_to_hex_with_alpha(hex_values)
145
                has_alpha = len(hex_values) == 8
146
147
                return low, high, with_alpha, False, is_lower, has_alpha
148
            else:
149
                logger.info(low)
150
                logger.info(high)
151
                logger.info(caret)
152
153
        return None
154
155
    def __get_selected_color_or_none(self):
156
        """Return None in case of not finding the color aka no color is selected."""
157
        caret = self.__get_first_selection().begin()
158
        line_index = self.__get_selected_line_index()
159
        line_content = self.__get_selected_line_content()
160
161
        # catch case with multiple colors in same line
162
        selected_dec_or_none = self.__get_selected_dec_or_none(caret, line_index, line_content)
163
        if selected_dec_or_none is not None:
164
            return selected_dec_or_none
165
166
        # if no match was iterated we process furthere starting here
167
        selected_hex_or_none = self.__get_selected_hex_or_none(caret, line_index, line_content)
168
        if selected_hex_or_none is not None:
169
            return selected_hex_or_none
170
171
        return None, None, None, None, None, None
172
173
    @staticmethod
174
    def __get_picker_path():
175
        packages = sublime.packages_path()
176
        picker_path = os.path.join(
177
            packages,
178
            "User",
179
            "Rainmeter",
180
            "color",
181
            "picker",
182
            "ColorPicker_win.exe"
183
        )
184
        if not os.path.exists(picker_path):
185
            logger.error("color picker was suposed to be copied to '" + picker_path + "'")
186
187
        logger.info("found picker in '" + picker_path + "'")
188
189
        return picker_path
190
191
    def __replace_color(self, maybe_none, raw_output):
192
        low, high, _, is_dec, is_lower, has_alpha = maybe_none
193
        # if no color is selected we need to modify the low and high to match the caret
194
        if all(value is None for value in maybe_none):
195
            caret = self.__get_first_selection().begin()
196
            low = caret
197
            high = caret
198
            output = raw_output[1:]
199
        else:
200
            output = self.__transform_raw_to_original_fmt(
201
                raw_output,
202
                is_dec,
203
                has_alpha,
204
                is_lower
205
            )
206
207
        self.view.run_command(
208
            "rainmeter_replace_color",
209
            {
210
                "low": low,
211
                "high": high,
212
                "output": output
213
            }
214
        )
215
216
    def __run_picker(self):
217
        maybe_none = self.__get_selected_color_or_none()
218
        _, _, maybe_color, _, _, _ = maybe_none
219
220
        # no color selected, we call the color picker and insert the color at that position
221
        color = color_or_default(maybe_color)
222
223
        picker = subprocess.Popen(
224
            [self.__get_picker_path(), color],
225
            stdout=subprocess.PIPE,
226
            stderr=subprocess.PIPE,
227
            shell=False
228
        )
229
        output_channel, error_channel = picker.communicate()
230
        raw_output = output_channel.decode("utf-8")
231
        logger.info("output: " + raw_output)
232
233
        # checking for errors first
234
        error = error_channel.decode("utf-8")
235
        if error is not None and len(error) != 0:
236
            logger.error("Color Picker Error:\n" + error)
237
            return
238
239
        # len is 9 because of RGBA and '#' resulting into 9 characters
240
        if raw_output is not None and len(raw_output) == 9 and raw_output != 'CANCEL':
241
            logger.info("can write back: " + raw_output)
242
243
            self.__replace_color(maybe_none, raw_output)
244
245
    @staticmethod
246
    def __transform_raw_to_original_fmt(raw, is_dec, has_alpha, is_lower):
247
        # cut output from the '#' because Rainmeter does not use # for color codes
248
        output = raw[1:]
249
        if is_dec:
250
            output = converter.convert_hex_str_to_rgba_str(output, has_alpha)
251
252
        # in case of hexadecimial representation
253
        else:
254
            # doing alpha calculation first so we do not need to catch ff and FF
255
            alpha = output[-2:]
256
            if not has_alpha and alpha is "FF":
257
                output = output[:-2]
258
259
            # it can be either originally in lower or upper case
260
            if is_lower:
261
                output = output.lower()
262
263
        return output
264
265
    def is_enabled(self, **dummy_args):  # pylint: disable=R0201; sublime text API, no need for class reference
266
        """
267
        Return True if the command is able to be run at this time.
268
269
        The default implementation simply always returns True.
270
        """
271
        # Check if current syntax is rainmeter
272
        israinmeter = self.view.score_selector(self.view.sel()[0].a,
273
                                               "source.rainmeter")
274
275
        return israinmeter > 0
276
277
    def is_visible(self, **args):
278
        """."""
279
        if self.is_enabled():
280
            return True
281
282
        env = args.get("call_env", "")
283
284
        return env != "context"
285
286
287
def __require_path(path):
288
    if not os.path.exists(path):
289
        os.makedirs(path)
290
291
292
def plugin_loaded():
293
    """Called automatically from ST3 if plugin is loaded.
294
295
    Is required now due to async call and ignoring sublime.* from main routine
296
    """
297
    packages = sublime.packages_path()
298
    colorpicker_dir = os.path.join(packages, "User", "Rainmeter", "color", "picker")
299
300
    __require_path(colorpicker_dir)
301
302
    colorpicker_exe = os.path.join(colorpicker_dir, "ColorPicker_win.exe")
303
304
    # could be already copied on a previous run
305
    need_picker_exe = not os.path.exists(colorpicker_exe)
306
    binary_picker = sublime.load_binary_resource(
307
        "Packages/Rainmeter/color/picker/ColorPicker_win.exe"
308
    )
309
310
    # could be a newer version of the color picker be there
311
    # only check if no newer is required since expensive
312
    # generally happens in consecutive calls without updates
313
    if not need_picker_exe:
314
        need_picker_exe = os.path.getsize(colorpicker_exe) != len(binary_picker)
315
    if need_picker_exe:
316
        logger.info(
317
            "Newer version of color picker found. Copying data over to '" + colorpicker_exe + "'"
318
        )
319
        with open(colorpicker_exe, "wb") as file_handler:
320
            file_handler.write(binary_picker)
321
    else:
322
        logger.info(
323
            "You are using the most current version of color picker. Continue loading..."
324
        )
325