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 ( 14b837...27d348 )
by thatsIch
01:03
created

RainmeterReplaceColorCommand.run()   B

Complexity

Conditions 1

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
c 3
b 0
f 0
dl 0
loc 24
rs 8.9713
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(
60
            __file__,
61
            "run(self, edit, **args)",
62
            "Replacing '" + original_str + "' with '" + output + "'"
63
        )
64
65
# encodes RRR,GGG,BBB,AAA with optional alpha channel and supporting all numbers from 0 to 999
66
# converter will check for 255
67
# numbers can be spaced anyway
68
DEC_COLOR_EXP = re.compile(r"(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d{1,3}))?")
69
# support lower and upper case hexadecimal with optional alpha channel
70
HEX_COLOR_EXP = re.compile(r"(?:[0-9a-fA-F]{2}){3,4}")
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, _):
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(__file__, "__get_selected_color_or_none(self)", low)
148
                logger.info(__file__, "__get_selected_color_or_none(self)", high)
149
                logger.info(__file__, "__get_selected_color_or_none(self)", 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
170
171
172
    @staticmethod
173
    def __get_picker_path():
174
        project_root = os.path.dirname(__file__)
175
        picker_path = os.path.join(project_root, "color", "picker", "ColorPicker_win.exe")
176
177
        return picker_path
178
179
    def __run_picker(self):
180
        low, high, maybe_color, is_dec, is_lower, has_alpha = self.__get_selected_color_or_none()
181
182
        # no color selected, we call the color picker and insert the color at that position
183
        color = "FFFFFFFF" if maybe_color is None else maybe_color
184
185
        picker_path = self.__get_picker_path()
186
        picker = subprocess.Popen(
187
            [picker_path, color],
188
            stdout=subprocess.PIPE,
189
            stderr=subprocess.PIPE,
190
            shell=False
191
        )
192
        output_channel, error_channel = picker.communicate()
193
        raw_output = output_channel.decode("utf-8")
194
        logger.info(__file__, "__run_picker(self)", "output: " + raw_output)
195
196
        # checking for errors first
197
        error = error_channel.decode("utf-8")
198
        if error is not None and len(error) != 0:
199
            logger.error(__file__, "__run_picker(self)", "Color Picker Error:\n" + error_channel)
200
            return
201
202
        # len is 9 because of RGBA and '#' resulting into 9 characters
203
        if raw_output is not None and len(raw_output) == 9 and raw_output != 'CANCEL':
204
            logger.info(__file__, "__write_back(self)", "can write back: " + raw_output)
205
206
            output = self.__transform_raw_to_original_fmt(raw_output, is_dec, has_alpha, is_lower)
207
208
            self.view.run_command(
209
                "rainmeter_replace_color",
210
                {
211
                    "low": low,
212
                    "high": high,
213
                    "output": output
214
                }
215
            )
216
217
    @staticmethod
218
    def __transform_raw_to_original_fmt(raw, is_dec, has_alpha, is_lower):
219
        # cut output from the '#' because Rainmeter does not use # for color codes
220
        output = raw[1:]
221
        if is_dec:
222
            output = converter.convert_hex_str_to_rgba_str(output, has_alpha)
223
224
        # in case of hexadecimial representation
225
        else:
226
            # doing alpha calculation first so we do not need to catch ff and FF
227
            alpha = output[-2:]
228
            if not has_alpha and alpha is "FF":
229
                output = output[:-2]
230
231
            # it can be either originally in lower or upper case
232
            if is_lower:
233
                output = output.lower()
234
235
        return output
236