colorpicker.py
"""
This module is about the integration with the color picker.
The color picker can detect a color in a substring
and launch a tool to display the current color,
change it and thus also replace the old color.
It supports both ways Rainmeter defines color.
* RRGGBB
* RRGGBBAA
* RRR,GGG,BBB
* RRR,GGG,BBB,AAA
which is hexadecimal and decimal format.
"""
import os
import re
import subprocess
import sublime
import sublime_plugin
from . import logger
from .color import converter
class RainmeterReplaceColorCommand(sublime_plugin.TextCommand): # pylint: disable=R0903; we only need one method
"""
Replace a region with a text.
This command is required because the edit objects passed to TextCommand
are not transferable. We have to call this from the other command
to get a valid edit object.
"""
def run(self, edit, **args):
"""
Method is provided by Sublime Text through the super class TextCommand.
This is run automatically if you initialize the command
through an "command": "rainmeter_replace_color" command
with additional arguments:
* low: start of the region to replace
* high: end of the region to replace
* output: text which will replace the region
"""
low = args["low"]
high = args["high"]
output = args["output"]
region = sublime.Region(low, high)
original_str = self.view.substr(region)
self.view.replace(edit, region, output)
logger.info("Replacing '" + original_str + "' with '" + output + "'")
# encodes RRR,GGG,BBB,AAA with optional alpha channel and supporting all numbers from 0 to 999
# converter will check for 255
# numbers can be spaced anyway
DEC_COLOR_EXP = re.compile(r"(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d{1,3}))?")
# support lower and upper case hexadecimal with optional alpha channel
HEX_COLOR_EXP = re.compile(r"(?:[0-9a-fA-F]{2}){3,4}")
def color_or_default(color):
"""If the color is none it returns FFFFFFFF."""
return "FFFFFFFF" if color is None else color
class RainmeterColorPickCommand(sublime_plugin.TextCommand): # pylint: disable=R0903; we only need one method
"""Sublime Text integration running this through an action."""
def run(self, dummy_edit, **dummy_args):
"""
Method is provided by Sublime Text through the super class TextCommand.
This is run automatically if you initialize the command
through an "command": "rainmeter_color_pick" command.
"""
sublime.set_timeout_async(self.__run_picker, 0)
def __get_first_selection(self):
selections = self.view.sel()
first_selection = selections[0]
return first_selection
def __get_selected_line_index(self):
first_selection = self.__get_first_selection()
selection_start = first_selection.begin()
line_cursor = self.view.line(selection_start)
line_index = line_cursor.begin()
return line_index
def __get_selected_line_content(self):
first_selection = self.__get_first_selection()
selection_start = first_selection.begin()
line_cursor = self.view.line(selection_start)
line_content = self.view.substr(line_cursor)
return line_content
@staticmethod
def __get_selected_dec_or_none(caret, line_index, line_content):
# catch case with multiple colors in same line
for match in DEC_COLOR_EXP.finditer(line_content):
low = line_index + match.start()
high = line_index + match.end()
# need to shift the caret to the current line
if low <= caret <= high:
rgba_raw = match.groups()
rgba = [int(color) for color in rgba_raw if color is not None]
hexes = converter.rgbs_to_hexes(rgba)
hex_string = converter.hexes_to_string(hexes)
with_alpha = converter.convert_hex_to_hex_with_alpha(hex_string)
has_alpha = len(rgba) == 4
return low, high, with_alpha, True, False, has_alpha
return None
@staticmethod
def __get_selected_hex_or_none(caret, line_index, line_content):
# we can find multiple color values in the same row
# after iterating through the single elements
# we can use start() and end() of each match to determine the length
# and thus the area the caret had to be in,
# to identify th1e one we are currently in
for match in HEX_COLOR_EXP.finditer(line_content):
low = line_index + match.start()
high = line_index + match.end()
if low <= caret <= high:
hex_values = match.group(0)
is_lower = hex_values.islower()
# color picker requires RGBA
with_alpha = converter.convert_hex_to_hex_with_alpha(hex_values)
has_alpha = len(hex_values) == 8
return low, high, with_alpha, False, is_lower, has_alpha
else:
logger.info(low)
logger.info(high)
logger.info(caret)
return None
def __get_selected_color_or_none(self):
"""Return None in case of not finding the color aka no color is selected."""
caret = self.__get_first_selection().begin()
line_index = self.__get_selected_line_index()
line_content = self.__get_selected_line_content()
# catch case with multiple colors in same line
selected_dec_or_none = self.__get_selected_dec_or_none(caret, line_index, line_content)
if selected_dec_or_none is not None:
return selected_dec_or_none
# if no match was iterated we process furthere starting here
selected_hex_or_none = self.__get_selected_hex_or_none(caret, line_index, line_content)
if selected_hex_or_none is not None:
return selected_hex_or_none
return None, None, None, None, None, None
@staticmethod
def __get_picker_path():
packages = sublime.packages_path()
picker_path = os.path.join(
packages,
"User",
"Rainmeter",
"color",
"picker",
"ColorPicker_win.exe"
)
if not os.path.exists(picker_path):
logger.error("color picker was suposed to be copied to '" + picker_path + "'")
logger.info("found picker in '" + picker_path + "'")
return picker_path
def __replace_color(self, maybe_none, raw_output):
low, high, _, is_dec, is_lower, has_alpha = maybe_none
# if no color is selected we need to modify the low and high to match the caret
if all(value is None for value in maybe_none):
caret = self.__get_first_selection().begin()
low = caret
high = caret
output = raw_output[1:]
else:
output = self.__transform_raw_to_original_fmt(
raw_output,
is_dec,
has_alpha,
is_lower
)
self.view.run_command(
"rainmeter_replace_color",
{
"low": low,
"high": high,
"output": output
}
)
def __run_picker(self):
maybe_none = self.__get_selected_color_or_none()
_, _, maybe_color, _, _, _ = maybe_none
# no color selected, we call the color picker and insert the color at that position
color = color_or_default(maybe_color)
picker = subprocess.Popen(
[self.__get_picker_path(), color],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False
)
output_channel, error_channel = picker.communicate()
raw_output = output_channel.decode("utf-8")
logger.info("output: " + raw_output)
# checking for errors first
error = error_channel.decode("utf-8")
if error is not None and len(error) != 0:
logger.error("Color Picker Error:\n" + error)
return
# len is 9 because of RGBA and '#' resulting into 9 characters
if raw_output is not None and len(raw_output) == 9 and raw_output != 'CANCEL':
logger.info("can write back: " + raw_output)
self.__replace_color(maybe_none, raw_output)
@staticmethod
def __transform_raw_to_original_fmt(raw, is_dec, has_alpha, is_lower):
# cut output from the '#' because Rainmeter does not use # for color codes
output = raw[1:]
if is_dec:
output = converter.convert_hex_str_to_rgba_str(output, has_alpha)
# in case of hexadecimial representation
else:
# doing alpha calculation first so we do not need to catch ff and FF
alpha = output[-2:]
if not has_alpha and alpha is "FF":
output = output[:-2]
# it can be either originally in lower or upper case
if is_lower:
output = output.lower()
return output
def is_enabled(self, **dummy_args): # pylint: disable=R0201; sublime text API, no need for class reference
"""
Return True if the command is able to be run at this time.
The default implementation simply always returns True.
"""
# Check if current syntax is rainmeter
israinmeter = self.view.score_selector(self.view.sel()[0].a,
"source.rainmeter")
return israinmeter > 0
def is_visible(self, **args):
"""."""
if self.is_enabled():
return True
env = args.get("call_env", "")
return env != "context"
def __require_path(path):
if not os.path.exists(path):
os.makedirs(path)
def plugin_loaded():
"""Called automatically from ST3 if plugin is loaded.
Is required now due to async call and ignoring sublime.* from main routine
"""
packages = sublime.packages_path()
colorpicker_dir = os.path.join(packages, "User", "Rainmeter", "color", "picker")
__require_path(colorpicker_dir)
colorpicker_exe = os.path.join(colorpicker_dir, "ColorPicker_win.exe")
# could be already copied on a previous run
need_picker_exe = not os.path.exists(colorpicker_exe)
binary_picker = sublime.load_binary_resource(
"Packages/Rainmeter/color/picker/ColorPicker_win.exe"
)
# could be a newer version of the color picker be there
# only check if no newer is required since expensive
# generally happens in consecutive calls without updates
if not need_picker_exe:
need_picker_exe = os.path.getsize(colorpicker_exe) != len(binary_picker)
if need_picker_exe:
logger.info(
"Newer version of color picker found. Copying data over to '" + colorpicker_exe + "'"
)
with open(colorpicker_exe, "wb") as file_handler:
file_handler.write(binary_picker)
else:
logger.info(
"You are using the most current version of color picker. Continue loading..."
)