New OSC 21 protocol for color control
Needs to be specced up
This commit is contained in:
parent
a927e1e4f4
commit
deff40df8a
9 changed files with 129 additions and 13 deletions
|
|
@ -464,7 +464,7 @@ static Color* alloc_color(unsigned char r, unsigned char g, unsigned char b, uns
|
|||
} else if (PyObject_TypeCheck(v, &Color_Type)) { \
|
||||
Color *c = (Color*)v; self->overridden.name.rgb = c->color.rgb; self->overridden.name.type = COLOR_IS_RGB; \
|
||||
} else if (v == Py_None) { \
|
||||
if (!nullable) { PyErr_SetString(PyExc_ValueError, #name " cannot be set to None"); return -1; } \
|
||||
if (!nullable) { PyErr_SetString(PyExc_TypeError, #name " cannot be set to None"); return -1; } \
|
||||
self->overridden.name.type = COLOR_IS_SPECIAL; self->overridden.name.rgb = 0; \
|
||||
} \
|
||||
self->dirty = true; return 0; \
|
||||
|
|
|
|||
19
kitty/rgb.py
generated
19
kitty/rgb.py
generated
|
|
@ -41,6 +41,17 @@ def parse_rgb(spec: str) -> Optional[Color]:
|
|||
return None
|
||||
|
||||
|
||||
def parse_single_intensity(x: str) -> int:
|
||||
return int(max(0, min(abs(float(x)), 1)) * 255)
|
||||
|
||||
|
||||
def parse_rgbi(spec: str) -> Optional[Color]:
|
||||
colors = spec.split('/')
|
||||
if len(colors) == 3:
|
||||
return Color(*map(parse_single_intensity, colors))
|
||||
return None
|
||||
|
||||
|
||||
def color_from_int(x: int) -> Color:
|
||||
return Color((x >> 16) & 255, (x >> 8) & 255, x & 255)
|
||||
|
||||
|
|
@ -67,8 +78,12 @@ def to_color(raw: str, validate: bool = False) -> Optional[Color]:
|
|||
with suppress(Exception):
|
||||
if raw.startswith('#'):
|
||||
val = parse_sharp(raw[1:])
|
||||
elif raw.startswith('rgb:'):
|
||||
val = parse_rgb(raw[4:])
|
||||
else:
|
||||
k, sep, v = raw.partition(':')
|
||||
if k == 'rgb':
|
||||
val = parse_rgb(v)
|
||||
elif k == 'rgbi':
|
||||
val = parse_rgbi(v)
|
||||
if val is None and validate:
|
||||
raise ValueError(f'Invalid color name: {raw!r}')
|
||||
return val
|
||||
|
|
|
|||
|
|
@ -2283,6 +2283,11 @@ set_dynamic_color(Screen *self, unsigned int code, PyObject *color) {
|
|||
else { CALLBACK("set_dynamic_color", "IO", code, color); }
|
||||
}
|
||||
|
||||
void
|
||||
color_control(Screen *self, unsigned int code, PyObject *spec) {
|
||||
if (spec) CALLBACK("color_control", "IO", code, spec);
|
||||
}
|
||||
|
||||
void
|
||||
clipboard_control(Screen *self, int code, PyObject *data) {
|
||||
if (code == 52 || code == -52) { CALLBACK("clipboard_control", "OO", data, code == -52 ? Py_True: Py_False); }
|
||||
|
|
|
|||
|
|
@ -227,6 +227,7 @@ void set_title(Screen *self, PyObject*);
|
|||
void desktop_notify(Screen *self, unsigned int, PyObject*);
|
||||
void set_icon(Screen *self, PyObject*);
|
||||
void set_dynamic_color(Screen *self, unsigned int code, PyObject*);
|
||||
void color_control(Screen *self, unsigned int code, PyObject*);
|
||||
void clipboard_control(Screen *self, int code, PyObject*);
|
||||
void shell_prompt_marking(Screen *self, char *buf);
|
||||
void file_transmission(Screen *self, PyObject*);
|
||||
|
|
|
|||
|
|
@ -528,6 +528,10 @@ dispatch_osc(PS *self, uint8_t *buf, size_t limit, bool is_extended_osc) {
|
|||
START_DISPATCH
|
||||
DISPATCH_OSC_WITH_CODE(set_dynamic_color);
|
||||
END_DISPATCH
|
||||
case 21:
|
||||
START_DISPATCH
|
||||
DISPATCH_OSC_WITH_CODE(color_control);
|
||||
END_DISPATCH
|
||||
case 52: case 5522:
|
||||
START_DISPATCH
|
||||
if (is_extended_osc && code == 52) code = -52;
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
SCROLL_LINE,
|
||||
SCROLL_PAGE,
|
||||
Color,
|
||||
ColorProfile,
|
||||
KeyEvent,
|
||||
Screen,
|
||||
add_timer,
|
||||
|
|
@ -103,6 +104,7 @@
|
|||
from .types import MouseEvent, OverlayType, WindowGeometry, ac, run_once
|
||||
from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict
|
||||
from .utils import (
|
||||
color_as_int,
|
||||
docs_url,
|
||||
key_val_matcher,
|
||||
kitty_ansi_sanitizer_pat,
|
||||
|
|
@ -460,6 +462,62 @@ def process_remote_print(msg: memoryview) -> str:
|
|||
return replace_c0_codes_except_nl_space_tab(base64_decode(msg)).decode('utf-8', 'replace')
|
||||
|
||||
|
||||
def color_control(cp: ColorProfile, code: int, value: Union[str, bytes, memoryview] = '') -> str:
|
||||
if isinstance(value, (bytes, memoryview)):
|
||||
value = str(value, 'utf-8', 'replace')
|
||||
responses = {}
|
||||
for rec in value.split(';'):
|
||||
key, sep, val = rec.partition('=')
|
||||
attr = {
|
||||
'foreground': 'default_fg', 'background': 'default_bg',
|
||||
'selection_background': 'highlight_bg', 'selection_foreground': 'highlight_fg',
|
||||
'cursor': 'cursor_color', 'cursor_text': 'cursor_text_color',
|
||||
'visual_bell': 'visual_bell_color', 'second_transparent_background': 'second_transparent_bg',
|
||||
}.get(key, '')
|
||||
colnum = -1
|
||||
with suppress(Exception):
|
||||
colnum = int(key)
|
||||
|
||||
def serialize_color(c: Optional[Color]) -> str:
|
||||
return '' if c is None else f'rgb:{c.red:02x}/{c.green:02x}/{c.blue:02x}'
|
||||
|
||||
if sep == '=':
|
||||
if val == '?':
|
||||
if attr:
|
||||
c = getattr(cp, attr)
|
||||
responses[key] = serialize_color(c)
|
||||
else:
|
||||
if 0 <= colnum <= 255:
|
||||
c = cp.as_color((colnum << 8) | 1)
|
||||
responses[key] = serialize_color(c)
|
||||
else:
|
||||
responses[key] = '?'
|
||||
else:
|
||||
if attr:
|
||||
if val:
|
||||
col = to_color(val)
|
||||
if col is not None:
|
||||
setattr(cp, attr, col)
|
||||
else:
|
||||
with suppress(TypeError):
|
||||
setattr(cp, attr, None)
|
||||
else:
|
||||
if 0 <= colnum <= 255:
|
||||
col = to_color(val)
|
||||
if col is not None:
|
||||
cp.set_color(colnum, color_as_int(col))
|
||||
else:
|
||||
if attr:
|
||||
delattr(cp, attr)
|
||||
else:
|
||||
if 0 <= colnum <= 255:
|
||||
cp.set_color(colnum, get_options().color_table[colnum])
|
||||
if responses:
|
||||
payload = ';'.join(f'{k}={v}' for k, v in responses.items())
|
||||
return f'{code};{payload}'
|
||||
return ''
|
||||
|
||||
|
||||
class EdgeWidths:
|
||||
left: Optional[float]
|
||||
top: Optional[float]
|
||||
|
|
@ -1192,6 +1250,11 @@ def notify_child_of_resize(self) -> None:
|
|||
if pty_size[0] > -1 and self.screen.in_band_resize_notification:
|
||||
self.screen.send_escape_code_to_child(ESC_CSI, f'48;{pty_size[0]};{pty_size[1]};{pty_size[3]};{pty_size[2]}t')
|
||||
|
||||
def color_control(self, code: int, value: Union[str, bytes, memoryview] = '') -> None:
|
||||
response = color_control(self.screen.color_profile, code, value)
|
||||
if response:
|
||||
self.screen.send_escape_code_to_child(ESC_OSC, response)
|
||||
|
||||
def set_dynamic_color(self, code: int, value: Union[str, bytes, memoryview] = '') -> None:
|
||||
if isinstance(value, (bytes, memoryview)):
|
||||
value = str(value, 'utf-8', 'replace')
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
from kitty.fast_data_types import Cursor, HistoryBuf, LineBuf, Screen, get_options, monotonic, set_options
|
||||
from kitty.options.parse import merge_result_dicts
|
||||
from kitty.options.types import Options, defaults
|
||||
from kitty.rgb import to_color
|
||||
from kitty.types import MouseEvent
|
||||
from kitty.utils import read_screen_size
|
||||
from kitty.window import decode_cmdline, process_remote_print, process_title_from_child
|
||||
|
|
@ -53,6 +54,18 @@ def write(self, data) -> None:
|
|||
def notify_child_of_resize(self):
|
||||
self.num_of_resize_events += 1
|
||||
|
||||
def color_control(self, code, data) -> None:
|
||||
from kitty.window import color_control
|
||||
response = color_control(self.color_profile, code, data)
|
||||
if response:
|
||||
def p(x):
|
||||
ans = to_color(x)
|
||||
if ans is None:
|
||||
ans = x
|
||||
return ans
|
||||
parts = {x.partition('=')[0]:p(x.partition('=')[2]) for x in response.split(';')[1:]}
|
||||
self.color_control_responses.append(parts)
|
||||
|
||||
def title_changed(self, data, is_base64=False) -> None:
|
||||
self.titlebuf.append(process_title_from_child(data, is_base64, ''))
|
||||
|
||||
|
|
@ -100,6 +113,7 @@ def clear(self) -> None:
|
|||
self.iconbuf = self.colorbuf = self.ctbuf = ''
|
||||
self.titlebuf = []
|
||||
self.printbuf = []
|
||||
self.color_control_responses = []
|
||||
self.notifications = []
|
||||
self.open_urls = []
|
||||
self.cc_buf = []
|
||||
|
|
@ -242,6 +256,7 @@ def create_screen(self, cols=5, lines=5, scrollback=5, cell_width=10, cell_heigh
|
|||
self.set_options(options)
|
||||
c = Callbacks()
|
||||
s = Screen(c, lines, cols, scrollback, cell_width, cell_height, 0, c)
|
||||
c.color_profile = s.color_profile
|
||||
return s
|
||||
|
||||
def create_pty(
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@
|
|||
import sys
|
||||
import tempfile
|
||||
|
||||
from kitty.config import defaults
|
||||
from kitty.fast_data_types import (
|
||||
Color,
|
||||
ColorProfile,
|
||||
HistoryBuf,
|
||||
LineBuf,
|
||||
expand_ansi_c_escapes,
|
||||
|
|
@ -476,13 +474,6 @@ def on_csi(x):
|
|||
self.ae(sanitize_url_for_dispay_to_user(
|
||||
'h://a\u0430b.com/El%20Ni%C3%B1o/'), 'h://xn--ab-7kc.com/El Niño/')
|
||||
|
||||
def test_color_profile(self):
|
||||
c = ColorProfile(defaults)
|
||||
for i in range(8):
|
||||
col = getattr(defaults, f'color{i}')
|
||||
self.ae(c.as_color(i << 8 | 1), col)
|
||||
self.ae(c.as_color(255 << 8 | 1), Color(0xee, 0xee, 0xee))
|
||||
|
||||
def test_historybuf(self):
|
||||
lb = filled_line_buf()
|
||||
hb = HistoryBuf(5, 5)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from kitty.fast_data_types import DECAWM, DECCOLM, DECOM, IRM, VT_PARSER_BUFFER_SIZE, Cursor
|
||||
from kitty.config import defaults
|
||||
from kitty.fast_data_types import DECAWM, DECCOLM, DECOM, IRM, VT_PARSER_BUFFER_SIZE, Color, ColorProfile, Cursor
|
||||
from kitty.marks import marker_from_function, marker_from_regex
|
||||
from kitty.rgb import color_names
|
||||
from kitty.window import pagerhist
|
||||
|
||||
from . import BaseTest, parse_bytes
|
||||
|
|
@ -1258,3 +1260,23 @@ def t(q, e=None):
|
|||
t('<', '0')
|
||||
t('=left_ptr', 'default')
|
||||
t('=fleur', 'move')
|
||||
|
||||
def test_color_profile(self):
|
||||
c = ColorProfile(defaults)
|
||||
for i in range(8):
|
||||
col = getattr(defaults, f'color{i}')
|
||||
self.ae(c.as_color(i << 8 | 1), col)
|
||||
self.ae(c.as_color(255 << 8 | 1), Color(0xee, 0xee, 0xee))
|
||||
s = self.create_screen()
|
||||
s.color_profile.reload_from_opts(defaults)
|
||||
def q(send, expected=None):
|
||||
s.callbacks.clear()
|
||||
parse_bytes(s, b'\x1b]21;' + ';'.join(f'{k}={v}' for k, v in send.items()).encode() + b'\a')
|
||||
self.ae(s.callbacks.color_control_responses, [expected] if expected else [])
|
||||
q({k: '?' for k in 'background foreground 213 unknown'.split()}, {
|
||||
'background': defaults.background, 'foreground': defaults.foreground, '213': defaults.color213, 'unknown': '?'})
|
||||
q({'background':'aquamarine'})
|
||||
q({'background':'?', 'selection_background': '?'}, {'background': color_names['aquamarine'], 'selection_background': s.color_profile.highlight_bg})
|
||||
q({'selection_background': ''})
|
||||
self.assertIsNone(s.color_profile.highlight_bg)
|
||||
q({'selection_background': '?'}, {'selection_background': ''})
|
||||
|
|
|
|||
Loading…
Reference in a new issue