Implement parsing of OSC 8
Also start work on storing hyperlinks with cells
This commit is contained in:
@@ -41,6 +41,7 @@ void log_error(const char *fmt, ...) __attribute__ ((format (printf, 1, 2)));
|
|||||||
typedef unsigned long long id_type;
|
typedef unsigned long long id_type;
|
||||||
typedef uint32_t char_type;
|
typedef uint32_t char_type;
|
||||||
typedef uint32_t color_type;
|
typedef uint32_t color_type;
|
||||||
|
typedef uint32_t hyperlink_id_type;
|
||||||
typedef uint16_t combining_type;
|
typedef uint16_t combining_type;
|
||||||
typedef uint32_t pixel;
|
typedef uint32_t pixel;
|
||||||
typedef unsigned int index_type;
|
typedef unsigned int index_type;
|
||||||
@@ -160,6 +161,7 @@ typedef struct {
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
char_type ch;
|
char_type ch;
|
||||||
combining_type cc_idx[2];
|
combining_type cc_idx[2];
|
||||||
|
hyperlink_id_type hyperlink_id;
|
||||||
} CPUCell;
|
} 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
|
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;
|
GPUCell *g = self->gpu_cells + at;
|
||||||
if (cursor == NULL) {
|
if (cursor == NULL) {
|
||||||
g->attrs = (self->gpu_cells[at].attrs & ATTRS_MASK_WITHOUT_WIDTH) | width;
|
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;
|
g->decoration_fg = cursor->decoration_fg & COL_MASK;
|
||||||
}
|
}
|
||||||
self->cpu_cells[at].ch = ch;
|
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));
|
memset(self->cpu_cells[at].cc_idx, 0, sizeof(self->cpu_cells[at].cc_idx));
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject*
|
static PyObject*
|
||||||
set_char(Line *self, PyObject *args) {
|
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;
|
unsigned int at, width=1;
|
||||||
int ch;
|
int ch;
|
||||||
Cursor *cursor = NULL;
|
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) {
|
if (at >= self->xnum) {
|
||||||
PyErr_SetString(PyExc_ValueError, "Out of bounds");
|
PyErr_SetString(PyExc_ValueError, "Out of bounds");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
line_set_char(self, at, ch, width, cursor, false);
|
line_set_char(self, at, ch, width, cursor, hyperlink_id);
|
||||||
Py_RETURN_NONE;
|
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_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);
|
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);
|
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_right_shift(Line *, unsigned int , unsigned int );
|
||||||
void line_add_combining_char(Line *, uint32_t , unsigned int );
|
void line_add_combining_char(Line *, uint32_t , unsigned int );
|
||||||
index_type line_url_start_at(Line *self, index_type x);
|
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) \
|
#define REPORT_OSC2(name, code, string) \
|
||||||
Py_XDECREF(PyObject_CallFunction(dump_callback, "sIO", #name, code, string)); PyErr_Clear();
|
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
|
#else
|
||||||
|
|
||||||
#define DUMP_UNUSED UNUSED
|
#define DUMP_UNUSED UNUSED
|
||||||
@@ -134,6 +137,7 @@ _report_params(PyObject *dump_callback, const char *name, unsigned int *params,
|
|||||||
#define FLUSH_DRAW
|
#define FLUSH_DRAW
|
||||||
#define REPORT_OSC(name, string)
|
#define REPORT_OSC(name, string)
|
||||||
#define REPORT_OSC2(name, code, string)
|
#define REPORT_OSC2(name, code, string)
|
||||||
|
#define REPORT_HYPERLINK(id, url)
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -302,6 +306,30 @@ handle_esc_mode_char(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_cal
|
|||||||
|
|
||||||
// OSC mode {{{
|
// 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
|
static inline void
|
||||||
dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
|
||||||
#define DISPATCH_OSC_WITH_CODE(name) REPORT_OSC2(name, code, string); name(screen, code, string);
|
#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
|
START_DISPATCH
|
||||||
DISPATCH_OSC_WITH_CODE(set_color_table_color);
|
DISPATCH_OSC_WITH_CODE(set_color_table_color);
|
||||||
END_DISPATCH
|
END_DISPATCH
|
||||||
|
case 8:
|
||||||
|
dispatch_hyperlink(screen, i, limit-i, dump_callback);
|
||||||
|
break;
|
||||||
case 9:
|
case 9:
|
||||||
case 99:
|
case 99:
|
||||||
START_DISPATCH
|
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->historybuf = alloc_historybuf(MAX(scrollback, lines), columns, OPT(scrollback_pager_history_size));
|
||||||
self->main_grman = grman_alloc();
|
self->main_grman = grman_alloc();
|
||||||
self->alt_grman = grman_alloc();
|
self->alt_grman = grman_alloc();
|
||||||
|
self->active_hyperlink_id = 0;
|
||||||
|
|
||||||
self->grman = self->main_grman;
|
self->grman = self->main_grman;
|
||||||
self->pending_mode.wait_time = s_double_to_monotonic_t(2.0);
|
self->pending_mode.wait_time = s_double_to_monotonic_t(2.0);
|
||||||
@@ -143,6 +144,7 @@ screen_reset(Screen *self) {
|
|||||||
historybuf_clear(self->historybuf);
|
historybuf_clear(self->historybuf);
|
||||||
grman_clear(self->grman, false, self->cell_size);
|
grman_clear(self->grman, false, self->cell_size);
|
||||||
self->modes = empty_modes;
|
self->modes = empty_modes;
|
||||||
|
self->active_hyperlink_id = 0;
|
||||||
#define R(name) self->color_profile->overridden.name = 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);
|
R(default_fg); R(default_bg); R(cursor_color); R(highlight_fg); R(highlight_bg);
|
||||||
#undef R
|
#undef R
|
||||||
@@ -359,6 +361,15 @@ selection_has_screen_line(Selection *s, int y) {
|
|||||||
return top <= y && y <= bottom;
|
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) {
|
static inline bool is_flag_pair(char_type a, char_type b) {
|
||||||
return is_flag_codepoint(a) && is_flag_codepoint(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;
|
CPUCell *cpu_cell = self->linebuf->line->cpu_cells + xpos;
|
||||||
GPUCell *gpu_cell = self->linebuf->line->gpu_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 ((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;
|
gpu_cell->attrs = (gpu_cell->attrs & !WIDTH_MASK) | 2;
|
||||||
if (xpos == self->columns - 1) move_widened_char(self, cpu_cell, gpu_cell, xpos, ypos);
|
if (xpos == self->columns - 1) move_widened_char(self, cpu_cell, gpu_cell, xpos, ypos);
|
||||||
else self->cursor->x++;
|
else self->cursor->x++;
|
||||||
@@ -476,10 +487,10 @@ screen_draw(Screen *self, uint32_t och) {
|
|||||||
if (self->modes.mIRM) {
|
if (self->modes.mIRM) {
|
||||||
line_right_shift(self->linebuf->line, self->cursor->x, char_width);
|
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++;
|
self->cursor->x++;
|
||||||
if (char_width == 2) {
|
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->cursor->x++;
|
||||||
}
|
}
|
||||||
self->is_dirty = true;
|
self->is_dirty = true;
|
||||||
@@ -646,6 +657,7 @@ screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const u
|
|||||||
void
|
void
|
||||||
screen_toggle_screen_buffer(Screen *self, bool save_cursor, bool clear_alt_screen) {
|
screen_toggle_screen_buffer(Screen *self, bool save_cursor, bool clear_alt_screen) {
|
||||||
bool to_alt = self->linebuf == self->main_linebuf;
|
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
|
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 (to_alt) {
|
||||||
if (clear_alt_screen) linebuf_clear(self->alt_linebuf, BLANK_CHAR);
|
if (clear_alt_screen) linebuf_clear(self->alt_linebuf, BLANK_CHAR);
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ typedef struct {
|
|||||||
PyObject *marker;
|
PyObject *marker;
|
||||||
bool has_focus;
|
bool has_focus;
|
||||||
bool has_activity_since_last_focus;
|
bool has_activity_since_last_focus;
|
||||||
|
hyperlink_id_type active_hyperlink_id;
|
||||||
} Screen;
|
} Screen;
|
||||||
|
|
||||||
|
|
||||||
@@ -206,6 +207,8 @@ bool screen_history_scroll(Screen *self, int amt, bool upwards);
|
|||||||
Line* screen_visual_line(Screen *self, index_type y);
|
Line* screen_visual_line(Screen *self, index_type y);
|
||||||
unsigned long screen_current_char_width(Screen *self);
|
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);
|
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);
|
void screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const uint8_t *payload);
|
||||||
bool screen_open_url(Screen*);
|
bool screen_open_url(Screen*);
|
||||||
void screen_dirty_sprite_positions(Screen *self);
|
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]9;test it\x07', ('desktop_notify', 9, 'test it'))
|
||||||
pb('\033]99;moo=foo;test it\x07', ('desktop_notify', 99, 'moo=foo;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')])
|
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):
|
def test_desktop_notify(self):
|
||||||
reset_registry()
|
reset_registry()
|
||||||
|
|||||||
Reference in New Issue
Block a user