Implement parsing of OSC 8

Also start work on storing hyperlinks with cells
This commit is contained in:
Kovid Goyal 2020-08-25 19:50:13 +05:30
parent 0cc54484a4
commit e99d93ca30
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
8 changed files with 92 additions and 8 deletions

View File

@ -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
View 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;
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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()