aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--hex.c450
-rw-r--r--tui.c251
2 files changed, 339 insertions, 362 deletions
diff --git a/hex.c b/hex.c
index 2491fc0..3a6bb09 100644
--- a/hex.c
+++ b/hex.c
@@ -44,10 +44,6 @@ enum
ATTRIBUTE_COUNT
};
-// My battle-tested C framework acting as a GLib replacement. Its one big
-// disadvantage is missing support for i18n but that can eventually be added
-// as an optional feature. Localised applications look super awkward, though.
-
// User data for logger functions to enable formatted logging
#define print_fatal_data ((void *) ATTRIBUTE_ERROR)
#define print_error_data ((void *) ATTRIBUTE_ERROR)
@@ -63,39 +59,10 @@ enum
#ifndef TIOCGWINSZ
#include <sys/ioctl.h>
#endif // ! TIOCGWINSZ
-#include <ncurses.h>
-
-// ncurses is notoriously retarded for input handling, we need something
-// different if only to receive mouse events reliably.
+#include "tui.c"
#include "termo.h"
-// It is surprisingly hard to find a good library to handle Unicode shenanigans,
-// and there's enough of those for it to be impractical to reimplement them.
-//
-// GLib ICU libunistring utf8proc
-// Decently sized . . x x
-// Grapheme breaks . x . x
-// Character width x . x x
-// Locale handling . . x .
-// Liberal license . x . x
-//
-// Also note that the ICU API is icky and uses UTF-16 for its primary encoding.
-//
-// Currently we're chugging along with libunistring but utf8proc seems viable.
-// Non-Unicode locales can mostly be handled with simple iconv like in sdtui.
-// Similarly grapheme breaks can be guessed at using character width (a basic
-// test here is Zalgo text).
-//
-// None of this is ever going to work too reliably anyway because terminals
-// and Unicode don't go awfully well together. In particular, character cell
-// devices have some problems with double-wide characters.
-
-#include <unistr.h>
-#include <uniwidth.h>
-#include <uniconv.h>
-#include <unicase.h>
-
#define APP_TITLE PROGRAM_NAME ///< Left top corner
// --- Utilities ---------------------------------------------------------------
@@ -121,41 +88,14 @@ update_curses_terminal_size (void)
#endif // HAVE_RESIZETERM && TIOCGWINSZ
}
-static char *
-latin1_to_utf8 (const char *latin1)
-{
- struct str converted;
- str_init (&converted);
- while (*latin1)
- {
- uint8_t c = *latin1++;
- if (c < 0x80)
- str_append_c (&converted, c);
- else
- {
- str_append_c (&converted, 0xC0 | (c >> 6));
- str_append_c (&converted, 0x80 | (c & 0x3F));
- }
- }
- return str_steal (&converted);
-}
-
// --- Application -------------------------------------------------------------
-// Function names are prefixed mostly because of curses which clutters the
-// global namespace and makes it harder to distinguish what functions relate to.
-
-struct attrs
+enum
{
- short fg; ///< Foreground colour index
- short bg; ///< Background colour index
- chtype attrs; ///< Other attributes
+ ROW_SIZE = 16, ///< How many bytes on a row
+ FOOTER_SIZE = 4, ///< How many rows form the footer
};
-// Basically a container for most of the globals; no big sense in handing
-// around a pointer to this, hence it is a simple global variable as well.
-// There is enough global state as it is.
-
static struct app_context
{
// Event loop:
@@ -175,7 +115,9 @@ static struct app_context
uint8_t *data; ///< Target data
uint64_t data_len; ///< Length of the data
uint64_t data_offset; ///< Offset of the data within the file
- uint64_t data_cursor; ///< Current position within the data
+
+ uint64_t view_top; ///< Offset of the top of the screen
+ uint64_t view_cursor; ///< Offset of the cursor
// TODO: get rid of this as it can be computed from "data*"
size_t item_count; ///< Total item count
@@ -184,10 +126,6 @@ static struct app_context
// Emulated widgets:
- // TODO: make this the footer;
- // remove this, we know how high the footer is
- int header_height; ///< Height of the header
-
struct poller_idle refresh_event; ///< Refresh the screen
// Terminal:
@@ -225,43 +163,6 @@ get_config_string (struct config_item *root, const char *key)
return item->value.string.str;
}
-/// Load configuration for a color using a subset of git config colors
-static void
-app_load_color (struct config_item *subtree, const char *name, int id)
-{
- const char *value = get_config_string (subtree, name);
- if (!value)
- return;
-
- struct str_vector v;
- str_vector_init (&v);
- cstr_split (value, " ", true, &v);
-
- int colors = 0;
- struct attrs attrs = { -1, -1, 0 };
- for (char **it = v.vector; *it; it++)
- {
- char *end = NULL;
- long n = strtol (*it, &end, 10);
- if (*it != end && !*end && n >= SHRT_MIN && n <= SHRT_MAX)
- {
- if (colors == 0) attrs.fg = n;
- if (colors == 1) attrs.bg = n;
- colors++;
- }
- else if (!strcmp (*it, "bold")) attrs.attrs |= A_BOLD;
- else if (!strcmp (*it, "dim")) attrs.attrs |= A_DIM;
- else if (!strcmp (*it, "ul")) attrs.attrs |= A_UNDERLINE;
- else if (!strcmp (*it, "blink")) attrs.attrs |= A_BLINK;
- else if (!strcmp (*it, "reverse")) attrs.attrs |= A_REVERSE;
-#ifdef A_ITALIC
- else if (!strcmp (*it, "italic")) attrs.attrs |= A_ITALIC;
-#endif // A_ITALIC
- }
- str_vector_free (&v);
- g_ctx.attrs[id] = attrs;
-}
-
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
@@ -272,8 +173,10 @@ load_config_colors (struct config_item *subtree, void *user_data)
// The attributes cannot be changed dynamically right now, so it doesn't
// make much sense to make use of "on_change" callbacks either.
// For simplicity, we should reload the entire table on each change anyway.
+ const char *value;
#define XX(name, config, fg_, bg_, attrs_) \
- app_load_color (subtree, config, ATTRIBUTE_ ## name);
+ if ((value = get_config_string (subtree, config))) \
+ g_ctx.attrs[ATTRIBUTE_ ## name] = attrs_decode (value);
ATTRIBUTE_TABLE (XX)
#undef XX
}
@@ -342,12 +245,9 @@ app_init_terminal (void)
TERMO_CHECK_VERSION;
if (!(g_ctx.tk = termo_new (STDIN_FILENO, NULL, 0)))
abort ();
- if (!initscr () || nonl () == ERR)
+ if (!initscr () || nonl () == ERR || curs_set (1) == ERR)
abort ();
- // Disable cursor, we're not going to use it most of the time
- curs_set (0);
-
// By default we don't use any colors so they're not required...
if (start_color () == ERR
|| use_default_colors () == ERR
@@ -407,179 +307,6 @@ app_is_character_in_locale (ucs4_t ch)
return true;
}
-// --- Terminal output ---------------------------------------------------------
-
-// Necessary abstraction to simplify aligned, formatted character output
-
-struct row_char
-{
- ucs4_t c; ///< Unicode codepoint
- chtype attrs; ///< Special attributes
- int width; ///< How many cells this takes
-};
-
-struct row_buffer
-{
- struct row_char *chars; ///< Characters
- size_t chars_len; ///< Character count
- size_t chars_alloc; ///< Characters allocated
- int total_width; ///< Total width of all characters
-};
-
-static void
-row_buffer_init (struct row_buffer *self)
-{
- memset (self, 0, sizeof *self);
- self->chars = xcalloc (sizeof *self->chars, (self->chars_alloc = 256));
-}
-
-static void
-row_buffer_free (struct row_buffer *self)
-{
- free (self->chars);
-}
-
-/// Replace invalid chars and push all codepoints to the array w/ attributes.
-static void
-row_buffer_append (struct row_buffer *self, const char *str, chtype attrs)
-{
- // The encoding is only really used internally for some corner cases
- const char *encoding = locale_charset ();
-
- // Note that this function is a hotspot, try to keep it decently fast
- struct row_char current = { .attrs = attrs };
- struct row_char invalid = { .attrs = attrs, .c = '?', .width = 1 };
- const uint8_t *next = (const uint8_t *) str;
- while ((next = u8_next (&current.c, next)))
- {
- if (self->chars_len >= self->chars_alloc)
- self->chars = xreallocarray (self->chars,
- sizeof *self->chars, (self->chars_alloc <<= 1));
-
- current.width = uc_width (current.c, encoding);
- if (current.width < 0 || !app_is_character_in_locale (current.c))
- current = invalid;
-
- self->chars[self->chars_len++] = current;
- self->total_width += current.width;
- }
-}
-
-static void
-row_buffer_addv (struct row_buffer *self, const char *s, ...)
- ATTRIBUTE_SENTINEL;
-
-static void
-row_buffer_addv (struct row_buffer *self, const char *s, ...)
-{
- va_list ap;
- va_start (ap, s);
-
- while (s)
- {
- row_buffer_append (self, s, va_arg (ap, chtype));
- s = va_arg (ap, const char *);
- }
- va_end (ap);
-}
-
-/// Pop as many codepoints as needed to free up "space" character cells.
-/// Given the suffix nature of combining marks, this should work pretty fine.
-static int
-row_buffer_pop_cells (struct row_buffer *self, int space)
-{
- int made = 0;
- while (self->chars_len && made < space)
- made += self->chars[--self->chars_len].width;
- self->total_width -= made;
- return made;
-}
-
-static void
-row_buffer_space (struct row_buffer *self, int width, chtype attrs)
-{
- if (width < 0)
- return;
-
- while (self->chars_len + width >= self->chars_alloc)
- self->chars = xreallocarray (self->chars,
- sizeof *self->chars, (self->chars_alloc <<= 1));
-
- struct row_char space = { .attrs = attrs, .c = ' ', .width = 1 };
- self->total_width += width;
- while (width-- > 0)
- self->chars[self->chars_len++] = space;
-}
-
-static void
-row_buffer_ellipsis (struct row_buffer *self, int target)
-{
- if (self->total_width <= target
- || !row_buffer_pop_cells (self, self->total_width - target))
- return;
-
- // We use attributes from the last character we've removed,
- // assuming that we don't shrink the array (and there's no real need)
- ucs4_t ellipsis = L'…';
- if (app_is_character_in_locale (ellipsis))
- {
- if (self->total_width >= target)
- row_buffer_pop_cells (self, 1);
- if (self->total_width + 1 <= target)
- row_buffer_append (self, "…", self->chars[self->chars_len].attrs);
- }
- else if (target >= 3)
- {
- if (self->total_width >= target)
- row_buffer_pop_cells (self, 3);
- if (self->total_width + 3 <= target)
- row_buffer_append (self, "...", self->chars[self->chars_len].attrs);
- }
-}
-
-static void
-row_buffer_align (struct row_buffer *self, int target, chtype attrs)
-{
- row_buffer_ellipsis (self, target);
- row_buffer_space (self, target - self->total_width, attrs);
-}
-
-static void
-row_buffer_print (uint32_t *ucs4, chtype attrs)
-{
- // This assumes that we can reset the attribute set without consequences
- char *str = u32_strconv_to_locale (ucs4);
- if (str)
- {
- attrset (attrs);
- addstr (str);
- attrset (0);
- free (str);
- }
-}
-
-static void
-row_buffer_flush (struct row_buffer *self)
-{
- if (!self->chars_len)
- return;
-
- // We only NUL-terminate the chunks because of the libunistring API
- uint32_t chunk[self->chars_len + 1], *insertion_point = chunk;
- for (size_t i = 0; i < self->chars_len; i++)
- {
- struct row_char *iter = self->chars + i;
- if (i && iter[0].attrs != iter[-1].attrs)
- {
- row_buffer_print (chunk, iter[-1].attrs);
- insertion_point = chunk;
- }
- *insertion_point++ = iter->c;
- *insertion_point = 0;
- }
- row_buffer_print (chunk, self->chars[self->chars_len - 1].attrs);
-}
-
// --- Rendering ---------------------------------------------------------------
static void
@@ -609,39 +336,75 @@ app_write_line (const char *str, chtype attrs)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-static void
-app_flush_header (struct row_buffer *buf, chtype attrs)
+static int
+app_visible_items (void)
{
- move (g_ctx.header_height++, 0);
- app_flush_buffer (buf, COLS, attrs);
+ return MAX (0, LINES - FOOTER_SIZE);
}
static void
-app_draw_status (void)
+app_draw_view (void)
{
- // XXX: can we get rid of this and still make it look acceptable?
- chtype a_normal = APP_ATTR (HEADER);
- chtype a_highlight = APP_ATTR (HIGHLIGHT);
+ uint64_t end_addr = g_ctx.data_offset + g_ctx.data_len;
+ for (int y = 0; y < app_visible_items (); y++)
+ {
+ uint64_t row_addr = g_ctx.view_top + y * ROW_SIZE;
+ if (row_addr > end_addr)
+ break;
- struct row_buffer buf;
- row_buffer_init (&buf);
- // ...
- app_flush_header (&buf, a_normal);
+ int row_attrs = (row_addr / ROW_SIZE & 1)
+ ? APP_ATTR (ODD) : APP_ATTR (EVEN);
+
+ struct row_buffer buf;
+ row_buffer_init (&buf);
+
+ char *row_addr_str = xstrdup_printf ("%08" PRIX64, row_addr);
+ row_buffer_append (&buf, row_addr_str, row_attrs);
+ free (row_addr_str);
+
+ struct str ascii;
+ str_init (&ascii);
+ str_append (&ascii, " ");
+
+ for (int x = 0; x < ROW_SIZE; x++)
+ {
+ if (x % 8 == 0) row_buffer_append (&buf, " ", row_attrs);
+ if (x % 2 == 0) row_buffer_append (&buf, " ", row_attrs);
+
+ uint64_t cell_addr = row_addr + x;
+ if (cell_addr < g_ctx.data_offset
+ || cell_addr >= end_addr)
+ {
+ row_buffer_append (&buf, " ", row_attrs);
+ str_append_c (&ascii, ' ');
+ }
+ else
+ {
+ uint8_t cell = g_ctx.data[cell_addr - g_ctx.data_offset];
+ char *hex = xstrdup_printf ("%02x", cell);
+ row_buffer_append (&buf, hex, row_attrs);
+ free (hex);
+
+ if (cell >= 32 && cell < 127)
+ str_append_c (&ascii, cell);
+ else
+ str_append_c (&ascii, '.');
+ }
+ }
+ row_buffer_append (&buf, ascii.str, row_attrs);
+ str_free (&ascii);
+
+ move (y, 0);
+ app_flush_buffer (&buf, COLS, row_attrs);
+ }
}
static void
-app_draw_header (void)
+app_draw_footer (void)
{
- // TODO: call app_fix_view_range() if it changes from the previous value
- g_ctx.header_height = 0;
-
- if (true)
- app_draw_status ();
- else
- {
- move (g_ctx.header_height++, 0);
- app_write_line ("Connecting to MPD...", APP_ATTR (HEADER));
- }
+ move (app_visible_items (), 0);
+ // TODO: write the footer
+ app_write_line ("Connecting to MPD...", APP_ATTR (HEADER));
// XXX: can we get rid of this and still make it look acceptable?
chtype a_normal = APP_ATTR (BAR);
@@ -655,53 +418,7 @@ app_draw_header (void)
row_buffer_append (&buf, " ", a_normal);
// TODO: endian indication, position indication
- app_flush_header (&buf, a_normal);
-}
-
-static int
-app_visible_items (void)
-{
- // This may eventually include a header bar and/or a status bar
- return MAX (0, LINES - g_ctx.header_height);
-}
-
-static void
-app_draw_view (void)
-{
- move (g_ctx.header_height, 0);
- clrtobot ();
-
- int view_width = COLS;
-
- int to_show = MIN (LINES - g_ctx.header_height,
- (int) g_ctx.item_count - g_ctx.item_top);
- for (int row = 0; row < to_show; row++)
- {
- int item_index = g_ctx.item_top + row;
- int row_attrs = (item_index & 1) ? APP_ATTR (ODD) : APP_ATTR (EVEN);
- if (item_index == g_ctx.item_selected)
- row_attrs = APP_ATTR (SELECTION);
-
- struct row_buffer buf;
- row_buffer_init (&buf);
- // TODO: draw the row using view_width
-
- // Combine attributes used by the handler with the defaults.
- // Avoiding attrset() because of row_buffer_flush().
- for (size_t i = 0; i < buf.chars_len; i++)
- {
- chtype *attrs = &buf.chars[i].attrs;
- if (item_index == g_ctx.item_selected)
- *attrs = (*attrs & ~(A_COLOR | A_REVERSE)) | row_attrs;
- else if ((*attrs & A_COLOR) && (row_attrs & A_COLOR))
- *attrs |= (row_attrs & ~A_COLOR);
- else
- *attrs |= row_attrs;
- }
-
- move (g_ctx.header_height + row, 0);
- app_flush_buffer (&buf, view_width, row_attrs);
- }
+ app_flush_buffer (&buf, COLS, a_normal);
}
static void
@@ -710,8 +427,11 @@ app_on_refresh (void *user_data)
(void) user_data;
poller_idle_reset (&g_ctx.refresh_event);
- app_draw_header ();
+ erase ();
app_draw_view ();
+ app_draw_footer ();
+
+ // TODO: move the cursor where it belongs
refresh ();
}
@@ -853,12 +573,12 @@ app_process_action (enum action action)
break;
case ACTION_GOTO_PAGE_PREVIOUS:
- app_scroll ((int) g_ctx.header_height - LINES);
- app_move_selection ((int) g_ctx.header_height - LINES);
+ app_scroll (FOOTER_SIZE - LINES);
+ app_move_selection (FOOTER_SIZE - LINES);
break;
case ACTION_GOTO_PAGE_NEXT:
- app_scroll (LINES - (int) g_ctx.header_height);
- app_move_selection (LINES - (int) g_ctx.header_height);
+ app_scroll (LINES - FOOTER_SIZE);
+ app_move_selection (LINES - FOOTER_SIZE);
break;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -877,12 +597,13 @@ app_process_action (enum action action)
static bool
app_process_left_mouse_click (int line, int column)
{
- if (line == g_ctx.header_height - 1)
+ if (line == app_visible_items ())
{
+ // TODO: LE/BE switch, maybe something else
}
- else
+ else if (line < app_visible_items ())
{
- int row_index = line - g_ctx.header_height;
+ int row_index = line;
if (row_index < 0
|| row_index >= (int) g_ctx.item_count - g_ctx.item_top)
return false;
@@ -936,6 +657,7 @@ g_default_bindings[] =
{ "C-n", ACTION_GOTO_ITEM_NEXT },
{ "C-b", ACTION_GOTO_PAGE_PREVIOUS },
{ "C-f", ACTION_GOTO_PAGE_NEXT },
+ // TODO: C-e and C-y scroll up and down
// Not sure how to set these up, they're pretty arbitrary so far
{ "Enter", ACTION_CHOOSE },
@@ -1271,6 +993,9 @@ main (int argc, char *argv[])
g_ctx.data = (uint8_t *) buf.str;
g_ctx.data_len = buf.len;
+ g_ctx.view_top = g_ctx.data_offset / ROW_SIZE * ROW_SIZE;
+ g_ctx.view_cursor = g_ctx.data_offset;
+
// We only need to convert to and from the terminal encoding
if (!setlocale (LC_CTYPE, ""))
print_warning ("failed to set the locale");
@@ -1280,6 +1005,7 @@ main (int argc, char *argv[])
app_init_terminal ();
signals_setup_handlers ();
app_init_poller_events ();
+ app_invalidate ();
// Redirect all messages from liberty so that they don't disrupt display
g_log_message_real = app_log_handler;
diff --git a/tui.c b/tui.c
new file mode 100644
index 0000000..3887369
--- /dev/null
+++ b/tui.c
@@ -0,0 +1,251 @@
+// This file is to be moved to liberty, along with FindUnistring.cmake,
+// and then used in both hex and nncmpp
+
+// This file includes some common stuff to build TUI applications with
+
+#include <ncurses.h>
+
+// It is surprisingly hard to find a good library to handle Unicode shenanigans,
+// and there's enough of those for it to be impractical to reimplement them.
+//
+// GLib ICU libunistring utf8proc
+// Decently sized . . x x
+// Grapheme breaks . x . x
+// Character width x . x x
+// Locale handling . . x .
+// Liberal license . x . x
+//
+// Also note that the ICU API is icky and uses UTF-16 for its primary encoding.
+//
+// Currently we're chugging along with libunistring but utf8proc seems viable.
+// Non-Unicode locales can mostly be handled with simple iconv like in sdtui.
+// Similarly grapheme breaks can be guessed at using character width (a basic
+// test here is Zalgo text).
+//
+// None of this is ever going to work too reliably anyway because terminals
+// and Unicode don't go awfully well together. In particular, character cell
+// devices have some problems with double-wide characters.
+
+#include <unistr.h>
+#include <uniwidth.h>
+#include <uniconv.h>
+#include <unicase.h>
+
+// --- Configurable display attributes -----------------------------------------
+
+struct attrs
+{
+ short fg; ///< Foreground colour index
+ short bg; ///< Background colour index
+ chtype attrs; ///< Other attributes
+};
+
+/// Decode attributes in the value using a subset of the git config format,
+/// ignoring all errors since it doesn't affect functionality
+static struct attrs
+attrs_decode (const char *value)
+{
+ struct str_vector v;
+ str_vector_init (&v);
+ cstr_split (value, " ", true, &v);
+
+ int colors = 0;
+ struct attrs attrs = { -1, -1, 0 };
+ for (char **it = v.vector; *it; it++)
+ {
+ char *end = NULL;
+ long n = strtol (*it, &end, 10);
+ if (*it != end && !*end && n >= SHRT_MIN && n <= SHRT_MAX)
+ {
+ if (colors == 0) attrs.fg = n;
+ if (colors == 1) attrs.bg = n;
+ colors++;
+ }
+ else if (!strcmp (*it, "bold")) attrs.attrs |= A_BOLD;
+ else if (!strcmp (*it, "dim")) attrs.attrs |= A_DIM;
+ else if (!strcmp (*it, "ul")) attrs.attrs |= A_UNDERLINE;
+ else if (!strcmp (*it, "blink")) attrs.attrs |= A_BLINK;
+ else if (!strcmp (*it, "reverse")) attrs.attrs |= A_REVERSE;
+#ifdef A_ITALIC
+ else if (!strcmp (*it, "italic")) attrs.attrs |= A_ITALIC;
+#endif // A_ITALIC
+ }
+ str_vector_free (&v);
+ return attrs;
+}
+
+// --- Terminal output ---------------------------------------------------------
+
+// Necessary abstraction to simplify aligned, formatted character output
+
+// This callback you need to implement in the application
+static bool app_is_character_in_locale (ucs4_t ch);
+
+struct row_char
+{
+ ucs4_t c; ///< Unicode codepoint
+ chtype attrs; ///< Special attributes
+ int width; ///< How many cells this takes
+};
+
+struct row_buffer
+{
+ struct row_char *chars; ///< Characters
+ size_t chars_len; ///< Character count
+ size_t chars_alloc; ///< Characters allocated
+ int total_width; ///< Total width of all characters
+};
+
+static void
+row_buffer_init (struct row_buffer *self)
+{
+ memset (self, 0, sizeof *self);
+ self->chars = xcalloc (sizeof *self->chars, (self->chars_alloc = 256));
+}
+
+static void
+row_buffer_free (struct row_buffer *self)
+{
+ free (self->chars);
+}
+
+/// Replace invalid chars and push all codepoints to the array w/ attributes.
+static void
+row_buffer_append (struct row_buffer *self, const char *str, chtype attrs)
+{
+ // The encoding is only really used internally for some corner cases
+ const char *encoding = locale_charset ();
+
+ // Note that this function is a hotspot, try to keep it decently fast
+ struct row_char current = { .attrs = attrs };
+ struct row_char invalid = { .attrs = attrs, .c = '?', .width = 1 };
+ const uint8_t *next = (const uint8_t *) str;
+ while ((next = u8_next (&current.c, next)))
+ {
+ if (self->chars_len >= self->chars_alloc)
+ self->chars = xreallocarray (self->chars,
+ sizeof *self->chars, (self->chars_alloc <<= 1));
+
+ current.width = uc_width (current.c, encoding);
+ if (current.width < 0 || !app_is_character_in_locale (current.c))
+ current = invalid;
+
+ self->chars[self->chars_len++] = current;
+ self->total_width += current.width;
+ }
+}
+
+static void
+row_buffer_addv (struct row_buffer *self, const char *s, ...)
+ ATTRIBUTE_SENTINEL;
+
+static void
+row_buffer_addv (struct row_buffer *self, const char *s, ...)
+{
+ va_list ap;
+ va_start (ap, s);
+
+ while (s)
+ {
+ row_buffer_append (self, s, va_arg (ap, chtype));
+ s = va_arg (ap, const char *);
+ }
+ va_end (ap);
+}
+
+/// Pop as many codepoints as needed to free up "space" character cells.
+/// Given the suffix nature of combining marks, this should work pretty fine.
+static int
+row_buffer_pop_cells (struct row_buffer *self, int space)
+{
+ int made = 0;
+ while (self->chars_len && made < space)
+ made += self->chars[--self->chars_len].width;
+ self->total_width -= made;
+ return made;
+}
+
+static void
+row_buffer_space (struct row_buffer *self, int width, chtype attrs)
+{
+ if (width < 0)
+ return;
+
+ while (self->chars_len + width >= self->chars_alloc)
+ self->chars = xreallocarray (self->chars,
+ sizeof *self->chars, (self->chars_alloc <<= 1));
+
+ struct row_char space = { .attrs = attrs, .c = ' ', .width = 1 };
+ self->total_width += width;
+ while (width-- > 0)
+ self->chars[self->chars_len++] = space;
+}
+
+static void
+row_buffer_ellipsis (struct row_buffer *self, int target)
+{
+ if (self->total_width <= target
+ || !row_buffer_pop_cells (self, self->total_width - target))
+ return;
+
+ // We use attributes from the last character we've removed,
+ // assuming that we don't shrink the array (and there's no real need)
+ ucs4_t ellipsis = L'…';
+ if (app_is_character_in_locale (ellipsis))
+ {
+ if (self->total_width >= target)
+ row_buffer_pop_cells (self, 1);
+ if (self->total_width + 1 <= target)
+ row_buffer_append (self, "…", self->chars[self->chars_len].attrs);
+ }
+ else if (target >= 3)
+ {
+ if (self->total_width >= target)
+ row_buffer_pop_cells (self, 3);
+ if (self->total_width + 3 <= target)
+ row_buffer_append (self, "...", self->chars[self->chars_len].attrs);
+ }
+}
+
+static void
+row_buffer_align (struct row_buffer *self, int target, chtype attrs)
+{
+ row_buffer_ellipsis (self, target);
+ row_buffer_space (self, target - self->total_width, attrs);
+}
+
+static void
+row_buffer_print (uint32_t *ucs4, chtype attrs)
+{
+ // This assumes that we can reset the attribute set without consequences
+ char *str = u32_strconv_to_locale (ucs4);
+ if (str)
+ {
+ attrset (attrs);
+ addstr (str);
+ attrset (0);
+ free (str);
+ }
+}
+
+static void
+row_buffer_flush (struct row_buffer *self)
+{
+ if (!self->chars_len)
+ return;
+
+ // We only NUL-terminate the chunks because of the libunistring API
+ uint32_t chunk[self->chars_len + 1], *insertion_point = chunk;
+ for (size_t i = 0; i < self->chars_len; i++)
+ {
+ struct row_char *iter = self->chars + i;
+ if (i && iter[0].attrs != iter[-1].attrs)
+ {
+ row_buffer_print (chunk, iter[-1].attrs);
+ insertion_point = chunk;
+ }
+ *insertion_point++ = iter->c;
+ *insertion_point = 0;
+ }
+ row_buffer_print (chunk, self->chars[self->chars_len - 1].attrs);
+}