xF: add link detection and opening

This commit is contained in:
2026-08-03 01:36:55 +02:00
parent 8f3df19786
commit e4ce659174
+238 -59
View File
@@ -42,6 +42,8 @@ enum buffer_line_item_style
BUFFER_LINE_ITEM_INVERSE = 1 << 3,
BUFFER_LINE_ITEM_CROSSED_OUT = 1 << 4,
BUFFER_LINE_ITEM_MONOSPACE = 1 << 5,
BUFFER_LINE_ITEM_LINK = 1 << 6,
BUFFER_LINE_ITEM_LINK_START = 1 << 7,
};
// See: union relay_item_data
@@ -72,30 +74,69 @@ struct buffer_line
};
static void
buffer_line_item_add (struct buffer_line_item **head,
struct buffer_line_item **tail, const char *text, int16_t fg, int16_t bg,
unsigned style)
buffer_line_add_item (struct buffer_line *line, const char *text,
int16_t fg, int16_t bg, unsigned style)
{
size_t len = strlen (text);
struct buffer_line_item *item = xcalloc (1, sizeof *item + len + 1);
item->fg = fg;
item->bg = bg;
item->style = style;
memcpy (item->text, text, len + 1);
LIST_APPEND_WITH_TAIL (*head, *tail, item);
LIST_APPEND_WITH_TAIL (line->items, line->items_tail, item);
}
static struct buffer_line_item *
buffer_line_split_item (struct buffer_line *line,
struct buffer_line_item *item, size_t offset)
{
size_t len = strlen (item->text);
hard_assert (offset && offset < len);
struct buffer_line_item *suffix =
xcalloc (1, sizeof *suffix + len - offset + 1);
suffix->fg = item->fg;
suffix->bg = item->bg;
suffix->style = item->style;
memcpy (suffix->text, item->text + offset, len - offset + 1);
item->text[offset] = 0;
struct buffer_line_item *following = item->next;
LIST_INSERT_WITH_TAIL (line->items, line->items_tail, suffix, following);
return suffix;
}
static struct buffer_line_item *
buffer_line_split_at (struct buffer_line *line, size_t offset)
{
LIST_FOR_EACH (struct buffer_line_item, item, line->items)
{
size_t len = strlen (item->text);
if (!offset)
return item;
if (offset < len)
return buffer_line_split_item (line, item, offset);
offset -= len;
}
hard_assert (!offset);
return NULL;
}
static void
buffer_line_items_free (struct buffer_line_item *items)
buffer_line_reset (struct buffer_line *line)
{
LIST_FOR_EACH (struct buffer_line_item, item, items)
LIST_FOR_EACH (struct buffer_line_item, item, line->items)
free (item);
line->items = line->items_tail = NULL;
}
static void
buffer_line_free (struct buffer_line *self)
buffer_line_destroy (struct buffer_line *self)
{
buffer_line_items_free (self->items);
buffer_line_reset (self);
free (self);
}
@@ -109,7 +150,7 @@ buffer_line_clone (const struct buffer_line *line)
self->is_unimportant = line->is_unimportant;
self->is_highlight = line->is_highlight;
LIST_FOR_EACH (struct buffer_line_item, item, line->items)
buffer_line_item_add (&self->items, &self->items_tail,
buffer_line_add_item (self,
item->text, item->fg, item->bg, item->style);
return self;
}
@@ -175,7 +216,7 @@ static void
buffer_clear (struct buffer *self)
{
LIST_FOR_EACH (struct buffer_line, iter, self->lines)
buffer_line_free (iter);
buffer_line_destroy (iter);
self->lines = self->lines_tail = NULL;
}
@@ -194,7 +235,7 @@ buffer_pop_excess_lines (struct buffer *self)
{
struct buffer_line *line = self->lines;
LIST_UNLINK_WITH_TAIL (self->lines, self->lines_tail, line);
buffer_line_free (line);
buffer_line_destroy (line);
}
}
@@ -203,7 +244,8 @@ buffer_destroy (struct buffer *self)
{
free (self->buffer_name);
free (self->server_name);
buffer_line_items_free (self->topic);
buffer_line_reset (&(struct buffer_line)
{ .items = self->topic, .items_tail = self->topic_tail });
free (self->modes);
free (self->input);
strv_free (&self->history);
@@ -313,6 +355,7 @@ struct binding
XX (ERROR, 1, -1, A_BOLD) \
XX (ACTION, 1, -1, 0) \
XX (LEAKED, 248, -1, 0) \
XX (LINK, 21, -1, A_UNDERLINE) \
XX (PROMPT, -1, 255, A_BOLD) \
XX (SCROLLBAR, 250, -1, 0) \
XX (IRC_0, 16, -1, 0) \
@@ -358,6 +401,7 @@ enum app_widget_id
{
WIDGET_NONE,
WIDGET_BUTTON,
WIDGET_LINK,
WIDGET_BUFFER,
WIDGET_BUFFER_LIST,
WIDGET_TRANSCRIPT,
@@ -450,6 +494,7 @@ static struct
struct buffer_line *local_lines_tail;
struct completion_request completion;
struct log_view log;
regex_t *link_re; ///< Finds links in buffer line text
// User interface:
@@ -576,7 +621,7 @@ app_queue_local_line (enum relay_rendition rendition, const char *text)
struct buffer_line *line = xcalloc (1, sizeof *line);
line->rendition = rendition;
line->when = when;
buffer_line_item_add (&line->items, &line->items_tail, text, -1, -1, 0);
buffer_line_add_item (line, text, -1, -1, 0);
LIST_APPEND_WITH_TAIL (g.local_lines, g.local_lines_tail, line);
app_flush_local_lines ();
}
@@ -958,8 +1003,37 @@ buffer_find (const char *name)
}
static void
buffer_line_items_convert (const union relay_item_data *items, uint32_t len,
struct buffer_line_item **head, struct buffer_line_item **tail)
buffer_line_linkify (struct buffer_line *line)
{
struct str text = str_make ();
LIST_FOR_EACH (struct buffer_line_item, item, line->items)
str_append (&text, item->text);
regmatch_t match = {};
size_t offset = 0;
int result = 0;
while (!(result = regexec (g.link_re, text.str + offset, 1, &match, 0)))
{
size_t start = offset + match.rm_so;
size_t end = offset + match.rm_eo;
offset = end;
struct buffer_line_item *first = buffer_line_split_at (line, start);
struct buffer_line_item *after = buffer_line_split_at (line, end);
hard_assert (first && start < end);
first->style |= BUFFER_LINE_ITEM_LINK_START;
for (struct buffer_line_item *item = first;
item != after; item = item->next)
item->style |= BUFFER_LINE_ITEM_LINK;
}
soft_assert (result == REG_NOMATCH);
str_free (&text);
}
static void
buffer_line_convert_items (struct buffer_line *line,
const union relay_item_data *items, uint32_t len)
{
int16_t fg = -1, bg = -1;
unsigned style = 0;
@@ -969,8 +1043,7 @@ buffer_line_items_convert (const union relay_item_data *items, uint32_t len,
switch (item->kind)
{
case RELAY_ITEM_TEXT:
buffer_line_item_add (head, tail,
item->text.text.str, fg, bg, style);
buffer_line_add_item (line, item->text.text.str, fg, bg, style);
break;
case RELAY_ITEM_RESET:
fg = bg = -1;
@@ -1004,6 +1077,7 @@ buffer_line_items_convert (const union relay_item_data *items, uint32_t len,
break;
}
}
buffer_line_linkify (line);
}
static struct buffer_line *
@@ -1015,8 +1089,7 @@ buffer_line_new (const struct relay_event_data_buffer_line *m)
self->is_unimportant = m->is_unimportant;
self->is_highlight = m->is_highlight;
buffer_line_items_convert (m->items, m->items_len,
&self->items, &self->items_tail);
buffer_line_convert_items (self, m->items, m->items_len);
return self;
}
@@ -1113,17 +1186,20 @@ relay_process_buffer_update (struct buffer *b,
if (server_name)
b->server_name = xstrdup (server_name);
buffer_line_items_free (b->topic);
b->topic = b->topic_tail = NULL;
struct buffer_line topic =
{ .items = b->topic, .items_tail = b->topic_tail };
buffer_line_reset (&topic);
cstr_set (&b->modes, NULL);
if (b->kind == RELAY_BUFFER_KIND_CHANNEL)
{
const struct relay_buffer_context_channel *channel =
&e->context.channel;
buffer_line_items_convert (channel->topic, channel->topic_len,
&b->topic, &b->topic_tail);
buffer_line_convert_items
(&topic, channel->topic, channel->topic_len);
b->modes = xstrdup (channel->modes.str);
}
b->topic = topic.items;
b->topic_tail = topic.items_tail;
}
static void
@@ -2032,10 +2108,9 @@ app_line_item_color (int16_t color)
}
static chtype
app_line_item_attrs (enum app_attribute attribute, bool leaked,
app_line_item_attrs (chtype attrs, bool leaked,
const struct buffer_line_item *item)
{
chtype attrs = g_attrs[attribute].attrs;
if (item->style & BUFFER_LINE_ITEM_BOLD)
attrs |= A_BOLD;
#ifdef A_ITALIC
@@ -2078,6 +2153,46 @@ app_line_item_attrs (enum app_attribute attribute, bool leaked,
return attrs;
}
static chtype
app_link_attrs (chtype attrs)
{
chtype link = g_attrs[ATTRIBUTE_LINK].attrs;
if (attrs & A_COLOR)
link &= ~A_COLOR;
return attrs | link;
}
static void
app_append_line_items (struct layout *row, chtype base,
const struct buffer_line *line)
{
LIST_FOR_EACH (struct buffer_line_item, item, line->items)
{
chtype attrs = app_line_item_attrs (base, line->leaked, item);
unsigned extended = item->style & BUFFER_LINE_ITEM_MONOSPACE
? XUI_ATTR_MONOSPACE
: 0;
bool link = item->style & BUFFER_LINE_ITEM_LINK;
if (link)
attrs = app_link_attrs (attrs);
struct widget *widget =
app_label_attrs (attrs, extended, item->text, !item->next && !link);
if (link)
{
widget->widget_id = WIDGET_LINK;
widget->userdata = item->style;
}
app_push (row, widget);
}
// We don't want the "hitbox" of links to extend until the end of line.
if (!line->items_tail
|| line->items_tail->style & BUFFER_LINE_ITEM_LINK)
app_push (row, app_label_attrs (base, 0, "", true));
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct widget *
@@ -2099,15 +2214,8 @@ app_make_title (void)
struct layout row = {};
app_push (&row, app_padding (ATTRIBUTE_CHROME, 0.25));
if (g.buffer_current && g.buffer_current->topic)
LIST_FOR_EACH (struct buffer_line_item, item, g.buffer_current->topic)
{
unsigned extended = item->style & BUFFER_LINE_ITEM_MONOSPACE
? XUI_ATTR_MONOSPACE
: 0;
chtype attrs = app_line_item_attrs (ATTRIBUTE_CHROME, false, item);
app_push (&row, app_label_attrs (attrs, extended, item->text,
item == g.buffer_current->topic_tail));
}
app_append_line_items (&row, g_attrs[ATTRIBUTE_CHROME].attrs,
&(struct buffer_line) { .items = g.buffer_current->topic });
else
app_push (&row, app_label (ATTRIBUTE_CHROME, "", true));
@@ -2228,7 +2336,7 @@ static struct widget *
app_make_mark (const struct buffer_line *line)
{
chtype attrs = line->leaked
? ATTRIBUTE_LEAKED
? g_attrs[ATTRIBUTE_LEAKED].attrs
: g_attrs[app_mark_attribute (line->rendition)].attrs;
return app_label_attrs (attrs, XUI_ATTR_MONOSPACE,
app_line_mark (line->rendition), false);
@@ -2243,27 +2351,16 @@ app_make_line (const struct buffer_line *line)
if (localtime_r (&when, &tm))
strftime (timestamp, sizeof timestamp, "%H:%M:%S", &tm);
enum app_attribute attribute = ATTRIBUTE_NORMAL;
if (line->leaked)
attribute = ATTRIBUTE_LEAKED;
// XXX: We still use too much ATTRIBUTE_NORMAL, which basically names
// the <-1, -1> colour pair. We don't use it with line items as a default
// because then links attribute application would get blocked.
struct layout row = {};
app_push (&row, app_padding (ATTRIBUTE_TIME, 0.25));
app_push (&row, app_label (ATTRIBUTE_TIME, timestamp, false));
app_push (&row, app_padding (ATTRIBUTE_TIME, 0.25));
app_push (&row, app_make_mark (line));
LIST_FOR_EACH (struct buffer_line_item, item, line->items)
{
unsigned extended = item->style & BUFFER_LINE_ITEM_MONOSPACE
? XUI_ATTR_MONOSPACE
: 0;
chtype attrs = app_line_item_attrs (attribute, line->leaked, item);
app_push (&row, app_label_attrs (attrs, extended, item->text,
item == line->items_tail));
}
if (!line->items)
app_push (&row, app_label (attribute, "", true));
app_append_line_items (&row,
line->leaked ? g_attrs[ATTRIBUTE_LEAKED].attrs : 0, line);
app_push (&row, app_padding (ATTRIBUTE_NORMAL, 0.25));
return xui_hbox (row.head);
}
@@ -2302,10 +2399,15 @@ app_make_unread_marker (void)
static struct widget *
app_make_log_line (const char *text)
{
struct buffer_line model = {};
buffer_line_add_item (&model, text, -1, -1, 0);
buffer_line_linkify (&model);
struct layout line = {};
app_push (&line, app_padding (ATTRIBUTE_NORMAL, 0.25));
app_push (&line, app_label (ATTRIBUTE_NORMAL, text, true));
app_append_line_items (&line, 0, &model);
app_push (&line, app_padding (ATTRIBUTE_NORMAL, 0.25));
buffer_line_reset (&model);
return xui_hbox (line.head);
}
@@ -2699,6 +2801,24 @@ app_widget_by_id (struct widget *list, int widget_id)
return NULL;
}
static char *
app_link_from_widget (struct widget *w)
{
// No need to store it extra when we can collect it naturally.
while (w && w->widget_id == WIDGET_LINK
&& !(w->userdata & BUFFER_LINE_ITEM_LINK_START))
w = w->prev;
if (!w || w->widget_id != WIDGET_LINK
|| !(w->userdata & BUFFER_LINE_ITEM_LINK_START))
return NULL;
struct str url = str_make ();
for (; w && w->widget_id == WIDGET_LINK
&& (w->userdata & BUFFER_LINE_ITEM_LINK); w = w->next)
str_append (&url, w->text);
return str_steal (&url);
}
static bool
app_scrollbar_seek (struct widget *scrollbar, int y)
{
@@ -2847,6 +2967,37 @@ app_toggle_log (void)
return true;
}
static void
app_open (const char *object)
{
pid_t child = fork ();
if (child < 0)
{
print_error ("failed to open link: %s", strerror (errno));
return;
}
if (!child)
{
if (setsid () < 0)
_exit (EXIT_FAILURE);
pid_t grandchild = fork ();
if (grandchild < 0)
_exit (EXIT_FAILURE);
if (grandchild)
_exit (EXIT_SUCCESS);
#ifdef __APPLE__
execlp ("open", "open", object, NULL);
#else
execlp ("xdg-open", "xdg-open", object, NULL);
#endif
_exit (EXIT_FAILURE);
}
while (waitpid (child, NULL, 0) < 0 && errno == EINTR)
;
}
// ~~~ Action and mouse dispatch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
static const enum line_editor_action g_editor_actions[] =
@@ -2983,7 +3134,9 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button,
int modifiers)
{
(void) modifiers;
app_notify_active ();
if (type == TERMO_MOUSE_RELEASE)
{
g.dragged_scrollbar = WIDGET_NONE;
@@ -2996,8 +3149,9 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button,
struct widget *scrollbar =
app_widget_by_id (g_xui.widgets, g.dragged_scrollbar);
return app_scrollbar_seek (scrollbar,
scrollbar ? y - scrollbar->y : 0);
if (scrollbar)
return app_scrollbar_seek (scrollbar, y - scrollbar->y);
return false;
}
if (type != TERMO_MOUSE_PRESS)
return true;
@@ -3020,15 +3174,31 @@ app_process_mouse (termo_mouse_event_t type, int x, int y, int button,
return false;
struct widget *target = app_interactive_widget_at (g_xui.widgets, x, y);
if (target && (target->widget_id == WIDGET_BUFFER_LIST_SCROLLBAR
|| target->widget_id == WIDGET_TRANSCRIPT_SCROLLBAR))
if (!target)
return false;
if (target->widget_id == WIDGET_BUFFER_LIST_SCROLLBAR
|| target->widget_id == WIDGET_TRANSCRIPT_SCROLLBAR)
{
g.dragged_scrollbar = target->widget_id;
return app_scrollbar_seek (target, y - target->y);
}
if (target && target->widget_id == WIDGET_BUTTON)
if (target->widget_id == WIDGET_LINK)
{
char *url = app_link_from_widget (target);
if (!url)
return false;
#ifdef WITH_X11
if (g_xui.ui == &x11_ui)
app_open (url);
#endif // WITH_X11
free (url);
return true;
}
if (target->widget_id == WIDGET_BUTTON)
return app_process_action (target->userdata);
if (target && target->widget_id == WIDGET_BUFFER)
if (target->widget_id == WIDGET_BUFFER)
{
app_send_buffer_activate (app_buffer_by_index (target->userdata));
return true;
@@ -3279,11 +3449,19 @@ signals_on_readable (const struct pollfd *fd, void *user_data)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static const char g_link_pattern[] =
"https?://"
"([^][(){}<>\"'[:space:]]|\\([^][(){}<>\"'[:space:]]*\\))+"
"([^][(){}<>\"'[:space:],.:]|\\([^][(){}<>\"'[:space:]]*\\))";
static void
app_init_context (const char *host, const char *port)
{
poller_init (&g.poller);
app_editor_init ();
struct error *e = NULL;
if (!(g.link_re = regex_compile (g_link_pattern, REG_EXTENDED, &e)))
exit_fatal ("Failed to compile link regex: %s", e->message);
g.relay.host = xstrdup (host);
g.relay.port = xstrdup (port);
@@ -3302,7 +3480,7 @@ app_free_context (void)
{
app_reset_relay_state ();
LIST_FOR_EACH (struct buffer_line, iter, g.local_lines)
buffer_line_free (iter);
buffer_line_destroy (iter);
g.local_lines = g.local_lines_tail = NULL;
log_view_free (&g.log);
@@ -3315,6 +3493,7 @@ app_free_context (void)
str_free (&g.relay.read_buffer);
str_free (&g.relay.write_buffer);
free (g.relay.session_id);
regex_free (g.link_re);
free (g.keys);
app_editor_free ();