From 2cc84db97978388475504592c88911046bcd2eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 31 Jan 2021 11:12:07 +0100 Subject: [PATCH] urls: initial support for detecting URLs and rendering jump-labels The jump labels work, but is currently hardcoded to use xdg-open --- input.c | 13 +- meson.build | 1 + pgo/pgo.c | 1 + render.c | 123 +++++++++++++++---- render.h | 1 + shm.h | 3 + terminal.c | 3 + terminal.h | 16 +++ url-mode.c | 337 ++++++++++++++++++++++++++++++++++++++++++++++++++++ url-mode.h | 18 +++ wayland.c | 8 ++ wayland.h | 7 ++ 12 files changed, 503 insertions(+), 28 deletions(-) create mode 100644 url-mode.c create mode 100644 url-mode.h diff --git a/input.c b/input.c index 892b7f32..9dbdaad1 100644 --- a/input.c +++ b/input.c @@ -34,6 +34,7 @@ #include "spawn.h" #include "terminal.h" #include "tokenize.h" +#include "url-mode.h" #include "util.h" #include "vt.h" #include "xmalloc.h" @@ -272,8 +273,11 @@ execute_binding(struct seat *seat, struct terminal *term, } case BIND_ACTION_SHOW_URLS: - assert(false); - break; + xassert(!urls_mode_is_active(term)); + + urls_collect(term); + render_refresh_urls(term); + return true; case BIND_ACTION_SELECT_BEGIN: selection_start( @@ -854,6 +858,11 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, start_repeater(seat, key); search_input(seat, term, key, sym, effective_mods, serial); return; + } else if (urls_mode_is_active(term)) { + if (should_repeat) + start_repeater(seat, key); + urls_input(seat, term, key, sym, effective_mods, serial); + return; } #if 0 diff --git a/meson.build b/meson.build index b9168206..f2e6da2b 100644 --- a/meson.build +++ b/meson.build @@ -180,6 +180,7 @@ executable( 'slave.c', 'slave.h', 'spawn.c', 'spawn.h', 'tokenize.c', 'tokenize.h', + 'url-mode.c', 'url-mode.h', 'user-notification.h', 'wayland.c', 'wayland.h', wl_proto_src + wl_proto_headers, version, diff --git a/pgo/pgo.c b/pgo/pgo.c index fa95a0ec..4bb67d35 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -138,6 +138,7 @@ notify_notify(const struct terminal *term, const char *title, const char *body) void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data) {} void reaper_del(struct reaper *reaper, pid_t pid) {} +void urls_reset(struct terminal *term) {} int main(int argc, const char *const *argv) diff --git a/render.c b/render.c index aa415a18..e19d187c 100644 --- a/render.c +++ b/render.c @@ -28,13 +28,14 @@ #include "config.h" #include "grid.h" #include "hsl.h" +#include "ime.h" #include "quirks.h" #include "selection.h" -#include "sixel.h" #include "shm.h" +#include "sixel.h" +#include "url-mode.h" #include "util.h" #include "xmalloc.h" -#include "ime.h" #define TIME_SCROLL_DAMAGE 0 @@ -1706,10 +1707,10 @@ static void render_osd(struct terminal *term, struct wl_surface *surf, struct wl_subsurface *sub_surf, struct buffer *buf, - const wchar_t *text, uint32_t _fg, uint32_t _bg, + const wchar_t *text, uint32_t _fg, uint32_t _bg, int alpha, unsigned width, unsigned height, unsigned x, unsigned y) { - pixman_color_t bg = color_hex_to_pixman(_bg); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &bg, 1, &(pixman_rectangle16_t){0, 0, width, height}); @@ -1896,7 +1897,7 @@ render_scrollback_position(struct terminal *term) term, win->scrollback_indicator_surface, win->scrollback_indicator_sub_surface, buf, text, - term->colors.table[0], term->colors.table[8 + 4], + term->colors.table[0], term->colors.table[8 + 4], 0xffff, width, height, width - margin - wcslen(text) * term->cell_width, margin); } @@ -1928,7 +1929,7 @@ render_render_timer(struct terminal *term, struct timeval render_time) term, win->render_timer_surface, win->render_timer_sub_surface, buf, text, - term->colors.table[0], term->colors.table[8 + 1], + term->colors.table[0], term->colors.table[8 + 1], 0xffff, width, height, margin, margin); } @@ -2517,6 +2518,61 @@ render_search_box(struct terminal *term) #undef WINDOW_Y } +static void +render_urls(struct terminal *term) +{ + struct wl_window *win = term->window; + xassert(tll_length(win->urls) > 0); + + tll_foreach(win->urls, it) { + const struct url *url = it->item.url; + const wchar_t *text = url->text; + const wchar_t *key = url->key; + + struct wl_surface *surf = it->item.surf; + struct wl_subsurface *sub_surf = it->item.sub_surf; + + if (surf == NULL || sub_surf == NULL) + continue; + + size_t text_len = wcslen(text); + size_t chars = wcslen(key) + (text_len > 0 ? 3 + text_len : 0); + + const size_t max_chars = 50; + chars = max(chars, max_chars); + + wchar_t label[chars + 2]; + if (text_len == 0) + wcscpy(label, key); + else { + int count = swprintf(label, chars, L"%s - %s", key, text); + if (count >= max_chars) { + label[max_chars] = L'…'; + label[max_chars + 1] = L'\0'; + } + } + + size_t len = wcslen(label); + int cols = wcswidth(label, len); + + const int margin = 3 * term->scale; + int width = 2 * margin + cols * term->cell_width; + int height = 2 * margin + term->cell_height; + + struct buffer *buf = shm_get_buffer( + term->wl->shm, width, height, shm_cookie_url(url), false, 1); + + const struct coord *pos = &url->start; + wl_subsurface_set_position( + sub_surf, + (term->margins.left + pos->col * term->cell_width - term->cell_width) / term->scale, + (term->margins.top + pos->row * term->cell_height - term->cell_height / 2) / term->scale); + render_osd(term, surf, sub_surf, buf, label, + term->colors.table[0], term->colors.table[3], 0xf000, + width, height, margin, margin); + } +} + static void render_update_title(struct terminal *term) { @@ -2545,13 +2601,15 @@ frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_da bool grid = term->render.pending.grid; bool csd = term->render.pending.csd; - bool search = term->render.pending.search; + bool search = term->is_searching && term->render.pending.search; bool title = term->render.pending.title; + bool urls = urls_mode_is_active(term) > 0 && term->render.pending.urls; term->render.pending.grid = false; term->render.pending.csd = false; term->render.pending.search = false; term->render.pending.title = false; + term->render.pending.urls = false; if (csd && term->window->use_csd == CSD_YES) { quirk_weston_csd_on(term); @@ -2562,15 +2620,19 @@ frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_da if (title) render_update_title(term); - if (search && term->is_searching) + if (search) render_search_box(term); - tll_foreach(term->wl->seats, it) + if (urls) + render_urls(term); + + if (grid && (!term->delayed_render_timer.is_armed | csd | search | urls)) + grid_render(term); + + tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) ime_update_cursor_rect(&it->item, term); - - if (grid && (!term->delayed_render_timer.is_armed || csd || search)) - grid_render(term); + } } static void @@ -2755,6 +2817,9 @@ maybe_resize(struct terminal *term, int width, int height, bool force) /* Cancel an application initiated "Synchronized Update" */ term_disable_app_sync_updates(term); + /* Drop out of URL mode */ + urls_reset(term); + term->width = width; term->height = height; term->scale = scale; @@ -2981,27 +3046,21 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) bool grid = term->render.refresh.grid; bool csd = term->render.refresh.csd; - bool search = term->render.refresh.search; + bool search = term->is_searching && term->render.refresh.search; bool title = term->render.refresh.title; + bool urls = urls_mode_is_active(term) && term->render.refresh.urls; - if (!term->is_searching) - search = false; - - if (!(grid | csd | search | title)) + if (!(grid | csd | search | title | urls)) continue; - if (term->render.app_sync_updates.enabled && !(csd | search | title)) + if (term->render.app_sync_updates.enabled && !(csd | search | title | urls)) continue; - if (csd | search) { - /* Force update of parent surface */ - grid = true; - } - term->render.refresh.grid = false; term->render.refresh.csd = false; term->render.refresh.search = false; term->render.refresh.title = false; + term->render.refresh.urls = false; if (term->window->frame_callback == NULL) { if (csd && term->window->use_csd == CSD_YES) { @@ -3013,17 +3072,22 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) render_update_title(term); if (search) render_search_box(term); - tll_foreach(term->wl->seats, it) + if (urls) + render_urls(term); + if (grid | csd | search | urls) + grid_render(term); + + tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) ime_update_cursor_rect(&it->item, term); - if (grid) - grid_render(term); + } } else { /* Tells the frame callback to render again */ term->render.pending.grid |= grid; term->render.pending.csd |= csd; term->render.pending.search |= search; term->render.pending.title |= title; + term->render.pending.urls |= urls; } } @@ -3065,6 +3129,13 @@ render_refresh_search(struct terminal *term) term->render.refresh.search = true; } +void +render_refresh_urls(struct terminal *term) +{ + if (urls_mode_is_active(term)) + term->render.refresh.urls = true; +} + bool render_xcursor_set(struct seat *seat, struct terminal *term, const char *xcursor) { diff --git a/render.h b/render.h index 05c79322..1179a5dc 100644 --- a/render.h +++ b/render.h @@ -16,6 +16,7 @@ void render_refresh(struct terminal *term); void render_refresh_csd(struct terminal *term); void render_refresh_search(struct terminal *term); void render_refresh_title(struct terminal *term); +void render_refresh_urls(struct terminal *term); bool render_xcursor_set(struct seat *seat, struct terminal *term, const char *xcursor); struct render_worker_context { diff --git a/shm.h b/shm.h index fc48308e..2e819369 100644 --- a/shm.h +++ b/shm.h @@ -52,3 +52,6 @@ static inline unsigned long shm_cookie_search(const struct terminal *term) { ret static inline unsigned long shm_cookie_scrollback_indicator(const struct terminal *term) { return (unsigned long)(uintptr_t)term + 2; } static inline unsigned long shm_cookie_render_timer(const struct terminal *term) { return (unsigned long)(uintptr_t)term + 3; } static inline unsigned long shm_cookie_csd(const struct terminal *term, int n) { return (unsigned long)((uintptr_t)term + 4 + (n)); } + +struct url; +static inline unsigned long shm_cookie_url(const struct url *url) { return (unsigned long)(uintptr_t)url; } diff --git a/terminal.c b/terminal.c index 701313cc..1eed0966 100644 --- a/terminal.c +++ b/terminal.c @@ -36,6 +36,7 @@ #include "sixel.h" #include "slave.h" #include "spawn.h" +#include "url-mode.h" #include "util.h" #include "vt.h" #include "xmalloc.h" @@ -1485,6 +1486,7 @@ term_destroy(struct terminal *term) tll_free(term->alt.sixel_images); sixel_fini(term); + urls_reset(term); term_ime_reset(term); free(term->foot_exe); @@ -2963,3 +2965,4 @@ term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width, } #endif } + diff --git a/terminal.h b/terminal.h index 17873c40..24183c7b 100644 --- a/terminal.h +++ b/terminal.h @@ -223,6 +223,14 @@ enum term_surface { typedef tll(struct ptmx_buffer) ptmx_buffer_list_t; +struct url { + wchar_t *url; + wchar_t *text; + wchar_t key[4]; + struct coord start; + struct coord end; +}; + struct terminal { struct fdm *fdm; struct reaper *reaper; @@ -421,6 +429,7 @@ struct terminal { bool csd; bool search; bool title; + bool urls; } refresh; /* Scheduled for rendering, in the next frame callback */ @@ -429,6 +438,7 @@ struct terminal { bool csd; bool search; bool title; + bool urls; } pending; bool margins; /* Someone explicitly requested a refresh of the margins */ @@ -499,6 +509,9 @@ struct terminal { unsigned max_height; /* Maximum image height, in pixels */ } sixel; + tll(struct url) urls; + wchar_t url_keys[5]; + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED struct { bool enabled; @@ -646,3 +659,6 @@ void term_ime_disable(struct terminal *term); void term_ime_reset(struct terminal *term); void term_ime_set_cursor_rect( struct terminal *term, int x, int y, int width, int height); + +void term_urls_reset(struct terminal *term); +void term_collect_urls(struct terminal *term); diff --git a/url-mode.c b/url-mode.c new file mode 100644 index 00000000..aabb9032 --- /dev/null +++ b/url-mode.c @@ -0,0 +1,337 @@ +#include "url-mode.h" + +#include +#include + +#define LOG_MODULE "url-mode" +#define LOG_ENABLE_DBG 1 +#include "log.h" +#include "grid.h" +#include "spawn.h" +#include "terminal.h" +#include "util.h" +#include "xmalloc.h" + +static bool +execute_binding(struct seat *seat, struct terminal *term, + enum bind_action_url action, uint32_t serial) +{ + switch (action) { + case BIND_ACTION_URL_NONE: + return false; + + case BIND_ACTION_URL_CANCEL: + urls_reset(term); + return true; + + case BIND_ACTION_URL_COUNT: + return false; + + } + return true; +} + +void +urls_input(struct seat *seat, struct terminal *term, uint32_t key, + xkb_keysym_t sym, xkb_mod_mask_t mods, uint32_t serial) +{ + /* Key bindings */ + tll_foreach(seat->kbd.bindings.url, it) { + if (it->item.bind.mods != mods) + continue; + + /* Match symbol */ + if (it->item.bind.sym == sym) { + execute_binding(seat, term, it->item.action, serial); + return; + } + + /* Match raw key code */ + tll_foreach(it->item.bind.key_codes, code) { + if (code->item == key) { + execute_binding(seat, term, it->item.action, serial); + return; + } + } + } + + wchar_t wc = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); + + /* + * Determine if this is a “valid” key. I.e. if there is an URL + * label with a key combo where this key is the next in + * sequence. + */ + + size_t seq_len = wcslen(term->url_keys); + + bool is_valid = false; + const struct url *match = NULL; + + tll_foreach(term->urls, it) { + const struct url *url = &it->item; + const size_t key_len = wcslen(it->item.key); + + if (key_len >= seq_len + 1 && + wcsncmp(url->key, term->url_keys, seq_len) == 0 && + url->key[seq_len] == wc) + { + is_valid = true; + if (key_len == seq_len + 1) { + match = url; + break; + } + } + } + + if (match) { + size_t chars = wcstombs(NULL, match->url, 0); + + char url_utf8[chars + 1]; + wcstombs(url_utf8, match->url, chars); + + spawn(term->reaper, term->cwd, (char *const[]){"xdg-open", url_utf8, NULL}, -1, -1, -1); + urls_reset(term); + } else if (is_valid) { + xassert(seq_len + 1 <= ALEN(term->url_keys)); + term->url_keys[seq_len] = wc; + } +} + +IGNORE_WARNING("-Wpedantic") + +static void +auto_detected(struct terminal *term) +{ + static const wchar_t *const prots[] = { + L"http://", + L"https://", + }; + + size_t max_prot_len = 0; + for (size_t i = 0; i < ALEN(prots); i++) { + size_t len = wcslen(prots[i]); + if (len > max_prot_len) + max_prot_len = len; + } + + wchar_t proto_chars[max_prot_len]; + struct coord proto_start[max_prot_len]; + size_t proto_char_count = 0; + + enum { + STATE_PROTOCOL, + STATE_URL, + } state = STATE_PROTOCOL; + + struct coord start = {-1, -1}; + wchar_t url[term->cols * term->rows + 1]; + size_t len = 0; + + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + + for (int c = 0; c < term->cols; c++) { + const struct cell *cell = &row->cells[c]; + wchar_t wc = towlower(cell->wc); + + switch (state) { + case STATE_PROTOCOL: + for (size_t i = 0; i < max_prot_len - 1; i++) { + proto_chars[i] = proto_chars[i + 1]; + proto_start[i] = proto_start[i + 1]; + } + + if (proto_char_count == max_prot_len) + proto_char_count--; + + proto_chars[proto_char_count] = wc; + proto_start[proto_char_count] = (struct coord){c, r}; + proto_char_count++; + + for (size_t i = 0; i < ALEN(prots); i++) { + size_t prot_len = wcslen(prots[i]); + + if (proto_char_count < prot_len) + continue; + + const wchar_t *proto = &proto_chars[max_prot_len - prot_len]; + + if (wcsncmp(prots[i], proto, prot_len) == 0) { + state = STATE_URL; + start = proto_start[max_prot_len - prot_len]; + + wcsncpy(url, proto, prot_len); + len = prot_len; + break; + } + } + break; + + case STATE_URL: { + // static const wchar_t allowed[] = + // L"abcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;="; + // static const wchar_t unwise[] = L"{}|\\^[]`"; + // static const wchar_t reserved[] = L";/?:@&=+$,"; + + switch (wc) { + case L'a'...L'z': + case L'0'...L'9': + case L'-': case L'.': case L'_': case L'~': case L':': + case L'/': case L'?': case L'#': case L'[': case L']': + case L'@': case L'!': case L'$': case L'&': case L'\'': + case L'(': case L')': case L'*': case L'+': case L',': + case L';': case L'=': case L'"': + url[len++] = wc; + break; + + default: { + /* Heuristic to remove trailing characters that + * are valid URL characters, but typically not at + * the end of the URL */ + bool done = false; + struct coord end = {c, r}; + + do { + switch (url[len - 1]) { + case L'.': case L',': case L':': case L';': case L'?': + case L'!': case L'"': case L'\'': + len--; + end.col--; + if (end.col < 0) { + end.row--; + end.col = term->cols - 1; + } + break; + + default: + done = true; + break; + } + } while (!done); + + url[len] = L'\0'; + + tll_push_back( + term->urls, + ((struct url){ + .url = xwcsdup(url), + .text = xwcsdup(L""), + .start = start, + .end = end})); + + state = STATE_PROTOCOL; + len = 0; + break; + } + } + break; + } + } + } + } +} + +UNIGNORE_WARNINGS + +void +urls_collect(struct terminal *term) +{ + xassert(tll_length(term->urls) == 0); + auto_detected(term); + + size_t count = tll_length(term->urls); + + /* Assign key combos */ + + static const wchar_t *const single[] = { + L"f", L"j", L"d", L"k", L"e", L"i", L"c", L"m", L"r", + L"u", L"s", L"l", L"w", L"o", L"x", L"a", L"q", L"p", + }; + + if (count < ALEN(single)) { + size_t idx = 0; + tll_foreach(term->urls, it) { + xassert(wcslen(single[idx]) < ALEN(it->item.key) - 1); + wcscpy(it->item.key, single[idx++]); + } + } else { + LOG_ERR("unimplemented: more URLs than %zu", ALEN(single)); + assert(false); + } + +#if defined(_DEBUG) && LOG_ENABLE_DBG + tll_foreach(term->urls, it) { + char url[1024]; + wcstombs(url, it->item.url, sizeof(url) - 1); + + char key[32]; + wcstombs(key, it->item.key, sizeof(key) - 1); + + LOG_DBG("URL: %s (%s)", url, key); + } +#endif + + struct wl_window *win = term->window; + struct wayland *wayl = term->wl; + + xassert(tll_length(win->urls) == 0); + tll_foreach(win->term->urls, it) { + struct wl_surface *surf = wl_compositor_create_surface(wayl->compositor); + wl_surface_set_user_data(surf, win); + + struct wl_subsurface *sub_surf = NULL; + + if (surf != NULL) { + sub_surf = wl_subcompositor_get_subsurface( + wayl->sub_compositor, surf, win->surface); + + if (sub_surf != NULL) + wl_subsurface_set_sync(sub_surf); + } + + if (surf == NULL || sub_surf == NULL) { + LOG_WARN("failed to create URL (sub)-surface"); + + if (surf != NULL) { + wl_surface_destroy(surf); + surf = NULL; + } + + if (sub_surf != NULL) { + wl_subsurface_destroy(sub_surf); + sub_surf = NULL; + } + } + + struct wl_url url = { + .url = &it->item, + .surf = surf, + .sub_surf = sub_surf, + }; + + tll_push_back(win->urls, url); + } +} + +void +urls_reset(struct terminal *term) +{ + if (term->window != NULL) { + tll_foreach(term->window->urls, it) { + if (it->item.sub_surf != NULL) + wl_subsurface_destroy(it->item.sub_surf); + if (it->item.surf != NULL) + wl_surface_destroy(it->item.surf); + } + tll_free(term->window->urls); + } + + tll_foreach(term->urls, it) { + free(it->item.url); + free(it->item.text); + } + tll_free(term->urls); + + memset(term->url_keys, 0, sizeof(term->url_keys)); +} diff --git a/url-mode.h b/url-mode.h new file mode 100644 index 00000000..150a0222 --- /dev/null +++ b/url-mode.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +#include "terminal.h" + +static inline bool urls_mode_is_active(const struct terminal *term) +{ + return tll_length(term->urls) > 0; +} + +void urls_collect(struct terminal *term); +void urls_reset(struct terminal *term); + +void urls_input(struct seat *seat, struct terminal *term, uint32_t key, + xkb_keysym_t sym, xkb_mod_mask_t mods, uint32_t serial); diff --git a/wayland.c b/wayland.c index f30e1b6f..2a79bb10 100644 --- a/wayland.c +++ b/wayland.c @@ -1426,6 +1426,14 @@ wayl_win_destroy(struct wl_window *win) tll_free(win->on_outputs); + tll_foreach(win->urls, it) { + if (it->item.sub_surf != NULL) + wl_subsurface_destroy(it->item.sub_surf); + if (it->item.surf != NULL) + wl_surface_destroy(it->item.surf); + } + tll_free(win->urls); + csd_destroy(win); if (win->render_timer_sub_surface != NULL) wl_subsurface_destroy(win->render_timer_sub_surface); diff --git a/wayland.h b/wayland.h index fdfe905c..2e5997e4 100644 --- a/wayland.h +++ b/wayland.h @@ -362,6 +362,12 @@ struct monitor { bool use_output_release; }; +struct wl_url { + const struct url *url; + struct wl_surface *surf; + struct wl_subsurface *sub_surf; +}; + struct wayland; struct wl_window { struct terminal *term; @@ -393,6 +399,7 @@ struct wl_window { struct wl_callback *frame_callback; tll(const struct monitor *) on_outputs; /* Outputs we're mapped on */ + tll(struct wl_url) urls; bool is_configured; bool is_fullscreen;