urls: initial support for detecting URLs and rendering jump-labels

The jump labels work, but is currently hardcoded to use xdg-open
This commit is contained in:
Daniel Eklöf 2021-01-31 11:12:07 +01:00
parent b255aea3ed
commit 2cc84db979
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
12 changed files with 503 additions and 28 deletions

13
input.c
View file

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

View file

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

View file

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

123
render.c
View file

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

View file

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

3
shm.h
View file

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

View file

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

View file

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

337
url-mode.c Normal file
View file

@ -0,0 +1,337 @@
#include "url-mode.h"
#include <string.h>
#include <wctype.h>
#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));
}

18
url-mode.h Normal file
View file

@ -0,0 +1,18 @@
#pragma once
#include <stdbool.h>
#include <xkbcommon/xkbcommon.h>
#include <tllist.h>
#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);

View file

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

View file

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