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 ( 13da76...50c0cd )
by thatsIch
01:09
created

RainmeterColorPickCommand.is_visible()   A

Complexity

Conditions 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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