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 ( 2e732b...977e0c )
by thatsIch
01:07
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
# TO DO spacing of decimal if required, but maybe too much overhead
19
20
import os
21
import re
22
import subprocess
23
24
import sublime
25
import sublime_plugin
26
27
from . import logger
28
from .color import converter
29
30
class RainmeterReplaceColorCommand(sublime_plugin.TextCommand): # pylint: disable=R0903; we only need one method
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
# 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, _):
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
168
    @staticmethod
169
    def __get_picker_path():
170
        project_root = os.path.dirname(__file__)
171
        picker_path = os.path.join(project_root, "color", "picker", "ColorPicker_win.exe")
172
173
        return picker_path
174
175
    def __run_picker(self):
176
        maybe_none = self.__get_selected_color_or_none()
177
        low, high, maybe_color, is_dec, is_lower, has_alpha = maybe_none
178
179
        # no color selected, we call the color picker and insert the color at that position
180
        color = "FFFFFFFF" if maybe_color is None else maybe_color
181
182
        picker_path = self.__get_picker_path()
183
        picker = subprocess.Popen(
184
            [picker_path, color],
185
            stdout=subprocess.PIPE,
186
            stderr=subprocess.PIPE,
187
            shell=False
188
        )
189
        output_channel, error_channel = picker.communicate()
190
        raw_output = output_channel.decode("utf-8")
191
        logger.info("output: " + raw_output)
192
193
        # checking for errors first
194
        error = error_channel.decode("utf-8")
195
        if error is not None and len(error) != 0:
196
            logger.error("Color Picker Error:\n" + error)
197
            return
198
199
        # len is 9 because of RGBA and '#' resulting into 9 characters
200
        if raw_output is not None and len(raw_output) == 9 and raw_output != 'CANCEL':
201
            logger.info("can write back: " + raw_output)
202
203
            # if no color is selected we need to modify the low and high to match the caret
204
            if all(value is None for value in maybe_none):
205
                caret = self.__get_first_selection().begin()
206
                low = caret
207
                high = caret
208
                output = raw_output[1:]
209
            else:
210
                output = self.__transform_raw_to_original_fmt(
211
                    raw_output,
212
                    is_dec,
213
                    has_alpha,
214
                    is_lower
215
                )
216
217
            self.view.run_command(
218
                "rainmeter_replace_color",
219
                {
220
                    "low": low,
221
                    "high": high,
222
                    "output": output
223
                }
224
            )
225
226
    @staticmethod
227
    def __transform_raw_to_original_fmt(raw, is_dec, has_alpha, is_lower):
228
        # cut output from the '#' because Rainmeter does not use # for color codes
229
        output = raw[1:]
230
        if is_dec:
231
            output = converter.convert_hex_str_to_rgba_str(output, has_alpha)
232
233
        # in case of hexadecimial representation
234
        else:
235
            # doing alpha calculation first so we do not need to catch ff and FF
236
            alpha = output[-2:]
237
            if not has_alpha and alpha is "FF":
238
                output = output[:-2]
239
240
            # it can be either originally in lower or upper case
241
            if is_lower:
242
                output = output.lower()
243
244
        return output
245
246
    def is_enabled(self): #pylint: disable=R0201; sublime text API, no need for class reference
247
        """
248
        Return True if the command is able to be run at this time.
249
250
        The default implementation simply always returns True.
251
        """
252
        # Check if current syntax is rainmeter
253
        israinmeter = self.view.score_selector(self.view.sel()[0].a,
254
                                               "source.rainmeter")
255
256
        return israinmeter > 0
257
258
    def is_visible(self, **args):
259
        """."""
260
        if self.is_enabled():
261
            return True
262
263
        env = args["env"]
264
265
        return env != "context"
266