Compare commits

...

3 Commits

Author SHA1 Message Date
d01a1ff034 Turn liberty-tui into a terminal/X11 hybrid
Importing code from nncmpp, adjusting it to work with hex as well.
2023-06-19 13:06:12 +02:00
bd1013f16a Parse block attribute list lines
This code is of strategic importance, but its output is so far unused.
2023-06-11 10:02:16 +02:00
29bf109a51 asciiman: improve attribute handling 2022-10-09 18:43:37 +02:00
5 changed files with 2277 additions and 316 deletions

View File

@@ -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 Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted. purpose with or without fee is hereby granted.

View File

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

2197
liberty-xui.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -32,13 +32,6 @@
#define LIBERTY_WANT_PROTO_MPD #define LIBERTY_WANT_PROTO_MPD
#include "../liberty.c" #include "../liberty.c"
#include "../liberty-tui.c"
static bool
app_is_character_in_locale (ucs4_t ch)
{
return ch < 128;
}
// --- UTF-8 ------------------------------------------------------------------- // --- UTF-8 -------------------------------------------------------------------
@@ -211,19 +204,6 @@ test_config_item_parse (const uint8_t *data, size_t size)
config_item_destroy (item); 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 --------------------------------------------------------------------- // --- MPD ---------------------------------------------------------------------
static void static void
@@ -266,7 +246,6 @@ LLVMFuzzerInitialize (int *argcp, char ***argvp)
REGISTER (fcgi_parser_push) REGISTER (fcgi_parser_push)
REGISTER (fcgi_nv_parser_push) REGISTER (fcgi_nv_parser_push)
REGISTER (config_item_parse) REGISTER (config_item_parse)
REGISTER (attrs_decode)
REGISTER (mpd_client_process_input) REGISTER (mpd_client_process_input)
char **argv = *argvp, *option = "-test=", *name = NULL; char **argv = *argvp, *option = "-test=", *name = NULL;

View File

@@ -1,6 +1,6 @@
# asciiman.awk: simplified AsciiDoc to manual page converter # asciiman.awk: simplified AsciiDoc to manual page converter
# #
# Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name> # Copyright (c) 2022 - 2023, Přemysl Eric Janouch <p@janouch.name>
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
# #
# This is not intended to produce great output, merely useful output. # This is not intended to produce great output, merely useful output.
@@ -20,21 +20,17 @@ function fatal(message) {
exit 1 exit 1
} }
function haveattribute(name) { BEGIN {
return name in Attrs || ("asciidoc-" name) in ENVIRON for (name in ENVIRON)
if (match(name, /^asciidoc-/))
Attrs[substr(name, RSTART + RLENGTH)] = ENVIRON[name]
} }
function getattribute(name) { function expand(s, attrname, v) {
if (!(name in Attrs) && ("asciidoc-" name) in ENVIRON) while (match(s, /[{][^{}]+[}]/)) {
Attrs[name] = ENVIRON["asciidoc-" name] attrname = substr(s, RSTART + 1, RLENGTH - 2)
return Attrs[name] if (attrname in Attrs)
} v = v substr(s, 1, RSTART - 1) Attrs[attrname]
function expand(s, attr, v) {
while (match(s, /[{][^{}]*[}]/)) {
attr = substr(s, RSTART + 1, RLENGTH - 2)
if (haveattribute(attr))
v = v substr(s, 1, RSTART - 1) getattribute(attr)
else else
v = v substr(s, 1, RSTART + RLENGTH - 1) v = v substr(s, 1, RSTART + RLENGTH - 1)
s = substr(s, RSTART + RLENGTH) s = substr(s, RSTART + RLENGTH)
@@ -49,13 +45,20 @@ function escape(s) {
return s return s
} }
function readattribute(line, attrname, attrvalue) { function readattribute(line, attrname) {
if (match(line, /^:[^:]*: /)) { if (match(line, /^:[^:]+:$/)) {
Attrs[substr(line, RSTART + 1, RLENGTH - 2)] = ""
} else if (match(line, /^:[^:]+!:$/)) {
delete Attrs[substr(line, RSTART + 1, RLENGTH - 3)]
} else if (match(line, /^:![^:]+:$/)) {
delete Attrs[substr(line, RSTART + 2, RLENGTH - 3)]
} else if (match(line, /^:[^:]+: /)) {
attrname = substr(line, RSTART + 1, RLENGTH - 3) attrname = substr(line, RSTART + 1, RLENGTH - 3)
attrvalue = substr(line, RSTART + RLENGTH) Attrs[attrname] = expand(substr(line, RSTART + RLENGTH))
Attrs[attrname] = expand(attrvalue) } else {
return 1 return 0
} }
return 1
} }
NR == 1 { NR == 1 {
@@ -80,15 +83,56 @@ NR == 1 {
# Requesting tbl(1), even though we currently do not support tables. # Requesting tbl(1), even though we currently do not support tables.
print "'\\\" t" print "'\\\" t"
printf ".TH \"%s\" \"%s\" \"\" \"%s\"", printf ".TH \"%s\" \"%s\" \"\" \"%s\"",
toupper(name), section, getattribute("mansource") toupper(name), section, Attrs["mansource"]
if (getattribute("manmanual")) if ("manmanual" in Attrs)
printf " \"%s\"", getattribute("manmanual") printf " \"%s\"", Attrs["manmanual"]
print "" print ""
# Hyphenation is indeed rather annoying, in particular with long links. # Hyphenation is indeed rather annoying, in particular with long links.
print ".nh" print ".nh"
} }
function readattrlist(line, posattrs, namedattrs, name, value, n) {
if (!match(line, /^\[.*\]$/))
return 0
line = expand(substr(line, RSTART + 1, RLENGTH - 2))
while (line) {
name = ""
if (match(line, /^[[:alnum:]][[:alnum:]-]*/)) {
value = substr(line, RSTART, RLENGTH)
if (match(substr(line, RSTART + RLENGTH),
/^[[:space:]]*=[[:space:]]*/)) {
name = value
line = substr(line, 1 + length(name) + RLENGTH)
}
}
# The quoting syntax actually is awful like this.
if (match(line, /^"(\\.|[^"\\])*"/)) {
value = substr(line, RSTART + 1, RLENGTH - 2)
gsub(/\\"/, "\"", value)
} else if (match(line, /^'(\\.|[^'\\])*'/)) {
value = substr(line, RSTART + 1, RLENGTH - 2)
gsub(/\\'/, "'", value)
} else {
match(line, /^[^,]*/)
value = substr(line, RSTART, RLENGTH)
sub(/[[:space:]]*$/, "", value)
}
line = substr(line, RSTART + RLENGTH)
sub(/^[[:space:]]*,[[:space:]]*/, "", line)
if (!name)
posattrs[++n] = value
else if (value == "None")
delete namedattrs[name]
else
namedattrs[name] = value
}
return 1
}
function format(line, v) { function format(line, v) {
# Pass-through, otherwise useful for hacks, is a bit of a lie here, # Pass-through, otherwise useful for hacks, is a bit of a lie here,
# and formatting doesn't fully respect word boundaries. # and formatting doesn't fully respect word boundaries.
@@ -151,7 +195,7 @@ function inline(line) {
} }
# Returns 1 iff the left-over $0 should be processed further. # Returns 1 iff the left-over $0 should be processed further.
function process(firstline) { function process(firstline, posattrs, namedattrs) {
if (readattribute(firstline)) if (readattribute(firstline))
return 0 return 0
if (getline <= 0) { if (getline <= 0) {
@@ -159,6 +203,17 @@ function process(firstline) {
return 0 return 0
} }
# Block attribute list lines.
delete posattrs[0]
delete namedattrs[0]
while (readattrlist(firstline, posattrs, namedattrs)) {
firstline = $0
if (getline <= 0) {
inline(firstline)
return 0
}
}
# mandoc(1) automatically precedes section headers with blank lines. # mandoc(1) automatically precedes section headers with blank lines.
if (length(firstline) == length($0) && /^-+$/) { if (length(firstline) == length($0) && /^-+$/) {
print ".SH \"" escape(toupper(expand(firstline))) "\"" print ".SH \"" escape(toupper(expand(firstline))) "\""
@@ -196,7 +251,7 @@ function process(firstline) {
return 1 return 1
} }
# We generally assume these block end with a blank line. # We generally assume these blocks end with a blank line.
if (match(firstline, /^[[:space:]]*[*][[:space:]]+/)) { if (match(firstline, /^[[:space:]]*[*][[:space:]]+/)) {
flushspace() flushspace()