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.
Completed
Push — master ( c0dd2a...428f11 )
by thatsIch
01:15
created

RainmeterColorPickCommand   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 210
Duplicated Lines 0 %

Importance

Changes 23
Bugs 0 Features 0
Metric Value
c 23
b 0
f 0
dl 0
loc 210
rs 9.2
wmc 34

13 Methods

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