aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2023-06-19 13:02:44 +0200
committerPřemysl Eric Janouch <p@janouch.name>2023-06-19 13:06:12 +0200
commitd01a1ff0348174f91bb2d3ba53145cc2c9f50a7f (patch)
tree851cdb15a533d8b6da5bae12368a13d05f783e8f
parentbd1013f16a40be5458c0a8cd95625e28309295f1 (diff)
downloadliberty-d01a1ff0348174f91bb2d3ba53145cc2c9f50a7f.tar.gz
liberty-d01a1ff0348174f91bb2d3ba53145cc2c9f50a7f.tar.xz
liberty-d01a1ff0348174f91bb2d3ba53145cc2c9f50a7f.zip
Turn liberty-tui into a terminal/X11 hybrid
Importing code from nncmpp, adjusting it to work with hex as well.
-rw-r--r--LICENSE2
-rw-r--r--liberty-tui.c270
-rw-r--r--liberty-xui.c2197
-rw-r--r--tests/fuzz.c21
4 files changed, 2198 insertions, 292 deletions
diff --git a/LICENSE b/LICENSE
index 4b31682..0610bbd 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2014 - 2022, Přemysl Eric Janouch <p@janouch.name>
+Copyright (c) 2014 - 2023, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
diff --git a/liberty-tui.c b/liberty-tui.c
deleted file mode 100644
index 2f54f6d..0000000
--- a/liberty-tui.c
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * liberty-tui.c: the ultimate C unlibrary: TUI
- *
- * Copyright (c) 2016 - 2017, Přemysl Eric Janouch <p@janouch.name>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
- * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
- * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
- * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- *
- */
-
-// 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 strv v = strv_make ();
- 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
- }
- strv_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
-{
- ARRAY (struct row_char, chars) ///< Characters
- int total_width; ///< Total width of all characters
-};
-
-static struct row_buffer
-row_buffer_make (void)
-{
- struct row_buffer self = {};
- ARRAY_INIT_SIZED (self.chars, 256);
- return self;
-}
-
-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)))
- {
- current.width = uc_width (current.c, encoding);
- if (current.width < 0 || !app_is_character_in_locale (current.c))
- current = invalid;
-
- ARRAY_RESERVE (self->chars, 1);
- self->chars[self->chars_len++] = current;
- self->total_width += current.width;
- }
-}
-
-static void
-row_buffer_append_args (struct row_buffer *self, const char *s, ...)
- ATTRIBUTE_SENTINEL;
-
-static void
-row_buffer_append_args (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);
-}
-
-static void
-row_buffer_append_buffer (struct row_buffer *self, const struct row_buffer *rb)
-{
- ARRAY_RESERVE (self->chars, rb->chars_len);
- memcpy (self->chars + self->chars_len, rb->chars,
- rb->chars_len * sizeof *rb->chars);
-
- self->chars_len += rb->chars_len;
- self->total_width += rb->total_width;
-}
-
-/// 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;
-
- ARRAY_RESERVE (self->chars, (size_t) width);
-
- 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 = 0x2026; // …
- 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);
-}
diff --git a/liberty-xui.c b/liberty-xui.c
new file mode 100644
index 0000000..b6b5e24
--- /dev/null
+++ b/liberty-xui.c
@@ -0,0 +1,2197 @@
+/*
+ * liberty-xui.c: the ultimate C unlibrary: hybrid terminal/X11 UI
+ *
+ * Copyright (c) 2016 - 2023, Přemysl Eric Janouch <p@janouch.name>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ */
+
+// This file includes some common stuff to build terminal/X11 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 tdv.
+// 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>
+
+#include <termios.h>
+#ifndef TIOCGWINSZ
+#include <sys/ioctl.h>
+#endif // ! TIOCGWINSZ
+
+// ncurses is notoriously retarded for input handling, and in past versions
+// used to process mouse events unreliably. Moreover, rxvt-unicode only
+// supports the 1006 mode that ncurses also supports mode starting with 9.25.
+#include "termo.h"
+
+// Carefully chosen to limit the possibility of ever hitting termo keymods.
+enum { XUI_KEYMOD_DOUBLE_CLICK = 1 << 15 };
+
+// Elementary port of TUI facilities to X11.
+#ifdef LIBERTY_XUI_WANT_X11
+#include <X11/Xatom.h>
+#include <X11/Xlib.h>
+#include <X11/keysym.h>
+#include <X11/XKBlib.h>
+#include <X11/Xft/Xft.h>
+#endif // LIBERTY_XUI_WANT_X11
+
+// The application needs to implement these.
+static void app_quit (void);
+static void app_layout (void);
+static bool app_process_termo_event (termo_key_t *event);
+static bool app_process_mouse (termo_mouse_event_t type,
+ int x, int y, int button, int modifiers);
+static bool app_on_insufficient_color (void);
+static void app_on_clipboard_copy (const char *text);
+
+// This could be overridable, however thus far row_buffer and line_editor both
+// depend on XUI being initialized.
+static bool xui_is_character_in_locale (ucs4_t ch);
+
+// --- Utilities ---------------------------------------------------------------
+
+// Unlike poller_timers_get_current_time(), this has a hard dependency
+// on _POSIX_TIMERS, and can be used with both realtime nad monotonic clocks.
+static int64_t
+clock_msec (clockid_t clock)
+{
+ struct timespec tp;
+ hard_assert (clock_gettime (clock, &tp) != -1);
+ return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_nsec / 1000000;
+}
+
+// --- 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 strv v = strv_make ();
+ 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
+ }
+ strv_free (&v);
+ return attrs;
+}
+
+// --- Line editor -------------------------------------------------------------
+
+enum line_editor_action
+{
+ LINE_EDITOR_B_CHAR, ///< Go back a character
+ LINE_EDITOR_F_CHAR, ///< Go forward a character
+ LINE_EDITOR_B_WORD, ///< Go back a word
+ LINE_EDITOR_F_WORD, ///< Go forward a word
+ LINE_EDITOR_HOME, ///< Go to start of line
+ LINE_EDITOR_END, ///< Go to end of line
+
+ LINE_EDITOR_UPCASE_WORD, ///< Convert word to uppercase
+ LINE_EDITOR_DOWNCASE_WORD, ///< Convert word to lowercase
+ LINE_EDITOR_CAPITALIZE_WORD, ///< Capitalize word
+
+ LINE_EDITOR_B_DELETE, ///< Delete last character
+ LINE_EDITOR_F_DELETE, ///< Delete next character
+ LINE_EDITOR_B_KILL_WORD, ///< Delete last word
+ LINE_EDITOR_B_KILL_LINE, ///< Delete everything up to BOL
+ LINE_EDITOR_F_KILL_LINE, ///< Delete everything up to EOL
+};
+
+struct line_editor
+{
+ int point; ///< Caret index into line data
+ ucs4_t *line; ///< Line data, 0-terminated
+ int *w; ///< Codepoint widths, 0-terminated
+ size_t len; ///< Editor length
+ size_t alloc; ///< Editor allocated
+ char prompt; ///< Prompt character
+
+ void (*on_changed) (void); ///< Callback on text change
+ void (*on_end) (bool); ///< Callback on abort
+};
+
+static void
+line_editor_free (struct line_editor *self)
+{
+ free (self->line);
+ free (self->w);
+}
+
+/// Notify whomever invoked the editor that it's been either confirmed or
+/// cancelled and clean up editor state
+static void
+line_editor_abort (struct line_editor *self, bool status)
+{
+ self->on_end (status);
+ self->on_changed = NULL;
+
+ free (self->line);
+ self->line = NULL;
+ free (self->w);
+ self->w = NULL;
+ self->alloc = 0;
+ self->len = 0;
+ self->point = 0;
+ self->prompt = 0;
+}
+
+/// Start the line editor; remember to fill in "change" and "end" callbacks
+static void
+line_editor_start (struct line_editor *self, char prompt)
+{
+ self->alloc = 16;
+ self->line = xcalloc (sizeof *self->line, self->alloc);
+ self->w = xcalloc (sizeof *self->w, self->alloc);
+ self->len = 0;
+ self->point = 0;
+ self->prompt = prompt;
+}
+
+static void
+line_editor_changed (struct line_editor *self)
+{
+ self->line[self->len] = 0;
+ self->w[self->len] = 0;
+
+ if (self->on_changed)
+ self->on_changed ();
+}
+
+static void
+line_editor_move (struct line_editor *self, int to, int from, int len)
+{
+ memmove (self->line + to, self->line + from,
+ sizeof *self->line * len);
+ memmove (self->w + to, self->w + from,
+ sizeof *self->w * len);
+}
+
+static void
+line_editor_insert (struct line_editor *self, ucs4_t codepoint)
+{
+ while (self->alloc - self->len < 2 /* inserted + sentinel */)
+ {
+ self->alloc <<= 1;
+ self->line = xreallocarray
+ (self->line, sizeof *self->line, self->alloc);
+ self->w = xreallocarray
+ (self->w, sizeof *self->w, self->alloc);
+ }
+
+ line_editor_move (self, self->point + 1, self->point,
+ self->len - self->point);
+ self->line[self->point] = codepoint;
+ self->w[self->point] = xui_is_character_in_locale (codepoint)
+ ? uc_width (codepoint, locale_charset ())
+ : 1 /* the replacement question mark */;
+
+ self->point++;
+ self->len++;
+ line_editor_changed (self);
+}
+
+static bool
+line_editor_action (struct line_editor *self, enum line_editor_action action)
+{
+ switch (action)
+ {
+ default:
+ return soft_assert (!"unknown line editor action");
+
+ case LINE_EDITOR_B_CHAR:
+ if (self->point < 1)
+ return false;
+ do self->point--;
+ while (self->point > 0
+ && !self->w[self->point]);
+ return true;
+ case LINE_EDITOR_F_CHAR:
+ if (self->point + 1 > (int) self->len)
+ return false;
+ do self->point++;
+ while (self->point < (int) self->len
+ && !self->w[self->point]);
+ return true;
+ case LINE_EDITOR_B_WORD:
+ {
+ if (self->point < 1)
+ return false;
+ int i = self->point;
+ while (i && self->line[--i] == ' ');
+ while (i-- && self->line[i] != ' ');
+ self->point = ++i;
+ return true;
+ }
+ case LINE_EDITOR_F_WORD:
+ {
+ if (self->point + 1 > (int) self->len)
+ return false;
+ int i = self->point;
+ while (i < (int) self->len && self->line[i] == ' ') i++;
+ while (i < (int) self->len && self->line[i] != ' ') i++;
+ self->point = i;
+ return true;
+ }
+ case LINE_EDITOR_HOME:
+ self->point = 0;
+ return true;
+ case LINE_EDITOR_END:
+ self->point = self->len;
+ return true;
+
+ case LINE_EDITOR_UPCASE_WORD:
+ {
+ int i = self->point;
+ for (; i < (int) self->len && self->line[i] == ' '; i++);
+ for (; i < (int) self->len && self->line[i] != ' '; i++)
+ self->line[i] = uc_toupper (self->line[i]);
+ self->point = i;
+ line_editor_changed (self);
+ return true;
+ }
+ case LINE_EDITOR_DOWNCASE_WORD:
+ {
+ int i = self->point;
+ for (; i < (int) self->len && self->line[i] == ' '; i++);
+ for (; i < (int) self->len && self->line[i] != ' '; i++)
+ self->line[i] = uc_tolower (self->line[i]);
+ self->point = i;
+ line_editor_changed (self);
+ return true;
+ }
+ case LINE_EDITOR_CAPITALIZE_WORD:
+ {
+ int i = self->point;
+ ucs4_t (*converter) (ucs4_t) = uc_totitle;
+ for (; i < (int) self->len && self->line[i] == ' '; i++);
+ for (; i < (int) self->len && self->line[i] != ' '; i++)
+ {
+ self->line[i] = converter (self->line[i]);
+ converter = uc_tolower;
+ }
+ self->point = i;
+ line_editor_changed (self);
+ return true;
+ }
+
+ case LINE_EDITOR_B_DELETE:
+ {
+ if (self->point < 1)
+ return false;
+ int len = 1;
+ while (self->point - len > 0
+ && !self->w[self->point - len])
+ len++;
+ line_editor_move (self, self->point - len, self->point,
+ self->len - self->point);
+ self->len -= len;
+ self->point -= len;
+ line_editor_changed (self);
+ return true;
+ }
+ case LINE_EDITOR_F_DELETE:
+ {
+ if (self->point + 1 > (int) self->len)
+ return false;
+ int len = 1;
+ while (self->point + len < (int) self->len
+ && !self->w[self->point + len])
+ len++;
+ self->len -= len;
+ line_editor_move (self, self->point, self->point + len,
+ self->len - self->point);
+ line_editor_changed (self);
+ return true;
+ }
+ case LINE_EDITOR_B_KILL_WORD:
+ {
+ if (self->point < 1)
+ return false;
+
+ int i = self->point;
+ while (i && self->line[--i] == ' ');
+ while (i-- && self->line[i] != ' ');
+ i++;
+
+ line_editor_move (self, i, self->point, (self->len - self->point));
+ self->len -= self->point - i;
+ self->point = i;
+ line_editor_changed (self);
+ return true;
+ }
+ case LINE_EDITOR_B_KILL_LINE:
+ self->len -= self->point;
+ line_editor_move (self, 0, self->point, self->len);
+ self->point = 0;
+ line_editor_changed (self);
+ return true;
+ case LINE_EDITOR_F_KILL_LINE:
+ self->len = self->point;
+ line_editor_changed (self);
+ 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
+{
+ ARRAY (struct row_char, chars) ///< Characters
+ int total_width; ///< Total width of all characters
+};
+
+static struct row_buffer
+row_buffer_make (void)
+{
+ struct row_buffer self = {};
+ ARRAY_INIT_SIZED (self.chars, 256);
+ return self;
+}
+
+static void
+row_buffer_free (struct row_buffer *self)
+{
+ free (self->chars);
+}
+
+static void
+row_buffer_append_c (struct row_buffer *self, ucs4_t c, chtype attrs)
+{
+ struct row_char current = { .attrs = attrs, .c = c };
+ struct row_char invalid = { .attrs = attrs, .c = '?', .width = 1 };
+
+ current.width = uc_width (current.c, locale_charset ());
+ if (current.width < 0 || !xui_is_character_in_locale (current.c))
+ current = invalid;
+
+ ARRAY_RESERVE (self->chars, 1);
+ self->chars[self->chars_len++] = current;
+ self->total_width += current.width;
+}
+
+/// 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)))
+ {
+ current.width = uc_width (current.c, encoding);
+ if (current.width < 0 || !xui_is_character_in_locale (current.c))
+ current = invalid;
+
+ ARRAY_RESERVE (self->chars, 1);
+ self->chars[self->chars_len++] = current;
+ self->total_width += current.width;
+ }
+}
+
+/// 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;
+
+ ARRAY_RESERVE (self->chars, (size_t) width);
+
+ 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 = 0x2026; // …
+ if (xui_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);
+}
+
+// --- XUI ---------------------------------------------------------------------
+
+struct widget;
+
+/// Draw a widget on the window
+typedef void (*widget_render_fn) (struct widget *self);
+
+/// The widget has been placed
+typedef void (*widget_allocated_fn) (struct widget *self);
+
+/// Extended attributes
+enum { XUI_ATTR_MONOSPACE = 1 << 0 };
+
+/// A minimal abstraction appropriate for both TUI and GUI widgets.
+/// Units for the widget's region are frontend-specific.
+/// Having this as a linked list simplifies layouting and memory management.
+struct widget
+{
+ LIST_HEADER (struct widget)
+
+ int x; ///< X coordinate
+ int y; ///< Y coordinate
+ int width; ///< Width, initialized by UI methods
+ int height; ///< Height, initialized by UI methods
+
+ widget_render_fn on_render; ///< Render callback
+ widget_allocated_fn on_allocated; ///< Allocation callback
+ struct widget *children; ///< Child widgets of containers
+ chtype attrs; ///< Rendition, in Curses terms
+ unsigned extended_attrs; ///< XUI-specific attributes
+
+ int id; ///< Post-layouting identification
+ int userdata; ///< Action ID/Tab index/...
+ char text[]; ///< Any text label
+};
+
+static void
+widget_destroy (struct widget *self)
+{
+ LIST_FOR_EACH (struct widget, w, self->children)
+ widget_destroy (w);
+ free (self);
+}
+
+static void
+widget_move (struct widget *w, int dx, int dy)
+{
+ w->x += dx;
+ w->y += dy;
+ LIST_FOR_EACH (struct widget, child, w->children)
+ widget_move (child, dx, dy);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+struct ui
+{
+ struct widget *(*padding)
+ (chtype attrs, float width, float height);
+ struct widget *(*label)
+ (chtype attrs, unsigned extended, const char *label);
+
+ void (*render) (void);
+ void (*flip) (void);
+ void (*winch) (void);
+ void (*destroy) (void);
+};
+
+#ifdef LIBERTY_XUI_WANT_X11
+
+/// Wraps Xft fonts into a linked list with fallbacks.
+struct x11_font_link
+{
+ struct x11_font_link *next;
+ XftFont *font;
+};
+
+enum
+{
+ X11_FONT_BOLD = 1 << 0,
+ X11_FONT_ITALIC = 1 << 1,
+ X11_FONT_MONOSPACE = 1 << 2,
+};
+
+struct x11_font
+{
+ struct x11_font *next; ///< Next in a linked list
+
+ struct x11_font_link *list; ///< Fonts of varying Unicode coverage
+ unsigned style; ///< X11_FONT_* flags
+ FcPattern *pattern; ///< Original unsubstituted pattern
+ FcCharSet *unavailable; ///< Couldn't find a font for these
+};
+
+#endif // LIBERTY_XUI_WANT_X11
+
+struct xui
+{
+ struct poller_idle refresh_event; ///< Refresh the window's contents
+ struct poller_idle flip_event; ///< Draw rendered widgets on screen
+
+ // User interface:
+
+ struct ui *ui; ///< User interface interface
+ struct widget *widgets; ///< Layouted widgets
+ int width; ///< Window width
+ int height; ///< Window height
+ int hunit; ///< Horizontal unit
+ int vunit; ///< Vertical unit
+ bool focused; ///< Whether the window has focus
+
+ // Terminal:
+
+ termo_t *tk; ///< termo handle (TUI/X11)
+ struct poller_fd tty_event; ///< Terminal input event
+ struct poller_timer tk_timer; ///< termo timeout timer
+ bool locale_is_utf8; ///< The locale is Unicode
+
+ // X11:
+
+#ifdef LIBERTY_XUI_WANT_X11
+ XIM x11_im; ///< Input method
+ XIC x11_ic; ///< Input method context
+ Display *dpy; ///< X display handle
+ struct poller_fd x11_event; ///< X11 events on wire
+ struct poller_idle xpending_event; ///< X11 events possibly in I/O queues
+ int xkb_base_event_code; ///< Xkb base event code
+ Window x11_window; ///< Application window
+ Pixmap x11_pixmap; ///< Off-screen bitmap
+ Region x11_clip; ///< Invalidated region
+ Picture x11_pixmap_picture; ///< XRender wrap for x11_pixmap
+ XftDraw *xft_draw; ///< Xft rendering context
+ struct x11_font *xft_fonts; ///< Font collection
+ char *x11_selection; ///< CLIPBOARD selection
+
+ const char *x11_fontname; ///< Fontconfig font name
+ const char *x11_fontname_monospace; ///< Fontconfig monospace font name
+ XRenderColor *x_fg; ///< Foreground per attribute
+ XRenderColor *x_bg; ///< Background per attribute
+#endif // LIBERTY_XUI_WANT_X11
+}
+g_xui;
+
+static void
+xui_invalidate (void)
+{
+ poller_idle_set (&g_xui.refresh_event);
+}
+
+static bool
+xui_process_termo_event (termo_key_t *event)
+{
+ if (event->type == TERMO_TYPE_FOCUS)
+ g_xui.focused = !!event->code.focused;
+ return app_process_termo_event (event);
+}
+
+// --- TUI ---------------------------------------------------------------------
+
+static void
+tui_flush_buffer (struct widget *self, struct row_buffer *buf)
+{
+ move (self->y, self->x);
+
+ int space = MIN (self->width, g_xui.width - self->x);
+ row_buffer_align (buf, space, self->attrs);
+ row_buffer_flush (buf);
+ row_buffer_free (buf);
+}
+
+static void
+tui_render_padding (struct widget *self)
+{
+ // TODO: This should work even for heights != 1.
+ struct row_buffer buf = row_buffer_make ();
+ tui_flush_buffer (self, &buf);
+}
+
+static struct widget *
+tui_make_padding (chtype attrs, float width, float height)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 2);
+ w->text[0] = ' ';
+ w->on_render = tui_render_padding;
+ w->attrs = attrs;
+ w->width = width * 2;
+ w->height = height;
+ return w;
+}
+
+static void
+tui_render_label (struct widget *self)
+{
+ struct row_buffer buf = row_buffer_make ();
+ row_buffer_append (&buf, self->text, self->attrs);
+ tui_flush_buffer (self, &buf);
+}
+
+static struct widget *
+tui_make_label (chtype attrs, unsigned extended, const char *label)
+{
+ (void) extended;
+
+ size_t len = strlen (label);
+ struct widget *w = xcalloc (1, sizeof *w + len + 1);
+ w->on_render = tui_render_label;
+ w->attrs = attrs;
+ w->extended_attrs = extended;
+ memcpy (w->text, label, len);
+
+ struct row_buffer buf = row_buffer_make ();
+ row_buffer_append (&buf, w->text, w->attrs);
+ w->width = buf.total_width;
+ w->height = 1;
+ row_buffer_free (&buf);
+ return w;
+}
+
+static void
+tui_render_widgets (struct widget *head)
+{
+ LIST_FOR_EACH (struct widget, w, head)
+ {
+ if (w->width < 0 || w->height < 0)
+ continue;
+ if (w->on_render)
+ w->on_render (w);
+ tui_render_widgets (w->children);
+ }
+}
+
+static void
+tui_render (void)
+{
+ erase ();
+ tui_render_widgets (g_xui.widgets);
+}
+
+static void
+tui_flip (void)
+{
+ // Curses handles double-buffering for us automatically.
+ refresh ();
+}
+
+static void
+tui_winch (void)
+{
+ // The standard endwin/refresh sequence makes the terminal flicker
+#if defined HAVE_RESIZETERM && defined TIOCGWINSZ
+ struct winsize size;
+ if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
+ {
+ char *row = getenv ("LINES");
+ char *col = getenv ("COLUMNS");
+ unsigned long tmp;
+ resizeterm (
+ (row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row,
+ (col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col);
+ }
+#else // HAVE_RESIZETERM && TIOCGWINSZ
+ endwin ();
+ refresh ();
+#endif // HAVE_RESIZETERM && TIOCGWINSZ
+
+ g_xui.width = COLS;
+ g_xui.height = LINES;
+ xui_invalidate ();
+}
+
+static void
+tui_destroy (void)
+{
+ endwin ();
+}
+
+static struct ui tui_ui =
+{
+ .padding = tui_make_padding,
+ .label = tui_make_label,
+
+ .render = tui_render,
+ .flip = tui_flip,
+ .winch = tui_winch,
+ .destroy = tui_destroy,
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+tui_on_tty_event (termo_key_t *event, int64_t event_ts)
+{
+ // Simple double click detection via release--press delay, only a bit
+ // complicated by the fact that we don't know what's being released
+ static termo_key_t last_event;
+ static int64_t last_event_ts;
+ static int last_button;
+
+ int y, x, button, y_last, x_last, modifiers = 0;
+ termo_mouse_event_t type, type_last;
+ if (termo_interpret_mouse (g_xui.tk, event, &type, &button, &y, &x))
+ {
+ if (termo_interpret_mouse
+ (g_xui.tk, &last_event, &type_last, NULL, &y_last, &x_last)
+ && event_ts - last_event_ts < 500
+ && type_last == TERMO_MOUSE_RELEASE && type == TERMO_MOUSE_PRESS
+ && y_last == y && x_last == x && last_button == button)
+ {
+ modifiers |= XUI_KEYMOD_DOUBLE_CLICK;
+ // Prevent interpreting triple clicks as two double clicks.
+ last_button = 0;
+ }
+ else if (type == TERMO_MOUSE_PRESS)
+ last_button = button;
+
+ if (!app_process_mouse (type, x, y, button, modifiers))
+ beep ();
+ }
+ else if (!xui_process_termo_event (event))
+ beep ();
+
+ last_event = *event;
+ last_event_ts = event_ts;
+}
+
+static void
+tui_on_tty_readable (const struct pollfd *fd, void *user_data)
+{
+ (void) user_data;
+ if (fd->revents & ~(POLLIN | POLLHUP | POLLERR))
+ print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents);
+
+ poller_timer_reset (&g_xui.tk_timer);
+ termo_advisereadable (g_xui.tk);
+
+ termo_key_t event = {};
+ int64_t event_ts = clock_msec (CLOCK_BEST);
+ termo_result_t res;
+ while ((res = termo_getkey (g_xui.tk, &event)) == TERMO_RES_KEY)
+ tui_on_tty_event (&event, event_ts);
+
+ if (res == TERMO_RES_AGAIN)
+ poller_timer_set (&g_xui.tk_timer, termo_get_waittime (g_xui.tk));
+ else if (res == TERMO_RES_ERROR || res == TERMO_RES_EOF)
+ app_quit ();
+}
+
+static void
+tui_on_key_timer (void *user_data)
+{
+ (void) user_data;
+
+ termo_key_t event;
+ if (termo_getkey_force (g_xui.tk, &event) == TERMO_RES_KEY)
+ if (!xui_process_termo_event (&event))
+ beep ();
+}
+
+static void
+tui_init (struct poller *poller, struct attrs *attrs, size_t attrs_len)
+{
+ (void) poller;
+
+ poller_fd_set (&g_xui.tty_event, POLLIN);
+ if (!termo_start (g_xui.tk) || !initscr () || nonl () == ERR)
+ exit_fatal ("failed to set up the terminal");
+
+ termo_set_mouse_tracking_mode (g_xui.tk, TERMO_MOUSE_TRACKING_DRAG);
+
+ curs_set (0);
+
+ g_xui.ui = &tui_ui;
+ g_xui.width = COLS;
+ g_xui.height = LINES;
+ g_xui.vunit = 1;
+ g_xui.hunit = 1;
+
+ // The application should fall back to something at least nearly colourless
+ if (start_color () == ERR
+ || use_default_colors () == ERR
+ || COLOR_PAIRS <= (int) attrs_len)
+ {
+ app_on_insufficient_color ();
+ return;
+ }
+
+ for (size_t a = 0; a < attrs_len; a++)
+ {
+ if (attrs[a].fg >= -1 && attrs[a].bg >= -1
+ && init_pair (a + 1, attrs[a].fg, attrs[a].bg) == OK)
+ attrs[a].attrs |= COLOR_PAIR (a + 1);
+ else if (app_on_insufficient_color ())
+ return;
+ }
+}
+
+// --- X11 ---------------------------------------------------------------------
+
+#ifdef LIBERTY_XUI_WANT_X11
+
+static XRenderColor x11_default_fg = { .alpha = 0xffff };
+static XRenderColor x11_default_bg = { 0xffff, 0xffff, 0xffff, 0xffff };
+static XErrorHandler x11_default_error_handler;
+
+static struct x11_font_link *
+x11_font_link_new (XftFont *font)
+{
+ struct x11_font_link *self = xcalloc (1, sizeof *self);
+ self->font = font;
+ return self;
+}
+
+static void
+x11_font_link_destroy (struct x11_font_link *self)
+{
+ XftFontClose (g_xui.dpy, self->font);
+ free (self);
+}
+
+static struct x11_font_link *
+x11_font_link_open (FcPattern *pattern)
+{
+ XftFont *font = XftFontOpenPattern (g_xui.dpy, pattern);
+ if (!font)
+ {
+ FcPatternDestroy (pattern);
+ return NULL;
+ }
+ return x11_font_link_new (font);
+}
+
+static struct x11_font *
+x11_font_open (unsigned style)
+{
+ FcPattern *pattern = (style & X11_FONT_MONOSPACE)
+ ? FcNameParse ((const FcChar8 *) g_xui.x11_fontname_monospace)
+ : FcNameParse ((const FcChar8 *) g_xui.x11_fontname);
+ if (style & X11_FONT_BOLD)
+ FcPatternAdd (pattern, FC_STYLE, (FcValue) {
+ .type = FcTypeString, .u.s = (FcChar8 *) "Bold" }, FcFalse);
+ if (style & X11_FONT_ITALIC)
+ FcPatternAdd (pattern, FC_STYLE, (FcValue) {
+ .type = FcTypeString, .u.s = (FcChar8 *) "Italic" }, FcFalse);
+
+ FcPattern *substituted = FcPatternDuplicate (pattern);
+ FcConfigSubstitute (NULL, substituted, FcMatchPattern);
+
+ FcResult result = 0;
+ FcPattern *match = XftFontMatch (g_xui.dpy,
+ DefaultScreen (g_xui.dpy), substituted, &result);
+ FcPatternDestroy (substituted);
+ struct x11_font_link *link = NULL;
+ if (!match || !(link = x11_font_link_open (match)))
+ {
+ FcPatternDestroy (pattern);
+ return NULL;
+ }
+
+ struct x11_font *self = xcalloc (1, sizeof *self);
+ self->list = link;
+ self->style = style;
+ self->pattern = pattern;
+ self->unavailable = FcCharSetCreate ();
+ return self;
+}
+
+static void
+x11_font_destroy (struct x11_font *self)
+{
+ FcPatternDestroy (self->pattern);
+ FcCharSetDestroy (self->unavailable);
+ LIST_FOR_EACH (struct x11_font_link, iter, self->list)
+ x11_font_link_destroy (iter);
+ free (self);
+
+}
+
+/// Find or instantiate a font that can render the character given by cp.
+static XftFont *
+x11_font_cover_codepoint (struct x11_font *self, ucs4_t cp)
+{
+ if (FcCharSetHasChar (self->unavailable, cp))
+ return self->list->font;
+
+ struct x11_font_link **used = &self->list;
+ for (; *used; used = &(*used)->next)
+ if (XftCharExists (g_xui.dpy, (*used)->font, cp))
+ return (*used)->font;
+
+ FcCharSet *set = FcCharSetCreate ();
+ FcCharSetAddChar (set, cp);
+ FcPattern *needle = FcPatternDuplicate (self->pattern);
+ FcPatternAddCharSet (needle, FC_CHARSET, set);
+ FcConfigSubstitute (NULL, needle, FcMatchPattern);
+
+ FcResult result = 0;
+ FcPattern *match
+ = XftFontMatch (g_xui.dpy, DefaultScreen (g_xui.dpy), needle, &result);
+ FcCharSetDestroy (set);
+ FcPatternDestroy (needle);
+ if (!match)
+ goto fail;
+
+ struct x11_font_link *new = x11_font_link_open (match);
+ if (!new)
+ goto fail;
+
+ // The reverse may happen simply due to race conditions.
+ if (XftCharExists (g_xui.dpy, new->font, cp))
+ return (*used = new)->font;
+
+ x11_font_link_destroy (new);
+fail:
+ FcCharSetAddChar (self->unavailable, cp);
+ return self->list->font;
+}
+
+// TODO: Perhaps produce an array of FT_UInt glyph indexes, mainly so that
+// x11_font_{hadvance,draw,render}() can use the same data, through the use
+// of a new function that collects the spans in a data structure.
+static size_t
+x11_font_span (struct x11_font *self, const uint8_t *text, XftFont **font)
+{
+ hard_assert (self->list != NULL);
+
+ // Xft similarly just stops on invalid UTF-8.
+ ucs4_t cp = 0;
+ const uint8_t *p = text;
+ if (!(p = u8_next (&cp, p)))
+ return 0;
+
+ *font = x11_font_cover_codepoint (self, cp);
+ for (const uint8_t *end = NULL; (end = u8_next (&cp, p)); p = end)
+ {
+ if (x11_font_cover_codepoint (self, cp) != *font)
+ break;
+ }
+ return p - text;
+}
+
+static int
+x11_font_draw (struct x11_font *self, XftColor *color, int x, int y,
+ const char *text)
+{
+ int advance = 0;
+ size_t len = 0;
+ XftFont *font = NULL;
+ while ((len = x11_font_span (self, (const uint8_t *) text, &font)))
+ {
+ if (color)
+ {
+ XftDrawStringUtf8 (g_xui.xft_draw, color, font,
+ x + advance, y + self->list->font->ascent,
+ (const FcChar8 *) text, len);
+ }
+
+ XGlyphInfo extents = {};
+ XftTextExtentsUtf8 (g_xui.dpy,
+ font, (const FcChar8 *) text, len, &extents);
+ text += len;
+ advance += extents.xOff;
+ }
+ return advance;
+}
+
+static int
+x11_font_hadvance (struct x11_font *self, const char *text)
+{
+ return x11_font_draw (self, NULL, 0, 0, text);
+}
+
+static int
+x11_font_render (struct x11_font *self, int op, Picture src, int srcx, int srcy,
+ int x, int y, const char *text)
+{
+ int advance = 0;
+ size_t len = 0;
+ XftFont *font = NULL;
+ while ((len = x11_font_span (self, (const uint8_t *) text, &font)))
+ {
+ if (src)
+ {
+ XftTextRenderUtf8 (g_xui.dpy,
+ op, src, font, g_xui.x11_pixmap_picture,
+ srcx, srcy, x + advance, y + self->list->font->ascent,
+ (const FcChar8 *) text, len);
+ }
+
+ XGlyphInfo extents = {};
+ XftTextExtentsUtf8 (g_xui.dpy,
+ font, (const FcChar8 *) text, len, &extents);
+ text += len;
+ advance += extents.xOff;
+ }
+ return advance;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static struct x11_font *
+x11_widget_font (struct widget *self)
+{
+ unsigned style = 0;
+ if (self->attrs & A_BOLD)
+ style |= X11_FONT_BOLD;
+ if (self->attrs & A_ITALIC)
+ style |= X11_FONT_ITALIC;
+ if (self->extended_attrs & XUI_ATTR_MONOSPACE)
+ style |= X11_FONT_MONOSPACE;
+
+ struct x11_font **font = &g_xui.xft_fonts;
+ for (; *font; font = &(*font)->next)
+ if ((*font)->style == style)
+ return *font;
+ if ((*font = x11_font_open (style)))
+ return *font;
+
+ // But FontConfig has a tendency to always return something.
+ return g_xui.xft_fonts;
+}
+
+static XRenderColor *
+x11_fg_attrs (chtype attrs)
+{
+ int pair = PAIR_NUMBER (attrs);
+ if (!pair--)
+ return &x11_default_fg;
+ return (attrs & A_REVERSE) ? &g_xui.x_bg[pair] : &g_xui.x_fg[pair];
+}
+
+static XRenderColor *
+x11_fg (struct widget *self)
+{
+ return x11_fg_attrs (self->attrs);
+}
+
+static XRenderColor *
+x11_bg_attrs (chtype attrs)
+{
+ int pair = PAIR_NUMBER (attrs);
+ if (!pair--)
+ return &x11_default_bg;
+ return (attrs & A_REVERSE) ? &g_xui.x_fg[pair] : &g_xui.x_bg[pair];
+}
+
+static XRenderColor *
+x11_bg (struct widget *self)
+{
+ return x11_bg_attrs (self->attrs);
+}
+
+static void
+x11_render_padding (struct widget *self)
+{
+ if (PAIR_NUMBER (self->attrs))
+ {
+ XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
+ x11_bg (self), self->x, self->y, self->width, self->height);
+ }
+ if (self->attrs & A_UNDERLINE)
+ {
+ XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
+ x11_fg (self), self->x, self->y + self->height - 1, self->width, 1);
+ }
+}
+
+static struct widget *
+x11_make_padding (chtype attrs, float width, float height)
+{
+ struct widget *w = xcalloc (1, sizeof *w + 2);
+ w->text[0] = ' ';
+ w->on_render = x11_render_padding;
+ w->attrs = attrs;
+ w->width = g_xui.vunit * width;
+ w->height = g_xui.vunit * height;
+ return w;
+}
+
+static void
+x11_render_label (struct widget *self)
+{
+ x11_render_padding (self);
+
+ int space = MIN (self->width, g_xui.width - self->x);
+ if (space <= 0)
+ return;
+
+ // TODO: Try to avoid re-measuring on each render.
+ struct x11_font *font = x11_widget_font (self);
+ int advance = x11_font_hadvance (font, self->text);
+ if (advance <= space)
+ {
+ XftColor color = { .color = *x11_fg (self) };
+ x11_font_draw (font, &color, self->x, self->y, self->text);
+ return;
+ }
+
+ // XRender doesn't extend gradients beyond their end stops.
+ XRenderColor solid = *x11_fg (self), colors[3] = { solid, solid, solid };
+ colors[2].alpha = 0;
+
+ double portion = MIN (1, 2.0 * font->list->font->height / space);
+ XFixed stops[3] = { 0, XDoubleToFixed (1 - portion), XDoubleToFixed (1) };
+ XLinearGradient gradient = { {}, { XDoubleToFixed (space), 0 } };
+
+ // Note that this masking is a very expensive operation.
+ Picture source =
+ XRenderCreateLinearGradient (g_xui.dpy, &gradient, stops, colors, 3);
+ x11_font_render (font, PictOpOver, source, -self->x, 0, self->x, self->y,
+ self->text);
+ XRenderFreePicture (g_xui.dpy, source);
+}
+
+static struct widget *
+x11_make_label (chtype attrs, unsigned extended, const char *label)
+{
+ // Xft renders combining marks by themselves, NFC improves it a bit.
+ // We'd have to use HarfBuzz to do this correctly.
+ size_t label_len = strlen (label) + 1, normalized_len = 0;
+ uint8_t *normalized = u8_normalize (UNINORM_NFC,
+ (const uint8_t *) label, label_len, NULL, &normalized_len);
+ if (!normalized)
+ {
+ normalized = memcpy (xmalloc (label_len), label, label_len);
+ normalized_len = label_len;
+ }
+
+ struct widget *w = xcalloc (1, sizeof *w + normalized_len);
+ w->on_render = x11_render_label;
+ w->attrs = attrs;
+ w->extended_attrs = extended;
+ memcpy (w->text, normalized, normalized_len);
+ free (normalized);
+
+ struct x11_font *font = x11_widget_font (w);
+ w->width = x11_font_hadvance (font, w->text);
+ w->height = font->list->font->height;
+ return w;
+}
+
+static void
+x11_render_widget (struct widget *w, const XRectangle *clip)
+{
+ if (w->width < 0 || w->height < 0)
+ return;
+
+ // Children may set their own clips, so reset before each sibling.
+ // We need to go through Xft, or XftTextRenderUtf8() might skip glyphs.
+ if (clip)
+ XftDrawSetClipRectangles (g_xui.xft_draw, 0, 0, clip, 1);
+ else
+ XftDrawSetClip (g_xui.xft_draw, None);
+
+ if (w->on_render)
+ w->on_render (w);
+
+ // We set clips on containers, not on individual widgets.
+ XRectangle subclip = { w->x, w->y, w->width, w->height };
+ if (clip)
+ {
+ int x1 = MAX (clip->x, w->x);
+ int y1 = MAX (clip->y, w->y);
+ int x2 = MIN (clip->x + clip->width, w->x + w->width);
+ int y2 = MIN (clip->y + clip->height, w->y + w->height);
+ if (x1 >= x2 || y1 >= y2)
+ return;
+
+ subclip.x = x1;
+ subclip.y = y1;
+ subclip.width = x2 - x1;
+ subclip.height = y2 - y1;
+ }
+
+ LIST_FOR_EACH (struct widget, child, w->children)
+ x11_render_widget (child, &subclip);
+}
+
+static void
+x11_render (void)
+{
+ XRenderFillRectangle (g_xui.dpy, PictOpSrc, g_xui.x11_pixmap_picture,
+ &x11_default_bg, 0, 0, g_xui.width, g_xui.height);
+
+ LIST_FOR_EACH (struct widget, w, g_xui.widgets)
+ x11_render_widget (w, NULL);
+
+ XRectangle r = { 0, 0, g_xui.width, g_xui.height };
+ XUnionRectWithRegion (&r, g_xui.x11_clip, g_xui.x11_clip);
+ poller_idle_set (&g_xui.xpending_event);
+}
+
+static void
+x11_flip (void)
+{
+ // This exercise in futility doesn't seem to affect CPU usage much.
+ XRectangle r = {};
+ XClipBox (g_xui.x11_clip, &r);
+ XCopyArea (g_xui.dpy, g_xui.x11_pixmap, g_xui.x11_window,
+ DefaultGC (g_xui.dpy, DefaultScreen (g_xui.dpy)),
+ r.x, r.y, r.width, r.height, r.x, r.y);
+
+ XSubtractRegion (g_xui.x11_clip, g_xui.x11_clip, g_xui.x11_clip);
+ poller_idle_set (&g_xui.xpending_event);
+}
+
+static void
+x11_destroy (void)
+{
+ XDestroyIC (g_xui.x11_ic);
+ XCloseIM (g_xui.x11_im);
+ XDestroyRegion (g_xui.x11_clip);
+ XDestroyWindow (g_xui.dpy, g_xui.x11_window);
+ XRenderFreePicture (g_xui.dpy, g_xui.x11_pixmap_picture);
+ XFreePixmap (g_xui.dpy, g_xui.x11_pixmap);
+ XftDrawDestroy (g_xui.xft_draw);
+ LIST_FOR_EACH (struct x11_font, font, g_xui.xft_fonts)
+ x11_font_destroy (font);
+ cstr_set (&g_xui.x11_selection, NULL);
+
+ free (g_xui.x_fg);
+ free (g_xui.x_bg);
+
+ poller_fd_reset (&g_xui.x11_event);
+ XCloseDisplay (g_xui.dpy);
+
+ // Xft hooks called in XCloseDisplay() don't clean up everything.
+ FcFini ();
+}
+
+static struct ui x11_ui =
+{
+ .padding = x11_make_padding,
+ .label = x11_make_label,
+
+ .render = x11_render,
+ .flip = x11_flip,
+ .destroy = x11_destroy,
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static termo_sym_t
+x11_convert_keysym (KeySym keysym)
+{
+ // Leaving out TERMO_TYPE_FUNCTION, TERMO_SYM_DEL (N/A),
+ // and TERMO_SYM_SPACE (governed by TERMO_FLAG_SPACESYMBOL, not in use).
+ switch (keysym)
+ {
+ case XK_BackSpace: return TERMO_SYM_BACKSPACE;
+ case XK_Tab: return TERMO_SYM_TAB;
+ case XK_ISO_Left_Tab: return TERMO_SYM_TAB;
+ case XK_Return: return TERMO_SYM_ENTER;
+ case XK_Escape: return TERMO_SYM_ESCAPE;
+
+ case XK_Up: return TERMO_SYM_UP;
+ case XK_Down: return TERMO_SYM_DOWN;
+ case XK_Left: return TERMO_SYM_LEFT;
+ case XK_Right: return TERMO_SYM_RIGHT;
+ case XK_Begin: return TERMO_SYM_BEGIN;
+ case XK_Find: return TERMO_SYM_FIND;
+ case XK_Insert: return TERMO_SYM_INSERT;
+ case XK_Delete: return TERMO_SYM_DELETE;
+ case XK_Select: return TERMO_SYM_SELECT;
+ case XK_Page_Up: return TERMO_SYM_PAGEUP;
+ case XK_Page_Down: return TERMO_SYM_PAGEDOWN;
+ case XK_Home: return TERMO_SYM_HOME;
+ case XK_End: return TERMO_SYM_END;
+
+ case XK_Cancel: return TERMO_SYM_CANCEL;
+ case XK_Clear: return TERMO_SYM_CLEAR;
+ // TERMO_SYM_CLOSE
+ // TERMO_SYM_COMMAND
+ // TERMO_SYM_COPY
+ // TERMO_SYM_EXIT
+ case XK_Help: return TERMO_SYM_HELP;
+ // TERMO_SYM_MARK
+ // TERMO_SYM_MESSAGE
+ // TERMO_SYM_MOVE
+ // TERMO_SYM_OPEN
+ // TERMO_SYM_OPTIONS
+ case XK_Print: return TERMO_SYM_PRINT;
+ case XK_Redo: return TERMO_SYM_REDO;
+ // TERMO_SYM_REFERENCE
+ // TERMO_SYM_REFRESH
+ // TERMO_SYM_REPLACE
+ // TERMO_SYM_RESTART
+ // TERMO_SYM_RESUME
+ // TERMO_SYM_SAVE
+ // TERMO_SYM_SUSPEND
+ case XK_Undo: return TERMO_SYM_UNDO;
+
+ case XK_KP_0: return TERMO_SYM_KP0;
+ case XK_KP_1: return TERMO_SYM_KP1;
+ case XK_KP_2: return TERMO_SYM_KP2;
+ case XK_KP_3: return TERMO_SYM_KP3;
+ case XK_KP_4: return TERMO_SYM_KP4;
+ case XK_KP_5: return TERMO_SYM_KP5;
+ case XK_KP_6: return TERMO_SYM_KP6;
+ case XK_KP_7: return TERMO_SYM_KP7;
+ case XK_KP_8: return TERMO_SYM_KP8;
+ case XK_KP_9: return TERMO_SYM_KP9;
+ case XK_KP_Enter: return TERMO_SYM_KPENTER;
+ case XK_KP_Add: return TERMO_SYM_KPPLUS;
+ case XK_KP_Subtract: return TERMO_SYM_KPMINUS;
+ case XK_KP_Multiply: return TERMO_SYM_KPMULT;
+ case XK_KP_Divide: return TERMO_SYM_KPDIV;
+ case XK_KP_Separator: return TERMO_SYM_KPCOMMA;
+ case XK_KP_Decimal: return TERMO_SYM_KPPERIOD;
+ case XK_KP_Equal: return TERMO_SYM_KPEQUALS;
+ }
+ return TERMO_SYM_UNKNOWN;
+}
+
+static bool
+on_x11_keypress (XEvent *e)
+{
+ // A kibibyte long buffer will have to suffice for anyone.
+ XKeyEvent *ev = &e->xkey;
+ char buf[1 << 10] = {}, *p = buf;
+ KeySym keysym = None;
+ Status status = 0;
+ int len = Xutf8LookupString
+ (g_xui.x11_ic, ev, buf, sizeof buf, &keysym, &status);
+ if (status == XBufferOverflow)
+ print_warning ("input method overflow");
+
+ termo_key_t key = {};
+ if (ev->state & ShiftMask)
+ key.modifiers |= TERMO_KEYMOD_SHIFT;
+ if (ev->state & ControlMask)
+ key.modifiers |= TERMO_KEYMOD_CTRL;
+ if (ev->state & Mod1Mask)
+ key.modifiers |= TERMO_KEYMOD_ALT;
+
+ if (keysym >= XK_F1 && keysym <= XK_F35)
+ {
+ key.type = TERMO_TYPE_FUNCTION;
+ key.code.number = 1 + keysym - XK_F1;
+ return xui_process_termo_event (&key);
+ }
+ if ((key.code.sym = x11_convert_keysym (keysym)) != TERMO_SYM_UNKNOWN)
+ {
+ key.type = TERMO_TYPE_KEYSYM;
+ return xui_process_termo_event (&key);
+ }
+
+ bool result = true;
+ if (len)
+ {
+ key.type = TERMO_TYPE_KEY;
+ key.modifiers &= ~TERMO_KEYMOD_SHIFT;
+
+ int32_t cp = 0;
+ struct utf8_iter iter = { .s = buf, .len = len };
+ size_t cp_len = 0;
+ while ((cp = utf8_iter_next (&iter, &cp_len)) >= 0)
+ {
+ termo_key_t k = key;
+ memcpy (k.multibyte, p, MIN (cp_len, sizeof k.multibyte - 1));
+ p += cp_len;
+
+ // This is all unfortunate, but probably in the right place.
+ if (!cp)
+ {
+ k.code.codepoint = ' ';
+ if (ev->state & ShiftMask)
+ k.modifiers |= TERMO_KEYMOD_SHIFT;
+ }
+ else if (cp >= 32)
+ k.code.codepoint = cp;
+ else if (ev->state & ShiftMask)
+ k.code.codepoint = cp + 64;
+ else
+ k.code.codepoint = cp + 96;
+ if (!xui_process_termo_event (&k))
+ result = false;
+ }
+ }
+ return result;
+}
+
+static void
+x11_init_pixmap (void)
+{
+ int screen = DefaultScreen (g_xui.dpy);
+ g_xui.x11_pixmap = XCreatePixmap (g_xui.dpy, g_xui.x11_window,
+ g_xui.width, g_xui.height, DefaultDepth (g_xui.dpy, screen));
+
+ Visual *visual = DefaultVisual (g_xui.dpy, screen);
+ XRenderPictFormat *format = XRenderFindVisualFormat (g_xui.dpy, visual);
+ g_xui.x11_pixmap_picture
+ = XRenderCreatePicture (g_xui.dpy, g_xui.x11_pixmap, format, 0, NULL);
+}
+
+static char *
+x11_find_text (struct widget *list, int x, int y)
+{
+ struct widget *target = NULL;
+ LIST_FOR_EACH (struct widget, w, list)
+ if (x >= w->x && x < w->x + w->width
+ && y >= w->y && y < w->y + w->height)
+ target = w;
+ if (!target)
+ return NULL;
+
+ char *result = x11_find_text (target->children, x, y);
+ if (result)
+ return result;
+ return xstrdup (target->text);
+}
+
+// TODO: OSC 52 exists for terminals, so make it possible to enable that there.
+static bool
+x11_process_press (int x, int y, int button, int modifiers)
+{
+ if (button != Button3)
+ goto out;
+
+ char *text = x11_find_text (g_xui.widgets, x, y);
+ if (!text || !*(cstr_strip_in_place (text, " \t")))
+ {
+ free (text);
+ goto out;
+ }
+
+ cstr_set (&g_xui.x11_selection, text);
+ XSetSelectionOwner (g_xui.dpy, XInternAtom (g_xui.dpy, "CLIPBOARD", False),
+ g_xui.x11_window, CurrentTime);
+ app_on_clipboard_copy (g_xui.x11_selection);
+ return true;
+
+out:
+ return app_process_mouse (TERMO_MOUSE_PRESS, x, y, button, modifiers);
+}
+
+static int
+x11_state_to_modifiers (unsigned int state)
+{
+ int modifiers = 0;
+ if (state & ShiftMask) modifiers |= TERMO_KEYMOD_SHIFT;
+ if (state & ControlMask) modifiers |= TERMO_KEYMOD_CTRL;
+ if (state & Mod1Mask) modifiers |= TERMO_KEYMOD_ALT;
+ return modifiers;
+}
+
+static bool
+on_x11_input_event (XEvent *ev)
+{
+ static XEvent last_press_event;
+ if (ev->type == KeyPress)
+ {
+ last_press_event = (XEvent) {};
+ return on_x11_keypress (ev);
+ }
+ if (ev->type == MotionNotify)
+ {
+ return app_process_mouse (TERMO_MOUSE_DRAG,
+ ev->xmotion.x, ev->xmotion.y, 1 /* Button1MotionMask */,
+ x11_state_to_modifiers (ev->xmotion.state));
+ }
+
+ // This is nearly the same as tui_on_tty_event().
+ int x = ev->xbutton.x, y = ev->xbutton.y;
+ unsigned int button = ev->xbutton.button;
+ int modifiers = x11_state_to_modifiers (ev->xbutton.state);
+ if (ev->type == ButtonPress
+ && ev->xbutton.time - last_press_event.xbutton.time < 500
+ && abs (last_press_event.xbutton.x - x) < 5
+ && abs (last_press_event.xbutton.y - y) < 5
+ && last_press_event.xbutton.button == button)
+ {
+ modifiers |= XUI_KEYMOD_DOUBLE_CLICK;
+ // Prevent interpreting triple clicks as two double clicks.
+ last_press_event = (XEvent) {};
+ }
+ else if (ev->type == ButtonPress)
+ last_press_event = *ev;
+
+ if (ev->type == ButtonPress)
+ return x11_process_press (x, y, button, modifiers);
+ if (ev->type == ButtonRelease)
+ return app_process_mouse
+ (TERMO_MOUSE_RELEASE, x, y, button, modifiers);
+ return false;
+}
+
+static void
+on_x11_selection_request (XSelectionRequestEvent *ev)
+{
+ Atom xa_targets = XInternAtom (g_xui.dpy, "TARGETS", False);
+ Atom xa_compound_text = XInternAtom (g_xui.dpy, "COMPOUND_TEXT", False);
+ Atom xa_utf8 = XInternAtom (g_xui.dpy, "UTF8_STRING", False);
+ Atom targets[] = { xa_targets, XA_STRING, xa_compound_text, xa_utf8 };
+
+ XEvent response = {};
+ bool ok = false;
+ Atom property = ev->property ? ev->property : ev->target;
+ if (!g_xui.x11_selection)
+ goto out;
+
+ XICCEncodingStyle style = 0;
+ if ((ok = ev->target == xa_targets))
+ {
+ XChangeProperty (g_xui.dpy, ev->requestor, property,
+ XA_ATOM, 32, PropModeReplace,
+ (const unsigned char *) targets, N_ELEMENTS (targets));
+ goto out;
+ }
+ else if (ev->target == XA_STRING)
+ style = XStringStyle;
+ else if (ev->target == xa_compound_text)
+ style = XCompoundTextStyle;
+ else if (ev->target == xa_utf8)
+ style = XUTF8StringStyle;
+ else
+ goto out;
+
+ // XXX: We let it crash us with BadLength, but we may, e.g., use INCR.
+ XTextProperty text = {};
+ if ((ok = !Xutf8TextListToTextProperty
+ (g_xui.dpy, &g_xui.x11_selection, 1, style, &text)))
+ {
+ XChangeProperty (g_xui.dpy, ev->requestor, property,
+ text.encoding, text.format, PropModeReplace,
+ text.value, text.nitems);
+ }
+ XFree (text.value);
+
+out:
+ response.xselection.type = SelectionNotify;
+ // XXX: We should check it against the event causing XSetSelectionOwner().
+ response.xselection.time = ev->time;
+ response.xselection.requestor = ev->requestor;
+ response.xselection.selection = ev->selection;
+ response.xselection.target = ev->target;
+ response.xselection.property = ok ? property : None;
+ XSendEvent (g_xui.dpy, ev->requestor, False, 0, &response);
+}
+
+static void
+on_x11_event (XEvent *ev)
+{
+ termo_key_t key = {};
+ switch (ev->type)
+ {
+ case Expose:
+ {
+ XRectangle r = { ev->xexpose.x, ev->xexpose.y,
+ ev->xexpose.width, ev->xexpose.height };
+ XUnionRectWithRegion (&r, g_xui.x11_clip, g_xui.x11_clip);
+ poller_idle_set (&g_xui.flip_event);
+ break;
+ }
+ case ConfigureNotify:
+ if (g_xui.width == ev->xconfigure.width
+ && g_xui.height == ev->xconfigure.height)
+ break;
+
+ g_xui.width = ev->xconfigure.width;
+ g_xui.height = ev->xconfigure.height;
+
+ XRenderFreePicture (g_xui.dpy, g_xui.x11_pixmap_picture);
+ XFreePixmap (g_xui.dpy, g_xui.x11_pixmap);
+ x11_init_pixmap ();
+ XftDrawChange (g_xui.xft_draw, g_xui.x11_pixmap);
+ xui_invalidate ();
+ break;
+ case SelectionRequest:
+ on_x11_selection_request (&ev->xselectionrequest);
+ break;
+ case SelectionClear:
+ cstr_set (&g_xui.x11_selection, NULL);
+ break;
+ // UnmapNotify can be received when restarting the window manager.
+ // Should this turn out to be unreliable (window not destroyed by WM
+ // upon closing), opt for the WM_DELETE_WINDOW protocol as well.
+ case DestroyNotify:
+ app_quit ();
+ break;
+ case FocusIn:
+ key.type = TERMO_TYPE_FOCUS;
+ key.code.focused = true;
+ xui_process_termo_event (&key);
+ break;
+ case FocusOut:
+ key.type = TERMO_TYPE_FOCUS;
+ key.code.focused = false;
+ xui_process_termo_event (&key);
+ break;
+ case KeyPress:
+ case ButtonPress:
+ case ButtonRelease:
+ case MotionNotify:
+ if (!on_x11_input_event (ev))
+ XkbBell (g_xui.dpy, ev->xany.window, 0, None);
+ }
+}
+
+static void
+on_x11_pending (void *user_data)
+{
+ (void) user_data;
+
+ XkbEvent ev;
+ while (XPending (g_xui.dpy))
+ {
+ if (XNextEvent (g_xui.dpy, &ev.core))
+ exit_fatal ("XNextEvent returned non-zero");
+ if (XFilterEvent (&ev.core, None))
+ continue;
+
+ on_x11_event (&ev.core);
+ }
+
+ poller_idle_reset (&g_xui.xpending_event);
+}
+
+static void
+on_x11_ready (const struct pollfd *pfd, void *user_data)
+{
+ (void) pfd;
+ on_x11_pending (user_data);
+}
+
+static int
+on_x11_error (Display *dpy, XErrorEvent *event)
+{
+ // Without opting for WM_DELETE_WINDOW, this window can become destroyed
+ // and hence invalid at any time. We don't use the Window much,
+ // so we should be fine ignoring these errors.
+ if ((event->error_code == BadWindow
+ && event->resourceid == g_xui.x11_window)
+ || (event->error_code == BadDrawable
+ && event->resourceid == g_xui.x11_window))
+ return app_quit (), 0;
+
+ // XXX: The simplest possible way of discarding selection management errors.
+ // XCB would be a small win here, but it is a curse at the same time.
+ if (event->error_code == BadWindow && event->resourceid != g_xui.x11_window)
+ return 0;
+
+ return x11_default_error_handler (dpy, event);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static XRenderColor
+x11_convert_color (int color)
+{
+ hard_assert (color >= 0 && color <= 255);
+
+ static const uint16_t base[16] =
+ {
+ 0x000, 0x800, 0x080, 0x880, 0x008, 0x808, 0x088, 0xccc,
+ 0x888, 0xf00, 0x0f0, 0xff0, 0x00f, 0xf0f, 0x0ff, 0xfff,
+ };
+
+ XRenderColor c = { .alpha = 0xffff };
+ if (color < 16)
+ {
+ c.red = 0x1111 * (base[color] >> 8);
+ c.green = 0x1111 * (0xf & (base[color] >> 4));
+ c.blue = 0x1111 * (0xf & (base[color]));
+ }
+ else if (color >= 232)
+ c.red = c.green = c.blue = 0x0101 * (8 + (color - 232) * 10);
+ else
+ {
+ color -= 16;
+
+ int r = color / 36;
+ int g = (color / 6) % 6;
+ int b = (color % 6);
+ c.red = 0x0101 * !!r * (55 + 40 * r);
+ c.green = 0x0101 * !!g * (55 + 40 * g);
+ c.blue = 0x0101 * !!b * (55 + 40 * b);
+ }
+ return c;
+}
+
+static void
+x11_init_attributes (struct attrs *attrs, size_t attrs_len)
+{
+ g_xui.x_fg = xcalloc (attrs_len, sizeof g_xui.x_fg[0]);
+ g_xui.x_bg = xcalloc (attrs_len, sizeof g_xui.x_bg[0]);
+ for (size_t a = 0; a < attrs_len; a++)
+ {
+ g_xui.x_fg[a] = x11_default_fg;
+ g_xui.x_bg[a] = x11_default_bg;
+ if (attrs[a].fg >= 256 || attrs[a].fg < -1
+ || attrs[a].bg >= 256 || attrs[a].bg < -1)
+ continue;
+
+ if (attrs[a].fg != -1)
+ g_xui.x_fg[a] = x11_convert_color (attrs[a].fg);
+ if (attrs[a].bg != -1)
+ g_xui.x_bg[a] = x11_convert_color (attrs[a].bg);
+
+ attrs[a].attrs |= COLOR_PAIR (a + 1);
+ }
+}
+
+static void
+x11_init (struct poller *poller, struct attrs *app_attrs, size_t app_attrs_len)
+{
+ // https://tedyin.com/posts/a-brief-intro-to-linux-input-method-framework/
+ if (!XSupportsLocale ())
+ print_warning ("locale not supported by Xlib");
+ XSetLocaleModifiers ("");
+
+ if (!(g_xui.dpy = XkbOpenDisplay
+ (NULL, &g_xui.xkb_base_event_code, NULL, NULL, NULL, NULL)))
+ exit_fatal ("cannot open display");
+ if (!XftDefaultHasRender (g_xui.dpy))
+ exit_fatal ("XRender is not supported");
+ if (!(g_xui.x11_im = XOpenIM (g_xui.dpy, NULL, NULL, NULL)))
+ exit_fatal ("failed to open an input method");
+
+ x11_default_error_handler = XSetErrorHandler (on_x11_error);
+
+ set_cloexec (ConnectionNumber (g_xui.dpy));
+ g_xui.x11_event = poller_fd_make (poller, ConnectionNumber (g_xui.dpy));
+ g_xui.x11_event.dispatcher = on_x11_ready;
+ poller_fd_set (&g_xui.x11_event, POLLIN);
+
+ // Whenever something causes Xlib to read its socket, it can make
+ // the I/O event above fail to trigger for whatever might have ended up
+ // in its queue. So always use this instead of XSync:
+ g_xui.xpending_event = poller_idle_make (poller);
+ g_xui.xpending_event.dispatcher = on_x11_pending;
+ poller_idle_set (&g_xui.xpending_event);
+
+ x11_init_attributes (app_attrs, app_attrs_len);
+
+ if (!FcInit ())
+ print_warning ("Fontconfig initialization failed");
+ if (!(g_xui.xft_fonts = x11_font_open (0)))
+ exit_fatal ("cannot open a font");
+
+ int screen = DefaultScreen (g_xui.dpy);
+ Colormap cmap = DefaultColormap (g_xui.dpy, screen);
+ XColor default_bg =
+ {
+ .red = x11_default_bg.red,
+ .green = x11_default_bg.green,
+ .blue = x11_default_bg.blue,
+ };
+ if (!XAllocColor (g_xui.dpy, cmap, &default_bg))
+ exit_fatal ("X11 setup failed");
+
+ XSetWindowAttributes attrs =
+ {
+ .event_mask = StructureNotifyMask | ExposureMask | FocusChangeMask
+ | KeyPressMask | ButtonPressMask | ButtonReleaseMask
+ | Button1MotionMask,
+ .bit_gravity = NorthWestGravity,
+ .background_pixel = default_bg.pixel,
+ };
+
+ // Approximate the average width of a character to half of the em unit.
+ g_xui.vunit = g_xui.xft_fonts->list->font->height;
+ g_xui.hunit = g_xui.vunit / 2;
+ // Base the window's size on the regular font size.
+ // Roughly trying to match the 80x24 default dimensions of terminals.
+ g_xui.height = 24 * g_xui.vunit;
+ g_xui.width = g_xui.height * 4 / 3;
+
+ long im_event_mask = 0;
+ if (!XGetIMValues (g_xui.x11_im, XNFilterEvents, &im_event_mask, NULL))
+ attrs.event_mask |= im_event_mask;
+
+ Visual *visual = DefaultVisual (g_xui.dpy, screen);
+ g_xui.x11_window = XCreateWindow (g_xui.dpy,
+ RootWindow (g_xui.dpy, screen), 100, 100,
+ g_xui.width, g_xui.height, 0, CopyFromParent, InputOutput, visual,
+ CWEventMask | CWBackPixel | CWBitGravity, &attrs);
+ g_xui.x11_clip = XCreateRegion ();
+
+ XTextProperty prop = {};
+ char *name = PROGRAM_NAME;
+ if (!Xutf8TextListToTextProperty (g_xui.dpy,
+ &name, 1, XUTF8StringStyle, &prop))
+ XSetWMName (g_xui.dpy, g_xui.x11_window, &prop);
+ XFree (prop.value);
+
+ // TODO: It is possible to do, e.g., on-the-spot.
+ XIMStyle im_style = XIMPreeditNothing | XIMStatusNothing;
+ XIMStyles *im_styles = NULL;
+ bool im_style_found = false;
+ if (!XGetIMValues (g_xui.x11_im, XNQueryInputStyle, &im_styles, NULL)
+ && im_styles)
+ {
+ for (unsigned i = 0; i < im_styles->count_styles; i++)
+ im_style_found |= im_styles->supported_styles[i] == im_style;
+ XFree (im_styles);
+ }
+ if (!im_style_found)
+ print_warning ("failed to find the desired input method style");
+ if (!(g_xui.x11_ic = XCreateIC (g_xui.x11_im,
+ XNInputStyle, im_style,
+ XNClientWindow, g_xui.x11_window,
+ NULL)))
+ exit_fatal ("failed to open an input context");
+
+ XSetICFocus (g_xui.x11_ic);
+
+ x11_init_pixmap ();
+ g_xui.xft_draw = XftDrawCreate (g_xui.dpy, g_xui.x11_pixmap, visual, cmap);
+ g_xui.ui = &x11_ui;
+
+ XMapWindow (g_xui.dpy, g_xui.x11_window);
+}
+
+#endif // LIBERTY_XUI_WANT_X11
+
+// --- Containers --------------------------------------------------------------
+
+static void
+xui_on_hbox_allocated (struct widget *self)
+{
+ int parts = 0, width = self->width;
+ LIST_FOR_EACH (struct widget, w, self->children)
+ {
+ if (w->width < 0)
+ parts -= w->width;
+ else
+ width -= w->width;
+ }
+
+ int remaining = MAX (width, 0),
+ part_width = parts ? remaining / parts : 0;
+ struct widget *last = NULL;
+ LIST_FOR_EACH (struct widget, w, self->children)
+ {
+ w->height = self->height;
+ if (w->width < 0)
+ {
+ remaining -= (w->width *= -part_width);
+ last = w;
+ }
+ }
+ if (last)
+ last->width += remaining;
+
+ int x = self->x, y = self->y;
+ LIST_FOR_EACH (struct widget, w, self->children)
+ {
+ widget_move (w, x - w->x, y - w->y);
+ x += w->width;
+
+ if (w->on_allocated)
+ w->on_allocated (w);
+ }
+}
+
+static struct widget *
+xui_hbox (struct widget *head)
+{
+ struct widget *self = xcalloc (1, sizeof *self);
+ self->children = head;
+ self->on_allocated = xui_on_hbox_allocated;
+
+ LIST_FOR_EACH (struct widget, w, head)
+ {
+ self->height = MAX (self->height, w->height);
+ self->width += MAX (0, w->width);
+ }
+ return self;
+}
+
+static void
+xui_on_vbox_allocated (struct widget *self)
+{
+ int parts = 0, height = self->height;
+ LIST_FOR_EACH (struct widget, w, self->children)
+ {
+ if (w->height < 0)
+ parts -= w->height;
+ else
+ height -= w->height;
+ }
+
+ int remaining = MAX (height, 0),
+ part_height = parts ? remaining / parts : 0;
+ struct widget *last = NULL;
+ LIST_FOR_EACH (struct widget, w, self->children)
+ {
+ w->width = self->width;
+ if (w->height < 0)
+ {
+ remaining -= (w->height *= -part_height);
+ last = w;
+ }
+ }
+ if (last)
+ last->height += remaining;
+
+ int x = self->x, y = self->y;
+ LIST_FOR_EACH (struct widget, w, self->children)
+ {
+ widget_move (w, x - w->x, y - w->y);
+ y += w->height;
+
+ if (w->on_allocated)
+ w->on_allocated (w);
+ }
+}
+
+static struct widget *
+xui_vbox (struct widget *head)
+{
+ struct widget *self = xcalloc (1, sizeof *self);
+ self->children = head;
+ self->on_allocated = xui_on_vbox_allocated;
+
+ LIST_FOR_EACH (struct widget, w, head)
+ {
+ self->width = MAX (self->width, w->width);
+ self->height += MAX (0, w->height);
+ }
+ return self;
+}
+
+// --- XUI ---------------------------------------------------------------------
+
+static bool
+xui_is_character_in_locale (ucs4_t ch)
+{
+ // Avoid the overhead joined with calling iconv() for all characters.
+ if (g_xui.locale_is_utf8)
+ return true;
+
+ // The library really creates a new conversion object every single time
+ // and doesn't provide any smarter APIs. Luckily, most users use UTF-8.
+ size_t len;
+ char *tmp = u32_conv_to_encoding (locale_charset (), iconveh_error,
+ &ch, 1, NULL, NULL, &len);
+ if (!tmp)
+ return false;
+ free (tmp);
+ return true;
+}
+
+static void
+xui_on_flip (void *user_data)
+{
+ (void) user_data;
+ poller_idle_reset (&g_xui.flip_event);
+
+ // Waste of time, and may cause X11 to render uninitialised pixmaps.
+ if (/*g.polling &&*/ !g_xui.refresh_event.active)
+ g_xui.ui->flip ();
+}
+
+static void
+xui_on_refresh (void *user_data)
+{
+ (void) user_data;
+ poller_idle_reset (&g_xui.refresh_event);
+
+ LIST_FOR_EACH (struct widget, w, g_xui.widgets)
+ widget_destroy (w);
+
+ g_xui.widgets = NULL;
+ app_layout ();
+
+ // Keep whatever the application gave them, for flexibility.
+ LIST_FOR_EACH (struct widget, w, g_xui.widgets)
+ if (w->on_allocated)
+ w->on_allocated (w);
+
+ g_xui.ui->render ();
+ poller_idle_set (&g_xui.flip_event);
+}
+
+static void
+xui_preinit (void)
+{
+ TERMO_CHECK_VERSION;
+ if (!(g_xui.tk = termo_new (STDIN_FILENO, NULL, TERMO_FLAG_NOSTART)))
+ exit_fatal ("failed to initialize termo");
+
+ // This is also approximately what libunistring does internally,
+ // since the locale name is canonicalized by locale_charset().
+ // Note that non-Unicode locales are handled pretty inefficiently.
+ g_xui.locale_is_utf8 = !strcasecmp_ascii (locale_charset (), "UTF-8");
+
+ // Presumably, although not necessarily; unsure if queryable at all.
+ g_xui.focused = true;
+
+ // TODO: Try to use Gtk/FontName from the _XSETTINGS_S%d selection,
+ // as well as Net/DoubleClick*. See the XSETTINGS proposal for details.
+ // https://www.freedesktop.org/wiki/Specifications/XSettingsRegistry/
+ // Note that this needs an X11 connection already.
+#ifdef LIBERTY_XUI_WANT_X11
+ g_xui.x11_fontname = "sans\\-serif-11";
+ g_xui.x11_fontname_monospace = "monospace-11";
+#endif // LIBERTY_XUI_WANT_X11
+}
+
+static void
+xui_start (struct poller *poller,
+ bool force_x11, struct attrs *attrs, size_t attrs_len)
+{
+ (void) force_x11;
+
+ g_xui.refresh_event = poller_idle_make (poller);
+ g_xui.refresh_event.dispatcher = xui_on_refresh;
+ g_xui.flip_event = poller_idle_make (poller);
+ g_xui.flip_event.dispatcher = xui_on_flip;
+
+ // Always initialized, but only activated with the TUI.
+ g_xui.tty_event = poller_fd_make (poller, STDIN_FILENO);
+ g_xui.tty_event.dispatcher = tui_on_tty_readable;
+ g_xui.tk_timer = poller_timer_make (poller);
+ g_xui.tk_timer.dispatcher = tui_on_key_timer;
+
+#ifdef LIBERTY_XUI_WANT_X11
+ if (force_x11 || (!isatty (STDIN_FILENO) && getenv ("DISPLAY")))
+ x11_init (poller, attrs, attrs_len);
+ else
+#endif // LIBERTY_XUI_WANT_X11
+ tui_init (poller, attrs, attrs_len);
+}
+
+static void
+xui_stop (void)
+{
+ poller_idle_reset (&g_xui.refresh_event);
+ poller_idle_reset (&g_xui.flip_event);
+ poller_fd_reset (&g_xui.tty_event);
+ poller_timer_reset (&g_xui.tk_timer);
+
+ g_xui.ui->destroy ();
+ LIST_FOR_EACH (struct widget, w, g_xui.widgets)
+ widget_destroy (w);
+
+ termo_destroy (g_xui.tk);
+}
diff --git a/tests/fuzz.c b/tests/fuzz.c
index 74db020..1e3c995 100644
--- a/tests/fuzz.c
+++ b/tests/fuzz.c
@@ -32,13 +32,6 @@
#define LIBERTY_WANT_PROTO_MPD
#include "../liberty.c"
-#include "../liberty-tui.c"
-
-static bool
-app_is_character_in_locale (ucs4_t ch)
-{
- return ch < 128;
-}
// --- UTF-8 -------------------------------------------------------------------
@@ -211,19 +204,6 @@ test_config_item_parse (const uint8_t *data, size_t size)
config_item_destroy (item);
}
-// --- TUI ---------------------------------------------------------------------
-
-static void
-test_attrs_decode (const uint8_t *data, size_t size)
-{
- struct str wrap = str_make ();
- str_append_data (&wrap, data, size);
-
- attrs_decode (wrap.str);
-
- str_free (&wrap);
-}
-
// --- MPD ---------------------------------------------------------------------
static void
@@ -266,7 +246,6 @@ LLVMFuzzerInitialize (int *argcp, char ***argvp)
REGISTER (fcgi_parser_push)
REGISTER (fcgi_nv_parser_push)
REGISTER (config_item_parse)
- REGISTER (attrs_decode)
REGISTER (mpd_client_process_input)
char **argv = *argvp, *option = "-test=", *name = NULL;