Implement control code to change colors

This commit is contained in:
Kovid Goyal 2016-11-24 16:28:08 +05:30
parent 3031d41e72
commit abd09464f0
8 changed files with 290 additions and 218 deletions

View File

@ -20,7 +20,7 @@ import glfw_constants
from .constants import appname
from .char_grid import CharGrid
from .keys import interpret_text_event, interpret_key_event, get_shortcut
from .utils import sanitize_title
from .utils import sanitize_title, parse_color_set
from .fast_data_types import (
BRACKETED_PASTE_START, BRACKETED_PASTE_END, Screen, read_bytes_dump, read_bytes
)
@ -274,8 +274,27 @@ class Boss(Thread):
code += 1
self.queue_action(self.apply_change_colors)
def refresh(self):
self.screen.mark_as_dirty()
self.wakeup()
def set_color_table_color(self, code, value):
print(11111111, code, value)
if code == 4:
for c, val in parse_color_set(value):
self.char_grid.color_profile.set_color(c, val)
self.refresh()
elif code == 104:
if not value.strip():
self.char_grid.color_profile.reset_color_table()
else:
for c in value.split(';'):
try:
c = int(c)
except Exception:
continue
if 0 <= c <= 255:
self.char_grid.color_profile.reset_color(c)
self.refresh()
def apply_change_colors(self):
self.char_grid.change_colors(self.pending_color_changes)

View File

@ -7,10 +7,10 @@ from ctypes import c_uint, addressof, memmove, sizeof
from queue import Queue, Empty
from threading import Lock
from .config import build_ansi_color_table, to_color
from .config import build_ansi_color_table
from .fonts import set_font_family
from .shaders import Sprites, ShaderProgram
from .utils import get_logical_dpi
from .utils import get_logical_dpi, to_color
from .fast_data_types import (
glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, glClear,
GL_COLOR_BUFFER_BIT, glClearColor, glViewport, glUniform2ui, glUniform4f,

View File

@ -106,6 +106,32 @@ as_color(ColorProfile *self, PyObject *val) {
return ans;
}
static PyObject*
reset_color_table(ColorProfile *self) {
#define reset_color_table_doc "Reset all customized colors back to defaults"
memcpy(self->color_table, self->orig_color_table, sizeof(FG_BG_256));
Py_RETURN_NONE;
}
static PyObject*
reset_color(ColorProfile *self, PyObject *val) {
#define reset_color_doc "Reset the specified color"
uint8_t i = PyLong_AsUnsignedLong(val) & 0xff;
self->color_table[i] = self->orig_color_table[i];
Py_RETURN_NONE;
}
static PyObject*
set_color(ColorProfile *self, PyObject *args) {
#define set_color_doc "Set the specified color"
unsigned char i;
unsigned long val;
if (!PyArg_ParseTuple(args, "Bk", &i, &val)) return NULL;
self->color_table[i] = val;
Py_RETURN_NONE;
}
uint32_t
to_color(ColorProfile *self, uint32_t entry, uint32_t defval) {
unsigned int t = entry & 0xFF, r;
@ -125,7 +151,10 @@ to_color(ColorProfile *self, uint32_t entry, uint32_t defval) {
static PyMethodDef methods[] = {
METHOD(update_ansi_color_table, METH_O)
METHOD(reset_color_table, METH_NOARGS)
METHOD(as_color, METH_O)
METHOD(reset_color, METH_O)
METHOD(set_color, METH_VARARGS)
{NULL} /* Sentinel */
};

View File

@ -9,178 +9,9 @@ from collections import namedtuple
import glfw_constants as glfw
from .fast_data_types import CURSOR_BLOCK, CURSOR_BEAM, CURSOR_UNDERLINE
from .utils import to_color
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
# Color definitions {{{
color_pat = re.compile(r'^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$')
color_names = {
'aliceblue': 'f0f8ff',
'antiquewhite': 'faebd7',
'aqua': '00ffff',
'aquamarine': '7fffd4',
'azure': 'f0ffff',
'beige': 'f5f5dc',
'bisque': 'ffe4c4',
'black': '000000',
'blanchedalmond': 'ffebcd',
'blue': '0000ff',
'blueviolet': '8a2be2',
'brown': 'a52a2a',
'burlywood': 'deb887',
'cadetblue': '5f9ea0',
'chartreuse': '7fff00',
'chocolate': 'd2691e',
'coral': 'ff7f50',
'cornflowerblue': '6495ed',
'cornsilk': 'fff8dc',
'crimson': 'dc143c',
'cyan': '00ffff',
'darkblue': '00008b',
'darkcyan': '008b8b',
'darkgoldenrod': 'b8860b',
'darkgray': 'a9a9a9',
'darkgrey': 'a9a9a9',
'darkgreen': '006400',
'darkkhaki': 'bdb76b',
'darkmagenta': '8b008b',
'darkolivegreen': '556b2f',
'darkorange': 'ff8c00',
'darkorchid': '9932cc',
'darkred': '8b0000',
'darksalmon': 'e9967a',
'darkseagreen': '8fbc8f',
'darkslateblue': '483d8b',
'darkslategray': '2f4f4f',
'darkslategrey': '2f4f4f',
'darkturquoise': '00ced1',
'darkviolet': '9400d3',
'deeppink': 'ff1493',
'deepskyblue': '00bfff',
'dimgray': '696969',
'dimgrey': '696969',
'dodgerblue': '1e90ff',
'firebrick': 'b22222',
'floralwhite': 'fffaf0',
'forestgreen': '228b22',
'fuchsia': 'ff00ff',
'gainsboro': 'dcdcdc',
'ghostwhite': 'f8f8ff',
'gold': 'ffd700',
'goldenrod': 'daa520',
'gray': '808080',
'grey': '808080',
'green': '008000',
'greenyellow': 'adff2f',
'honeydew': 'f0fff0',
'hotpink': 'ff69b4',
'indianred': 'cd5c5c',
'indigo': '4b0082',
'ivory': 'fffff0',
'khaki': 'f0e68c',
'lavender': 'e6e6fa',
'lavenderblush': 'fff0f5',
'lawngreen': '7cfc00',
'lemonchiffon': 'fffacd',
'lightblue': 'add8e6',
'lightcoral': 'f08080',
'lightcyan': 'e0ffff',
'lightgoldenrodyellow': 'fafad2',
'lightgray': 'd3d3d3',
'lightgrey': 'd3d3d3',
'lightgreen': '90ee90',
'lightpink': 'ffb6c1',
'lightsalmon': 'ffa07a',
'lightseagreen': '20b2aa',
'lightskyblue': '87cefa',
'lightslategray': '778899',
'lightslategrey': '778899',
'lightsteelblue': 'b0c4de',
'lightyellow': 'ffffe0',
'lime': '00ff00',
'limegreen': '32cd32',
'linen': 'faf0e6',
'magenta': 'ff00ff',
'maroon': '800000',
'mediumaquamarine': '66cdaa',
'mediumblue': '0000cd',
'mediumorchid': 'ba55d3',
'mediumpurple': '9370db',
'mediumseagreen': '3cb371',
'mediumslateblue': '7b68ee',
'mediumspringgreen': '00fa9a',
'mediumturquoise': '48d1cc',
'mediumvioletred': 'c71585',
'midnightblue': '191970',
'mintcream': 'f5fffa',
'mistyrose': 'ffe4e1',
'moccasin': 'ffe4b5',
'navajowhite': 'ffdead',
'navy': '000080',
'oldlace': 'fdf5e6',
'olive': '808000',
'olivedrab': '6b8e23',
'orange': 'ffa500',
'orangered': 'ff4500',
'orchid': 'da70d6',
'palegoldenrod': 'eee8aa',
'palegreen': '98fb98',
'paleturquoise': 'afeeee',
'palevioletred': 'db7093',
'papayawhip': 'ffefd5',
'peachpuff': 'ffdab9',
'per': 'cd853f',
'pink': 'ffc0cb',
'plum': 'dda0dd',
'powderblue': 'b0e0e6',
'purple': '800080',
'red': 'ff0000',
'rosybrown': 'bc8f8f',
'royalblue': '4169e1',
'saddlebrown': '8b4513',
'salmon': 'fa8072',
'sandybrown': 'f4a460',
'seagreen': '2e8b57',
'seashell': 'fff5ee',
'sienna': 'a0522d',
'silver': 'c0c0c0',
'skyblue': '87ceeb',
'slateblue': '6a5acd',
'slategray': '708090',
'slategrey': '708090',
'snow': 'fffafa',
'springgreen': '00ff7f',
'steelblue': '4682b4',
'tan': 'd2b48c',
'teal': '008080',
'thistle': 'd8bfd8',
'tomato': 'ff6347',
'turquoise': '40e0d0',
'violet': 'ee82ee',
'wheat': 'f5deb3',
'white': 'ffffff',
'whitesmoke': 'f5f5f5',
'yellow': 'ffff00',
'yellowgreen': '9acd32',
}
Color = namedtuple('Color', 'red green blue')
# }}}
def to_color(raw, validate=False):
x = raw.strip().lower()
m = color_pat.match(x)
val = None
if m is not None:
val = m.group(1)
if len(val) == 3:
val = ''.join(2 * s for s in val)
else:
val = color_names.get(x)
if val is None:
if validate:
raise ValueError('Invalid color name: {}'.format(raw))
return
return Color(int(val[:2], 16), int(val[2:4], 16), int(val[4:], 16))
def to_font_size(x):

View File

@ -53,8 +53,9 @@ void screen_reset(Screen *self) {
cursor_reset(self->cursor);
tracker_cursor_changed(self->change_tracker);
screen_cursor_position(self, 1, 1);
screen_change_default_color(self, FG, 0);
screen_change_default_color(self, BG, 0);
set_dynamic_color(self, 110, NULL);
set_dynamic_color(self, 111, NULL);
set_color_table_color(self, 104, NULL);
tracker_update_screen(self->change_tracker);
}
static inline HistoryBuf* realloc_hb(HistoryBuf *old, unsigned int lines, unsigned int columns) {
@ -173,15 +174,6 @@ screen_draw(Screen *self, uint32_t ch) {
// Graphics {{{
void screen_change_default_color(Screen *self, unsigned int which, uint32_t col) {
if (self->callbacks == Py_None) return;
if (col & 0xFF) PyObject_CallMethod(self->callbacks, "change_default_color", "s(III)", which == FG ? "fg" : "bg",
(col >> 24) & 0xFF, (col >> 16) & 0xFF, (col >> 8) & 0xFF);
else PyObject_CallMethod(self->callbacks, "change_default_color", "sO", which == FG ? "fg" : "bg", Py_None);
if (PyErr_Occurred()) PyErr_Print();
PyErr_Clear();
}
void screen_alignment_display(Screen *self) {
// http://www.vt100.net/docs/vt510-rm/DECALN.html
screen_cursor_position(self, 1, 1);
@ -810,12 +802,14 @@ void set_icon(Screen *self, PyObject *icon) {
}
void set_dynamic_color(Screen *self, unsigned int code, PyObject *color) {
PyObject_CallMethod(self->callbacks, "set_dynamic_color", "IO", code, color);
if (color == NULL) PyObject_CallMethod(self->callbacks, "set_dynamic_color", "Is", code, "");
else PyObject_CallMethod(self->callbacks, "set_dynamic_color", "IO", code, color);
if (PyErr_Occurred()) { PyErr_Print(); PyErr_Clear(); }
}
void set_color_table_color(Screen *self, unsigned int code, PyObject *color) {
PyObject_CallMethod(self->callbacks, "set_color_table_color", "IO", code, color);
if (color == NULL) PyObject_CallMethod(self->callbacks, "set_color_table_color", "Is", code, "");
else PyObject_CallMethod(self->callbacks, "set_color_table_color", "IO", code, color);
if (PyErr_Occurred()) { PyErr_Print(); PyErr_Clear(); }
}
@ -1008,6 +1002,11 @@ static PyObject* is_dirty(Screen *self) {
return ans;
}
static PyObject* mark_as_dirty(Screen *self) {
tracker_update_screen(self->change_tracker);
Py_RETURN_NONE;
}
static PyObject* current_char_width(Screen *self) {
#define current_char_width_doc "The width of the character under the cursor"
unsigned long ans = 1;
@ -1063,6 +1062,7 @@ static PyMethodDef methods[] = {
MND(index, METH_NOARGS)
MND(reverse_index, METH_NOARGS)
MND(is_dirty, METH_NOARGS)
MND(mark_as_dirty, METH_NOARGS)
MND(resize, METH_VARARGS)
MND(set_scroll_cell_data, METH_VARARGS)
{"update_cell_data", (PyCFunction)screen_update_cell_data, METH_VARARGS, ""},

View File

@ -5,6 +5,7 @@
import re
import subprocess
import ctypes
from collections import namedtuple
from contextlib import contextmanager
from functools import lru_cache
from time import monotonic
@ -56,3 +57,195 @@ def get_dpi():
dpiy = vmode.height / (height / 25.4)
get_dpi.ans = {'physical': (dpix, dpiy), 'logical': get_logical_dpi()}
return get_dpi.ans
# Color names {{{
color_pat = re.compile(r'^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$')
color_pat2 = re.compile(r'rgb:([a-f0-9]{2})/([a-f0-9]{2})/([a-f0-9]{2})$', re.IGNORECASE)
color_names = {
'aliceblue': 'f0f8ff',
'antiquewhite': 'faebd7',
'aqua': '00ffff',
'aquamarine': '7fffd4',
'azure': 'f0ffff',
'beige': 'f5f5dc',
'bisque': 'ffe4c4',
'black': '000000',
'blanchedalmond': 'ffebcd',
'blue': '0000ff',
'blueviolet': '8a2be2',
'brown': 'a52a2a',
'burlywood': 'deb887',
'cadetblue': '5f9ea0',
'chartreuse': '7fff00',
'chocolate': 'd2691e',
'coral': 'ff7f50',
'cornflowerblue': '6495ed',
'cornsilk': 'fff8dc',
'crimson': 'dc143c',
'cyan': '00ffff',
'darkblue': '00008b',
'darkcyan': '008b8b',
'darkgoldenrod': 'b8860b',
'darkgray': 'a9a9a9',
'darkgrey': 'a9a9a9',
'darkgreen': '006400',
'darkkhaki': 'bdb76b',
'darkmagenta': '8b008b',
'darkolivegreen': '556b2f',
'darkorange': 'ff8c00',
'darkorchid': '9932cc',
'darkred': '8b0000',
'darksalmon': 'e9967a',
'darkseagreen': '8fbc8f',
'darkslateblue': '483d8b',
'darkslategray': '2f4f4f',
'darkslategrey': '2f4f4f',
'darkturquoise': '00ced1',
'darkviolet': '9400d3',
'deeppink': 'ff1493',
'deepskyblue': '00bfff',
'dimgray': '696969',
'dimgrey': '696969',
'dodgerblue': '1e90ff',
'firebrick': 'b22222',
'floralwhite': 'fffaf0',
'forestgreen': '228b22',
'fuchsia': 'ff00ff',
'gainsboro': 'dcdcdc',
'ghostwhite': 'f8f8ff',
'gold': 'ffd700',
'goldenrod': 'daa520',
'gray': '808080',
'grey': '808080',
'green': '008000',
'greenyellow': 'adff2f',
'honeydew': 'f0fff0',
'hotpink': 'ff69b4',
'indianred': 'cd5c5c',
'indigo': '4b0082',
'ivory': 'fffff0',
'khaki': 'f0e68c',
'lavender': 'e6e6fa',
'lavenderblush': 'fff0f5',
'lawngreen': '7cfc00',
'lemonchiffon': 'fffacd',
'lightblue': 'add8e6',
'lightcoral': 'f08080',
'lightcyan': 'e0ffff',
'lightgoldenrodyellow': 'fafad2',
'lightgray': 'd3d3d3',
'lightgrey': 'd3d3d3',
'lightgreen': '90ee90',
'lightpink': 'ffb6c1',
'lightsalmon': 'ffa07a',
'lightseagreen': '20b2aa',
'lightskyblue': '87cefa',
'lightslategray': '778899',
'lightslategrey': '778899',
'lightsteelblue': 'b0c4de',
'lightyellow': 'ffffe0',
'lime': '00ff00',
'limegreen': '32cd32',
'linen': 'faf0e6',
'magenta': 'ff00ff',
'maroon': '800000',
'mediumaquamarine': '66cdaa',
'mediumblue': '0000cd',
'mediumorchid': 'ba55d3',
'mediumpurple': '9370db',
'mediumseagreen': '3cb371',
'mediumslateblue': '7b68ee',
'mediumspringgreen': '00fa9a',
'mediumturquoise': '48d1cc',
'mediumvioletred': 'c71585',
'midnightblue': '191970',
'mintcream': 'f5fffa',
'mistyrose': 'ffe4e1',
'moccasin': 'ffe4b5',
'navajowhite': 'ffdead',
'navy': '000080',
'oldlace': 'fdf5e6',
'olive': '808000',
'olivedrab': '6b8e23',
'orange': 'ffa500',
'orangered': 'ff4500',
'orchid': 'da70d6',
'palegoldenrod': 'eee8aa',
'palegreen': '98fb98',
'paleturquoise': 'afeeee',
'palevioletred': 'db7093',
'papayawhip': 'ffefd5',
'peachpuff': 'ffdab9',
'per': 'cd853f',
'pink': 'ffc0cb',
'plum': 'dda0dd',
'powderblue': 'b0e0e6',
'purple': '800080',
'red': 'ff0000',
'rosybrown': 'bc8f8f',
'royalblue': '4169e1',
'saddlebrown': '8b4513',
'salmon': 'fa8072',
'sandybrown': 'f4a460',
'seagreen': '2e8b57',
'seashell': 'fff5ee',
'sienna': 'a0522d',
'silver': 'c0c0c0',
'skyblue': '87ceeb',
'slateblue': '6a5acd',
'slategray': '708090',
'slategrey': '708090',
'snow': 'fffafa',
'springgreen': '00ff7f',
'steelblue': '4682b4',
'tan': 'd2b48c',
'teal': '008080',
'thistle': 'd8bfd8',
'tomato': 'ff6347',
'turquoise': '40e0d0',
'violet': 'ee82ee',
'wheat': 'f5deb3',
'white': 'ffffff',
'whitesmoke': 'f5f5f5',
'yellow': 'ffff00',
'yellowgreen': '9acd32',
}
Color = namedtuple('Color', 'red green blue')
# }}}
def to_color(raw, validate=False):
x = raw.strip().lower()
m = color_pat.match(x)
val = None
if m is not None:
val = m.group(1)
if len(val) == 3:
val = ''.join(2 * s for s in val)
else:
m = color_pat2.match(x)
if m is not None:
val = m.group(1) + m.group(2) + m.group(3)
else:
val = color_names.get(x)
if val is None:
if validate:
raise ValueError('Invalid color name: {}'.format(raw))
return
return Color(int(val[:2], 16), int(val[2:4], 16), int(val[4:], 16))
def parse_color_set(raw):
parts = raw.split(';')
for c, spec in [parts[i:i + 2] for i in range(0, len(parts), 2)]:
try:
c = int(c)
if c < 0 or c > 255:
raise IndexError('Out of bounds')
r, g, b = to_color(spec)
yield c, r << 16 | g << 8 | b
except Exception:
continue

View File

@ -7,6 +7,34 @@ from unittest import TestCase
from kitty.fast_data_types import LineBuf, Cursor, Screen, HistoryBuf
class Callbacks:
def __init__(self):
self.clear()
def write_to_child(self, data):
self.wtcbuf += data
def title_changed(self, data):
self.titlebuf += data
def icon_changed(self, data):
self.iconbuf += data
def set_dynamic_color(self, code, data):
self.colorbuf += data or ''
def set_color_table_color(self, code, data):
self.ctbuf += ''
def request_capabilities(self, q):
self.qbuf += q
def clear(self):
self.wtcbuf = b''
self.iconbuf = self.titlebuf = self.colorbuf = self.qbuf = self.ctbuf = ''
def filled_line_buf(ynum=5, xnum=5, cursor=Cursor()):
ans = LineBuf(ynum, xnum)
cursor.x = 0
@ -38,7 +66,7 @@ class BaseTest(TestCase):
ae = TestCase.assertEqual
def create_screen(self, cols=5, lines=5, scrollback=5):
return Screen(None, lines, cols, scrollback)
return Screen(Callbacks(), lines, cols, scrollback)
def assertEqualAttributes(self, c1, c2):
x1, y1, c1.x, c1.y = c1.x, c1.y, 0, 0

View File

@ -15,31 +15,6 @@ class CmdDump(list):
self.append(a)
class Callbacks:
def __init__(self):
self.clear()
def write_to_child(self, data):
self.wtcbuf += data
def title_changed(self, data):
self.titlebuf += data
def icon_changed(self, data):
self.iconbuf += data
def set_dynamic_color(self, code, data):
self.colorbuf += data
def request_capabilities(self, q):
self.qbuf += q
def clear(self):
self.wtcbuf = b''
self.iconbuf = self.titlebuf = self.colorbuf = self.qbuf = ''
class TestParser(BaseTest):
def parse_bytes_dump(self, s, x, *cmds):
@ -131,8 +106,7 @@ class TestParser(BaseTest):
pb('\033[38;2;1;2;3;48;2;7;8;9m', ('select_graphic_rendition', 10))
self.ae(s.cursor.fg, 1 << 24 | 2 << 16 | 3 << 8 | 2)
self.ae(s.cursor.bg, 7 << 24 | 8 << 16 | 9 << 8 | 2)
c = Callbacks()
s.callbacks = c
c = s.callbacks
pb('\033[5n', ('report_device_status', 5, 0))
self.ae(c.wtcbuf, b'\033[0n')
c.clear()
@ -153,8 +127,7 @@ class TestParser(BaseTest):
def test_osc_codes(self):
s = self.create_screen()
pb = partial(self.parse_bytes_dump, s)
c = Callbacks()
s.callbacks = c
c = s.callbacks
pb('a\033]2;xyz\x9cbcde', 'a', ('set_title', 'xyz'), 'bcde')
self.ae(str(s.line(0)), 'abcde')
self.ae(c.titlebuf, 'xyz')
@ -171,7 +144,6 @@ class TestParser(BaseTest):
def test_dcs_codes(self):
s = self.create_screen()
s.callbacks = Callbacks()
pb = partial(self.parse_bytes_dump, s)
pb('a\033P+q436f\x9cbcde', 'a', ('screen_request_capabilities', '436f'), 'bcde')
self.ae(str(s.line(0)), 'abcde')