xF: add date change marker

Also rename and reorganise attributes to match xC better.
This commit is contained in:
2026-08-06 05:26:23 +02:00
parent 7538dfad2d
commit f6a41d7c51
+194 -77
View File
@@ -303,38 +303,39 @@ struct binding
// --- Display model -----------------------------------------------------------
#define ATTRIBUTE_TABLE(XX) \
XX (NORMAL, -1, -1, 0) \
XX (CHROME, -1, 255, A_UNDERLINE) \
XX (TIME, 250, 255, A_DIM) \
XX (CURRENT, 0, 255, 0) \
XX (ACTIVITY, -1, -1, A_BOLD) \
XX (HIGHLIGHT, 202, -1, A_BOLD) \
XX (UNREAD, 202, -1, 0) \
XX (JOIN, 108, -1, 0) \
XX (PART, 138, -1, 0) \
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) \
XX (IRC_1, 124, -1, 0) \
XX (IRC_2, 34, -1, 0) \
XX (IRC_3, 142, -1, 0) \
XX (IRC_4, 19, -1, 0) \
XX (IRC_5, 127, -1, 0) \
XX (IRC_6, 37, -1, 0) \
XX (IRC_7, 252, -1, 0) \
XX (IRC_8, 245, -1, 0) \
XX (IRC_9, 196, -1, 0) \
XX (IRC_10, 46, -1, 0) \
XX (IRC_11, 226, -1, 0) \
XX (IRC_12, 21, -1, 0) \
XX (IRC_13, 201, -1, 0) \
XX (IRC_14, 51, -1, 0) \
XX (IRC_15, 231, -1, 0)
#define ATTRIBUTE_TABLE(XX) \
XX (NORMAL, -1, -1, 0) \
XX (CHROME, -1, 255, A_UNDERLINE) \
XX (SCROLLBAR, 250, -1, 0) \
XX (PROMPT, -1, 255, A_BOLD) \
XX (DATE_CHANGE, -1, -1, A_BOLD) \
XX (READ_MARKER, 202, -1, 0) \
XX (BUFFER_CURRENT, 0, 255, 0) \
XX (BUFFER_ACTIVITY, -1, -1, A_BOLD) \
XX (BUFFER_HIGHLIGHT, 202, -1, A_BOLD) \
XX (ERROR, 1, -1, A_BOLD) \
XX (EXTERNAL, 248, -1, 0) \
XX (TIMESTAMP, 250, 255, A_DIM) \
XX (ACTION, 1, -1, 0) \
XX (LINK, 21, -1, A_UNDERLINE) \
XX (JOIN, 108, -1, 0) \
XX (PART, 138, -1, 0) \
XX (IRC_0, 16, -1, 0) \
XX (IRC_1, 124, -1, 0) \
XX (IRC_2, 34, -1, 0) \
XX (IRC_3, 142, -1, 0) \
XX (IRC_4, 19, -1, 0) \
XX (IRC_5, 127, -1, 0) \
XX (IRC_6, 37, -1, 0) \
XX (IRC_7, 252, -1, 0) \
XX (IRC_8, 245, -1, 0) \
XX (IRC_9, 196, -1, 0) \
XX (IRC_10, 46, -1, 0) \
XX (IRC_11, 226, -1, 0) \
XX (IRC_12, 21, -1, 0) \
XX (IRC_13, 201, -1, 0) \
XX (IRC_14, 51, -1, 0) \
XX (IRC_15, 231, -1, 0)
enum app_attribute
{
@@ -368,7 +369,7 @@ app_init_line_item_colors (void)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
enum { UNREAD_MARKER_HEIGHT = 1 };
enum { READ_MARKER_HEIGHT = 1 };
enum app_widget_id
{
@@ -478,6 +479,8 @@ static struct
int transcript_width; ///< Width used by buffer line wrapping
int transcript_height; ///< Current buffer content height
struct poller_timer date_change_event;
struct line_editor editor; ///< Message editor
char *editor_running_for; ///< Buffer name being edited externally
char *editor_filename; ///< File being edited by the user
@@ -554,7 +557,7 @@ app_measure_buffer_line (struct buffer_line *line, const struct str *text)
line->wraps = xcalloc (MAX (line->chunks_len, 1), sizeof *line->wraps);
size_t chunk_start = 0, i = 0;
chtype base = line->leaked ? g_attrs[ATTRIBUTE_LEAKED].attrs : 0;
chtype base = line->leaked ? g_attrs[ATTRIBUTE_EXTERNAL].attrs : 0;
while (chunk_start < text->len)
{
size_t chunk_end = chunk_start +
@@ -650,7 +653,7 @@ app_recheck_current_highlight (void)
}
static bool
buffer_has_unread_marker (const struct buffer *self)
buffer_has_read_marker (const struct buffer *self)
{
return self->lines
&& (self->new_messages || self->new_unimportant_messages);
@@ -692,11 +695,11 @@ app_push_buffer_line (struct buffer *b,
&& (b == current || leak_to_active);
bool visible = !g.log.visible && !current->scroll_offset
&& (b == current || leak_to_active);
bool current_had_unread = buffer_has_unread_marker (current);
bool current_had_unread = buffer_has_read_marker (current);
// TODO: Also discount an unmapped/minimized window once XUI exposes that.
LIST_APPEND_WITH_TAIL (b->lines, b->lines_tail, line);
if (!(visible || leak_to_active) || buffer_has_unread_marker (b))
if (!(visible || leak_to_active) || buffer_has_read_marker (b))
{
if (line->is_unimportant || leak_to_active)
b->new_unimportant_messages++;
@@ -709,7 +712,7 @@ app_push_buffer_line (struct buffer *b,
leaked->leaked = true;
buffer_line_finalize (leaked);
LIST_APPEND_WITH_TAIL (current->lines, current->lines_tail, leaked);
if (!visible || buffer_has_unread_marker (current))
if (!visible || buffer_has_read_marker (current))
{
if (leaked->is_unimportant)
current->new_unimportant_messages++;
@@ -723,8 +726,8 @@ app_push_buffer_line (struct buffer *b,
if (displayed)
current->scroll_offset += app_buffer_line_height
(leak_to_active ? current->lines_tail : line);
if (!current_had_unread && buffer_has_unread_marker (current))
current->scroll_offset += UNREAD_MARKER_HEIGHT;
if (!current_had_unread && buffer_has_read_marker (current))
current->scroll_offset += READ_MARKER_HEIGHT;
}
buffer_pop_excess_lines (b);
@@ -802,6 +805,53 @@ app_log_handler (void *user_data, const char *quote, const char *fmt,
in_processing = false;
}
// --- Date change -------------------------------------------------------------
static bool
app_date_changed (time_t when, struct tm *last, bool *have_last)
{
struct tm current = {};
if (!localtime_r (&when, &current))
return false;
bool changed = !*have_last
|| last->tm_year != current.tm_year
|| last->tm_mon != current.tm_mon
|| last->tm_mday != current.tm_mday;
*last = current;
*have_last = true;
return changed;
}
static void
app_rearm_date_change_timer (void)
{
struct tm tm_ = {};
const time_t now = time (NULL);
if (!localtime_r (&now, &tm_))
return;
tm_.tm_sec = tm_.tm_min = tm_.tm_hour = 0;
tm_.tm_mday++;
tm_.tm_isdst = -1;
const time_t midnight = mktime (&tm_);
if (midnight == (time_t) -1 || midnight <= now)
return;
int64_t delay = (int64_t) (midnight - now) * 1000 + 1;
poller_timer_set (&g.date_change_event, MIN (delay, INT_MAX));
}
static void
app_on_date_change_timer (void *user_data)
{
(void) user_data;
xui_invalidate ();
app_rearm_date_change_timer ();
}
// --- Editor ------------------------------------------------------------------
static void
@@ -2480,11 +2530,11 @@ app_make_buffer_row (struct buffer *b, int index)
{
enum app_attribute attribute = ATTRIBUTE_NORMAL;
if (b == g.buffer_current)
attribute = ATTRIBUTE_CURRENT;
attribute = ATTRIBUTE_BUFFER_CURRENT;
else if (b->highlighted)
attribute = ATTRIBUTE_HIGHLIGHT;
attribute = ATTRIBUTE_BUFFER_HIGHLIGHT;
else if (b->new_messages)
attribute = ATTRIBUTE_ACTIVITY;
attribute = ATTRIBUTE_BUFFER_ACTIVITY;
char *label = b != g.buffer_current && b->new_messages
? xstrdup_printf ("%s (%u)", b->buffer_name, b->new_messages)
@@ -2583,12 +2633,37 @@ static struct widget *
app_make_mark (const struct buffer_line *line)
{
chtype attrs = line->leaked
? g_attrs[ATTRIBUTE_LEAKED].attrs
? g_attrs[ATTRIBUTE_EXTERNAL].attrs
: g_attrs[app_mark_attribute (line->rendition)].attrs;
return app_label_attrs (attrs, XUI_ATTR_MONOSPACE,
app_line_mark (line->rendition), false);
}
static const char *
app_format_date (time_t when)
{
static char date[64];
struct tm tm = {};
*date = '\0';
if (localtime_r (&when, &tm))
strftime (date, sizeof date, "%x", &tm);
return date;
}
static struct widget *
app_make_date_change_marker (time_t when)
{
struct layout row = {};
app_push (&row, app_padding (ATTRIBUTE_NORMAL, 0.25));
app_push (&row, app_label (ATTRIBUTE_DATE_CHANGE,
app_format_date (when), true));
app_push (&row, app_padding (ATTRIBUTE_NORMAL, 0.25));
struct widget *marker = xui_hbox (row.head);
marker->height = g_xui.vunit;
return marker;
}
static const char *
app_format_line_timestamp (const struct buffer_line *line)
{
@@ -2605,14 +2680,15 @@ app_format_line_timestamp (const struct buffer_line *line)
static struct widget *
app_make_line_timestamp (const struct buffer_line *line)
{
return app_label (ATTRIBUTE_TIME, app_format_line_timestamp (line), false);
return app_label (ATTRIBUTE_TIMESTAMP, app_format_line_timestamp (line),
false);
}
static void
app_wrap_buffer_line_to_width (struct buffer_line *line, int width)
{
struct widget *timestamp = app_make_line_timestamp (line);
struct widget *time_padding = app_padding (ATTRIBUTE_TIME, 0.25);
struct widget *time_padding = app_padding (ATTRIBUTE_TIMESTAMP, 0.25);
struct widget *mark = app_make_mark (line);
struct widget *padding = app_padding (ATTRIBUTE_NORMAL, 0.25);
@@ -2659,7 +2735,7 @@ app_make_line (struct buffer_line *line)
// 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.
chtype base = line->leaked ? g_attrs[ATTRIBUTE_LEAKED].attrs : 0;
chtype base = line->leaked ? g_attrs[ATTRIBUTE_EXTERNAL].attrs : 0;
struct layout rows = {};
for (size_t i = 0; i < line->wraps_len; i++)
{
@@ -2675,46 +2751,46 @@ app_make_line (struct buffer_line *line)
struct layout timestamps = {};
app_push (&timestamps, app_make_line_timestamp (line));
struct widget *filler = app_label (ATTRIBUTE_TIME, "", false);
struct widget *filler = app_label (ATTRIBUTE_TIMESTAMP, "", false);
filler->height = -1;
app_push (&timestamps, filler);
struct layout columns = {};
app_push (&columns, app_padding (ATTRIBUTE_TIME, 0.25));
app_push (&columns, app_padding (ATTRIBUTE_TIMESTAMP, 0.25));
app_push (&columns, xui_vbox (timestamps.head));
app_push (&columns, app_padding (ATTRIBUTE_TIME, 0.25));
app_push (&columns, app_padding (ATTRIBUTE_TIMESTAMP, 0.25));
app_push (&columns, content);
return xui_hbox (columns.head);
}
static void
tui_render_unread_marker (struct widget *self)
tui_render_read_marker (struct widget *self)
{
struct row_buffer buffer = row_buffer_make ();
for (int i = 0; i < self->width; i++)
row_buffer_append_c (&buffer, '-',
i < 8 ? g_attrs[ATTRIBUTE_TIME].attrs : self->attrs);
i < 8 ? g_attrs[ATTRIBUTE_TIMESTAMP].attrs : self->attrs);
tui_flush_buffer (self, 0, &buffer);
}
static struct widget *
app_make_unread_marker (void)
app_make_read_marker (void)
{
if (g_xui.ui == &tui_ui)
{
struct widget *marker = xcalloc (1, sizeof *marker + 1);
marker->on_render = tui_render_unread_marker;
marker->attrs = g_attrs[ATTRIBUTE_UNREAD].attrs;
marker->on_render = tui_render_read_marker;
marker->attrs = g_attrs[ATTRIBUTE_READ_MARKER].attrs;
marker->width = -1;
marker->height = UNREAD_MARKER_HEIGHT;
marker->height = READ_MARKER_HEIGHT;
return marker;
}
// Reversing swaps the orange foreground into the padding background.
struct widget *marker = g_xui.ui->padding
(g_attrs[ATTRIBUTE_UNREAD].attrs | A_REVERSE, 0, 0);
(g_attrs[ATTRIBUTE_READ_MARKER].attrs | A_REVERSE, 0, 0);
marker->width = -1;
marker->height = UNREAD_MARKER_HEIGHT;
marker->height = READ_MARKER_HEIGHT;
return marker;
}
@@ -2793,7 +2869,7 @@ app_append_log_rows (struct layout *rows)
}
static int
app_append_buffer_rows (struct layout *rows)
app_append_buffer_rows (struct layout *rows, time_t now)
{
struct buffer *b = g.buffer_current;
if (!b)
@@ -2806,28 +2882,39 @@ app_append_buffer_rows (struct layout *rows)
marker_before = line;
struct transcript_slice slice = app_make_transcript_slice ();
struct tm last_date = {};
bool have_last_date = false;
LIST_FOR_EACH (struct buffer_line, line, b->lines)
{
if (line == marker_before
&& app_transcript_row_is_visible (&slice, UNREAD_MARKER_HEIGHT))
app_push (rows, app_make_unread_marker ());
if (buffer_line_is_visible (b, line)
&& app_transcript_row_is_visible (&slice,
app_wrapped_line_height (line)))
&& app_transcript_row_is_visible (&slice, READ_MARKER_HEIGHT))
app_push (rows, app_make_read_marker ());
if (!buffer_line_is_visible (b, line))
continue;
time_t when = line->when / 1000;
if (app_date_changed (when, &last_date, &have_last_date)
&& app_transcript_row_is_visible (&slice, g_xui.vunit))
app_push (rows, app_make_date_change_marker (when));
if (app_transcript_row_is_visible
(&slice, app_wrapped_line_height (line)))
app_push (rows, app_make_line (line));
}
if (have_last_date && app_date_changed (now, &last_date, &have_last_date)
&& app_transcript_row_is_visible (&slice, g_xui.vunit))
app_push (rows, app_make_date_change_marker (now));
return app_transcript_slice_offset (&slice);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct widget *
app_make_transcript (void)
app_make_transcript (time_t now)
{
struct layout rows = {};
int offset = g.log.visible
? app_append_log_rows (&rows)
: app_append_buffer_rows (&rows);
: app_append_buffer_rows (&rows, now);
struct widget *transcript = xui_vbox (rows.head);
transcript->on_allocated = app_on_viewport_allocated;
@@ -2838,7 +2925,7 @@ app_make_transcript (void)
}
static int
app_transcript_height_at (int width)
app_transcript_height_at (int width, time_t now)
{
uint64_t height = 0;
if (g.log.visible)
@@ -2855,14 +2942,22 @@ app_transcript_height_at (int width)
if (!buffer)
return 0;
height = buffer_has_unread_marker (buffer) ? UNREAD_MARKER_HEIGHT : 0;
height = buffer_has_read_marker (buffer) ? READ_MARKER_HEIGHT : 0;
struct tm last_date = {};
bool have_last_date = false;
LIST_FOR_EACH (struct buffer_line, line, buffer->lines)
{
if (!buffer_line_is_visible (buffer, line))
continue;
if (app_date_changed (line->when / 1000, &last_date, &have_last_date))
height += g_xui.vunit;
app_wrap_buffer_line_to_width (line, width);
height += app_wrapped_line_height (line);
}
if (have_last_date && app_date_changed (now, &last_date, &have_last_date))
height += g_xui.vunit;
return MIN ((uint64_t) INT_MAX, height);
}
@@ -2873,19 +2968,22 @@ app_make_middle (void)
struct widget *buffer_list =
app_push (&columns, app_make_buffer_list ());
// Let's not race against time.
time_t now = time (NULL);
// XXX: We know when something changes, so we could cache this.
int width = MAX (0, g_xui.width - buffer_list->width);
int height = app_transcript_height_at (width);
int height = app_transcript_height_at (width, now);
if (height > app_viewport_height ())
{
width = MAX (0, width - app_scrollbar_width ());
height = app_transcript_height_at (width);
height = app_transcript_height_at (width, now);
}
g.transcript_width = width;
g.transcript_height = height;
struct layout viewport = {};
app_push (&viewport, app_make_transcript ());
app_push (&viewport, app_make_transcript (now));
struct widget *scrollbar = app_make_scrollbar (WIDGET_TRANSCRIPT_SCROLLBAR);
if (scrollbar)
app_push (&viewport, scrollbar);
@@ -3432,14 +3530,26 @@ app_external_editor_make_transcript (void)
for (size_t i = 0; i < g.log.lines_len; i++)
app_external_editor_append_line
(&transcript, g.log.lines[i], false);
return transcript;
}
else
struct buffer *buffer = g.buffer_current;
struct tm last_date = {};
bool have_last_date = false;
LIST_FOR_EACH (struct buffer_line, line, buffer->lines)
{
struct buffer *buffer = g.buffer_current;
LIST_FOR_EACH (struct buffer_line, line, buffer->lines)
if (buffer_line_is_visible (buffer, line))
app_external_editor_append_line (&transcript, line, true);
if (!buffer_line_is_visible (buffer, line))
continue;
time_t when = line->when / 1000;
if (app_date_changed (when, &last_date, &have_last_date))
str_append_printf (&transcript, "%s\n", app_format_date (when));
app_external_editor_append_line (&transcript, line, true);
}
time_t now = time (NULL);
if (have_last_date && app_date_changed (now, &last_date, &have_last_date))
str_append_printf (&transcript, "%s\n", app_format_date (now));
return transcript;
}
@@ -4140,6 +4250,11 @@ app_init_context (const char *host, const char *port)
{
poller_init (&g.poller);
app_editor_init ();
g.date_change_event = poller_timer_make (&g.poller);
g.date_change_event.dispatcher = app_on_date_change_timer;
app_rearm_date_change_timer ();
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);
@@ -4248,7 +4363,9 @@ main (int argc, char *argv[])
exit_fatal ("missing port number/service name");
if (!setlocale (LC_CTYPE, ""))
print_warning ("failed to set the locale");
print_warning ("failed to set the character locale");
if (!setlocale (LC_TIME, ""))
print_warning ("failed to set the time locale");
app_init_context (host, port);
free (address);