Implement parsing of OSC 8
Also start work on storing hyperlinks with cells
This commit is contained in:
parent
0cc54484a4
commit
e99d93ca30
@ -41,6 +41,7 @@ void log_error(const char *fmt, ...) __attribute__ ((format (printf, 1, 2)));
|
||||
typedef unsigned long long id_type;
|
||||
typedef uint32_t char_type;
|
||||
typedef uint32_t color_type;
|
||||
typedef uint32_t hyperlink_id_type;
|
||||
typedef uint16_t combining_type;
|
||||
typedef uint32_t pixel;
|
||||
typedef unsigned int index_type;
|
||||
@ -160,6 +161,7 @@ typedef struct {
|
||||
typedef struct {
|
||||
char_type ch;
|
||||
combining_type cc_idx[2];
|
||||
hyperlink_id_type hyperlink_id;
|
||||
} CPUCell;
|
||||
|
||||
|
||||
|
||||
28
kitty/hyperlink.c
Normal file
28
kitty/hyperlink.c
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* hyperlink.c
|
||||
* Copyright (C) 2020 Kovid Goyal <kovid at kovidgoyal.net>
|
||||
*
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <string.h>
|
||||
#include "screen.h"
|
||||
|
||||
bool
|
||||
parse_osc_8(char *buf, char **id, char **url) {
|
||||
char *boundary = strstr(buf, ";");
|
||||
if (boundary == NULL) return false;
|
||||
*boundary = 0;
|
||||
if (*(boundary + 1)) *url = boundary + 1;
|
||||
char *save, *token = strtok_r(buf, ":", &save);
|
||||
while (token != NULL) {
|
||||
size_t len = strlen(token);
|
||||
if (len > 3 && token[0] == 'i' && token[1] == 'd' && token[2] == '=' && token[3]) {
|
||||
*id = token + 3;
|
||||
break;
|
||||
}
|
||||
token = strtok_r(NULL, ":", &save);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
10
kitty/line.c
10
kitty/line.c
@ -558,7 +558,7 @@ line_get_char(Line *self, index_type at) {
|
||||
}
|
||||
|
||||
void
|
||||
line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Cursor *cursor, bool UNUSED is_second) {
|
||||
line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Cursor *cursor, hyperlink_id_type hyperlink_id) {
|
||||
GPUCell *g = self->gpu_cells + at;
|
||||
if (cursor == NULL) {
|
||||
g->attrs = (self->gpu_cells[at].attrs & ATTRS_MASK_WITHOUT_WIDTH) | width;
|
||||
@ -569,22 +569,24 @@ line_set_char(Line *self, unsigned int at, uint32_t ch, unsigned int width, Curs
|
||||
g->decoration_fg = cursor->decoration_fg & COL_MASK;
|
||||
}
|
||||
self->cpu_cells[at].ch = ch;
|
||||
self->cpu_cells[at].hyperlink_id = hyperlink_id;
|
||||
memset(self->cpu_cells[at].cc_idx, 0, sizeof(self->cpu_cells[at].cc_idx));
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
set_char(Line *self, PyObject *args) {
|
||||
#define set_char_doc "set_char(at, ch, width=1, cursor=None) -> Set the character at the specified cell. If cursor is not None, also set attributes from that cursor."
|
||||
#define set_char_doc "set_char(at, ch, width=1, cursor=None, hyperlink_id=0) -> Set the character at the specified cell. If cursor is not None, also set attributes from that cursor."
|
||||
unsigned int at, width=1;
|
||||
int ch;
|
||||
Cursor *cursor = NULL;
|
||||
unsigned int hyperlink_id = 0;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "IC|IO!", &at, &ch, &width, &Cursor_Type, &cursor)) return NULL;
|
||||
if (!PyArg_ParseTuple(args, "IC|IO!I", &at, &ch, &width, &Cursor_Type, &cursor, &hyperlink_id)) return NULL;
|
||||
if (at >= self->xnum) {
|
||||
PyErr_SetString(PyExc_ValueError, "Out of bounds");
|
||||
return NULL;
|
||||
}
|
||||
line_set_char(self, at, ch, width, cursor, false);
|
||||
line_set_char(self, at, ch, width, cursor, hyperlink_id);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
||||
@ -70,7 +70,7 @@ typedef Line*(get_line_func)(void *, int);
|
||||
void line_clear_text(Line *self, unsigned int at, unsigned int num, char_type ch);
|
||||
void line_apply_cursor(Line *self, Cursor *cursor, unsigned int at, unsigned int num, bool clear_char);
|
||||
char_type line_get_char(Line *self, index_type at);
|
||||
void line_set_char(Line *, unsigned int , uint32_t , unsigned int , Cursor *, bool);
|
||||
void line_set_char(Line *, unsigned int , uint32_t , unsigned int , Cursor *, hyperlink_id_type);
|
||||
void line_right_shift(Line *, unsigned int , unsigned int );
|
||||
void line_add_combining_char(Line *, uint32_t , unsigned int );
|
||||
index_type line_url_start_at(Line *self, index_type x);
|
||||
|
||||
@ -121,6 +121,9 @@ _report_params(PyObject *dump_callback, const char *name, unsigned int *params,
|
||||
#define REPORT_OSC2(name, code, string) \
|
||||
Py_XDECREF(PyObject_CallFunction(dump_callback, "sIO", #name, code, string)); PyErr_Clear();
|
||||
|
||||
#define REPORT_HYPERLINK(id, url) \
|
||||
Py_XDECREF(PyObject_CallFunction(dump_callback, "szz", "set_active_hyperlink", id, url)); PyErr_Clear();
|
||||
|
||||
#else
|
||||
|
||||
#define DUMP_UNUSED UNUSED
|
||||
@ -134,6 +137,7 @@ _report_params(PyObject *dump_callback, const char *name, unsigned int *params,
|
||||
#define FLUSH_DRAW
|
||||
#define REPORT_OSC(name, string)
|
||||
#define REPORT_OSC2(name, code, string)
|
||||
#define REPORT_HYPERLINK(id, url)
|
||||
|
||||
#endif
|
||||
|
||||
@ -302,6 +306,30 @@ handle_esc_mode_char(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_cal
|
||||
|
||||
// OSC mode {{{
|
||||
|
||||
static inline void
|
||||
dispatch_hyperlink(Screen *screen, size_t pos, size_t size, PyObject DUMP_UNUSED *dump_callback) {
|
||||
// since the spec says only ASCII printable chars are allowed in OSC 8, we
|
||||
// can just convert to char* directly
|
||||
if (!size) return; // ignore empty OSC 8 since it must have two semi-colons to be valid, which means one semi-colon here
|
||||
char *id = NULL, *url = NULL;
|
||||
char *data = malloc(size + 1);
|
||||
if (!data) fatal("Out of memory");
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
data[i] = screen->parser_buf[i + pos] & 0x7f;
|
||||
if (data[i] < 32 || data[i] > 126) data[i] = '_';
|
||||
}
|
||||
data[size] = 0;
|
||||
|
||||
if (parse_osc_8(data, &id, &url)) {
|
||||
REPORT_HYPERLINK(id, url);
|
||||
set_active_hyperlink(screen, id, url);
|
||||
} else {
|
||||
REPORT_ERROR("Ignoring malformed OSC 8 code");
|
||||
}
|
||||
|
||||
free(data);
|
||||
}
|
||||
|
||||
static inline void
|
||||
dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
#define DISPATCH_OSC_WITH_CODE(name) REPORT_OSC2(name, code, string); name(screen, code, string);
|
||||
@ -339,6 +367,9 @@ dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||
START_DISPATCH
|
||||
DISPATCH_OSC_WITH_CODE(set_color_table_color);
|
||||
END_DISPATCH
|
||||
case 8:
|
||||
dispatch_hyperlink(screen, i, limit-i, dump_callback);
|
||||
break;
|
||||
case 9:
|
||||
case 99:
|
||||
START_DISPATCH
|
||||
|
||||
@ -111,6 +111,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
||||
self->historybuf = alloc_historybuf(MAX(scrollback, lines), columns, OPT(scrollback_pager_history_size));
|
||||
self->main_grman = grman_alloc();
|
||||
self->alt_grman = grman_alloc();
|
||||
self->active_hyperlink_id = 0;
|
||||
|
||||
self->grman = self->main_grman;
|
||||
self->pending_mode.wait_time = s_double_to_monotonic_t(2.0);
|
||||
@ -143,6 +144,7 @@ screen_reset(Screen *self) {
|
||||
historybuf_clear(self->historybuf);
|
||||
grman_clear(self->grman, false, self->cell_size);
|
||||
self->modes = empty_modes;
|
||||
self->active_hyperlink_id = 0;
|
||||
#define R(name) self->color_profile->overridden.name = 0
|
||||
R(default_fg); R(default_bg); R(cursor_color); R(highlight_fg); R(highlight_bg);
|
||||
#undef R
|
||||
@ -359,6 +361,15 @@ selection_has_screen_line(Selection *s, int y) {
|
||||
return top <= y && y <= bottom;
|
||||
}
|
||||
|
||||
void
|
||||
set_active_hyperlink(Screen *self, char *id, char *url) {
|
||||
(void)id;
|
||||
if (!url || !url[0]) {
|
||||
self->active_hyperlink_id = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static inline bool is_flag_pair(char_type a, char_type b) {
|
||||
return is_flag_codepoint(a) && is_flag_codepoint(b);
|
||||
}
|
||||
@ -419,7 +430,7 @@ draw_combining_char(Screen *self, char_type ch) {
|
||||
CPUCell *cpu_cell = self->linebuf->line->cpu_cells + xpos;
|
||||
GPUCell *gpu_cell = self->linebuf->line->gpu_cells + xpos;
|
||||
if ((gpu_cell->attrs & WIDTH_MASK) != 2 && cpu_cell->cc_idx[0] == VS16 && is_emoji_presentation_base(cpu_cell->ch)) {
|
||||
if (self->cursor->x <= self->columns - 1) line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, true);
|
||||
if (self->cursor->x <= self->columns - 1) line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, self->active_hyperlink_id);
|
||||
gpu_cell->attrs = (gpu_cell->attrs & !WIDTH_MASK) | 2;
|
||||
if (xpos == self->columns - 1) move_widened_char(self, cpu_cell, gpu_cell, xpos, ypos);
|
||||
else self->cursor->x++;
|
||||
@ -476,10 +487,10 @@ screen_draw(Screen *self, uint32_t och) {
|
||||
if (self->modes.mIRM) {
|
||||
line_right_shift(self->linebuf->line, self->cursor->x, char_width);
|
||||
}
|
||||
line_set_char(self->linebuf->line, self->cursor->x, ch, char_width, self->cursor, false);
|
||||
line_set_char(self->linebuf->line, self->cursor->x, ch, char_width, self->cursor, self->active_hyperlink_id);
|
||||
self->cursor->x++;
|
||||
if (char_width == 2) {
|
||||
line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, true);
|
||||
line_set_char(self->linebuf->line, self->cursor->x, 0, 0, self->cursor, self->active_hyperlink_id);
|
||||
self->cursor->x++;
|
||||
}
|
||||
self->is_dirty = true;
|
||||
@ -646,6 +657,7 @@ screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const u
|
||||
void
|
||||
screen_toggle_screen_buffer(Screen *self, bool save_cursor, bool clear_alt_screen) {
|
||||
bool to_alt = self->linebuf == self->main_linebuf;
|
||||
self->active_hyperlink_id = 0;
|
||||
grman_clear(self->alt_grman, true, self->cell_size); // always clear the alt buffer graphics to free up resources, since it has to be cleared when switching back to it anyway
|
||||
if (to_alt) {
|
||||
if (clear_alt_screen) linebuf_clear(self->alt_linebuf, BLANK_CHAR);
|
||||
|
||||
@ -123,6 +123,7 @@ typedef struct {
|
||||
PyObject *marker;
|
||||
bool has_focus;
|
||||
bool has_activity_since_last_focus;
|
||||
hyperlink_id_type active_hyperlink_id;
|
||||
} Screen;
|
||||
|
||||
|
||||
@ -206,6 +207,8 @@ bool screen_history_scroll(Screen *self, int amt, bool upwards);
|
||||
Line* screen_visual_line(Screen *self, index_type y);
|
||||
unsigned long screen_current_char_width(Screen *self);
|
||||
void screen_mark_url(Screen *self, index_type start_x, index_type start_y, index_type end_x, index_type end_y);
|
||||
bool parse_osc_8(char *buf, char **id, char **url);
|
||||
void set_active_hyperlink(Screen*, char*, char*);
|
||||
void screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const uint8_t *payload);
|
||||
bool screen_open_url(Screen*);
|
||||
void screen_dirty_sprite_positions(Screen *self);
|
||||
|
||||
@ -227,6 +227,12 @@ class TestParser(BaseTest):
|
||||
pb('\033]9;test it\x07', ('desktop_notify', 9, 'test it'))
|
||||
pb('\033]99;moo=foo;test it\x07', ('desktop_notify', 99, 'moo=foo;test it'))
|
||||
self.ae(c.notifications, [(9, ''), (9, 'test it'), (99, 'moo=foo;test it')])
|
||||
c.clear()
|
||||
pb('\033]8;;\x07', ('set_active_hyperlink', None, None))
|
||||
pb('\033]8moo\x07', ('Ignoring malformed OSC 8 code',))
|
||||
pb('\033]8;moo\x07', ('Ignoring malformed OSC 8 code',))
|
||||
pb('\033]8;id=xyz;\x07', ('set_active_hyperlink', 'xyz', None))
|
||||
pb('\033]8;moo:x=z:id=xyz:id=abc;http://yay;.com\x07', ('set_active_hyperlink', 'xyz', 'http://yay;.com'))
|
||||
|
||||
def test_desktop_notify(self):
|
||||
reset_registry()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user